Installation
This page covers a production self-host install. For a quick local trial, see Quickstart.
Download the Compose file — it's the same one the Quickstart uses — and write the three credentials it requires before bringing it up:
curl -O https://useokapi.app/docker-compose.yml
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 32)
OKAPI_APP_DB_PASSWORD=$(openssl rand -hex 32)
OKAPI_SECRET_KEY=$(openssl rand -base64 32)
EOF
docker compose up -d
Those three have no defaults — Compose refuses to start until they exist, rather than booting on a value published in the file everybody downloads. The self-host page generates them in your browser if you'd rather not run the shell. First-run setup covers the rest.
The app service is pinned to the current published image; the download always tracks the latest release. To deploy a specific version instead, edit the image: tag (see Updating).
Compose file anatomy
Okapi's docker-compose.yml defines two services:
db—postgres:18-alpine. On a fresh volume, an inline init script (mounted from the composeconfigsblock) runs automatically and createsokapi_app, a non-superuser database role the app connects as.OKAPI_APP_DB_PASSWORDsets that role's password;POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DBare the bootstrap superuser credentials used only by thedbcontainer itself for initialization — the app never connects as that role.app— the Okapi binary, pinned to a published image (ghcr.io/useokapi/okapi:<version>). It waits for the database to report healthy (depends_on: condition: service_healthy), listens on:8080inside the container, and is published to the host onOKAPI_APP_PORT(default8480).
Postgres data persists in the okapi-db named volume — back it up before every upgrade (see Backups).
The non-superuser database role requirement
Okapi's row-level security backstops every tenant-scoped query: every table carrying an organization_id enforces FORCE ROW LEVEL SECURITY, and a query with no organization bound sees zero rows. Superusers and roles with BYPASSRLS bypass this entirely — so OKAPI_DATABASE_URL must point at a non-superuser role.
If you're using the bundled compose stack, this is automatic: the inline init script creates okapi_app on a fresh database and the default OKAPI_DATABASE_URL already points at it. If you're pointing Okapi at an external Postgres instance, create a dedicated non-superuser role yourself (it must own the schema it migrates) and use that role's connection string — never a superuser DSN, even temporarily.
Environment variables
All configuration is via environment variables, all prefixed OKAPI_.
| Variable | Required | Default | Purpose |
|---|---|---|---|
OKAPI_DATABASE_URL |
yes | — | Postgres connection string. Must be a non-superuser role (see above). The bundled compose stack creates okapi_app automatically on a fresh database. |
OKAPI_BASE_URL |
yes | — | Public base URL; used for emailed links (invites, email verification, password reset), alert deep links, and as the trusted origin for dashboard (CSRF) requests. https://… also enables the Secure cookie flag. Must match the URL browsers use to reach Okapi. |
OKAPI_SECRET_KEY |
yes | — | Server-side HMAC key for signed links/tokens. Use a long random value, e.g. openssl rand -base64 32. |
OKAPI_LISTEN_ADDR |
no | :8080 |
Address the server listens on (inside the container). |
OKAPI_SETUP_TOKEN |
no | — | If set, first-run setup requires this token in the X-Okapi-Setup-Token header. Recommended in production; if unset, Okapi auto-generates one and logs the setup URL. |
OKAPI_EVENT_RETENTION_DAYS |
no | 90 |
Instance default for event retention; a per-organization entitlement or per-project override wins when set. 0 disables pruning entirely. |
OKAPI_LOG_RETENTION_DAYS |
no | 30 |
Instance default for log retention; a per-organization entitlement wins when set. 0 disables pruning entirely. |
OKAPI_INGEST_RATE_LIMIT |
no | 200 |
Instance default ingest rate (events/sec, token bucket per DSN key); a per-organization entitlement overrides it. |
OKAPI_QUOTA_OVERAGE_PERCENT |
no | 150 |
Soft-overage ceiling as a percent of an organization's daily quota — see Quotas & usage. |
OKAPI_INGEST_QUEUE_LIMIT |
no | 0 |
Shed ingest with a 429 when the pending queue backlog reaches this depth (0 disables). Spike protection against a flood or a stalled worker. |
OKAPI_DB_MAX_CONNS |
no | pgx default (max(4, NumCPU)) |
Max Postgres pool connections. Set a bound for small VPSes or when packing many instances onto one database. |
OKAPI_SMTP_HOST |
no | — | SMTP host; enables outgoing email (alert notifications, invite emails, email verification, password reset) when set. Without it, invites fall back to a copyable link and administrators can generate password-reset links from Instance settings. |
OKAPI_SMTP_PORT |
no | 587 |
SMTP port. |
OKAPI_SMTP_USER |
no | — | SMTP username (enables PLAIN auth when set). |
OKAPI_SMTP_PASSWORD |
no | — | SMTP password. |
OKAPI_SMTP_FROM |
no | okapi@localhost |
From address for outgoing email. |
OKAPI_SKIP_MIGRATE |
no | unset | If set, serve skips auto-migration (run okapi migrate yourself). This also bypasses the license migration gate below — managing migrations yourself is the same explicit consent as OKAPI_FORCE_MIGRATE. |
OKAPI_UPDATE_ENDPOINT |
no | https://api.useokapi.app |
Base URL Okapi polls for the latest version, and — when licensed — for license check-ins. |
OKAPI_DISABLE_TELEMETRY |
no | unset | Set to 1 to stop sending anonymous usage telemetry. The version check still runs. See Telemetry. |
OKAPI_ORGANIZATION_NAME |
no | Default |
Name of the default organization created by createadmin. The interactive first-run setup screen collects the organization name directly instead. |
OKAPI_MULTI_ORGANIZATION |
no | false |
Enables cloud multi-organization chrome (self-serve signup, organization creation, the organization switcher). Leave unset on self-host. |
OKAPI_FORCE_MIGRATE |
no | unset | Explicit consent to migrate past the license migration gate (see below) regardless of coverage. |
The createadmin command additionally reads OKAPI_ADMIN_EMAIL, OKAPI_ADMIN_PASSWORD (≥8 characters), and OKAPI_ADMIN_NAME to create a superadmin non-interactively.
Two more variables live at the compose level (read by the db container and the compose file itself, not by the Okapi binary): OKAPI_APP_PORT (host port for the app, default 8480) and OKAPI_APP_DB_PASSWORD (the okapi_app role's password — see above). In the bundled artifact OKAPI_BASE_URL defaults to http://localhost:${OKAPI_APP_PORT:-8480}, so changing the published port alone keeps the trusted origin in step. Setting OKAPI_BASE_URL explicitly overrides that entirely — it then has to carry the port you actually serve on, or every dashboard POST is rejected as cross-origin. POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB configure the bootstrap superuser the db container starts with; the app never connects as that role.
Reverse proxy and TLS
Okapi speaks plain HTTP and does not terminate TLS itself. Put a reverse proxy in front of it. On a bare VPS, Caddy is the shortest path to a real certificate — this whole Caddyfile is the configuration:
okapi.example.com {
reverse_proxy localhost:8480
}
Point the DNS record at the box, reload Caddy, and it obtains and renews a Let's Encrypt certificate on its own. Then set OKAPI_BASE_URL=https://okapi.example.com in your .env and restart the app.
Any reverse proxy works, and there are only two facts to carry into whichever one you already run:
- Forward HTTP to the app's published port on the host —
OKAPI_APP_PORT, default8480. - Set
OKAPI_BASE_URLto the URL browsers use, scheme included. It is the trusted origin for dashboard requests; if it disagrees with what the browser sent, every POST is rejected as cross-origin — starting with first-run setup.
That is the entire contract. Okapi needs no special headers, no WebSocket upgrade, and no path rewriting, so nginx, Traefik, HAProxy, or a cloud load balancer are all a matter of expressing those two facts in their own syntax.
First-run setup
-
Create a
.envfile next todocker-compose.yml(compose reads it automatically) carrying the three credentials the artifact requires. These have no default — compose refuses to start until they exist, rather than booting on a value published in a file everybody downloads:cat > .env <<EOF POSTGRES_PASSWORD=$(openssl rand -hex 32) OKAPI_APP_DB_PASSWORD=$(openssl rand -hex 32) OKAPI_SECRET_KEY=$(openssl rand -base64 32) EOFThe self-host page generates the same three values in your browser if you would rather not run the shell. Everything else in the file has a working default behind a
${VAR:-default}seam, so addOKAPI_BASE_URL(your realhttps://…URL) to the same file and override anything else from the table above there too.OKAPI_APP_DB_PASSWORDis read when the database volume is empty and theokapi_approle is created. Setting it later does not change an existing role's password — that needs an explicitALTER ROLE. -
Complete first-run setup: on first boot Okapi logs a ready line with the full setup URL (token included). Open it to create the owner account and your organization. Reprint it anytime with
docker compose exec app /okapi setup-url. To pin a fixed token instead of relying on auto-generation, setOKAPI_SETUP_TOKEN. -
Put the instance behind a TLS-terminating reverse proxy and set
OKAPI_BASE_URLto thehttps://URL — see Reverse proxy and TLS above. -
Back up the Postgres volume (
okapi-db) — it holds every project, issue, and event.
Updating
Okapi publishes versioned images (ghcr.io/useokapi/okapi:<version>). The downloaded docker-compose.yml already pins the app service to the current release. To move to a specific version — an upgrade, or a rollback — change the tag:
services:
app:
image: ghcr.io/useokapi/okapi:1.8.2 # example — use the version you're deploying
# ...same environment/ports as before
then docker compose up -d to pull and restart. Re-downloading the file always gives you the latest published pin.
okapi serve auto-migrates the schema on boot unless OKAPI_SKIP_MIGRATE is set. A licensed, self-hosted instance is protected by a migration gate: if the running build is newer than your license's coverage window (updatesUntil) and that upgrade has pending schema migrations, boot holds in a static limbo page instead of migrating — the schema stays untouched and the instance stops accepting new events until you act. The limbo page offers two paths:
- Renew — completing a checkout unlocks and resumes boot live, no restart needed.
- Roll back — redeploy the newest version your license covers. The exact compose image pin to use is shown on the page (the same
image: ghcr.io/useokapi/okapi:X.Y.Zformat above).
Every other state migrates and serves normally: a fresh database, a dev/CI build, an unlicensed (trial) instance, an upgrade within your coverage window, or an out-of-coverage upgrade with no pending migrations at all. OKAPI_FORCE_MIGRATE=1 migrates through the gate anyway — an explicit operator override. See Licensing for the full coverage model.
Always back up before upgrading — see below. Upgrading within coverage, or with no pending migrations, is never blocked, but a backup costs you nothing and a bad migration is not something you want to discover after the fact.
Backups
Back up the Postgres volume (okapi-db) — it holds all projects, issues, and events. A standard pg_dump/pg_basebackup against the db service (or your external Postgres instance, if you're using one) works with no Okapi-specific steps; there's no separate metadata store to snapshot. Take a backup immediately before every upgrade, and on whatever recurring schedule your data is worth.
Next: SDK setup to start sending events.