§ DOCUMENTATION
Self-hosted deployment
Run the full Execlave governance platform on your own infrastructure with the published Docker images and a license key. Agent and trace data stay in your network — the only thing that crosses the boundary is a daily license heartbeat (license key, instance fingerprint, hostname, version, and trace/agent/user counts — no trace content or customer records); air-gapped mode sends nothing.
Data privacy: no external AI API calls by default
The default self-hosted deployment sends zero prompts, traces, or model inputs to any external AI provider. Semantic classification and policy generation can use an optional local LLM (Ollama, bundled or your own server) configured via LOCAL_LLM_URL (§06). Without one, or if it is unreachable, those features use deterministic heuristics — enforcement keeps working, you just lose the advisory classifications.
What this means for procurement reviews
- Prompts and traces never leave your network.
- No API keys for Anthropic, OpenAI, Google, or any other external LLM provider are required — or accepted — in the default configuration.
- Sign-in runs on a bundled identity service (Logto) inside the stack; user accounts and passwords stay on your infrastructure.
- If you want to plug in an external provider for a specific workflow, you are responsible for the data-transfer compliance implications.
Prerequisites
- Linux host (any cloud or bare metal) with 4 CPU and 8 GB RAM minimum; the optional local LLM (§06) needs another 8 GB
- Docker 24+ with Docker Compose v2
- PostgreSQL (with TimescaleDB), Redis and the sign-in service are bundled — no external database is needed
- A license key and its signing public key (request them at /get-license)
- Outbound HTTPS to
license.execlave.comfor the daily heartbeat — orLICENSE_AIRGAPPED=truefor no outbound calls (see §10)
Quickstart
# 1. Download the compose file and create .env with generated passwordscurl -fsSL https://get.execlave.com/install.sh | sh # 2. Add your license to .env (both values are provided with it):# LICENSE_KEY=exe_lic_...# LICENSE_SIGNING_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" # 3. Start the stackdocker compose up -d # 4. Open http://localhost:3000 and sign in as the administratorThe installer creates .env (readable only by you) with random passwords for the databases, Redis, the internal service token and the dashboard administrator, and prints the administrator password once. The username is admin (EXECLAVE_ADMIN_USERNAME). The first start pulls the images and runs the database migrations, which takes a few minutes; docker compose ps shows every service as healthy when it is ready.
Send your first trace
In the dashboard, create an API key under Settings → API Keys, then register an agent and send a trace:
export EXECLAVE_API_KEY=exe_prod_...curl -X POST http://localhost:4000/api/v1/agents \ -H "Authorization: Bearer $EXECLAVE_API_KEY" -H 'Content-Type: application/json' \ -d '{"agentId":"my-agent","name":"My agent","type":"chatbot","platform":"custom","environment":"production"}'curl -X POST http://localhost:4000/api/v1/traces/ingest \ -H "Authorization: Bearer $EXECLAVE_API_KEY" -H 'Content-Type: application/json' \ -d "{\"traces\":[{\"traceId\":\"tr_1\",\"agentId\":\"my-agent\",\"status\":\"success\",\"durationMs\":120,\"environment\":\"production\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}]}"The SDKs work the same way: point them at your API with baseUrl: 'http://localhost:4000' (see Getting Started).
Get a license key
Free and paid self-hosted licenses are issued manually at this stage. Request one at /get-license.
- Free plan: license arrives within 1 business hour
- Paid plans: we reach out to set up payment first
- Enterprise: we book a 30-min call
The license key is a signed JWT (exe_lic_…); you receive it together with the public key that verifies it. The license's plan applies to every organization on your instance.
Architecture
Everything runs inside your network. The only outbound traffic is the daily license heartbeat, and none in air-gapped mode.
+----------------------------------------------------------+| Your infrastructure || || browser --> +----------+ +---------+ +-----------+ || | Next.js |-->| Express |-->| Postgres | || | (3000) | | (4000) | | (5432) | || +----------+ +---------+ +-----------+ || | | || v v || +----------+ +---------+ +-----------+ || | Logto | | Worker |-->| Redis | || | sign-in | | (BullMQ)| | (6379) | || | (3301) | +---------+ +-----------+ || +----------+ || +---------------------------+ || | Processing (PDF, scans) | || +---------------------------+ |+----------------------------------------------------------+ | | outbound HTTPS, once a day (none when air-gapped) v+-----------------------------+| license.execlave.com || (key, fingerprint, counts) |+-----------------------------+Configuration
All settings live in .env next to docker-compose.yml; the downloaded .env.example documents each one. Passwords and tokens must be at least 16 URL-safe characters (the installer uses openssl rand -hex 32).
Required
| Variable | Description |
|---|---|
LICENSE_KEY | Your license key (exe_lic_…) |
LICENSE_SIGNING_PUBLIC_KEY | Public key that verifies the license, provided with it. One line in double quotes, with literal \n between PEM lines. |
LICENSE_INSTANCE_ID | Identifies this deployment to the license server (generated). Never change it — a new value registers a new instance (§09) — and never copy it to another deployment. |
POSTGRES_PASSWORD | Database superuser — used only for migrations and role setup (generated) |
APP_USER_DB_PASSWORD | Runtime database role app_user (generated; see §08) |
BOOTSTRAP_PASSWORD | Sign-in lookup role app_bootstrap (generated) |
LOGTO_DB_PASSWORD | The sign-in service's own database (generated) |
PROCESSING_INTERNAL_TOKEN | Shared secret between the API and the processing service (generated) |
REDIS_PASSWORD | Redis password (generated) |
EXECLAVE_ADMIN_USERNAME / _PASSWORD | First dashboard administrator, created on first start (password generated) |
Optional
| Variable | Default | Description |
|---|---|---|
FRONTEND_URL | http://localhost:3000 | Public dashboard URL (see §07 for access from other machines) |
NEXT_PUBLIC_API_URL | http://localhost:4000 | Public API URL; read when the container starts |
LOGTO_PUBLIC_URL / LOGTO_ADMIN_PUBLIC_URL | :3301 / :3302 | Public sign-in URL (also the token issuer) and user-management console |
SIGNING_PRIVATE_KEY / SIGNING_PUBLIC_KEY | unset | RSA pair that seals audit checkpoints and signs compliance exports; without it both are unsigned. Generation steps are in .env.example. |
SIGNING_REQUIRED | false | true = refuse to start without working signing keys |
LICENSE_AIRGAPPED | false | true = validate the license offline only, no license-server calls (§10) |
LICENSE_SERVER_URL | https://license.execlave.com/api/v1 | License server for validation and the heartbeat |
LOCAL_LLM_URL | unset | Local LLM for semantic classification and policy generation (see below); unset = heuristics |
EXECLAVE_VERSION | main | Image tag; pin a commit SHA tag for a reproducible install |
*_BIND | 127.0.0.1 | Which interface each service listens on (the dashboard: all) |
Local LLM (optional)
The bundled Ollama service starts only with the llm profile and needs about 8 GB of additional RAM. Set LOCAL_LLM_URL=http://llm:11434 in .env, start it, and pull the two models once:
docker compose --profile llm up -ddocker compose exec llm ollama pull mistral:7b-instruct-v0.3-q4_K_Mdocker compose exec llm ollama pull qwen2.5:14b-instruct-q4_K_MKeep --profile llm on later pull and up commands. To use an Ollama server you already run instead, point LOCAL_LLM_URL at it, pull the same models there, and skip the profile.
Sign-in and users
- Sign-in is handled by the bundled Logto service. On every start, the one-shot
logto-initjob makes sure the dashboard and API applications and the administrator exist, and passes their IDs to the other services. - Public registration is closed. Add users in the Logto admin console at
LOGTO_ADMIN_PUBLIC_URL(defaulthttp://localhost:3302, reachable from the host only). At first sign-in each user gets their own organization, as its owner. - Changing
EXECLAVE_ADMIN_PASSWORDafter the first start does not change the existing user; change the password in the admin console.
Access from other machines
The browser talks to three services: the dashboard, the API and sign-in. Set FRONTEND_URL, NEXT_PUBLIC_API_URL and LOGTO_PUBLIC_URL to addresses the browser can reach, open BACKEND_BIND and LOGTO_BIND only behind a firewall, VPN or TLS reverse proxy, and run docker compose up -d again. Logto verifies its own tokens through its public URL from inside its container: with plain host:port URLs keep LOGTO_PORT / LOGTO_ADMIN_PORT equal to the ports in those URLs; behind a reverse proxy, the Logto container must be able to reach the proxy's hostname.
Database roles and tenant isolation
Organizations are isolated by PostgreSQL row-level security, which a superuser bypasses. The stack therefore uses three roles:
| Role | Used by | Privileges |
|---|---|---|
execlave | Migrations and role setup only | Superuser |
app_user | API, worker, processing | No superuser, no BYPASSRLS — row-level security applies |
app_bootstrap | Sign-in and API-key lookups before the organization is known | BYPASSRLS, minimal grants |
The API gives app_user and app_bootstrap their passwords from .env on every start, so changing one there and restarting rotates it. The API and worker refuse to start if their database connection can bypass row-level security.
License keys: how they work
- Startup validation — the API and worker verify the JWT signature with
LICENSE_SIGNING_PUBLIC_KEYand check its plan and expiry. An invalid license stops them from starting. - Instance registration — unless air-gapped, the deployment registers a fingerprint (a hash of
LICENSE_INSTANCE_ID) with the license server, which counts distinct fingerprints against the license. API and worker share it, so one deployment is one instance, through restarts and updates. The Helm chart derives it from the namespace and release name. - Offline tolerance — if the license server cannot be reached, the instance starts from the signed license alone, as long as it is valid.
- Heartbeat — once a day the worker sends the license key, fingerprint, hostname, version, and trace/agent/user counts. A license the server reports as revoked shuts the instance down. Air-gapped mode sends nothing.
- Grace period — after a license expires it keeps working for the grace period written into it (14 days by default) and the organizations are marked past due; after that the instance no longer starts until the license is renewed.
Updating, backups, and air-gapped mode
Updating
docker compose pulldocker compose up -dDatabase migrations run automatically when the API starts. Take a backup first. To pick up a newer docker-compose.yml, run the installer again in the same directory: it keeps your .env and adds settings that became required.
Backups
Back up both databases. Traces and audit records live in TimescaleDB hypertables, which a plain-SQL pg_dumpall does not restore: back up the roles separately and the Execlave database in pg_dump's custom format. The Logto database holds your users.
docker compose exec -T postgres pg_dumpall -U execlave --roles-only > execlave-roles.sqldocker compose exec -T postgres pg_dump -U execlave -Fc execlave > execlave.dumpdocker compose exec -T logto-db pg_dumpall -U logto > logto.sqlpg_dump warns about circular foreign-key constraints in the TimescaleDB catalog; that is expected. Keep .env with the backups: it holds the passwords the restored databases expect.
Restoring
Restore into empty databases (a new host, or after docker compose down -v, which deletes all data), with the same .env and the same EXECLAVE_VERSION. Start only the databases, restore, then start the rest:
docker compose up -d postgres logto-dbdocker compose exec -T postgres psql -U execlave -d postgres < execlave-roles.sqldocker compose exec -T postgres psql -U execlave -d execlave -c "SELECT timescaledb_pre_restore();"docker compose exec -T postgres pg_restore -U execlave -d execlave < execlave.dumpdocker compose exec -T postgres psql -U execlave -d execlave -c "SELECT timescaledb_post_restore();"docker compose exec -T logto-db psql -U logto -d postgres < logto.sqldocker compose up -dThe errors role "execlave" already exists, role "logto" already exists and database "logto" already exists are expected: the containers create those on first start. Any other error means the restore is incomplete.
Air-gapped mode
Set LICENSE_AIRGAPPED=true in .env. The license is then validated from its signature alone and the instance makes no calls to the license server — no registration, no heartbeat. Because nothing can renew it online, ask us for a license with a validity that matches your update cycle; email support@execlave.com.
Troubleshooting
- Compose names the variable. Set it in
.env— the installer generates everything except the two license values.
- Check
LICENSE_KEYandLICENSE_SIGNING_PUBLIC_KEY(one line, double quotes, literal\nbetween the PEM lines) docker compose logs backend | grep -i licenseshows the reason (e.g. expired beyond the grace period)
- Its database connection could bypass row-level security. Do not point
DATABASE_URLat theexeclavesuperuser; the compose file already usesapp_user.
docker compose logs logto-initshows the failing step- If you changed the public URLs, keep
LOGTO_PORT/LOGTO_ADMIN_PORTequal to their ports (§07), thendocker compose up -dagain
- The API logs "Signing keys not configured": set
SIGNING_PRIVATE_KEYandSIGNING_PUBLIC_KEY(§06)
- Your license plan caps the number of registered instance fingerprints
- Each deployment needs its own
LICENSE_INSTANCE_ID; a changed value counts as a new instance for 30 days - Contact support to release stale instances, or upgrade your plan
docker compose logs backendshows the failing migration. Restore from your backup and contact support.
Support
- Documentation: execlave.com/docs
- Email: support@execlave.com
- Request a license: /get-license