📁 last Posts

How to Fix "Webhook URL shows Localhost" Error in n8n

How to Fix Webhook URL Shows Localhost Error in n8n — VPS Solution by TriggerWorkflow
The dreaded localhost webhook URL — and exactly how to replace it with your real domain in under 10 minutes.

🐞 You deployed n8n on a VPS, opened the Webhook node, and saw this: http://localhost:5678/webhook/your-path. You try to paste that URL into Shopify or Typeform and nothing works — of course it doesn't, localhost is your server talking to itself. The outside world has no idea where to find it.

I hit this exact wall the first time I deployed n8n on an Oracle Cloud free-tier instance. Spent two hours reading forum threads before I understood what was actually happening. This guide gives you everything I wish existed back then — the root cause, all four environment variable options, the Nginx proxy config that trips everyone up, and a full troubleshooting table for when the obvious fix doesn't work.

Quick Fix — 3 Steps

The cause: n8n builds webhook URLs from the WEBHOOK_URL environment variable. If it is not set, n8n falls back to http://localhost:5678. Setting it once fixes every webhook node in every workflow.

  1. Add WEBHOOK_URL=https://your-domain.com/ to your .env file or Docker environment.
  2. Restart n8n: docker restart n8n or sudo systemctl restart n8n.
  3. Toggle the workflow Active in the editor — webhooks only register on active workflows.

If that alone does not fix it, keep reading — your Nginx config or a conflicting variable is likely the culprit.

Why Does This Happen? The Real Explanation

Most guides jump straight to "set this variable and restart." That works, until it does not — and when it does not, you have no idea why. So let's spend two minutes understanding the actual mechanism.

How n8n Builds Webhook URLs

Every time you open a Webhook node in n8n, the editor generates a URL for you. It constructs that URL at runtime using environment variables in this priority order:

  1. WEBHOOK_URL — if this is set, it is used as the complete base URL and everything else is ignored.
  2. N8N_PROTOCOL + N8N_HOST + N8N_PORT — if WEBHOOK_URL is absent, n8n assembles the URL from these three pieces.
  3. Fallback: http://localhost:5678 — if none of the above are set, this is what you get.

When you run n8n locally for development, the fallback is fine — your browser and your n8n instance are on the same machine. The moment you move to a VPS, your n8n instance is no longer "localhost" to the outside world. But n8n does not know your domain unless you tell it explicitly.

Setting WEBHOOK_URL is not optional on a self-hosted production instance. It is a required configuration step that the official n8n documentation lists but many deployment tutorials skip entirely. — Confirmed from n8n's official environment variables reference: docs.n8n.io/hosting/configuration/environment-variables

Why Nginx Makes It Worse Without Proper Headers

Many VPS setups run n8n behind an Nginx reverse proxy. This introduces a second layer of confusion: even after you set WEBHOOK_URL correctly, your Nginx config may be stripping or not forwarding the headers that tell n8n it is operating over HTTPS from a public domain.

If Nginx does not pass X-Forwarded-Proto: https, n8n may still generate http:// URLs internally even when your browser sees HTTPS. This is the "I set the variable but it still shows localhost or HTTP" variant of the bug — and it has a different fix.

🧠

Mental model: n8n needs to know two things — what domain to put in webhook URLs (WEBHOOK_URL), and what protocol the connection is using (X-Forwarded-Proto from Nginx). Both must be correct for production webhooks to work reliably.


All Environment Variables That Affect Webhook URLs

Before jumping to code, here is a clear map of every variable involved. Understanding which one does what will save you from conflicting configurations — a surprisingly common problem.

⚡ Primary — Set This First
WEBHOOK_URL

The complete base URL for all webhooks. Must include trailing slash. Example: https://n8n.yourdomain.com/. When set, overrides N8N_HOST and N8N_PROTOCOL for URL generation.

Secondary — Fallback Only
N8N_HOST

The hostname only (no protocol, no port). Used if WEBHOOK_URL is absent. Example: n8n.yourdomain.com.

Secondary — Fallback Only
N8N_PROTOCOL

Either http or https. Works together with N8N_HOST when WEBHOOK_URL is not set. Default: http.

Optional
N8N_PORT

The port n8n listens on internally. Default: 5678. If using a reverse proxy, keep this at default and let Nginx handle 80/443.

