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
- Create a project from the dashboard.
- 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.