Nginx, Cloudflare, Caddy — Doing the Heavy Lifting
Behind a Reverse Proxy
In production, Express usually sits behind a reverse proxy. Set trust proxy correctly and you're 90% done.
What you'll learn
- Configure trust proxy
- Use real client IPs and protocol
- Coordinate gzip / TLS / static
Most production Express runs behind a reverse proxy — Nginx, Caddy, Cloudflare, or your cloud’s load balancer. The proxy handles:
- TLS termination (HTTPS)
- HTTP/2 and HTTP/3
- gzip / brotli compression
- Static file serving
- Caching
- DDoS protection
Express just speaks HTTP over localhost to the proxy.
trust proxy
When the proxy forwards a request, it adds headers:
X-Forwarded-For: 203.0.113.42
X-Forwarded-Proto: https
X-Forwarded-Host: api.example.com Without trust proxy, Express ignores these:
req.ipis the proxy’s IP (useless for rate limits)req.protocolishttp(cookies won’t getSecure)req.hostnameis wrong
Tell Express to trust the proxy:
app.set("trust proxy", 1); // 1 hop: direct from one trusted proxy Now req.ip is the real client, req.protocol === "https" works,
and req.secure === true when appropriate.
Trust Levels
trust proxy accepts:
false— don’t trust anything (default)true— trust everything (dangerous if exposed directly)- An integer (
1,2) — number of trusted proxies - A string of IPs / subnets
- A function
For most setups, 1 is right — you have one proxy in front of
Express.
A Sample Nginx Config
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Static files served directly
location /assets/ {
alias /var/www/assets/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Everything else goes to Node
location / {
proxy_pass http://localhost:3000;
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_set_header X-Forwarded-Host $host;
# WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
} SSE / WebSockets Behind Nginx
Two important tweaks:
location /events {
proxy_pass http://localhost:3000;
proxy_buffering off; # critical for SSE
proxy_set_header Connection "";
proxy_http_version 1.1;
} Without proxy_buffering off, Nginx buffers your SSE events and
your client sits silent.
Health Checks From the Proxy
The proxy will hit your app every few seconds. Make /healthz cheap:
app.get("/healthz", (req, res) => res.json({ ok: true })); Don’t run a DB query in /healthz — it gets hit constantly. Put DB
checks in /readyz instead, hit less often.
When No Proxy
In dev or on platforms (Render, Fly, Railway), the platform IS the
proxy. They set X-Forwarded-* headers. Set trust proxy as
above and let them do their job.