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.
What this is careful about
Section titled “What this is careful about”- 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.