Skip to content
Kodakodadocs
Deployment

VPS deployment

Single-node production with reverse proxy and TLS.

The single-node VPS deployment path is the supported production configuration. It uses the same compose topology as the local install and layers hardened defaults, a reverse proxy, TLS, and a production checklist on top.

Target environment

  • Linux host with Docker and Docker Compose.
  • A reverse proxy you control (Caddy, nginx, Traefik) or a managed tunnel (Tailscale, Cloudflare).
  • A persistent filesystem for Docker volumes — Postgres and SeaweedFS keep durable data there.
  • Enough resources for the stack: 2 CPU / 4 GB RAM is a comfortable floor.

Install

The same CLI handles VPS installs. --headless suppresses the browser launch the interactive path uses.

bash
npm install -g @openkodaai/koda
koda install --headless

Localhost bindings

The production compose overlay (docker-compose.prod.yml) binds web to 127.0.0.1:${WEB_PORT:-3000} and app to 127.0.0.1:${CONTROL_PLANE_PORT:-8090}. Nothing faces the public internet directly — a reverse proxy terminates TLS and fronts both services.

Reverse proxy model

The reverse proxy is responsible for TLS, hostname routing, and (if you use one) client-certificate auth. Koda only needs it to publish five paths:

  • /127.0.0.1:3000 — Koda web dashboard.
  • /control-plane/127.0.0.1:3000 — operator surface (same Next.js origin).
  • /api/control-plane/* 127.0.0.1:8090 — HTTP control-plane API.
  • /api/runtime/*127.0.0.1:8090 — runtime API.
  • /openapi/control-plane.json 127.0.0.1:8090 — OpenAPI contract.

/setup can be published if you want a compatibility redirect into the dashboard's first-run flow; otherwise it's optional.

Caddy as a one-liner
Caddy ships opinionated defaults that cover TLS (via Let's Encrypt) and security headers out of the box. A two-line Caddyfile is usually enough:
text
koda.example.com {
reverse_proxy 127.0.0.1:3000
reverse_proxy /api/* 127.0.0.1:8090
}

Production checklist

The ten items below are the production-readiness baseline. Koda refuses to boot in production when the first two are wrong.

  1. KODA_ENV=production — blocks CONTROL_PLANE_AUTH_MODE=development, CONTROL_PLANE_AUTH_MODE=open, and ALLOW_LOOPBACK_BOOTSTRAP=true at boot. If any of those slip through, the process exits before serving traffic.
  2. ALLOW_LOOPBACK_BOOTSTRAP=false — the first-owner flow now requires the short-lived bootstrap code.
  3. Bootstrap code via SSH. The code is written to ${STATE_ROOT_DIR}/control_plane/bootstrap.txt with mode 0600, printed once to the container log, and deleted after successful registration.
  4. HTTPS everywhere. The operator session cookie koda_operator_session ships with Secure, HttpOnly, and SameSite=Strict. Without HTTPS the browser will refuse to send it.
  5. Strict CSP on auth screens. /login, /setup, and /forgot-password enforce a strict Content-Security-Policy. Don't patch in 'unsafe-inline' to work around third-party scripts.
  6. Password policy. 12+ chars, 3 of 4 classes, top-500 blacklist, substring-of-identifier rejection. You can only override upward via CONTROL_PLANE_OPERATOR_PASSWORD_MIN_LENGTH.
  7. Recovery codes are single-use. Using any code invalidates all remaining ones; generate new codes from Settings › Security after a password reset.
  8. Account lockout + rate limits. 5 failed logins per 5 minutes per IP, 5 password resets per hour per IP, 3 regenerations per hour per user, responses floored at ~300 ms.
  9. Audit every auth event. Logins, failures, password changes, recovery-code uses, session revocations — every one emits a security.* structured event via emit_security().
  10. CONTROL_PLANE_API_TOKEN — leave blank unless you specifically need a break-glass CLI credential. Rotate when you set it.
Production refuses development modes
Setting KODA_ENV=production with CONTROL_PLANE_AUTH_MODE=development or open is a configuration error and the process refuses to start. Same with ALLOW_LOOPBACK_BOOTSTRAP=true. This is intentional — it makes it impossible to ship a debug-friendly config to production by accident.

Hardening baseline

Beyond the checklist, five habits keep a deployment healthy over time.

  • Keep the control plane on localhost unless it's fronted by a proxy. 127.0.0.1 bindings are the default for a reason.
  • Store secrets with restrictive permissions. The .env and bootstrap files should be owned by the service user with 0600-ish modes. They contain session secrets and DSN strings.
  • Never expose Postgres or SeaweedFS publicly. The compose network already keeps them internal — just don't punch holes through the firewall.
  • Use managed TLS at the proxy layer. Let the proxy do cert rotation and protocol negotiation.
  • Set and rotate WEB_OPERATOR_SESSION_SECRET. Stable across restarts, rotated on a schedule you control.

Verify the install

After a VPS install, run the doctor against the same URLs:

bash
python3 scripts/doctor.py \
--env-file .env \
--base-url http://127.0.0.1:8090 \
--dashboard-url http://127.0.0.1:3000

On an all-clean run, check the dashboard from the public URL and confirm the session cookie is marked Secure in DevTools.

Running under systemd

A systemd unit template ships at koda.service.example. Copy it, point it at your install directory, and enable it so the stack comes up after reboots.

Upgrades

Upgrades are CLI-driven. The doctor runs after the new version starts; if it's red, the CLI rolls back automatically.

bash
npx @openkodaai/koda@latest update

Or, if you installed globally:

bash
koda update

Next steps

  • Reverse proxy — detailed configs for Caddy, nginx, and Tailscale.
  • Security — the end-to-end hardening story and audit event taxonomy.
  • Monitoring — health checks, uptime probes, and where logs go.