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.
npm install -g @openkodaai/kodakoda install --headlessLocalhost 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.
Caddyfile is usually enough: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.
KODA_ENV=production— blocksCONTROL_PLANE_AUTH_MODE=development,CONTROL_PLANE_AUTH_MODE=open, andALLOW_LOOPBACK_BOOTSTRAP=trueat boot. If any of those slip through, the process exits before serving traffic.ALLOW_LOOPBACK_BOOTSTRAP=false— the first-owner flow now requires the short-lived bootstrap code.- Bootstrap code via SSH. The code is written to
${STATE_ROOT_DIR}/control_plane/bootstrap.txtwith mode0600, printed once to the container log, and deleted after successful registration. - HTTPS everywhere. The operator session cookie
koda_operator_sessionships withSecure,HttpOnly, andSameSite=Strict. Without HTTPS the browser will refuse to send it. - Strict CSP on auth screens.
/login,/setup, and/forgot-passwordenforce a strict Content-Security-Policy. Don't patch in'unsafe-inline'to work around third-party scripts. - 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. - Recovery codes are single-use. Using any code invalidates all remaining ones; generate new codes from Settings › Security after a password reset.
- 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.
- Audit every auth event. Logins, failures, password changes, recovery-code uses, session revocations — every one emits a
security.*structured event viaemit_security(). CONTROL_PLANE_API_TOKEN— leave blank unless you specifically need a break-glass CLI credential. Rotate when you set it.
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.1bindings are the default for a reason. - Store secrets with restrictive permissions. The
.envand bootstrap files should be owned by the service user with0600-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:
python3 scripts/doctor.py \ --env-file .env \ --base-url http://127.0.0.1:8090 \ --dashboard-url http://127.0.0.1:3000On 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.
npx @openkodaai/koda@latest updateOr, if you installed globally:
koda updateNext 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.