Reverse proxy
Publishing Koda through Caddy, nginx, or Tailscale.
Koda binds its HTTP surfaces to 127.0.0.1 in production. A reverse proxy fronts the stack, terminates TLS, and routes requests to the right port. This page shows working configurations for Caddy, nginx, and Tailscale.
What to publish
Only two backends, five paths. Everything else stays inside the compose network.
127.0.0.1:3000— Next.js dashboard. Owns/and/control-plane/.127.0.0.1:8090— control plane + runtime API. Owns/api/control-plane/*,/api/runtime/*, and the OpenAPI at/openapi/control-plane.json.- Optionally: publish
/setupon the dashboard if you want a compatibility redirect into the first-run flow.
Caddy
Caddy is the fastest path. It auto-provisions TLS through Let's Encrypt and ships sensible security headers by default.
koda.example.com { # Dashboard + operator surface reverse_proxy / 127.0.0.1:3000 reverse_proxy /control-plane/* 127.0.0.1:3000 # Control plane + runtime APIs reverse_proxy /api/control-plane/* 127.0.0.1:8090 reverse_proxy /api/runtime/* 127.0.0.1:8090 reverse_proxy /openapi/* 127.0.0.1:8090 # Optional: first-run compatibility reverse_proxy /setup 127.0.0.1:3000 # Hide server identity header -Server # Don't cache auth pages @auth path /login /setup /forgot-password header @auth Cache-Control "no-store, no-cache, must-revalidate"}nginx
Slightly more boilerplate but handles every custom case you might need. Assumes TLS is terminated by nginx — certificate paths need to match your setup.
server { listen 443 ssl http2; server_name koda.example.com; ssl_certificate /etc/letsencrypt/live/koda.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/koda.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # Strip server identity server_tokens off; more_clear_headers 'Server'; # Dashboard (Next.js) location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # Control plane + runtime APIs location ~ ^/(api/control-plane|api/runtime|openapi)/ { proxy_pass http://127.0.0.1:8090; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}# HTTP → HTTPS redirectserver { listen 80; server_name koda.example.com; return 301 https://$host$request_uri;}Tailscale (private access)
If you want operator-only access without a public hostname, Tailscale Funnel publishes a URL that only devices on your tailnet can reach.
- Install Tailscale on the Koda host and authenticate.
- Run
tailscale serve http://127.0.0.1:3000for the dashboard. - Run
tailscale serve --bg --https=8090 http://127.0.0.1:8090if you need the API reachable from other tailnet peers. - Optional: enable
tailscale funnelto publish the dashboard over the public internet (still Tailscale-authenticated).
Required response behaviour
The stack relies on a handful of transport properties that proxies must not strip.
- HTTPS everywhere. The session cookie
koda_operator_sessionis flaggedSecure— the browser will refuse to send it over plain HTTP. - WebSocket upgrade for the dashboard (Next.js uses upgraded connections for HMR in dev and for dashboard streaming in production).
X-Forwarded-ProtoandX-Forwarded-For— the control plane uses them for rate limiting by IP and for constructing canonical URLs.- Don't patch in
'unsafe-inline'Content-Security-Policy overrides. The auth pages enforce strict CSP deliberately.
127.0.0.1:3000 and 127.0.0.1:8090, and Cloudflare handles TLS and forwarding. Leave the default proxy enabled so the origin never hears from the public internet directly.Verify the setup
Four checks before going live:
curl -I https://koda.example.com/→ 200 OK, dashboard HTML.curl -I https://koda.example.com/api/control-plane/health→ JSON health payload.- Open the dashboard, sign in, check DevTools → Application → Cookies.
koda_operator_sessionmust showSecure,HttpOnly, andSameSite=Strict. - The auth pages (
/login,/setup,/forgot-password) send a strict CSP header. Confirm in DevTools → Network → Response Headers.
Next steps
- VPS deployment — the full production checklist the reverse proxy sits in front of.
- Security — the headers and cookie flags the proxy must preserve.