n8n Webhook Returns 404 Behind nginx (Sub-Path Proxy): The Trailing Slash in proxy_pass
Cannot POST /n8n/webhook/repro-hookShort answer: With n8n published under the nginx sub-path /n8n/, an active webhook returned 404 Not Found (Cannot POST /n8n/webhook/repro-hook) through the proxy but 200 when called directly. The one change that fixed it: add a trailing slash to proxy_pass (proxy_pass http://n8n:5678/;), so nginx strips the /n8n/ prefix before forwarding.

Symptom
When publishing n8n behind nginx under a sub-path (/n8n/), calling an active POST webhook through the reverse proxy returns HTTP/1.1 404 Not Found with an HTML body containing <pre>Cannot POST /n8n/webhook/repro-hook</pre>.
In this setup, n8n is served at its root and nginx publishes it under the sub-path /n8n/. The test workflow has a single Webhook node (POST, path repro-hook). It was imported with docker compose run --rm n8n import:workflow --input=/home/node/.n8n/wf.json, activated with n8n’s update:workflow --id=reproWebhook01 --active=true CLI command, and the stack was started with docker compose up -d. So the workflow is active and the path is correct.
The n8n version and the relevant environment variables (WEBHOOK_URL, N8N_PROXY_HOPS) inside the container:
$ docker compose exec -T n8n n8n --version
2.44.7
$ docker compose exec -T n8n env | grep -E "^N8N_|^WEBHOOK_URL" | sort
N8N_DIAGNOSTICS_ENABLED=false
N8N_PERSONALIZATION_ENABLED=false
N8N_PROXY_HOPS=1
N8N_RELEASE_TYPE=stable
N8N_SECURE_COOKIE=false
WEBHOOK_URL=http://repro.local:8081/n8n/
Sending a POST request through nginx on port 8081 fails with a 404 response:
$ curl -si -X POST -H 'Host: repro.local' http://127.0.0.1:8081/n8n/webhook/repro-hook -H 'Content-Type: application/json' -d '{}' # via nginx
HTTP/1.1 404 Not Found
Server: nginx/1.27.5
Date: Thu, 29 Oct 2026 09:21:22 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 162
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /n8n/webhook/repro-hook</pre>
</body>
</html>
Confirm Which Layer Is Failing
To isolate whether the workflow itself or the reverse proxy path handling is the problem, compare a direct request to the n8n container port (127.0.0.1:5679) against the request routed through nginx (127.0.0.1:8081/n8n/...).
Bypassing nginx and calling n8n directly at /webhook/repro-hook succeeds:
$ curl -si -X POST http://127.0.0.1:5679/webhook/repro-hook -H 'Content-Type: application/json' -d '{}' # direct, bypass proxy
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 34
Date: Thu, 29 Oct 2026 09:21:22 GMT
{"message":"Workflow was started"}
Meanwhile, the nginx log records the 404 response (162 bytes) for the proxied request:
$ docker compose logs nginx --tail 1
nginx-1 | 172.x.x.x - - [29/Oct/2026:09:21:22 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 404 162 "-" "curl/8.14.1" "-"
Two observations confirm that the issue is at the proxy path-forwarding layer:
- Direct requests to
/webhook/repro-hookreturn200 OK, proving the workflow is active and listening at root (/webhook/repro-hook). - The 404 response body (
Cannot POST /n8n/webhook/repro-hook) is an Express/n8n error page rather than a default nginx 404 page. This shows the request did reach n8n, but with the/n8n/prefix still attached.
Cause
Inspecting the active nginx server block shows proxy_pass defined without a trailing URI (/):
$ docker compose exec -T nginx nginx -T # excerpt: the relevant server block only
server {
listen 80;
server_name repro.local;
location /n8n/ {
# BROKEN: no trailing slash -> /n8n/ prefix forwarded to n8n unchanged
proxy_pass http://n8n:5678;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
As general nginx mechanism knowledge (documented in the nginx proxy_pass documentation and consistent with the output above):
- When
proxy_passis specified without a URI (proxy_pass http://n8n:5678;), nginx forwards the original request URI unchanged (/n8n/webhook/repro-hook) to the upstream server. Because n8n is served at root and expects/webhook/repro-hook, it responds withCannot POST /n8n/webhook/repro-hook. - When
proxy_passis specified with a URI—even a single trailing slash (proxy_pass http://n8n:5678/;)—nginx replaces the part of the request URI matching thelocationprefix (/n8n/) with the URI specified inproxy_pass(/), forwarding/webhook/repro-hookto n8n.
The One Change
Add a trailing slash to the proxy_pass directive inside location /n8n/ and reload nginx:
$ diff -u nginx.conf.bak-broken-20261029 nginx.conf
@@ -2,8 +2,8 @@
listen 80;
server_name repro.local;
location /n8n/ {
- # BROKEN: no trailing slash -> /n8n/ prefix forwarded to n8n unchanged
- proxy_pass http://n8n:5678;
+ # FIXED: trailing slash strips /n8n/ prefix
+ proxy_pass http://n8n:5678/;
proxy_http_version 1.1;
$ docker compose exec -T nginx nginx -s reload
Verify
Repeat the curl request through nginx:
$ curl -si -X POST -H 'Host: repro.local' http://127.0.0.1:8081/n8n/webhook/repro-hook -H 'Content-Type: application/json' -d '{}'
HTTP/1.1 200 OK
Server: nginx/1.27.5
Date: Thu, 29 Oct 2026 09:21:43 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 34
{"message":"Workflow was started"}
Toggling between the broken configuration (no trailing slash) and fixed configuration (with trailing slash) across two rounds (nginx -s reload against the same URL) confirms the 404 follows that one functional change, the trailing slash (the comment line in the diff is inert):
# toggle test (swap conf + nginx -s reload, same URL)
round1 broken: 404
round1 fixed: 200
round2 broken: 404
round2 fixed: 200
$ docker compose logs nginx | grep 'POST /n8n' | tail -5
172.x.x.x - - [29/Oct/2026:09:21:43 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 200 34
172.x.x.x - - [29/Oct/2026:09:21:44 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 404 162
172.x.x.x - - [29/Oct/2026:09:21:45 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 200 34
172.x.x.x - - [29/Oct/2026:09:21:46 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 404 162
172.x.x.x - - [29/Oct/2026:09:21:48 +0000] "POST /n8n/webhook/repro-hook HTTP/1.1" 200 34
Minimal Working Config
nginx.conf (server block)
server {
listen 80;
server_name repro.local;
location /n8n/ {
# FIXED: trailing slash strips /n8n/ prefix
proxy_pass http://n8n:5678/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
n8n environment variables
These are the values that were set during the test (from the env output above). The fix itself is only the nginx change; WEBHOOK_URL and N8N_PROXY_HOPS=1 are the proxy-related settings, and the other four are just test-environment settings, not needed for the fix.
N8N_DIAGNOSTICS_ENABLED=false
N8N_PERSONALIZATION_ENABLED=false
N8N_PROXY_HOPS=1
N8N_RELEASE_TYPE=stable
N8N_SECURE_COOKIE=false
WEBHOOK_URL=http://repro.local:8081/n8n/
Scope: This Config Is for Webhooks Only
The nginx config above only guarantees that webhooks work under /n8n/. It does not make the n8n editor usable. In a follow-up check with the same config and n8n 2.44.7 (2026-10-29), opening http://repro.local:8081/n8n/ in a browser never showed the sign-in form: the editor HTML references root-absolute paths such as /static/base-path.js and /favicon.ico, and this config has no location / block to serve them. A request for /assets/ through nginx returned 404 (we did not request the other paths individually). When we added a location / block that still used proxy_set_header Host $host, the editor loaded but its push WebSocket was rejected with Invalid origin! and the workflow canvas showed “Connection lost”.
If you also need the editor, serve it from the root path (add a location / block; the repro report also names N8N_PATH as an option, which we did not test) and use proxy_set_header Host $http_host. See n8n Behind nginx: ‘Connection lost’ and ‘Invalid origin!’ on a Non-Default Port for the reproduced fix. (That fix was reproduced on a root-path HTTPS setup; we did not test the $http_host + location / combination together with this /n8n/ config. Also note this config runs over plain HTTP with N8N_SECURE_COOKIE=false, which is a workaround; for the editor, prefer HTTPS with an http→https redirect, as described in n8n’s Secure Cookie Warning Behind nginx.)
Note on WEBHOOK_URL: Newer n8n versions should use N8N_WEBHOOK_URL. On 2.44.7 the startup log warns - WEBHOOK_URL -> Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks., but WEBHOOK_URL still takes effect: with either variable, /rest/settings reported the same urlBaseWebhook and urlBaseWebhookTest (http://repro.local:8081/n8n/). The 404 fix in this article is the trailing slash, not this variable.
What We Did Not Test
- Test URL (
/webhook-test/...) vs. Production URL (/webhook/...) confusion: Not tested or reproduced here. - Missing
WEBHOOK_URLenvironment variable: Not tested in isolation;WEBHOOK_URL=http://repro.local:8081/n8n/was already set throughout this reproduction. - n8n Editor UI webhook URL display: The URL shown inside the n8n web editor was not tested or captured.