n8n 502 Bad Gateway Behind a Proxy: Causes and Fixes
n8n showing 502 Bad Gateway, or webhooks not working behind Nginx, Traefik, Caddy or Cloudflare? Find the failing hop and fix it, with configs.
On this page
A hotel guest walks up to the front desk and asks for room 512.
The receptionist calls the room. No answer. Calls again. Nothing.
"I'm sorry," she says. "I can't reach him."
That's a 502 Bad Gateway.
The receptionist is your reverse proxy: Nginx, Traefik or Caddy. She's working fine. She's the one telling you there's a problem. The guest who won't pick up is n8n.
So the fix is never at the front desk. It's finding out why the room isn't answering.
Short on time? Run
docker psandcurl -sS http://127.0.0.1:5678/healthzon the server. If n8n isn't running or doesn't answer, the problem is n8n (logs, memory, database). If it answers there but not through your domain, the problem is the proxy config. If webhooks showlocalhost:5678, setWEBHOOK_URL.
Browser or webhook caller
Sees 502 Bad Gateway, or a webhook that never fires
Cloudflare (if proxied)
Its own 502 or 521 page: it can't reach your server at all
Reverse proxy: Nginx, Traefik, Caddy
Proxy is up, n8n isn't answering: wrong host, port or network
n8n on port 5678
Crashed, restarting, or out of memory mid-request
Postgres
n8n can't start without its database, so the proxy gets nothing
moarkid.com — Self-Hosting in Production
Step 1: Is n8n actually running?
docker ps -a --filter name=n8nIf the status says Restarting or Exited, the proxy is telling the truth: nobody's home. Look at why:
docker logs --tail 100 n8n
docker inspect -f 'OOMKilled={{.State.OOMKilled}} Restarts={{.RestartCount}}' n8nThe usual answers:
OOMKilled=true: n8n ran out of memory, often on a workflow that loads a big file or a large dataset into memory. Give the container more RAM, or process the data in batches.- Database errors on startup: n8n can't reach Postgres (wrong password, Postgres not started yet, disk full). n8n won't serve anything until its database is up. If the disk is full, see Docker no space left on device.
- Encryption key mismatch: the container was recreated with a different
N8N_ENCRYPTION_KEYthan the one that encrypted the stored credentials. Use the original key.
Step 2: Does n8n answer locally?
From the server itself, skipping the proxy:
curl -sS http://127.0.0.1:5678/healthz{"status":"ok"} means n8n is fine and the problem is between the proxy and n8n. If you use Postgres, /healthz/readiness also checks the database connection.
No answer here, but the container is running? Check the port mapping in docker-compose.yml. n8n listens on 5678 inside the container, and it has to be published or reachable on the network your proxy uses.
Step 3: Read the proxy's error log
The proxy writes down exactly why it gave up. For Nginx:
sudo tail -n 50 /var/log/nginx/error.log| Message in the log | What it means |
|---|---|
connect() failed (111: Connection refused) | Nothing listening at that host and port |
upstream prematurely closed connection | n8n accepted, then died mid-request (check OOM) |
no live upstreams | Every upstream failed its checks |
upstream timed out (110: Connection timed out) | n8n too slow to answer, usually a 504 instead |
The Docker networking trap
The most common cause of Connection refused: Nginx runs in its own container and the config says proxy_pass http://127.0.0.1:5678.
Inside a container, 127.0.0.1 means that container, not the server. Nginx is knocking on its own door. Put both containers on the same Docker network and use the service name:
proxy_pass http://n8n:5678;If Nginx runs directly on the server (not in Docker), 127.0.0.1:5678 is right, as long as the n8n container publishes the port (127.0.0.1:5678:5678 keeps it private to the server).
A working Nginx config for n8n
/etc/nginx/sites-available/n8n
server {
listen 443 ssl;
server_name n8n.example.com;
# ssl_certificate lines from certbot go here
location / {
proxy_pass http://127.0.0.1:5678;
# The editor uses a WebSocket; without these it shows "Connection lost"
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
}
}On Caddy, one line does the same job, WebSockets included:
Caddyfile
n8n.example.com {
reverse_proxy n8n:5678
}On Traefik, route the n8n service to port 5678 with a traefik.http.services.n8n.loadbalancer.server.port=5678 label. Traefik handles WebSockets without extra config.
The n8n side: four environment variables
Even with a perfect proxy, n8n needs to know it lives behind one:
.env
N8N_HOST=n8n.example.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.example.com/
N8N_PROXY_HOPS=1WEBHOOK_URLmakes n8n show your public address in webhook nodes. Without it, n8n builds URLs from its internal address, so the webhook URL you paste into Stripe or your form tool points tolocalhost:5678, which no one on the internet can reach.N8N_PROXY_HOPS=1tells n8n to trust one proxy'sX-Forwarded-Forheader. Without it, the logs can fill withERR_ERL_UNEXPECTED_X_FORWARDED_FORerrors. Use2if a cloud load balancer also sits in front of the proxy.
Restart n8n after changing them (docker compose up -d).
Webhooks still not firing?
Three things that aren't a proxy problem at all:
- Test URL vs production URL. The
/webhook-test/URL only works while you are clicking "Listen for test event" in the editor. Outside services need the/webhook/production URL. - The workflow must be published. In n8n 2.x the old "Active" toggle became Publish, and a production webhook only exists for a published workflow. Edits are saved as drafts: until you publish again, the webhook keeps running the last published version, which is why "I fixed it and nothing changed" is so common.
- Cloudflare error 524. If your domain is proxied through Cloudflare, it gives up after 100 seconds, and on Free, Pro and Business plans that can't be raised. For slow workflows, add a Respond to Webhook node early so the caller gets an answer right away, and let the slow part run after it.
The bottom line
A 502 is the front desk saying the room isn't answering. Don't argue with the receptionist.
Check the room first (docker ps, healthz), then the phone line between them (the proxy config and Docker network), then the address on the door (WEBHOOK_URL). The first one that fails is your cause.
P.S. Send me the last 20 lines of docker logs n8n and your proxy's error log (remove any secrets). I'll reply with what I think is wrong, in one message. Free.