DEV Community

DeployLark
DeployLark

Posted on Originally published at deploylark.com

Fix n8n 'Connection Lost' and Webhook URLs Behind a Proxy

You put n8n behind a reverse proxy, open https://n8n.example.com, and two things go wrong:

  1. 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.
  2. The Webhook node shows URLs like http://localhost:5678/webhook/..., or https://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: 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.
  • "Connection lost" in the editor: the proxy has to pass WebSocket upgrades (Upgrade and Connection headers over HTTP/1.1) and keep the original Host. In Nginx Proxy Manager, that's the Websockets Support toggle.
  • Set N8N_PROXY_HOPS=1 for one proxy, so n8n trusts the X-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:
Enter fullscreen mode Exit fullscreen mode

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:

n8n 2.43.4 Webhook node settings on a default install, with the Test URL shown as http://localhost:5678/webhook-test/deploylark-test

The common half-fix is to set N8N_HOST and N8N_PROTOCOL:

      - N8N_HOST=n8n.example.com
      - N8N_PROTOCOL=https
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then recreate the container (a restart doesn't pick up new env vars):

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

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 node after setting N8N_WEBHOOK_URL, with the Production URL shown as https://n8n.lab.test/webhook/deploylark-test and no port number

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.
Enter fullscreen mode Exit fullscreen mode

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:

  1. Pass the WebSocket upgrade. Forward the Upgrade and Connection headers, over HTTP/1.1.
  2. Keep the original Host. n8n compares the browser's Origin header with the host the request was sent to. It checks Forwarded, then X-Forwarded-Host, then Host. If they don't match, it rejects the connection.

Diagram of the request path. The browser sends HTTPS pages and webhooks and the WebSocket at /rest/push to the reverse proxy (NPM or nginx), which forwards plain HTTP to n8n on n8n:5678. The WebSocket leg needs the Upgrade and Connection headers and the original Host header. Webhook URLs come from N8N_WEBHOOK_URL, not from the request. Missing upgrade headers or a rewritten Host make the editor show Connection lost.

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")
Enter fullscreen mode Exit fullscreen mode

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:

n8n workflow editor with a single Webhook node and a Connection lost warning in the top right corner, loaded through an nginx block that did not pass the Host header

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

The same n8n workflow editor loaded through the corrected nginx server block, with no Connection lost warning in the top bar

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Create a workflow and add a Webhook trigger. Set HTTP Method to POST and give it a fixed Path, like deploylark-test.
  2. The node panel has two URLs, Test URL and Production URL. Both should start with https://n8n.example.com/. If either shows localhost or :5678, go back to Fix 1.
  3. 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'
Enter fullscreen mode Exit fullscreen mode

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).

  1. Production URL. Publish the workflow, then:
   curl -X POST https://n8n.example.com/webhook/deploylark-test -d 'hello=world'
Enter fullscreen mode Exit fullscreen mode

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_
Enter fullscreen mode Exit fullscreen mode

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's Origin. Fix the Host and X-Forwarded-Host headers 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)

Collapse
 
trigger-workflow profile image
Trigger Workflow •

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:

  1. Two proxies = N8N_PROXY_HOPS=2. The article's N8N_PROXY_HOPS=1 assumes a
    single 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 IP
    and rate limiting/brute-force logic misbehaves. Easy to miss after following a
    single-proxy guide.

  2. 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=sse rides
    one plain HTTP GET instead — but then disable response buffering for that
    route (X-Accel-Buffering: no), or npm/nginx will buffer the stream and the
    badge comes back.

  3. Long-running executions need bigger proxy timeouts too.
    proxy_read_timeout 60s (NPM's effective default) drops responses from
    webhook 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_URL exactly as the docs show, and recreate (not restart) the
container after changing any of these — docker restart doesn't re-read env.