
Your n8n editor opens fine at https://n8n.example.com, yet the Webhook node still shows http://localhost:5678/webhook/..., the service you connect it to gets a 404, or the editor keeps showing "Connection lost". The proxy works, but n8n doesn't know its public address, or a header it needs gets lost. Below are the fixes in order, for n8n in Docker behind Nginx or Caddy on Ubuntu 24.04 or Debian 12, with curl tests that show which part fails.
Quick answer: n8n builds webhook URLs from its own settings, not from the request, and the defaults (http, localhost, port 5678) produce http://localhost:5678/. Set N8N_WEBHOOK_URL=https://n8n.example.com/ and N8N_PROXY_HOPS=1, then recreate the container with docker compose up -d (restart keeps the old environment). Versions before 2.30.0 only read the older WEBHOOK_URL, which newer ones accept with a deprecation warning. Have Nginx send Host, the three X-Forwarded headers and the WebSocket upgrade headers; Caddy does that by default. Then test with curl: /webhook-test/ URLs work for 120 seconds after Listen for test event, /webhook/ URLs only once the workflow is published.
Why n8n Shows localhost:5678
n8n's reverse proxy page explains it: n8n creates the webhook URL from N8N_PROTOCOL, N8N_HOST and N8N_PORT, and behind a proxy "that won't work", because n8n runs on port 5678 while the proxy serves port 443. Its URL service takes the first of these that is set:
N8N_WEBHOOK_URLWEBHOOK_URL, the deprecated old nameprotocol://host:port/, leaving out the port only for http on 80 or https on 443
So the defaults give http://localhost:5678/, and N8N_HOST=n8n.example.com with N8N_PROTOCOL=https gives https://n8n.example.com:5678/, the other wrong URL you may see. Setting N8N_PORT=443 is no fix: it is also the port n8n listens on. n8n shows this base URL in the editor and registers it with outside services.
| Variable | Default | What it does |
|---|---|---|
N8N_WEBHOOK_URL | not set | Public base URL for test and production webhooks (endpoints) |
WEBHOOK_URL | not set | Old name, deprecated; still works as an alias and logs a warning |
N8N_PROXY_HOPS | 0 | Number of reverse proxies in front of n8n (deployment) |
N8N_EDITOR_BASE_URL | not set | Public editor URL, also used in emails and SAML redirects |
N8N_HOST, N8N_PROTOCOL, N8N_PORT | localhost, http, 5678 | Build the URL when no webhook URL is set |
Step 1: Set the Public URL and the Proxy Hops
Add both variables to the n8n service in your compose file, and publish the port on 127.0.0.1 so every request goes through the proxy. n8n's Postgres example publishes 5678:5678, which binds all network interfaces, and Docker routes published ports past UFW; our Docker and UFW guide explains the fix.
services:
n8n:
# keep your image, volumes and other variables
ports:
- "127.0.0.1:5678:5678"
environment:
- N8N_WEBHOOK_URL=https://n8n.example.com/
- N8N_PROXY_HOPS=1
N8N_PROXY_HOPS is the number of reverse proxies n8n runs behind, so 1 for one Nginx or Caddy. n8n passes it to Express as trust proxy, which then takes the visitor's address from X-Forwarded-For. Without it, every request seems to come from the proxy: the Webhook node's IP(s) Allowlist answers 403 IP is not allowed to access the webhook!, and the log can show ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default). from n8n's rate limiter. Set it only when n8n can't be reached around the proxy, as the 127.0.0.1 port ensures, or a client could send its own X-Forwarded-For.
Apply it and check what n8n uses now:
cd ~/n8n # the folder with your compose file
sudo docker compose up -d
sudo docker compose logs n8n | grep -A1 "Editor is now accessible via"
sudo docker compose logs n8n | grep "WEBHOOK_URL ->"
Docker's restart reference says changed environment variables "are not updated after restarting", while docker compose up recreates containers whose configuration changed and keeps their volumes. The first grep should print your public URL on the next line. The second finds n8n's deprecation warning, WEBHOOK_URL -> Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks., shown while the old name is set. The old name also behaves differently: it only sets production URLs, and test URLs then follow N8N_EDITOR_BASE_URL when that is set.
On a Space-Node VPS for n8n and AI agents you get KVM with full root access, so Docker, the proxy and ports 80 and 443 are all yours to set up.
Step 2: Configure Nginx
Install Nginx and certbot as in our Nginx reverse proxy guide, point the DNS record at the VPS, and save this as /etc/nginx/sites-available/n8n:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name n8n.example.com;
client_max_body_size 16m;
location / {
proxy_pass http://127.0.0.1:5678;
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-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_buffering off;
}
}
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d n8n.example.com
Certbot's nginx plugin obtains the certificate and adds it to your Nginx configuration. What the lines do, per the proxy module docs:
Upgrade,Connectionandproxy_http_version 1.1carry the editor's WebSocket. Both are hop-by-hop headers that nginx doesn't pass on by itself (WebSocket proxying), and before version 1.29.7 nginx proxies with HTTP/1.0 by default. Ubuntu 24.04 ships nginx 1.24 and Debian 12 ships 1.22.Host $hostreplaces nginx's default$proxy_host, which here is127.0.0.1:5678. n8n compares the host with the browser's Origin for the editor connection (more below).- The three
X-Forwardedheaders are what n8n's docs ask for. client_max_body_size 16m: nginx's default is1m, and bigger requests get "413 (Request Entity Too Large)". n8n accepts 16 MiB by default (N8N_PAYLOAD_SIZE_MAX) and files in form-data webhook payloads up to 200 MiB (N8N_FORMDATA_FILE_SIZE_MAX), so raise it if you receive files.proxy_read_timeout 300s: the default is 60 seconds, after which nginx answers 504 to a webhook that only responds when its last node finishes. n8n also pings the editor's WebSocket only every 60 seconds, so a longer timeout keeps idle editor tabs connected.proxy_buffering offpasses responses on as they arrive, which matters for the Webhook node's Streaming response mode.
Keep all proxy_set_header lines in one block: nginx only inherits them from an outer level when the current level has none. The stock nginx.conf on Ubuntu 24.04 and Debian 12 has no $connection_upgrade map; if another site file of yours defines one, drop this copy.
Step 3: Or Use Caddy
Caddy does all of this by itself. Install it from Caddy's apt repository, put this in /etc/caddy/Caddyfile and reload:
n8n.example.com {
reverse_proxy 127.0.0.1:5678
}
sudo systemctl reload caddy
sudo journalctl -u caddy --no-pager | less +G
With the DNS record pointing at the VPS and ports 80 and 443 open, Caddy gets and renews the certificate and redirects HTTP to HTTPS (automatic HTTPS). Its reverse_proxy passes Host through unchanged, sets X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host (ignoring values a client sends), proxies WebSockets, has no read timeout by default and flushes responses of unknown length, such as streams, immediately. Keep N8N_PROXY_HOPS=1. Caddy needs ports 80 and 443 itself, so run Caddy or Nginx, not both. A reload closes open WebSocket connections, so the editor shows "Connection lost" for a moment and reconnects.
Step 4: Call the Right Webhook URL
Every Webhook node has two URLs (workflow development):
| URL | Works when | For how long | Data in the editor |
|---|---|---|---|
/webhook-test/<path> | You select Listen for test event | 120 seconds, one call | Yes |
/webhook/<path> | The workflow is published | Until you unpublish it | No, see Executions |
When nothing matches, n8n answers 404 with one of these messages from its source and logs it after Received request for unknown webhook:.
The requested webhook "GET my-path" is not registered.For a test URL, the hint says to click Execute workflow first, after which the URL "only works for one call"; for a production URL, that the workflow must be active, which current n8n calls published.This webhook is not registered for POST requests. Did you mean to make a GET request?The path exists with another HTTP method. The node defaults to GET, so a POST from a form or service lands here.
Step 5: Test With curl
First on the VPS, past the proxy:
curl -i http://127.0.0.1:5678/healthz
A 200 with {"status":"ok"} means n8n runs and the port is published. If curl can't connect, n8n is down or the port isn't published on 127.0.0.1, and Nginx answers 502 Bad Gateway; our 502 guide covers that side. Then, from your own computer and with the workflow published:
curl -i https://n8n.example.com/healthz
curl -i https://n8n.example.com/webhook/my-path
curl -i --json '{"test": true}' https://n8n.example.com/webhook/my-path
Use the plain request for a GET webhook and the --json one for POST. --json sends a POST with Content-Type: application/json and needs curl 7.82.0 or newer (man page); Ubuntu 24.04 has 8.5.0 and Debian 12 has 7.88.1.
| You get | It means |
|---|---|
200 and {"message":"Workflow was started"} | Proxy and webhook work (the default Respond: Immediately reply) |
| 404 with one of the messages above | n8n answered; check the path, the method and publishing |
403 IP is not allowed to access the webhook! | The allowlist sees the proxy's address: set N8N_PROXY_HOPS |
| 413 Request Entity Too Large | Raise client_max_body_size |
| 502 Bad Gateway | Nginx can't reach 127.0.0.1:5678 |
| 504 Gateway Time-out | The workflow outlasted proxy_read_timeout |
For a 504, nginx's error log says "upstream timed out" (source). To check a test URL, select Listen for test event, then send the same request to /webhook-test/my-path within 120 seconds.
When the Editor Says "Connection lost"
The editor gets live updates over a WebSocket at /rest/push (N8N_PUSH_BACKEND defaults to websocket). The Docker image sets NODE_ENV=production, and then n8n's origin check compares the browser's Origin with the host from the Forwarded header, else X-Forwarded-Host, else Host. On a mismatch it logs a warning and closes the connection with Invalid origin!:
sudo docker compose logs n8n | grep "Origin header does NOT match"
It shows both hosts; an expected host of 127.0.0.1:5678 means the proxy sends neither Host $host nor X-Forwarded-Host. No warning, but still "Connection lost"? Then the upgrade headers don't arrive: in the browser's developer tools, the request to /rest/push should end with status 101. If something between the browser and n8n can't carry WebSockets, N8N_PUSH_BACKEND=sse switches to server-sent events, which n8n sends with X-Accel-Buffering: no so nginx doesn't hold them back.
If It Still Fails
- Long workflows end in 504, or in 524 behind Cloudflare. Cloudflare waits 125 seconds by default (Error 524), and only Enterprise zones can raise that. Let the Webhook node respond immediately, answer early with a Respond to Webhook node, or split the job into a start webhook and a status webhook, as n8n suggests.
- Cloudflare in front of Nginx. That is two proxies, so set
N8N_PROXY_HOPS=2: Cloudflare puts the visitor's address inX-Forwarded-For(headers) and Nginx appends Cloudflare's. Accept web traffic only from Cloudflare's ranges, as in our Cloudflare 521 guide, because Express warns that a shorter path lets clients pick their own address. Cloudflare proxies WebSockets on all plans; the switch is under Network. - n8n under a path such as
/n8n/.N8N_PATHexists, but n8n's docs advise a subdomain such asn8n.example.comwhen a reverse proxy is involved. - Only
/webhook/proxied. Proxy the whole host: the editor uses/rest/, forms use/form/and/form-test/, and waiting webhooks use/webhook-waiting/(endpoint settings). - Queue mode with webhook processors. Route
/webhook/*to the processors and/webhook-test/*to the main instance (queue mode). Our n8n automation guide has a queue mode compose file. - Moved to a new VPS. Bring these variables along; our n8n backup and restore guide covers the rest.
FAQ
Should I use N8N_WEBHOOK_URL or WEBHOOK_URL?
N8N_WEBHOOK_URL, on any n8n from 2.30.0. n8n's docs list WEBHOOK_URL as deprecated from 2.35.0; it still works as an alias but logs a warning at startup. Versions before 2.30.0, including the whole 1.x line, only read WEBHOOK_URL. Check yours with sudo docker compose exec n8n n8n --version.
Do I also need N8N_EDITOR_BASE_URL?
Only when the editor lives at a different address from your webhooks. While it's empty, n8n uses the webhook URL for the editor address and the OAuth callback URLs (frontend settings), so one variable covers both.
Why does the OAuth Redirect URL in my credentials show localhost:5678?
Same cause: n8n builds it from that instance URL plus /rest/oauth2-credential/callback. Set N8N_WEBHOOK_URL (or N8N_EDITOR_BASE_URL), recreate the container, and enter the new redirect URL in the provider's app settings.
Why does n8n say it is "configured to use a secure cookie"?
You opened it over plain HTTP, for example at http://203.0.113.10:5678. N8N_SECURE_COOKIE defaults to true, so cookies only go over HTTPS (security variables). Use the HTTPS address through your proxy; n8n's own message calls switching the setting off not recommended.
Sources
All opened on 30 October 2026:
- n8n docs: Configure webhook URLs with reverse proxy, Endpoint variables, Deployment variables, Security variables, Set up SSL, Webhook node, Workflow development, Webhook common issues, Respond to Webhook, Enable queue mode
- n8n source on GitHub: URL service, config, endpoint config, deprecation warnings, start command, server setup, push service, origin check, push pings, SSE push, webhook 404 messages, webhook request handler, immediate response, Webhook node, Webhook node defaults, rate limiter, frontend settings, CLI launcher, Dockerfile, editor texts, 2.30.0 URL service, 1.123.91 URL service, n8n-hosting Postgres example
- express-rate-limit validations (8.2.2), Express behind proxies
- nginx: WebSocket proxying, proxy module, core module, map module, command line, upstream source, status lines, Ubuntu 24.04 nginx.conf, Debian 12 nginx.conf
- Caddy: reverse_proxy, Automatic HTTPS, Install, Keep Caddy running
- Certbot user guide
- Docker: compose restart, compose up, compose logs, compose exec, services and ports, packet filtering and firewalls
- Cloudflare: Error 524, connection limits, WebSockets, HTTP headers
- MDN: X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto
- curl man page