Optional
N8N_PATH

A sub-path if n8n runs at a path like /n8n/. Rarely needed. Leave empty if n8n is at the root of your domain.

Optional
N8N_EDITOR_BASE_URL

Separate URL for the editor UI, if it differs from the webhook base URL. Only needed for advanced split-domain setups.

⚠️

Do not set both WEBHOOK_URL and N8N_HOST at the same time unless you understand the priority order. WEBHOOK_URL wins. Having both set to different values confuses the configuration and is the source of many "I set it but it still shows localhost" reports on the n8n community forum.

Variable Priority Comparison

Variable Priority What It Controls Recommended?
WEBHOOK_URL 🥇 Highest Full webhook base URL ✅ Yes — always set this
N8N_HOST 🥈 Medium Hostname only ⚠️ Only if WEBHOOK_URL absent
N8N_PROTOCOL 🥈 Medium http or https ⚠️ Pair with N8N_HOST
N8N_PORT 🥉 Low Internal listen port Leave at 5678 if using proxy
None set ❌ Fallback http://localhost:5678 ❌ This is the bug

Fix by Deployment Type (Choose Your Setup)

The fix is the same concept in every case, but the exact file and command differ depending on how you installed n8n. Find your setup below.

Fix A: Docker Compose (Most Common)

Open your docker-compose.yml file and add the environment variables to the n8n service block. The critical line is WEBHOOK_URL. Replace your-domain.com with your actual domain — including the trailing slash.

docker-compose.yml — complete n8n service block
version: '3.8'
services:
  n8n:
    image: n8nio/n8n:latest
    restart: unless-stopped
    environment:
      - WEBHOOK_URL=https://your-domain.com/   # ← This is the critical fix
      - N8N_HOST=your-domain.com               # Fallback, keep it consistent
      - N8N_PROTOCOL=https
      - N8N_PORT=5678
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=your-password
    ports:
      - "127.0.0.1:5678:5678"                  # Bind to localhost only — Nginx handles public traffic
    volumes:
      - n8n_data:/home/node/.n8n
volumes:
  n8n_data:

After saving the file, apply the changes:

bash — restart Docker Compose
docker compose down && docker compose up -d

# Or if using older docker-compose command:
docker-compose down && docker-compose up -d

Fix B: .env File (systemd or pm2)

If you installed n8n with npm install -g n8n and run it as a systemd service or with pm2, your environment variables live in a .env file — typically at /home/node/.n8n/.env or wherever your n8n data folder is.

/home/node/.n8n/.env
WEBHOOK_URL=https://your-domain.com/
N8N_HOST=your-domain.com
N8N_PROTOCOL=https
N8N_PORT=5678
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=your-secure-password

Then restart the service:

bash — restart systemd service
# systemd:
sudo systemctl daemon-reload
sudo systemctl restart n8n
sudo systemctl status n8n     # Verify it started cleanly

# pm2:
pm2 restart n8n
pm2 logs n8n --lines 20       # Check for startup errors

Fix C: Single Docker run Command

If you started n8n with a bare docker run command, stop the existing container and restart it with the -e flags:

bash — docker run with WEBHOOK_URL
docker stop n8n && docker rm n8n

docker run -d \
  --name n8n \
  --restart unless-stopped \
  -p 127.0.0.1:5678:5678 \
  -e WEBHOOK_URL=https://your-domain.com/ \
  -e N8N_HOST=your-domain.com \
  -e N8N_PROTOCOL=https \
  -e N8N_PORT=5678 \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n:latest

Fix D: Railway, Render, or Other Cloud Platforms

On cloud deployment platforms, environment variables are set through the platform dashboard, not a file. The variable name and value are the same — only the location changes.

  • Railway: Project → Service → Variables → Add Variable: WEBHOOK_URL = https://your-railway-domain.up.railway.app/
  • Render: Service → Environment → Add Environment Variable with the same key/value.
  • Fly.io: Add to fly.toml under [env], then run fly deploy.
💡

Verify the variable was applied: After restarting, open a Webhook node and look at the URL it generates. If it still shows localhost, the variable was not picked up — check for typos in the variable name or confirm the service actually restarted.


The Nginx Reverse Proxy Configuration (Critical Headers)

