You put n8n behind a reverse proxy, open https://n8n.example.com, and two things go wrong:
- A red "Connection lost" badge sits in the editor. Hovering it says: "You have a connection issue or the server is down. n8n should reconnect automatically once the issue is resolved." It never reconnects. Executions don't update live.
- The Webhook node shows URLs like
http://localhost:5678/webhook/..., orhttps://n8n.example.com:5678/webhook/..., that nobody outside the server can call.
They're separate problems with separate fixes. The first is the proxy. The second is n8n's environment variables. A third, quieter problem (n8n not trusting the proxy's headers) usually shows up in the logs at the same time. Each one was reproduced on a test box, using a test hostname over plain HTTP through the proxy, and checked every fix below.
Short version:
- Webhook URL shows
localhost:5678: setN8N_WEBHOOK_URL=https://n8n.example.com/(orWEBHOOK_URLbefore n8n 2.30.0) and recreate the container withdocker compose up -d. - "Connection lost" in the editor: the proxy has to pass WebSocket upgrades (
UpgradeandConnectionheaders over HTTP/1.1) and keep the originalHost. In Nginx Proxy Manager, that's the Websockets Support toggle. - Set
N8N_PROXY_HOPS=1for one proxy, so n8n trusts theX-Forwarded-*headers. - Test with a Webhook node. Both of its URLs should start with your domain, with no
:5678.
Why n8n breaks behind a reverse proxy
Symptoms and causes at a glance
| What you see | Cause | Fix |
|---|---|---|
Webhook URL starts with http://localhost:5678/
|
n8n doesn't know its public URL | Set N8N_WEBHOOK_URL |
Webhook URL is https://your-domain:5678/
|
N8N_HOST and N8N_PROTOCOL set, but no webhook URL |
Set N8N_WEBHOOK_URL |
"Connection lost" in the editor, browser console shows [WebSocketClient] Connection lost, code=...
|
Proxy isn't passing WebSocket upgrades, or is rewriting the Host header |
Fix the proxy |
Logs show ERR_ERL_UNEXPECTED_X_FORWARDED_FOR
|
n8n doesn't trust the proxy | Set N8N_PROXY_HOPS=1 |
| Login page warns about a secure cookie | You're on plain HTTP | See the FAQ |
A minimal n8n Docker Compose file
This is the setup the rest of the article fixes up. It follows the shape of the official Docker Compose docs: the image from n8n's own registry, a named volume for /home/node/.n8n, and the port bound to localhost only, so the proxy is the only way in.
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.43.4
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
Pin a version tag rather than latest, so an upgrade only happens when you choose. 2.43.4 was the current stable release at the time of testing. Check n8n's release notes for newer ones.
If your proxy also runs in Docker (Nginx Proxy Manager, Traefik, Caddy), put both containers on a shared network so the proxy can reach http://n8n:5678 by service name. You can then drop the ports: section entirely. The Nginx Proxy Manager guide shows the shared-network setup.
At idle, n8n used about 690 MiB of RAM on the test box (docker stats). n8n's docs don't list an official minimum.
Back up the n8n_data volume. Per n8n's docs, n8n generates a random encryption key on first launch, saves it in that folder, and uses it to encrypt your saved credentials. Lose the volume and you lose the key, and with it access to every credential. If you'd rather manage the key yourself, set N8N_ENCRYPTION_KEY before the first start.
n8n is one of the most popular self-hosted automation tools. On October 14, 2026, its GitHub repository had about 207,300 stars, and the n8nio/n8n image had been pulled more than 271 million times from Docker Hub. So the fixes below get searched a lot.
Fix 1: tell n8n its public URL
Set N8N_WEBHOOK_URL
With the file above, open any Webhook node and the URL is http://localhost:5678/webhook/.... n8n builds webhook URLs from what it knows about itself, and by default that's localhost, port 5678, plain HTTP. It can't learn your domain from the proxy for this.
This is the Webhook node on a default install in the lab:
The common half-fix is to set N8N_HOST and N8N_PROTOCOL:
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
On n8n 2.43.4 that produced https://n8n.example.com:5678/webhook/... in testing. The internal port leaks into the URL, because n8n doesn't know the proxy is listening on 443.
The full fix is to set the webhook base URL explicitly:
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- N8N_WEBHOOK_URL=https://n8n.example.com/
- N8N_PROXY_HOPS=1
Then recreate the container (a restart doesn't pick up new env vars):
docker compose up -d
Webhook URLs now read https://n8n.example.com/webhook/..., with no port.
In the lab, with n8n.lab.test standing in for the real domain, the Production URL looked like this:
N8N_WEBHOOK_URL or WEBHOOK_URL?
Most older guides and forum answers say WEBHOOK_URL. n8n's current docs use N8N_WEBHOOK_URL. The Docker Compose page says it's "available from n8n 2.30.0" and to use WEBHOOK_URL on earlier versions. The webhook URL page says WEBHOOK_URL "is deprecated from n8n 2.35.0". On 2.43.4, WEBHOOK_URL still works but prints this at startup:
WEBHOOK_URL -> Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks.
So: on 2.30.0 or later, use N8N_WEBHOOK_URL. On anything older, use WEBHOOK_URL. Don't set both.
If the editor itself lives at a different URL than the webhooks, there's also N8N_EDITOR_BASE_URL. For a normal single-domain setup you don't need it.
Fix 2: pass WebSockets and the Host header
Other apps with a live UI need the same thing from a proxy. Uptime Kuma and Open WebUI both break without WebSocket support.
"Connection lost" is about the editor's live connection to the server, called the push connection. The browser opens it at /rest/push. By default it's a WebSocket (N8N_PUSH_BACKEND defaults to websocket), so the proxy has to do two things:
- Pass the WebSocket upgrade. Forward the
UpgradeandConnectionheaders, over HTTP/1.1. - Keep the original Host. n8n compares the browser's
Originheader with the host the request was sent to. It checksForwarded, thenX-Forwarded-Host, thenHost. If they don't match, it rejects the connection.
Here's what each broken setup did in testing, so you can match it to your own:
| Proxy setup | Result on /rest/push
|
|---|---|
| Nginx Proxy Manager, Websockets Support off |
401 Unauthorized (the request reaches n8n as a plain GET, not an upgrade) |
nginx with only proxy_pass
|
HTTP 500 |
nginx with Upgrade/Connection headers but no Host header |
Connection opens, then closes with code 1008 and Invalid origin!
|
| Either proxy configured as below | Works |
For the third case, n8n's log spells it out:
Origin header does NOT match the expected origin. (Origin: "http://n8n.lab.test" -> "n8n.lab.test", Expected: "n8n:5678" -> "n8n:5678", Protocol: "http")
nginx sent Host: n8n:5678 (its upstream name) instead of the domain the browser used. n8n saw the mismatch and refused.
In the browser, that setup left the editor with the "Connection lost" badge in the top right corner:
Nginx Proxy Manager
Edit the proxy host and, on the Details tab, turn on Websockets Support. That's the whole fix. NPM already sends the original Host and the X-Forwarded-* headers. Use these settings:
- Domain Names:
n8n.example.com - Scheme:
http - Forward Hostname / IP:
n8n(the service name, if both are on the same Docker network) - Forward Port:
5678 - Websockets Support: on
- SSL tab: your certificate, with Force SSL on
After turning it on, the push connection through NPM stayed open and the badge went away.
Plain nginx
This is the server block that worked in testing. The map goes in the http {} context, for example at the top of a file in /etc/nginx/conf.d/:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name n8n.example.com;
# for HTTPS, use your usual "listen 443 ssl;" and ssl_certificate lines instead
location / {
proxy_pass http://n8n:5678; # or http://127.0.0.1:5678 if nginx runs on the host
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $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-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
}
proxy_buffering off keeps streamed responses flowing instead of nginx holding them back. The long proxy_read_timeout stops nginx from closing an idle push connection after its default 60 seconds.
Reload nginx (nginx -t && nginx -s reload), hard-refresh the editor, and the badge should be gone.
Through the working server block in the lab, the same workflow loaded with no badge:
If you reach n8n at something like https://n8n.example.com:8443, the browser's Origin includes :8443. If your proxy forwards only the hostname, n8n sees n8n.example.com vs n8n.example.com:8443 and rejects it with Invalid origin!. This reproduces through NPM. The simplest fix is to serve n8n on the standard port 443. If you can't, look at what the check compares. n8n's origin check (in its origin-validator code) compares the Origin's host and port with the forwarded host, so the Host / X-Forwarded-Host your proxy sends has to include the port too. In nginx, $http_host passes on the host exactly as the browser sent it, port included. $host drops the port.
Fix 3: trust the proxy, then test a webhook
Set N8N_PROXY_HOPS
Behind a proxy, log in once and check docker compose logs n8n. Without N8N_PROXY_HOPS, this appeared on every login through NPM:
ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). ... ERR_ERL_UNEXPECTED_X_FORWARDED_FOR
n8n's web server (Express) doesn't trust X-Forwarded-* headers unless told how many proxies are in front of it. N8N_PROXY_HOPS defaults to 0. Set it to the number of reverse proxies between the internet and n8n. For one Nginx Proxy Manager or one nginx, that's 1:
- N8N_PROXY_HOPS=1
With that set, the warning went away. If you also have Cloudflare proxying in front of your own proxy, that's two hops.
Verify with a test webhook
- Create a workflow and add a Webhook trigger. Set HTTP Method to
POSTand give it a fixed Path, likedeploylark-test. - The node panel has two URLs, Test URL and Production URL. Both should start with
https://n8n.example.com/. If either showslocalhostor:5678, go back to Fix 1. - Test URL. Click Listen for test event (or Execute workflow), then from any machine:
curl -X POST https://n8n.example.com/webhook-test/deploylark-test -d 'hello=world'
The data should show up in the editor right away. If it only shows up after a refresh, the push connection is still broken (Fix 2).
- Production URL. Publish the workflow, then:
curl -X POST https://n8n.example.com/webhook/deploylark-test -d 'hello=world'
With the default "Respond: Immediately", the response was {"message":"Workflow was started"}. Production runs don't appear on the canvas. Check the workflow's Executions tab.
The two 404s you'll hit while testing mean different things:
- Test URL, not listening:
The requested webhook "deploylark-test" is not registered.with a hint to click Execute workflow first. The test URL only works for one call after each click. - Production URL, workflow not published:
The requested webhook "POST deploylark-test" is not registered.The workflow must be published (the hint text still says "active" and "toggle", from older versions).
Both 404s came through the proxy with the right domain, so they mean the proxy is fine and n8n just isn't listening on that path.
FAQ
Webhooks and login
How do you change localhost:5678 in the webhook URL?
Set N8N_WEBHOOK_URL=https://n8n.example.com/ (or WEBHOOK_URL before n8n 2.30.0) and recreate the container with docker compose up -d. Setting only N8N_HOST and N8N_PROTOCOL gave https://n8n.example.com:5678/, with the internal port still in it. See Fix 1.
Why does the login page say the n8n server is configured to use a secure cookie?
The full message is "Your n8n server is configured to use a secure cookie, however you are either visiting this via an insecure URL, or using Safari." N8N_SECURE_COOKIE defaults to true, so the login cookie only works over HTTPS. Put a certificate on the proxy and use the https:// URL. Only set N8N_SECURE_COOKIE=false for a throwaway local test, never on an instance reachable from the internet.
Changed the env vars and nothing happened?
docker compose restart reuses the old container config. Run docker compose up -d so Compose recreates the container with the new environment. If you keep the values in a .env file, Compose only uses them to fill in ${...} placeholders in compose.yaml, so each variable still needs a line under environment:. Then check that it took effect:
docker compose exec n8n printenv | grep N8N_
Proxies and Cloudflare
My proxy can't do WebSockets at all. Is there another option?
Yes. Set N8N_PUSH_BACKEND=sse and n8n uses Server-Sent Events, which is ordinary HTTP streaming. This was tested through Nginx Proxy Manager with Websockets Support off. /rest/push returned 200 with Content-Type: text/event-stream and stayed open. On plain nginx, the server block above already has proxy_buffering off and a long proxy_read_timeout, both sensible for a long-lived stream. WebSockets is n8n's default, so only switch if you have to.
Why do you get "Connection lost" behind Cloudflare?
According to Cloudflare's docs, proxied WebSockets are supported on all plans, with a WebSockets toggle on the zone's Network page. Make sure it's on. Then check three things:
- If n8n's log shows
Invalid origin!, the host that reaches n8n doesn't match the browser'sOrigin. Fix theHostandX-Forwarded-Hostheaders as in Fix 2. - Cloudflare's proxy sits in front of yours, so count it in
N8N_PROXY_HOPS(see Fix 3). - Cloudflare documents redirect loops with SSL mode "Flexible" in front of an origin that forces HTTPS. The Nginx Proxy Manager guide covers that loop.
Originally published on DeployLark: Fix n8n 'Connection Lost' and Webhook URLs Behind a Proxy





