jngkadocs
Operations / Reverse proxy & TLS

Reverse proxy & TLS

Jangka doesn't terminate TLS itself. In production, put it behind a reverse proxy that serves HTTPS and forwards requests to the app on port 3000.

Checklist

SettingWhy
PUBLIC_URL=https://…Must be the exact address people open. Session cookies are marked Secure when it starts with https://.
No response bufferingAssistant replies stream as Server-Sent Events. A buffering proxy delivers them all at once at the end.
Long read timeoutDrafting a section can stream for minutes.
TRUST_PROXY=trueLogin rate limits and the audit log record the client's IP instead of the proxy's.
App port bound to localhostOnly the proxy should reach the app, so nobody can bypass it or spoof X-Forwarded-For.

Prepare the compose file

Bind the app's port to the host's loopback interface and trust the proxy:

docker-compose.ymlyaml
  app:
    # …
    environment:
      DATABASE_URL: postgres://jangka:jangka@postgres:5432/jangka
      MASTER_KEY: ${MASTER_KEY:?set MASTER_KEY (openssl rand -base64 32)}
      PUBLIC_URL: ${PUBLIC_URL:-http://localhost:3000}
      TRUST_PROXY: "true"
    ports:
      - "127.0.0.1:3000:3000"
.envsh
PUBLIC_URL=https://jangka.example.com

Apply it with docker compose up -d.

Caddy

Caddy gets and renews a certificate automatically, sets X-Forwarded-For, and flushes event streams as they arrive. No extra settings are needed:

Caddyfile
jangka.example.com {
	reverse_proxy 127.0.0.1:3000
}

nginx

nginx buffers responses by default, so turn buffering off and raise the read timeout:

/etc/nginx/conf.d/jangka.confnginx
server {
  listen 443 ssl;
  server_name jangka.example.com;

  ssl_certificate     /etc/letsencrypt/live/jangka.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/jangka.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;          # SSE: send tokens as they arrive
    proxy_read_timeout 300s;      # long drafting runs
  }
}

server {
  listen 80;
  server_name jangka.example.com;
  return 301 https://$host$request_uri;
}

Check it

Open PUBLIC_URL, start a draft with the assistant, and watch the reply: it should appear word by word. If it arrives all at once after a pause, the proxy is still buffering.

The health endpoint confirms that the proxy reaches the app and the app reaches the database:

terminalsh
curl -fsS https://jangka.example.com/api/v1/health
# {"status":"ok"}