Okapi

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:

  • dbpostgres:18-alpine. On a fresh volume, an inline init script (mounted from the compose configs block) runs automatically and creates okapi_app, a non-superuser database role the app connects as. OKAPI_APP_DB_PASSWORD sets that role's password; POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB are the bootstrap superuser credentials used only by the db container 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 :8080 inside the container, and is published to the host on OKAPI_APP_PORT (default 8480).

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, default 8480.
  • Set OKAPI_BASE_URL to 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

  1. Create a .env file next to docker-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)
    EOF
    

    The 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 add OKAPI_BASE_URL (your real https://… URL) to the same file and override anything else from the table above there too.

    OKAPI_APP_DB_PASSWORD is read when the database volume is empty and the okapi_app role is created. Setting it later does not change an existing role's password — that needs an explicit ALTER ROLE.

  2. 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, set OKAPI_SETUP_TOKEN.

  3. Put the instance behind a TLS-terminating reverse proxy and set OKAPI_BASE_URL to the https:// URL — see Reverse proxy and TLS above.

  4. 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.Z format 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.