This is the part most tutorials get wrong or skip entirely. Even with WEBHOOK_URL set correctly, if your Nginx configuration does not pass the right headers, n8n may still generate incorrect URLs or refuse incoming webhook requests.

Complete Working Nginx Config for n8n

/etc/nginx/sites-available/n8n.conf
server {
    listen 80;
    server_name your-domain.com;
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL — managed by Certbot / Let's Encrypt
    ssl_certificate     /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Increase timeout for long-running webhook responses
    proxy_read_timeout   300;
    proxy_connect_timeout 300;
    proxy_send_timeout   300;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;

        # ── These 4 headers are critical for n8n ──
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP         $remote_addr;

        # WebSocket support (required for n8n editor live updates)
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Disable buffering for SSE / streaming responses
        proxy_buffering off;
        proxy_cache off;
    }
}

Why Each Highlighted Header Matters

  • proxy_set_header Host $host — tells n8n the actual public hostname the request arrived at. Without this, n8n sees the internal 127.0.0.1 address and may generate localhost URLs.
  • proxy_set_header X-Forwarded-Proto $scheme — tells n8n whether the original request used HTTP or HTTPS. Without this, n8n generates http:// webhook URLs even though your site is on HTTPS.
  • proxy_set_header X-Forwarded-For — passes the real visitor IP. Required if you use IP-based rate limiting or logging inside n8n.
  • proxy_set_header X-Real-IP — similar to above, used by some n8n nodes that read the sender's IP from request headers.

After editing the Nginx config, always test before reloading:

bash — test and reload Nginx
sudo nginx -t                   # Test config for syntax errors
sudo systemctl reload nginx    # Apply if test passes
🔒

Getting a free SSL certificate: If you have not set up HTTPS yet, run sudo apt install certbot python3-certbot-nginx then sudo certbot --nginx -d your-domain.com. Certbot automatically edits your Nginx config and sets up auto-renewal.


Step That Everyone Forgets: Activate the Workflow

You have set the environment variables, restarted n8n, verified the Nginx config — and the webhook still returns 404. Nine times out of ten, this is the reason: the workflow is not Active.

n8n has two URL types for webhooks:

URL TypeFormatWhen it worksUse for
Test URL /webhook-test/your-path Only while editor is open + you clicked "Listen for Test Event" Development only
Production URL ✅ /webhook/your-path Always — as long as the workflow is Active Live external apps

Toggle the workflow Active using the switch in the top-right corner of the n8n editor. The switch turns green when active. At this point, the Production URL registers itself with n8n's router and will accept incoming POST requests 24/7.

⚠️

Always configure Shopify, Typeform, or any other external app with the Production URL — the one that contains /webhook/ (not /webhook-test/). The test URL stops working the moment you close the editor tab.


How to Test Your Webhook After Fixing It

Before connecting your real external app, verify the webhook works with a controlled test. Here are three methods, from quickest to most thorough.

Method 1: cURL (Fastest)

Open a terminal on your local machine and send a POST request directly:

bash — cURL webhook test
curl -X POST https://your-domain.com/webhook/your-path \
  -H "Content-Type: application/json" \
  -d '{"test": true, "source": "curl", "message": "Hello from TriggerWorkflow"}'

# Expected response: {"message":"Workflow was started"}

If you see {"message":"Workflow was started"}, your webhook is live and working. Switch to n8n's Execution log (left sidebar → Executions) to inspect the incoming data.

Method 2: webhook.site (Visual)

For a visual way to inspect the exact payload structure your webhook receives, use webhook.site. Set your external app to send to webhook.site temporarily, inspect the structure, then swap in your real n8n Production URL.

Method 3: Postman (Most Thorough)

Download Postman (free), create a new POST request, set the URL to your Production webhook URL, add a JSON body, and send. Postman shows you the full response headers and body — useful for debugging authentication issues.


Bonus: Test Webhooks Locally With ngrok (No VPS Needed)

If you are still in development and do not have a VPS yet — or if you want to test a new workflow without deploying to production — ngrok gives you a public HTTPS URL that tunnels directly to your local n8n instance.

1

Install ngrok and start a tunnel

Download ngrok from ngrok.com (free tier works), authenticate it once, then run: ngrok http 5678. ngrok will print a temporary HTTPS URL like https://abc123.ngrok.io.

