Okapi

Quickstart

Requires Docker and Docker Compose. This gets you a working instance on your own machine; see Installation for a production deployment.

1. Start the stack

Download the Compose file, write the three secrets it requires, and bring 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

If you'd rather not run the shell, the self-host page generates the same three values in your browser and gives you the block to paste — they are produced locally and never sent to us.

That's the whole stack — two services, db (Postgres) and app (the Okapi binary), pinned to the current published image. You get a 14-day trial with no license key.

Compose will refuse to start without those three values. That is deliberate: the alternative is booting on a password printed in a file that everybody downloads, which looks like it worked and isn't. Skip the .env and you get a message, not a running instance:

error while interpolating services.db.environment.POSTGRES_PASSWORD:
required variable POSTGRES_PASSWORD is missing a value:
generate one at https://useokapi.app/self-host (openssl rand -hex 32)

Compose stops at the first missing variable it happens to reach, and which one that is varies between runs — so set all three at once rather than fixing them one error at a time.

Here is the file you just downloaded, in full — this page renders the same artifact the download serves, so the two cannot drift apart:

# Okapi — self-host quickstart
# ─────────────────────────────────────────────────────────────────────────────
# One Go binary plus Postgres. This file runs a complete Okapi instance: the
# app, its database, persistent storage, and the database role it needs on the
# first boot.
#
#   1. Download   curl -O https://useokapi.app/docker-compose.yml
#   2. Secrets    write a .env next to this file (see below)
#   3. Start      docker compose up -d
#   4. Set up     docker compose logs app   # prints the one-time /setup?token=… URL
#                 # (or: docker compose exec app /okapi setup-url)
#
# Step 2 is not optional. Compose refuses to start until these three exist,
# rather than booting on a password published in the file everybody downloads.
# Generate them in your browser at https://useokapi.app/self-host — the values
# are produced locally and never sent to us — or in your own shell:
#
#   POSTGRES_PASSWORD=$(openssl rand -hex 32)       # the database superuser
#   OKAPI_APP_DB_PASSWORD=$(openssl rand -hex 32)   # the app's database role
#   OKAPI_SECRET_KEY=$(openssl rand -base64 32)     # signs sessions and links
#
# Compose reads that .env automatically. Then set the URL you serve on (★
# below) and terminate TLS at a reverse proxy in front of the app:
#
#   OKAPI_BASE_URL=https://errors.example.com
#
# Everything else has a working default, and you get a 14-day trial with no
# license key.
#
# Full guide: https://useokapi.app/docs/installation
# ─────────────────────────────────────────────────────────────────────────────

