Skip to content
Kodakodadocs
Integrations

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 /setup on 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.

text
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.

text
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 redirect
server {
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.

  1. Install Tailscale on the Koda host and authenticate.
  2. Run tailscale serve http://127.0.0.1:3000 for the dashboard.
  3. Run tailscale serve --bg --https=8090 http://127.0.0.1:8090 if you need the API reachable from other tailnet peers.
  4. Optional: enable tailscale funnel to 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_session is flagged Secure — 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-Proto and X-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.
Cloudflare Tunnel
Cloudflare Tunnel works similarly to Tailscale Funnel: create a tunnel, point two hostnames at 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_session must show Secure, HttpOnly, and SameSite=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.