TLS & reverse proxy
The frontend-app container ships nginx that reverse-proxies /api
and /ws to the internal api:3001. You only need to put a
TLS-terminating reverse proxy in front of frontend-app:8080.
Two common layouts:
- Same-origin (recommended) — SPA and api share one hostname,
e.g.
https://scani.example.comfor the SPA and/apion the same origin. Simplest cookie handling.frontend-app’s nginx does the /api routing internally. - Split-origin — SPA at
app.example.com, api atapi.example.com. RequiresCOOKIE_DOMAIN=.example.comso the session cookie reaches both.
Same-origin with Caddy
Section titled “Same-origin with Caddy”scani.example.com { encode gzip reverse_proxy localhost:8080}That’s it. Caddy provisions and renews the TLS certificate
automatically via Let’s Encrypt. frontend-app handles /api
and /ws internally.
FRONTEND_PORT=8080FRONTEND_URL=https://scani.example.comBACKEND_URL=https://scani.example.com# COOKIE_DOMAIN unset — single-originSame-origin with nginx
Section titled “Same-origin with nginx”server { listen 443 ssl http2; server_name scani.example.com;
ssl_certificate /etc/letsencrypt/live/scani.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/scani.example.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:8080; 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;
# WebSocket proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }}
server { listen 80; server_name scani.example.com; return 301 https://$server_name$request_uri;}Split-origin
Section titled “Split-origin”When the SPA lives on app.example.com and the api on
api.example.com:
app.example.com { reverse_proxy localhost:8080}
api.example.com { reverse_proxy localhost:3001 { # Forward WebSocket upgrade headers transport http { versions 1.1 } }}Note: in this layout, expose api:3001 on the host (publish the
port in your compose override) and skip frontend-app’s
internal proxying.
FRONTEND_URL=https://app.example.comBACKEND_URL=https://api.example.comCOOKIE_DOMAIN=.example.comWhat frontend-app’s internal nginx does
Section titled “What frontend-app’s internal nginx does”API_UPSTREAM (default http://api:3001) is where /api and
/ws requests are reverse-proxied:
# In docker-compose.prod.yml, the frontend-app service:environment: API_UPSTREAM: ${API_UPSTREAM:-http://api:3001}If you point your external reverse proxy directly at the api
container (split-origin), you don’t need this — the frontend-app
container becomes a static-file server only.
WebSockets
Section titled “WebSockets”The api exposes a WebSocket endpoint at /ws (realtime SSE / pub-sub
for live dashboard updates). Make sure your reverse proxy forwards
the Upgrade and Connection headers — both Caddy and the nginx
snippet above already do.
Healthcheck endpoints
Section titled “Healthcheck endpoints”| Service | Endpoint | Used by |
|---|---|---|
api | /health | Compose healthcheck. |
worker | none | Has no HTTP surface; observability is via logs. |
data-provider | /health | Compose healthcheck. |
frontend-app | /healthz | Compose healthcheck (served by internal nginx). |
Your reverse proxy can probe /healthz on the frontend container if
you want it to fail open when the SPA is unavailable.