services:
  db:
    image: postgres:18-alpine
    restart: unless-stopped
    environment:
      # Bootstrap superuser — used only by the db container to initialize the
      # cluster. The app never connects as this role, but a superuser bypasses
      # row-level security entirely, so it is the worst one to leave guessable.
      POSTGRES_USER: ${POSTGRES_USER:-okapi}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?generate one at https://useokapi.app/self-host (openssl rand -hex 32)}
      POSTGRES_DB: ${POSTGRES_DB:-okapi}
      # Password for okapi_app — the NON-SUPERUSER role the app connects as so
      # Postgres row-level tenant isolation binds (superusers bypass it). The
      # role is created on the first boot by the init script mounted below.
      OKAPI_APP_DB_PASSWORD: ${OKAPI_APP_DB_PASSWORD:?generate one at https://useokapi.app/self-host (openssl rand -hex 32)}
    volumes:
      # postgres:18 keeps data in a version-specific subdirectory — mount the
      # parent, not .../data. Your projects, issues, and events live here; back
      # this volume up before every upgrade.
      - okapi-db:/var/lib/postgresql
    configs:
      - source: okapi_app_role
        target: /docker-entrypoint-initdb.d/01-create-application-role.sh
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-okapi} -d ${POSTGRES_DB:-okapi}"]
      interval: 5s
      timeout: 5s
      retries: 10

  app:
    image: ghcr.io/useokapi/okapi:0.8.0
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      # The app connects as okapi_app (non-superuser). Password and database
      # name resolve from the same values the db service uses above.
      OKAPI_DATABASE_URL: postgres://okapi_app:${OKAPI_APP_DB_PASSWORD:?generate one at https://useokapi.app/self-host (openssl rand -hex 32)}@db:5432/${POSTGRES_DB:-okapi}?sslmode=disable
      # ★ Public URL browsers use. https:// also sets the Secure cookie flag.
      # This is also the trusted origin for dashboard requests, so it has to
      # match the URL in the address bar exactly — a mismatch rejects every
      # POST, starting with setup. The default tracks OKAPI_APP_PORT below so
      # changing the published port alone stays consistent.
      OKAPI_BASE_URL: ${OKAPI_BASE_URL:-http://localhost:${OKAPI_APP_PORT:-8480}}
      # Signs sessions and single-use links.
      OKAPI_SECRET_KEY: ${OKAPI_SECRET_KEY:?generate one at https://useokapi.app/self-host (openssl rand -base64 32)}
      # Optional: pin a fixed first-run setup token instead of the generated one.
      OKAPI_SETUP_TOKEN: ${OKAPI_SETUP_TOKEN:-}
      # Retention windows, in days. 0 disables pruning.
      OKAPI_EVENT_RETENTION_DAYS: ${OKAPI_EVENT_RETENTION_DAYS:-90}
      OKAPI_LOG_RETENTION_DAYS: ${OKAPI_LOG_RETENTION_DAYS:-30}
      # Optional email (invites, verification, password reset, alerts). Without
      # SMTP, invites fall back to copyable links.
      OKAPI_SMTP_HOST: ${OKAPI_SMTP_HOST:-}
      OKAPI_SMTP_PORT: ${OKAPI_SMTP_PORT:-587}
      OKAPI_SMTP_USER: ${OKAPI_SMTP_USER:-}
      OKAPI_SMTP_PASSWORD: ${OKAPI_SMTP_PASSWORD:-}
      OKAPI_SMTP_FROM: ${OKAPI_SMTP_FROM:-okapi@localhost}
    ports:
      # host:container — the app listens on 8080 inside the container.
      # Overriding this also moves OKAPI_BASE_URL's default above, so a plain
      # port change needs nothing else. Setting OKAPI_BASE_URL yourself takes
      # over completely — then it must name the port you actually serve on.
      - "${OKAPI_APP_PORT:-8480}:8080"

volumes:
  okapi-db:

configs:
  # Runs once, on a fresh database volume (docker-entrypoint-initdb.d). Okapi
  # connects as a non-superuser so Postgres row-level security binds: superusers
  # and BYPASSRLS roles skip RLS entirely, and FORCE only constrains the table
  # owner. okapi_app owns what it migrates and stays subject to the
  # tenant-isolation policies. Values are filled in by compose from the db
  # service's environment above, so the role's password matches the app's DSN.
  okapi_app_role:
    content: |
      #!/bin/sh
      set -eu
      psql -v ON_ERROR_STOP=1 \
        --username "${POSTGRES_USER:-okapi}" \
        --dbname "${POSTGRES_DB:-okapi}" \
        -v app_password="${OKAPI_APP_DB_PASSWORD:?generate one at https://useokapi.app/self-host (openssl rand -hex 32)}" \
        -v dbname="${POSTGRES_DB:-okapi}" <<'EOSQL'
      CREATE ROLE okapi_app LOGIN PASSWORD :'app_password' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
      GRANT ALL ON DATABASE :"dbname" TO okapi_app;
      GRANT ALL ON SCHEMA public TO okapi_app;
      EOSQL

OKAPI_APP_DB_PASSWORD sets the password for okapi_app, the non-superuser Postgres role the app connects as — the compose file's inline init script creates it automatically on a fresh database volume. Row-level tenant isolation only holds for non-superuser connections, so this role matters even for a local trial.

Note that the role is created once, while the database volume is empty. Changing OKAPI_APP_DB_PASSWORD later does not change the role's password — you would need an explicit ALTER ROLE. Set it before the first docker compose up, not after.

The app is published on host port 8480 by default (OKAPI_APP_PORT overrides it); Postgres data persists in the okapi-db volume. OKAPI_BASE_URL defaults to http://localhost: plus whatever OKAPI_APP_PORT resolves to, so moving the port alone needs nothing else. Once you set OKAPI_BASE_URL yourself it stops tracking the port and has to name the URL browsers actually use — it is the trusted origin for dashboard requests, and a mismatch rejects every POST, starting with setup.

2. Complete first-run setup

On first boot Okapi generates a one-time setup token and logs a ready line with the full URL. Read it from the app logs:

docker compose logs app
Okapi is ready — complete setup … /setup?token=…

Open that URL. It's a WordPress-style setup screen: create your superadmin account and name your organization. If you lose the line, reprint it anytime:

docker compose exec app /okapi setup-url

3. Create a project and copy a DSN

  1. Create a project from the dashboard.
  2. Open the project's DSN Keys tab and copy the public key — the DSN Keys tab shows a ready-to-copy DSN in the exact form your SDK expects.

4. Send a first event

Point your app's existing Sentry SDK at the DSN — see SDK setup for every language. As a quick smoke test in Node.js:

const Sentry = require("@sentry/node");

Sentry.init({ dsn: "http://YOUR_PUBLIC_KEY@localhost:8480/1" });

Sentry.captureException(new Error("Hello from Okapi"));

Or in Python:

import sentry_sdk

sentry_sdk.init(dsn="http://YOUR_PUBLIC_KEY@localhost:8480/1")

sentry_sdk.capture_exception(Exception("Hello from Okapi"))

Run it — or just let your app throw — and watch the issue appear under Issues within a few seconds.

Next: Installation for a real deployment, or SDK setup for your specific language/framework.