Top comments (1)
Solid writeup — the env-var / websockets / proxy-hops separation is exactly how
these two symptoms should be split. Three additions from the field that pair
well with this checklist:
Two proxies = N8N_PROXY_HOPS=2. The article's
N8N_PROXY_HOPS=1assumes asingle hop. If you put Cloudflare (or a CDN) in front of Nginx Proxy Manager,
that's two trusted proxies — with
1, n8n still sees the CDN as the client IPand rate limiting/brute-force logic misbehaves. Easy to miss after following a
single-proxy guide.
Idle timeouts kill the live connection even when headers are perfect.
Cloudflare terminates WebSockets after ~100s of inactivity, which surfaces as
the "Connection lost" badge flickering even after you've fixed Upgrade
headers. If you can't control the edge timeout,
N8N_PUSH_BACKEND=sseridesone plain HTTP GET instead — but then disable response buffering for that
route (
X-Accel-Buffering: no), or npm/nginx will buffer the stream and thebadge comes back.
Long-running executions need bigger proxy timeouts too.
proxy_read_timeout 60s(NPM's effective default) drops responses fromwebhook workflows that call slow APIs or sit on a Wait node. Raise it on the
n8n proxy host, or answer webhooks immediately and process async.
And a small footnote that saves people a re-deploy: keep the trailing slash in
N8N_WEBHOOK_URLexactly as the docs show, and recreate (not restart) thecontainer after changing any of these —
docker restartdoesn't re-read env.