The Docker Compose stack runs Govern (Keystone) on one host. Run every command from the root of the Keystone release; the root docker-compose.yml includes deploy/docker-compose.yml.
Requirements
- Docker 24.0 or later
- Docker Compose v2.20 or later (
docker compose version) - 2 GB of available memory and 10 GB of disk
Services
| Service | Port | Purpose |
|---|---|---|
postgres | 5432 | PostgreSQL 17 |
db-roles | Creates the three database roles on every start, then exits | |
migrate | Applies the migrations as keystone_owner, then exits | |
server | 8080 | The Govern server (ni-auth) |
dashboard | 3000 | Web dashboard and sign-in UI |
mailpit | 1025 (SMTP), 8025 (web) | Captures email for local development |
The server connects as keystone_app under row-level security and refuses to start as a superuser or a role that can bypass row-level security. The backend network is internal; only the published ports are reachable from the host.
Configure
Copy the example environment file:
cp .env.example .env
The stack refuses to start until these variables have values. Generate each one separately:
| Variable | Generate |
|---|---|
KEYSTONE_OWNER_PASSWORD, KEYSTONE_APP_PASSWORD, KEYSTONE_PLATFORM_PASSWORD | openssl rand -hex 24 (three different values) |
KMS_MASTER_KEY | openssl rand -hex 32 |
NI_AUTH_API_KEY_PEPPER, NI_AUTH_OAUTH2_PEPPER, NI_AUTH_EMAIL_TOKEN_PEPPER | openssl rand -base64 48 |
NI_AUTH_AUDIT_HMAC_KEY, NI_AUTH_AUDIT_SIGNING_KEY | openssl rand -base64 48 |
NI_OBSERVABILITY_SIGNING_KEY | openssl rand -base64 48 |
NI_AUTH_VAULT_ENCRYPTION_KEY, NI_AUTH_SECRETS_ENCRYPTION_KEY | openssl rand -base64 48 |
NI_AUTH_PASSPORT_KEY, NI_AUTH_APPROVAL_SECRET | openssl rand -base64 48 |
NI_AUTH_TOTP_ENCRYPTION_KEY, NI_AUTH_LDAP_ENCRYPTION_KEY, NI_AUTH_SOCIAL_ENCRYPTION_KEY, NI_AUTH_WEBHOOK_ENCRYPTION_KEY | openssl rand -base64 32 |
.env.example contains placeholder values for some of these. With NI_AUTH_DEPLOY_ENV unset (production), the server refuses placeholders, all-zero keys and anything containing change-me or replace-with. For local development only, NI_AUTH_DEPLOY_ENV=development accepts them with a warning at every start.
The OIDC signing key is mounted from deploy/dev/oidc-private.pem by default, a development key that ships with the repository. For any other deployment, generate a key and point NI_AUTH_OIDC_PRIVATE_KEY_HOST_FILE at it:
openssl genrsa -out oidc-private.pem 4096
Start
docker compose up --build -d
docker compose ps
curl http://localhost:8080/healthz
The server answers at http://localhost:8080, the dashboard at http://localhost:3000 and Mailpit at http://localhost:8025.
Run in production
Put TLS in front of the server
The server does not terminate TLS. A minimal Caddy configuration:
auth.example.com {
reverse_proxy server:8080
}
app.example.com {
reverse_proxy dashboard:3000
}
List the proxy's addresses in NI_AUTH_TRUSTED_PROXIES (CIDRs or IPs). Forwarding headers are believed only from the proxies you list, so a client cannot choose the address used for rate limits, lockouts, risk scoring and audit events. If a CDN sits in front of the proxies, also set NI_AUTH_TRUSTED_CDN_PROXIES and NI_AUTH_CLIENT_IP_HEADER.
Set the public URLs and email
NI_AUTH_PUBLIC_URL=https://auth.example.com
NI_AUTH_JWT_ISSUER=https://auth.example.com
NI_AUTH_SMTP_HOST=smtp.example.com
NI_AUTH_SMTP_PORT=587
NI_AUTH_SMTP_STARTTLS=true
NI_AUTH_SMTP_USERNAME=postmaster@example.com
NI_AUTH_SMTP_PASSWORD=<smtp-password>
NI_AUTH_SMTP_FROM=no-reply@example.com
POSTGRES_PASSWORD=<strong-random-password>
NI_AUTH_PUBLIC_URLmust be HTTPS in production, andNI_AUTH_JWT_ISSUERmust match it exactly.- If the API is served on its own host, set
NI_AUTH_API_PUBLIC_URL(for examplehttps://api.example.com). Model and MCP gateway addresses and other client-facing URLs then use it, while sign-in paths keepNI_AUTH_PUBLIC_URL. NI_AUTH_REGISTRATION_MODEisinviteby default (an invitation is required);openlets anyone create an account.- Never set
NI_AUTH_SMTP_INSECURE_SKIP_VERIFY=truein production.
Keep the database private
Change POSTGRES_PASSWORD, enable TLS to PostgreSQL, and do not publish port 5432 beyond the host. The internal worker API (port 9090) is published on loopback only; never bind it to a public address.
Store files
Exports and other files are written to the keystone_data volume (NI_OBJECT_STORE_DIR). To use an S3 or S3-compatible bucket instead, set NI_OBJECT_STORE_S3_BUCKET with its region, KMS key, prefix and endpoint variables.
Turn on optional features
| Feature | Variables |
|---|---|
| Model Access manifest signing | NI_AUTH_MODELS_SIGNING_KEY_PEM, NI_AUTH_MODELS_SIGNING_KEY_ID (Ed25519 PKCS#8); without them /api/v1/models/resolved answers 503 |
| Agent policy manifest signing | NI_AUTH_AGENTPOLICY_SIGNING_KEY_PEM, NI_AUTH_AGENTPOLICY_SIGNING_KEY_ID; without them the resolved policy answers 503 |
| MCP manifest signing | KEYSTONE_MCP_SIGNING_KEY_PEM (or _FILE), KEYSTONE_MCP_SIGNING_KEY_ID |
| DLP tokenization | NI_AUTH_DLP_TOKEN_ENCRYPTION_KEY (openssl rand -base64 32); without it tokenization answers 503 and stores nothing |
| Audit tiering | NI_AUTH_AUDIT_TIERING_*; see Retention and tiering |
| Prometheus scrapes | NI_AUTH_METRICS_TOKEN |
Generate an Ed25519 signing key with openssl genpkey -algorithm ed25519. Keep each key ID stable until you rotate.
Migrations
The migrate service applies migrations on every docker compose up, as the schema owner keystone_owner, and exits with code 0 when they are all applied. To run them by hand:
docker compose run --rm migrate
See Upgrades and migrations for the database roles involved and what to do when a migration fails.
Backup and restore
PostgreSQL data is in the postgres_data volume, and exports and artifacts in the keystone_data volume. Back up the database in the custom format with the bootstrap superuser:
docker compose exec -T postgres pg_dump -U ni_auth -d ni_auth \
--format custom --compress 9 > keystone-$(date +%Y%m%d-%H%M%S).pgdump
Restore into an empty database with the three roles in place, then let the role bootstrap and the migrations run: the steps are in Backup and restore. Keep the keys from .env with the backup's recovery plan; the database is not usable without them.
Upgrade
See Upgrades and migrations for the full procedure. In short:
- Read the release notes for steps the release requires before it starts.
- Back up the database.
- Pull or build the new version and restart:
docker compose up -d. Themigrateservice applies new migrations first.
Stop
docker compose down
docker compose down -v also deletes the PostgreSQL volume and every record in it.
Troubleshooting
More symptoms and fixes are in Troubleshooting.
| Symptom | Likely cause | Fix |
|---|---|---|
| Compose refuses to start and names a variable | A required variable is empty | Set it as shown under Configure |
server exits at start | A missing or placeholder key | Read docker compose logs server for the variable name |
| Sign-in fails with 500 | No OIDC private key | Set NI_AUTH_OIDC_PRIVATE_KEY_FILE or NI_AUTH_OIDC_PRIVATE_KEY_PEM |
| Token validation fails | Issuer mismatch | Make NI_AUTH_JWT_ISSUER match NI_AUTH_PUBLIC_URL exactly |
| Email is not delivered | SMTP not configured | Check the NI_AUTH_SMTP_* variables; use Mailpit locally |