Skip to content

A reverse proxy

On a VM the API binds to 127.0.0.1:4000 (API_HOST=127.0.0.1) and a proxy on the same box terminates TLS. TRUST_PROXY defaults to one hop in production, which is this proxy, so it must send X-Forwarded-For and X-Forwarded-Proto; the login rate limit and the Secure session cookie both read them.

Obtains and renews the certificate itself, and sets the forwarded headers by default.

shop.example.com {
encode gzip
reverse_proxy 127.0.0.1:4000
}
server {
listen 80;
server_name shop.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name shop.example.com;
ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem;
# Uploads are capped at MAX_UPLOAD_BYTES (20 MB) by Beluga; match it here or
# nginx answers 413 before Beluga sees the request.
client_max_body_size 21m;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
location / {
proxy_pass http://127.0.0.1:4000;
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;
proxy_read_timeout 60s;
}
}

certbot --nginx -d shop.example.com writes the certificate paths above.

  • One hop. If you put a CDN or load balancer in front of the proxy as well, set TRUST_PROXY=2, or to the CDN’s address ranges. Trusting more hops than exist lets a caller choose their own IP and defeat the login rate limit.
  • The Stripe webhook goes through the same proxy. Nothing special is needed; the raw body is preserved by a plain proxy_pass.
  • No caching layer in front of /api. /assets/ is safe to cache and already carries a 30-day header in production.