n8n Behind Reverse Proxy: X-Forwarded-For Trust Fix¶
The Problem¶
When running n8n behind a reverse proxy (Caddy, Nginx, Traefik), the logs fill with:
ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default).
This could indicate a misconfiguration which would prevent express-rate-limit from accurately identifying users.
This means n8n's Express rate limiter sees every request as coming from the proxy's internal IP, not the real client. Rate limiting and audit logging both break.
Why N8N_TRUST_PROXY=true Isn't Enough¶
Older n8n versions accepted N8N_TRUST_PROXY=true to trust proxy headers. Newer versions (1.x+) use Express's trust proxy setting, which requires knowing how many proxy hops to trust. Without N8N_PROXY_HOPS, Express ignores the X-Forwarded-For header entirely even if N8N_TRUST_PROXY=true is set.
The Fix¶
Add N8N_PROXY_HOPS=1 to your n8n environment:
Docker Compose¶
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
environment:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- N8N_TRUST_PROXY=true
- N8N_PROXY_HOPS=1 # <-- Add this
Set N8N_PROXY_HOPS to the number of reverse proxies between the client and n8n:
- 1 — single proxy (Caddy/Nginx directly in front of n8n)
- 2 — two proxies (e.g., Cloudflare → Caddy → n8n)
Recreate the Container¶
If you get a container name conflict:
Verifying the Fix¶
Check the logs after restart:
If the fix worked, there should be zero ValidationError lines. A clean startup looks like:
Key Notes¶
- Keep
N8N_TRUST_PROXY=truealongsideN8N_PROXY_HOPS— both are needed. - The
mount of type volume should not define bind optionwarning from Docker Compose when using:z(SELinux) volume labels is cosmetic and can be ignored. - If n8n reports "Last session crashed" after a
docker rm -frecreation, this is expected — the old container was force-killed, so n8n sees it as a crash. It recovers automatically.