2

Set WEBHOOK_URL to the ngrok address

Add WEBHOOK_URL=https://abc123.ngrok.io/ to your local .env file and restart n8n. Open a Webhook node — it now shows the ngrok URL instead of localhost.

3

Connect your external app and test

Paste the ngrok Production URL into Shopify, Typeform, or any other service. Submissions will tunnel through ngrok to your local n8n. Note: the free ngrok URL changes every time you restart ngrok, so update WEBHOOK_URL accordingly.

💡

Cloudflare Tunnel is a free ngrok alternative with a stable URL. Run cloudflared tunnel --url http://localhost:5678. Cloudflare assigns a consistent subdomain on your free account — no URL rotation on restart.


Advanced Troubleshooting: When the Standard Fix Does Not Work

If you set WEBHOOK_URL, restarted, and are still seeing problems, use this diagnostic table. These are the ten real-world failure scenarios I have personally debugged across different VPS providers and deployment setups.

Symptom Most Likely Cause Fix
URL still shows localhost after restart Variable not applied — service did not restart, or env file not found Verify with docker exec n8n env | grep WEBHOOK or printenv WEBHOOK_URL
URL shows correct domain but uses http:// not https:// Nginx missing X-Forwarded-Proto $scheme header Add the header to your Nginx location block and reload Nginx
Webhook URL correct but returns 404 Workflow is not Active, or path mismatch Toggle workflow Active; confirm URL path matches Webhook node setting exactly
Works from cURL but not from Shopify / Typeform HTTPS certificate issue or Nginx blocking non-standard User-Agent Test with curl -I https://your-domain.com/webhook/path; check SSL validity
Connection refused on port 443 Nginx not running or SSL cert missing Run sudo systemctl status nginx and sudo nginx -t
502 Bad Gateway on webhook call n8n is not running or crashed after restart docker ps or systemctl status n8n; check logs for startup errors
Works once, then returns 404 on next call n8n restarted and workflow toggle reset to inactive Re-activate workflow; check if a crash loop is causing restarts
Webhook fires but data is empty in n8n External app sending form-encoded, not JSON; or sending to wrong field path Inspect raw webhook data in n8n Executions log; check $json.body vs $json
Oracle Cloud — correct env but still localhost Oracle Security List (not OS firewall) blocking ports 80/443 Add Ingress Rules for port 80 and 443 in Oracle Cloud Console → Networking → Security Lists
Both WEBHOOK_URL and N8N_HOST set, conflict Two variables pointing to different domains Remove N8N_HOST and N8N_PROTOCOL — WEBHOOK_URL alone is sufficient

How to Verify the Variable Was Actually Applied

Never assume the restart worked. Verify the variable is live inside the running container or process:

bash — verify env vars in running n8n
# Docker:
docker exec n8n env | grep -E "WEBHOOK|N8N_HOST|N8N_PROTOCOL"

# systemd — check what the process sees:
sudo systemctl show n8n --property=Environment

# Expected output should include:
WEBHOOK_URL=https://your-domain.com/

The Definitive Verification: Open a Webhook Node

After restarting, open any Webhook node in n8n. The URL it generates is the ground truth. If it shows https://your-domain.com/webhook/..., everything is configured correctly. If it still shows localhost, the environment variable is not reaching the n8n process — no matter what the file says.


Oracle Cloud Free Tier: Extra Step Required

Oracle Cloud's free tier is popular for self-hosting n8n because it is genuinely free with no credit card expiry catch. But it has one networking quirk that catches almost everyone: two separate firewalls.

The Two Firewalls Problem

Most VPS providers have one firewall. Oracle Cloud has two:

  1. OS-level firewall (iptables / ufw) — the one you configure with sudo ufw allow 443.
  2. Oracle Security List (in the Oracle Cloud Console web UI) — a separate network-level firewall that sits in front of your instance.

If you only open port 443 in the OS firewall but forget the Oracle Security List, HTTPS traffic never even reaches your server. The webhook error persists regardless of any n8n configuration.

1

Open ports 80 and 443 in Oracle Security List

Log in to Oracle Cloud Console → Networking → Virtual Cloud Networks → your VCN → Security Lists → Default Security List → Add Ingress Rules. Add two rules: Source CIDR 0.0.0.0/0, Destination Port Range 80. Repeat for port 443.

