Reverse proxy and TLS

Putting the application behind nginx, Caddy or Traefik.

Run the application behind a proxy that terminates TLS and forwards to port 4000. Set PHX_HOST to the public name and PHX_SCHEME=https so links and emails carry the right address. The proxy must pass WebSocket upgrades: LiveView pages open one at /live/websocket.

Caddy

tickets.example.com {
    reverse_proxy app:4000
}

Caddy obtains certificates and proxies WebSockets automatically.

nginx

server {
    listen 443 ssl http2;
    server_name tickets.example.com;
    # ssl_certificate and ssl_certificate_key ...

    location / {
        proxy_pass http://app:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 1m;
    }
}

X-Forwarded-For matters to public request forms, whose throttle counts requests per client. It is trusted only when the connection comes from a private address (a proxy on the same host or network), and then only its last entry, which is the one your proxy wrote; anything a client puts in the header itself is ignored. A proxy that reaches the application over a public address is counted as one client.

Attachments never pass through the application, so the proxy body limit can stay small. Browsers upload straight to the storage endpoint; if that is the bundled RustFS, proxy it on its own host name (for example files.example.com to rustfs:9000) with a large body limit, set STORAGE_PUBLIC_BASE_URL=https://files.example.com, and set RUSTFS_CORS_ALLOWED_ORIGINS to the application's own address (https://tickets.example.com): RustFS answers a browser from no origin it has not been told about.

AI agents

Members connect AI agents through /mcp, /api/v1 and /oauth/, and agents find the sign-in endpoints at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server (see AI agents). Pass all of these to the application. A proxy that answers /.well-known/ itself, for example for certificate challenges, must still forward those two paths. Agents send their token in the Authorization header, which the proxy must pass on unchanged. No proxy settings beyond the ones above are needed: every answer is a single JSON body, never a stream.

Health checks

GET /health returns {"status":"ok","version":"X.Y.Z"} when the application can reach its database, and 503 otherwise. The version is the running release, so a deploy script can wait for the new one to answer. Point the proxy's or orchestrator's health check at it.