Skip to content
← Back to home

§ 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.

§ 01

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.
§ 02

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.com for the daily heartbeat — or LICENSE_AIRGAPPED=true for no outbound calls (see §10)
§ 03

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 administrator

The 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).

§ 04

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.

§ 05

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)  |+-----------------------------+
§ 06

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

VariableDescription
LICENSE_KEYYour license key (exe_lic_…)
LICENSE_SIGNING_PUBLIC_KEYPublic key that verifies the license, provided with it. One line in double quotes, with literal \n between PEM lines.
LICENSE_INSTANCE_IDIdentifies 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_PASSWORDDatabase superuser — used only for migrations and role setup (generated)
APP_USER_DB_PASSWORDRuntime database role app_user (generated; see §08)
BOOTSTRAP_PASSWORDSign-in lookup role app_bootstrap (generated)
LOGTO_DB_PASSWORDThe sign-in service's own database (generated)
PROCESSING_INTERNAL_TOKENShared secret between the API and the processing service (generated)
REDIS_PASSWORDRedis password (generated)
EXECLAVE_ADMIN_USERNAME / _PASSWORDFirst dashboard administrator, created on first start (password generated)

Optional

VariableDefaultDescription
FRONTEND_URLhttp://localhost:3000Public dashboard URL (see §07 for access from other machines)
NEXT_PUBLIC_API_URLhttp://localhost:4000Public API URL; read when the container starts
LOGTO_PUBLIC_URL / LOGTO_ADMIN_PUBLIC_URL:3301 / :3302Public sign-in URL (also the token issuer) and user-management console
SIGNING_PRIVATE_KEY / SIGNING_PUBLIC_KEYunsetRSA pair that seals audit checkpoints and signs compliance exports; without it both are unsigned. Generation steps are in .env.example.
SIGNING_REQUIREDfalsetrue = refuse to start without working signing keys
LICENSE_AIRGAPPEDfalsetrue = validate the license offline only, no license-server calls (§10)
LICENSE_SERVER_URLhttps://license.execlave.com/api/v1License server for validation and the heartbeat
LOCAL_LLM_URLunsetLocal LLM for semantic classification and policy generation (see below); unset = heuristics
EXECLAVE_VERSIONmainImage tag; pin a commit SHA tag for a reproducible install
*_BIND127.0.0.1Which 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_M

Keep --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.

§ 07

Sign-in and users

  • Sign-in is handled by the bundled Logto service. On every start, the one-shot logto-init job 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 (default http://localhost:3302, reachable from the host only). At first sign-in each user gets their own organization, as its owner.
  • Changing EXECLAVE_ADMIN_PASSWORD after 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.

§ 08

Database roles and tenant isolation

Organizations are isolated by PostgreSQL row-level security, which a superuser bypasses. The stack therefore uses three roles:

RoleUsed byPrivileges
execlaveMigrations and role setup onlySuperuser
app_userAPI, worker, processingNo superuser, no BYPASSRLS — row-level security applies
app_bootstrapSign-in and API-key lookups before the organization is knownBYPASSRLS, 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.

§ 09

License keys: how they work

  1. Startup validation — the API and worker verify the JWT signature with LICENSE_SIGNING_PUBLIC_KEY and check its plan and expiry. An invalid license stops them from starting.
  2. 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.
  3. Offline tolerance — if the license server cannot be reached, the instance starts from the signed license alone, as long as it is valid.
  4. 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.
  5. 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.
What we see: license key, instance fingerprint, hostname, version, and trace/agent/user counts.
What we don't see: prompts, trace content, agent configurations, policies, user accounts, or any other customer data.
§ 10

Updating, backups, and air-gapped mode

Updating

docker compose pulldocker compose up -d

Database 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.sql

pg_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 -d

The 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.

§ 11

Troubleshooting

A required variable is missing when running docker compose
  • Compose names the variable. Set it in .env — the installer generates everything except the two license values.
The API exits: "LICENSE VALIDATION FAILED"
  • Check LICENSE_KEY and LICENSE_SIGNING_PUBLIC_KEY (one line, double quotes, literal \n between the PEM lines)
  • docker compose logs backend | grep -i license shows the reason (e.g. expired beyond the grace period)
The API exits: "Runtime DB role BYPASSES row-level security"
  • Its database connection could bypass row-level security. Do not point DATABASE_URL at the execlave superuser; the compose file already uses app_user.
logto-init failed / sign-in does not work
  • docker compose logs logto-init shows the failing step
  • If you changed the public URLs, keep LOGTO_PORT / LOGTO_ADMIN_PORT equal to their ports (§07), then docker compose up -d again
Audit checkpoints are not sealed
  • The API logs "Signing keys not configured": set SIGNING_PRIVATE_KEY and SIGNING_PUBLIC_KEY (§06)
"Instance limit exceeded"
  • 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
Migration errors on first start
  • docker compose logs backend shows the failing migration. Restore from your backup and contact support.
§ 12

Support

Self-hosted deployment — Execlave Docs