GenerateSaaS

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

AppEnvDefaultWhen
FrontendPORT3000Always
BackendAPI_PORT3010separate architecture only
DocsPORT3040 under pnpm dev and in the Docker image, else 3000When 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

Route the root domain (/) to the frontend's PORT.
On 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.
When docs are enabled, route /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.

ProxyHow to strip
NginxTrailing slash on both location /docs/ and proxy_pass …:3040/
Traefik, Dokploy, managedAdd 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_PROXY unset when the proxy reaches the app over loopback or a private network: the backend reads that as proof of one forwarded hop. Set TRUSTED_PROXY=1 when the proxy reaches it over a public address, and cf-connecting-ip when 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.

AppWhere the knob livesDefaultRule
Web (next start)--keepAliveTimeout flag in apps/web/package.json's start script70000Keep it above your proxy's idle timeout
Separate backendKEEP_ALIVE_TIMEOUT_MS env var70000Same rule

The shipped defaults clear the common proxies. Raise them if yours holds idle connections longer.

ProxyIdle timeout
Nginx, AWS ALB60s
Heroku router55s
Traefik180s - 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;
  }
}

On this page