Reverse Proxy
Front the node target's long-running apps with a proxy that routes by host and path, terminates TLS, strips the docs prefix, and forwards the client IP.
On the node target a reverse proxy sits in front of your long-running apps, routes each request to the right port by host and path, terminates TLS, and forwards the client IP so the backend rate-limits correctly.
Ports
| App | Env | Default | When |
|---|---|---|---|
| Frontend | PORT | 3000 | Always |
| Backend | API_PORT | 3010 | separate architecture only |
| Docs | PORT | 3040 under pnpm dev and in the Docker image, else 3000 | When config.docs.enabled |
Both Next apps read the same PORT (Next's own convention), so on one host give each process its own - PORT=3000 for the frontend, PORT=3040 for the docs app. pnpm dev already pins the docs app with next dev -p 3040, and apps/docs/Dockerfile defaults PORT=3040 inside the image; a bare next start does neither.
Routing rules
/) to the frontend's PORT.separate, route the backend on API_PORT via either a subdomain (api.yourdomain.com, forwarding root) or a path (yourdomain.com/api/, forwarding /api). The backend derives its base path from API_URL: a path like …/api makes it serve under /api, a bare subdomain makes it serve at root. See Separate backend./docs to the docs app's PORT and strip the /docs prefix before forwarding.Stripping the /docs prefix
The docs app serves its pages at root - the public prefix comes from the proxy, not the app - so a proxy that forwards /docs intact 404s every page.
| Proxy | How to strip |
|---|---|
| Nginx | Trailing slash on both location /docs/ and proxy_pass …:3040/ |
| Traefik, Dokploy, managed | Add a strip-path rule for /docs |
Keep config.docs.url (default /docs) pointed at the public docs URL so generated links resolve. Skip this entirely when config.docs.enabled is false - the docs app is not deployed.
TLS and client IP
- Terminate TLS at the proxy; the apps speak plain HTTP behind it.
- Forward
X-Forwarded-For- the backend rate-limits by the client IP and stamps it on audit entries, and without it every request carries the proxy's own address. - Leave
TRUSTED_PROXYunset when the proxy reaches the app over loopback or a private network: the backend reads that as proof of one forwarded hop. SetTRUSTED_PROXY=1when the proxy reaches it over a public address, andcf-connecting-ipwhen a CDN fronts the proxy. See Client IP.
Idle connections
Your proxy pools connections to the apps and reuses them. If an app closes one first, the next request the proxy sends over it is reset, and the visitor gets a 502 that appears in no app log - nothing arrived to be logged.
| App | Where the knob lives | Default | Rule |
|---|---|---|---|
Web (next start) | --keepAliveTimeout flag in apps/web/package.json's start script | 70000 | Keep it above your proxy's idle timeout |
| Separate backend | KEEP_ALIVE_TIMEOUT_MS env var | 70000 | Same rule |
The shipped defaults clear the common proxies. Raise them if yours holds idle connections longer.
| Proxy | Idle timeout |
|---|---|
| Nginx, AWS ALB | 60s |
| Heroku router | 55s |
| Traefik | 180s - raise both knobs past it |
Applies to both long-running apps: the frontend passes it to next start, and the separate backend sets it on its own server. Node's own default is 5s, which is below every proxy above - so never unset it to "use the default".
Example: Nginx
Routing all three apps and stripping the docs prefix:
server {
listen 443 ssl;
server_name yourdomain.com;
# TLS termination
ssl_certificate /etc/ssl/certs/yourdomain.crt;
ssl_certificate_key /etc/ssl/private/yourdomain.key;
# Frontend (root)
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;
}
# Docs - trailing slashes strip the /docs prefix
location /docs/ {
proxy_pass http://127.0.0.1:3040/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# Backend (separate only) - no trailing slash keeps the /api prefix,
# so set API_URL to https://yourdomain.com/api
location /api/ {
proxy_pass http://127.0.0.1:3010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}