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 reads the URL like libpq: the user part ends at the first @ or /, and %XX is decoded. Percent-encode every password character that is not a letter or digit, for example write an @ in the password as %40. |
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 | — | Required at boot; currently unused. 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 (requests/sec, token bucket per DSN key); a per-organization entitlement overrides it. Each request costs one token; each further group of 100 accepted log records costs one more. |
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 | 1000000 |
Shed ingest with a 429 when the pending queue backlog reaches this depth. A healthy instance stays far below the default. Set 0 explicitly to disable the limit. 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 a configured email transport, 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 truthy (1, true, yes, on, or enabled, case-insensitive), serve skips auto-migration (run okapi migrate yourself). 0, false, and no leave auto-migration on. 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 the OS and architecture with the version check. An unlicensed instance then sends only a bare version-check GET; the version check itself always runs, except under OKAPI_SAFE_MODE. 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. |
OKAPI_SAFE_MODE |
no | unset | Set to 1 to suppress outbound email, Slack, webhooks, phone-home, and billing calls. Read once at boot. For booting a restored backup only — see Safe mode. |
OKAPI_ALERT_RULE_MAX_PER_HOUR |
no | 180 |
Most alerts one rule may send in a rolling hour, across all issues. 0 disables the cap. See Alerts & notifications. |
OKAPI_EMAIL_MAX_PER_MINUTE |
no | 60 |
Instance-wide limit on outgoing email recipients per minute, a backstop against a send loop. 0 disables it. |
OKAPI_SLACK_CLIENT_ID |
no | — | Client ID of your own Slack app. With OKAPI_SLACK_CLIENT_SECRET, enables Add to Slack — see Notification channels. |
OKAPI_SLACK_CLIENT_SECRET |
no | — | Client secret of the same Slack app. If only one of the two is set, Okapi starts with Add to Slack off and shows administrators a notice. |
OKAPI_INGEST_HOST |
no | — | Separate public ingest hostname used in DSNs; unset uses the dashboard host. Set it before issuing DSNs. |
OKAPI_BASIC_AUTH_USER |
no | — | Dashboard Basic Auth username. The gate is enabled only when both Basic Auth credentials are set, and it covers the dashboard's web pages only: the API, ingest and /mcp are not gated. |
OKAPI_BASIC_AUTH_PASSWORD |
no | — | Dashboard Basic Auth password. Changing either credential invalidates remembered staging-access cookies. |
OKAPI_EMAIL_LOG_RETENTION_DAYS |
no | 90 |
Outbound email log retention in days. 0 disables pruning. |
OKAPI_UNVERIFIED_ACCOUNT_RETENTION_DAYS |
no | 30 |
Days before an unverified account that belongs to no organization is deleted. Instance administrators and deactivated accounts are never pruned. 0 disables pruning. |
OKAPI_TRUSTED_PROXY_COUNT |
no | 0 |
Actual number of forwarding proxies in front of Okapi. With X-Forwarded-For, it lets the per-IP limits on password login, signup, and the MCP OAuth registration and token endpoints see the real client. Too low makes all clients share one limit; too high lets a client choose its own. See Reverse proxy and TLS. |
OKAPI_TRUSTED_PROXY_NETWORKS |
no | Local networks | Applies to Okapi releases after 0.9.0. Earlier images ignore this variable. Comma-separated CIDRs or bare IPs of the nearest proxy, the one that connects directly to Okapi. A non-empty list replaces the local-network defaults. Empty keeps those defaults. An invalid entry, including a trailing comma, stops boot. |
OKAPI_SESSION_LIFETIME |
no | 720h |
Ordinary session lifetime; a positive Go duration. |
OKAPI_ADMINISTRATOR_SESSION_LIFETIME |
no | 168h |
Instance administrator session lifetime; a positive Go duration, capped by the ordinary session lifetime. |
OKAPI_MAX_INGEST_BODY_BYTES |
no | 8388608 (8 MiB) |
Wire-body cap, clamped to 256 MiB. Decoded stages allow at least 16 MiB or twice the wire cap, whichever is larger. |
OKAPI_MAX_INGEST_INFLIGHT_BYTES |
no | 536870912 (512 MiB) |
Memory budget for concurrent ingest buffers; requests are shed when the budget is exhausted. |
OKAPI_SOURCE_MAP_CACHE_BYTES |
no | 134217728 (128 MiB) |
Parsed source-map cache budget. |
OKAPI_SOURCE_MAP_CHUNK_QUOTA_BYTES |
no | 4294967296 (4 GiB) |
Most source-map upload chunk bytes one organization can keep staged. Chunks stay staged for one hour, so this is also the upload volume per organization per hour. A chunk over the quota gets 429 with Retry-After. Values below 128 MiB are raised to 128 MiB; 0, negative and invalid values use the default. |
OKAPI_TRACE_RETENTION_DAYS |
no | 14 |
Instance default for how long raw spans, trace segments and performance issue occurrences are kept; a per-organization entitlement wins when set. 0 disables pruning of this data. Performance issues themselves stay until their project is deleted. See Performance monitoring. |
OKAPI_APM_QUEUE_LIMIT |
no | 100000 |
Safety limit on pending performance items. When the backlog reaches it, ingest sheds only transactions and spans; errors and logs are not affected. Must be a positive integer; other values use the default. |
OKAPI_WORKER_CONCURRENCY |
no | 1 |
Number of ingest worker loops, an integer from 1 to 4. Any other value stops every okapi command that reads the configuration, serve included. Above 1, the database pool must allow at least N+2 connections (OKAPI_DB_MAX_CONNS, or pool_max_conns in the URL). More loops help least when the events share one project and one release, or when one alert rule fires a lot. |
OKAPI_MAX_LOG_RECORDS_PER_ENVELOPE |
no | 10000 |
Log records accepted per envelope, clamped to 200,000. Excess records are dropped with a log-category rate-limit signal. |
OKAPI_DB_MIN_CONNS |
no | 0 |
Minimum database pool size: Okapi keeps at least this many connections open, busy or idle. 0 sets no minimum. |
OKAPI_DB_MAX_CONN_LIFETIME |
no | 1h (pgx default) |
Maximum lifetime of a pooled database connection; a Go duration. |
OKAPI_DB_HEALTH_CHECK_PERIOD |
no | 1m (pgx default) |
Database pool health-check interval; a Go duration. |
OKAPI_DB_REQUEST_TIMEOUT |
no | 15s |
Database operation budget on covered request paths; includes connection acquisition and statements. 0 disables it. |
OKAPI_DB_BACKGROUND_TIMEOUT |
no | 2m |
Database operation budget on covered paths marked as background work. 0 disables it. |
OKAPI_SMTP_REPLY_TO |
no | — | Optional Reply-To address for outgoing email. |
OKAPI_SMTP_FORCE_TO |
no | — | Send all outgoing email to this safety address; the outbound email log still records the intended recipients. |
OKAPI_SMTP_SUBJECT_PREFIX |
no | — | Optional prefix for outgoing email subjects. |
OKAPI_SMTP_IMPLICIT_TLS |
no | false |
Set to 1 for implicit TLS (SMTPS) instead of STARTTLS. |
OKAPI_EMAIL_TRANSPORT |
no | smtp |
Outgoing email transport: smtp or cloudflare; any other value stops Okapi from starting. Both use OKAPI_SMTP_FROM as the sender address. With cloudflare, Okapi also refuses to start unless OKAPI_SMTP_FROM is set to a real address (not the okapi@localhost default). |
OKAPI_CLOUDFLARE_EMAIL_ACCOUNT_ID |
no | — | Cloudflare account ID; required with OKAPI_EMAIL_TRANSPORT=cloudflare. |
OKAPI_CLOUDFLARE_EMAIL_API_TOKEN |
no | — | Cloudflare Email Sending API token; required with OKAPI_EMAIL_TRANSPORT=cloudflare. |
Go runtime memory (GOMEMLIMIT, GOGC)
Okapi does not change the Go garbage collector settings, and the defaults suit most installs, including every machine with 1 GB of memory or less. The Go runtime reads GOGC and GOMEMLIMIT from the app container's environment. The compose file does not pass them; set them in an override file (see Setting variables the compose file does not list).
- Do not increase
GOGCon a machine with 1 GB of memory or less. - If you increase
GOGC, also give the container a memory limit and setGOMEMLIMITto about 80% of it (for examplemem_limit: 2gandGOMEMLIMIT: 1600MiB). Ingest can hold up toOKAPI_MAX_INGEST_INFLIGHT_BYTES(512 MiB by default) of request bodies, so leave room for it or lower that budget. - Never set
GOGC=offwithoutGOMEMLIMIT. The heap then grows until the kernel stops the process. GOMEMLIMITis a soft limit. It cannot prevent an out-of-memory stop when the live data is larger than the limit.
The createadmin command additionally reads OKAPI_ADMIN_EMAIL, OKAPI_ADMIN_PASSWORD (≥8 characters), and OKAPI_ADMIN_NAME to create a superadmin non-interactively.
The downloaded compose file passes only the variables it lists to the app. See Setting variables the compose file does not list for the others.
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.
Setting variables the compose file does not list
Compose gives the app container only the variables named in its environment: block. A line in .env for any other variable does nothing. The current compose file passes these, so you can set them in .env:
OKAPI_BASE_URL, OKAPI_TRUSTED_PROXY_COUNT, OKAPI_TRUSTED_PROXY_NETWORKS, OKAPI_SECRET_KEY, OKAPI_SETUP_TOKEN, OKAPI_EVENT_RETENTION_DAYS, OKAPI_LOG_RETENTION_DAYS, OKAPI_SMTP_HOST, OKAPI_SMTP_PORT, OKAPI_SMTP_USER, OKAPI_SMTP_PASSWORD, OKAPI_SMTP_FROM, OKAPI_DISABLE_TELEMETRY, OKAPI_FORCE_MIGRATE, OKAPI_SAFE_MODE, OKAPI_SLACK_CLIENT_ID, OKAPI_SLACK_CLIENT_SECRET, OKAPI_ALERT_RULE_MAX_PER_HOUR, OKAPI_EMAIL_MAX_PER_MINUTE.
For every other variable in the table above, write a docker-compose.override.yml next to docker-compose.yml:
services:
app:
environment:
OKAPI_INGEST_RATE_LIMIT: "500"
Then recreate the app so it reads the new environment:
docker compose up -d --force-recreate app
Compose merges docker-compose.override.yml automatically. If you run Compose with -f, or set the COMPOSE_FILE variable (in the shell or in .env), it stops doing that: name both files (-f docker-compose.yml -f docker-compose.override.yml, or both in COMPOSE_FILE) or the override is ignored.
Use the override file for any of the variables in the list as well if your docker-compose.yml was downloaded before they were added to it. To check, search the file for the variable name. Downloading the file again also adds them, but keep your own image: tag if you pinned one.
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 and OKAPI_TRUSTED_PROXY_COUNT=1 in your .env and recreate the app. The current compose file passes both variables. For an older compose file, set the proxy count in docker-compose.override.yml:
services:
app:
environment:
OKAPI_TRUSTED_PROXY_COUNT: "1"
Any reverse proxy works. Carry these settings 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. - Have the proxy append
X-Forwarded-For(Caddy does this by default), and setOKAPI_TRUSTED_PROXY_COUNTto the actual number of forwarding proxies —1for the Caddy example above. With an undeclared local proxy, the per-IP limits on password login, signup and the MCP OAuth endpoints are off and Okapi logs anERRORonce. Login keeps only its per-account failure limit, and the other endpoints keep their instance-wide limits. - Keep the app's published port closed to the internet so every request passes through the proxy; a client that reaches it directly can forge
X-Forwarded-For.
In Okapi releases after 0.9.0, forwarded addresses require a nearest proxy on a local network by default. For a public proxy that connects directly to Okapi, list its CIDRs or bare IPs in OKAPI_TRUSTED_PROXY_NETWORKS. Earlier images ignore this variable. Only the nearest proxy, the one that connects directly to Okapi, is checked. A non-empty list replaces the local-network defaults.
CDN → Caddy on the host → Okapi needs no OKAPI_TRUSTED_PROXY_NETWORKS setting because Caddy is local. Listing only the CDN excludes Caddy and switches the per-IP limits off.
No WebSocket upgrade or path rewriting is needed.
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..envcan set only the variables the compose file lists. For the others, see Setting variables the compose file does not list.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.
Before you replace your compose file with a new download, check .env for leftover OKAPI_FORCE_MIGRATE or OKAPI_SAFE_MODE lines. The new file passes both to the app, so a value that an older file ignored takes effect on the next start.
okapi serve auto-migrates the schema on boot unless OKAPI_SKIP_MIGRATE is truthy (an override-file variable — see above). 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.
Upgrading to 0.9
Read this before you move from 0.8 to 0.9. Back up the database first.
Dead-lettered payloads are deleted
A dead-lettered payload is an event or log batch that failed processing too many times. Earlier versions kept these payloads forever. In 0.9, about one minute after the new version starts, the maintenance pass starts to permanently delete dead-lettered payloads that are older than their retention: events after the event retention (OKAPI_EVENT_RETENTION_DAYS, default 90 days), logs after the log retention (OKAPI_LOG_RETENTION_DAYS, default 30 days). It deletes in small batches, so a large backlog takes several passes, one per minute. A per-organization or per-project retention wins over these defaults, as it does for stored events and logs.
To see what you have, run this query before you upgrade:
docker compose exec db psql -U okapi -d okapi \
-c "SELECT kind, count(*), min(received_at) FROM ingest_queue WHERE status = 'dead' GROUP BY kind;"
Run it as the database superuser (POSTGRES_USER, okapi unless you changed it; use your POSTGRES_DB if you changed that too). Do not run it as okapi_app: the app role owns the table, and row-level security shows it zero rows outside a request.
To keep a copy, export the rows to a file. The file holds the raw payloads, which can contain personal data such as user emails and IP addresses, so store it as carefully as the database and delete it when you no longer need it:
docker compose exec -T db psql -U okapi -d okapi \
-c "\copy (SELECT * FROM ingest_queue WHERE status = 'dead') TO STDOUT CSV HEADER" > dead-letters.csv
To keep the payloads in the database instead, write down your current OKAPI_EVENT_RETENTION_DAYS and OKAPI_LOG_RETENTION_DAYS values, then set both to 0 in .env before you upgrade. 0 stops the deletion of dead-lettered payloads of that kind, whatever the organization's retention is. While it is set, 0 also stops all other pruning that follows these settings: stored events, logs, affected-user records (which hold personal data such as emails and IP addresses) and uploaded source maps. The database grows during that time.
Then, when you want the payloads processed:
- Upgrade to a version that can process them. A payload that failed because of an Okapi bug can process after a release fixes that bug; the changelog names such fixes. A malformed payload never processes.
- As an instance administrator, open Instance settings → System and use Requeue dead letters. Each use puts back between 1 and 1000 payloads (100 by default), oldest first. Repeat it until the dead-letter count stops falling. Requeued payloads keep their original receive time, so a payload that this version still cannot process dead-letters again and comes first again. This version cannot process those payloads: export them as shown above if you want them, then go on to step 3.
- Set both variables back to the values you had before. If you added the lines only for this step, delete them; the compose file then uses its defaults, 90 and 30 days. Do not delete lines that held your own values: the defaults can be shorter, and the next pass would delete everything older than them.
- Recreate the app:
docker compose up -d --force-recreate app.
One new issue per recurring error
0.9 groups events with new rules (see Grouping). Existing issues keep their old grouping keys. The next event of each recurring error opens a new issue, and New issue occurs rules alert once for each of them. The per-rule limit of OKAPI_ALERT_RULE_MAX_PER_HOUR (default 180) still applies.
- Errors that you ignored or snoozed can come back as new issues. Ignore or snooze the new issue again.
- An old resolved issue does not reopen, because new events no longer reach it. If an old issue gets no new events, that does not prove the fix held.
- Assignees and notes stay on the old issue.
Snoozes without an end time become unresolved
An issue that is snoozed without an end time goes back to unresolved during the upgrade. The database now also rejects a snooze without an end time.
The first start can be slow
The upgrade builds new indexes on the events and logs tables. Okapi builds them before it starts to serve, so on a large database the dashboard and ingest are not available for some minutes. Do not stop or restart the app while it works; watch docker compose logs -f app until it serves again. If you run migrations yourself with OKAPI_SKIP_MIGRATE, let okapi migrate finish before you start the new version.
Check these settings
-
Database password. Okapi now reads
OKAPI_DATABASE_URLlike libpq. If you write the URL yourself, percent-encode every character in the password that is not a letter or digit: for example@as%40,/as%2Fand%as%25. The bundled compose file putsOKAPI_APP_DB_PASSWORDinto the URL as it is, so that password must contain letters and digits only. The generated hex passwords are safe. If yours contains other characters, change it before you upgrade, while the old version still runs:-
Generate a new password:
openssl rand -hex 32. -
Set it on the database role. Changing
.envalone does not do this, because the role is created only on an empty volume:docker compose exec db psql -U okapi -d okapi \ -c "ALTER ROLE okapi_app PASSWORD '<new hex password>'"okapiis the database superuser (POSTGRES_USER) and the database (POSTGRES_DB) unless you changed them; use your own values if you did. -
Put the same value in
.envasOKAPI_APP_DB_PASSWORD, and recreate the app:docker compose up -d --force-recreate app. -
Then upgrade.
-
-
Proxy count. Set
OKAPI_TRUSTED_PROXY_COUNT(1behind the Caddy example). 0.9 limits password login attempts per IP address. Behind an undeclared proxy, that limit is off and Okapi logs anERROR. -
Source-map uploads.
sentry-clinow uploads chunks to a URL built fromOKAPI_BASE_URL. The machine that runs the upload must be able to reach that URL. -
Tracing. Okapi now stores the transactions and spans that SDKs send with a trace sample rate above 0, which uses disk. Raw trace data is kept for 14 days by default (
OKAPI_TRACE_RETENTION_DAYS). Lower the SDK sample rate if you do not want this. See Performance monitoring. -
MCP connections. A connection approved before 0.9 cannot use personal alert rules. Sign out of it and sign in again (see MCP & agents).
-
Ingest requests. A request with an unknown
Content-Encodingnow gets415, and a transaction sent to the legacy/store/endpoint gets400. Current SDKs do neither.
Rolling back to 0.8
To roll back, set the image: tag back to 0.8.0 and run docker compose up -d. 0.8 starts on the upgraded database: the 0.9 migrations add tables, columns, indexes, CHECK and foreign-key constraints and a trigger on issue deletion, make one column nullable, and repair snooze data. 0.8 works with all of them, so nothing needs to be undone. Never run goose down migrations: they delete alert rules, alert history and performance data.
After a rollback:
- grouping returns to the 0.8 rules, so new events go to the old issues again, and the issues opened under 0.9 stop receiving events;
- alert evaluations that were still pending are not run, so those alerts are lost;
- view rules, digests and performance rules do nothing, and pending retries of view-rule deliveries fail;
- while any rule scoped to a view exists, the organization-wide alert rules overview in organization settings fails to load;
- SDK transactions and spans are dropped again;
- the snooze repair stays.
Before you roll back, let pending alert work finish, or accept that it is lost. This query, run as the database superuser, shows how much is left:
docker compose exec db psql -U okapi -d okapi -c "SELECT
(SELECT count(*) FROM alert_evaluations WHERE status = 'pending') AS evaluations,
(SELECT count(*) FROM notification_deliveries WHERE status = 'pending') AS deliveries;"
Channel deliveries can retry for up to 7 days, so the count can stay above zero for a long time while a channel is failing.
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.
Safe mode (booting a restored backup)
Sometimes you must boot Okapi against a copy of real data: to rehearse an upgrade on a restored backup, or to recover on a new machine. That copy holds real email addresses, real Slack webhook URLs, real webhook endpoints, and your license. If it sends anything, people get duplicate alerts and emails, and the copy contacts the license server as if it were your instance.
OKAPI_SAFE_MODE=1 stops that. With it on, Okapi suppresses outbound email, Slack, webhooks, the update and license check-in, and billing calls:
- Background sends (alerts, digests, the check-in) are skipped and logged as
SAFE_MODE: suppressed …. Alert deliveries are recorded with the statussuppressed_safe_mode. - Actions you start yourself fail visibly instead: license activation and checkout return an error, and Send test on a channel or rule returns a conflict.
- In-app notifications still work, because they do not leave the instance.
- Ingest keeps accepting events and migrations still run. Safe mode controls outbound calls only.
The flag is read once at boot. You cannot turn it on in a running instance, so it must be set before the app first starts against the restored data.
Do these steps in this order:
-
Isolate the machine. Block outbound traffic with a firewall: your SMTP host, Slack, your webhook endpoints, and
api.useokapi.app(or yourOKAPI_UPDATE_ENDPOINT). The machine must not be reachable at any hostname that an SDK sends events to; safe mode does not stop ingest. -
Write the override next to
docker-compose.yml, asdocker-compose.override.yml, before the app can start against the restored data:services: app: environment: OKAPI_SAFE_MODE: "1"Use the override file even if your compose file passes
OKAPI_SAFE_MODE. A value in the override wins over.env, and an older compose file does not pass the variable at all. Do not run Compose with-fon this machine, and do not setCOMPOSE_FILE(in the shell or in.env). Both disable the automatic merge ofdocker-compose.override.yml, and the app then boots with safe mode off. -
Start only the database:
docker compose up -d db. If the app is already running, stop it first withdocker compose stop app. -
Restore the backup into the
dbservice. -
Start the app:
docker compose up -d --force-recreate app. -
Verify safe mode is on before you expose anything. The boot log shows
SAFE_MODE enabled — outbound email, Slack, webhooks, phone-home, and billing calls are suppressed, and the dashboard shows a persistent banner: "Safe mode — outbound email, Slack, webhooks, phone-home, and billing calls are suppressed."
What you do next depends on why you restored.
A rehearsal, or a copy made while the original instance may still be live. When you are done, destroy the machine and its volumes. Never delete the override on such a copy to turn it into a normal instance: it holds production data and credentials, and without safe mode it sends as your instance while the original does the same.
A real replacement for a dead host. After you verify the restored data:
- Make sure the old host is retired: shut down and unable to start again with its data. Two instances that send as one cause duplicate alerts and license check-ins.
- Turn safe mode off: delete
docker-compose.override.yml(or remove itsOKAPI_SAFE_MODEline, and the wholeenvironment:block if that leaves it empty), and make sure.envdoes not setOKAPI_SAFE_MODE. Then remove the outbound firewall blocks you added when isolating the machine. - Recreate the app:
docker compose up -d --force-recreate app. The boot log no longer shows theSAFE_MODE enabledline. - Only now point the hostnames that browsers and SDKs use at the new machine. Doing it while safe mode is still on would record alerts for new issues as suppressed, so they would never reach email, Slack or webhooks.
Next: SDK setup to start sending events.