2

Open ports in OS firewall

Run: sudo iptables -I INPUT 6 -m state --state NEW -p tcp --dport 443 -j ACCEPT and the same for port 80. Then: sudo netfilter-persistent save to make the rules persistent after reboot.

3

Verify connectivity from outside

From your laptop or phone, run curl -I https://your-oracle-domain.com. If you see a 200 or 301 response, both firewalls are open and Nginx is responding. Now test the webhook with cURL.


Frequently Asked Questions

Why does n8n show localhost instead of my domain for webhook URLs?
n8n builds its webhook URL from the WEBHOOK_URL environment variable. If that variable is not set, it falls back to http://localhost:5678. This is intentional default behavior for local development — you must explicitly set WEBHOOK_URL=https://your-domain.com/ after any production deployment.
What is the difference between WEBHOOK_URL and N8N_HOST?
WEBHOOK_URL is the master variable — it sets the complete base URL for all webhook nodes and overrides everything else. N8N_HOST and N8N_PROTOCOL are lower-priority fallbacks used when WEBHOOK_URL is absent. Best practice: always set WEBHOOK_URL and leave the other two either unset or consistent with it.
Do I need to restart n8n after changing environment variables?
Yes, always. Environment variables are read only at startup. Changing a .env file has zero effect on a running process. Run docker restart n8n (Docker) or sudo systemctl restart n8n (systemd) after every environment variable change, and verify the change took effect with docker exec n8n env | grep WEBHOOK.
My webhook URL is correct but I still get a 404 error — why?
The workflow is not Active. Webhooks only register with n8n's router when the workflow is toggled Active (green switch in the top-right corner of the editor). Also check that the path in your URL exactly matches the path configured in the Webhook node — this match is case-sensitive.
Can I test n8n webhooks locally without a public domain?
Yes. Use ngrok (ngrok http 5678) or Cloudflare Tunnel (cloudflared tunnel --url http://localhost:5678) to get a public HTTPS URL that tunnels to your local n8n. Set WEBHOOK_URL to that URL, restart n8n, and your local instance will accept webhooks from any external service.
Do my existing workflows break when I change WEBHOOK_URL?
The workflow logic does not break, but the public endpoint URL changes. Any external app (Shopify, Stripe, Typeform, Calendly) that was configured with the old localhost URL must be updated to use the new public URL. Go into each external app's webhook settings and replace the old URL with the new Production URL from your Webhook node.
How do I fix the localhost error on Oracle Cloud free tier?
Set WEBHOOK_URL=https://your-domain.com/ in your .env file. Then open ports 80 and 443 in both Oracle's Security List (in the Oracle Cloud Console web UI) AND in the OS-level iptables firewall. Oracle Cloud has two separate firewalls — the standard fix only addresses one of them. See the Oracle Cloud section above for exact commands.
What if n8n is behind a load balancer or reverse proxy?
Set WEBHOOK_URL to your public domain. Ensure the reverse proxy passes X-Forwarded-Proto: https and Host: your-domain.com headers to n8n. Without X-Forwarded-Proto, n8n may still generate http:// webhook URLs even though the public-facing URL uses HTTPS. The Nginx config in Section 4 above includes all required headers.

Continue Learning on TriggerWorkflow

🔧 Troubleshooting
Fix Webhook 404 "Not Registered" Error in n8n
📖 Foundation
What is a Webhook? Simple Example Without Code
🚀 Pillar Article
Ultimate Guide to AI Automation for Small Businesses
🔧 Troubleshooting Soon
Schedule Trigger Not Running? Cron Timezone Fix
🚀 Advanced Soon
Deploy n8n on Oracle Cloud Free Tier — Complete Guide
🚀 Advanced Soon
How to Secure Your n8n Instance With Basic Auth + Nginx

Webhook Fixed — Now Build Something With It

Your n8n instance is now publicly accessible. Import a ready-made workflow from our pillar guide and have your first real automation running in under 30 minutes.

Browse Free Workflows Soon →
Mr. Ahmed Ayari — TriggerWorkflow I have debugged this exact error on five different VPS providers. Every fix in this guide comes from a real failure I hit, not documentation copy-paste. If something here helped you, the webhook article is a good next read.
Comments