Artur
Artur
Founder

n8n Nginx Reverse Proxy Setup with SSL (Copy-Paste Config)

April 13, 2026

n8nself-hostingnginxdockerssl

n8n Nginx Reverse Proxy Setup with SSL

You got n8n running on port 5678. Now you need it on a real domain with HTTPS. This guide covers the complete nginx reverse proxy config for n8n - the one that actually handles websockets, webhooks, and doesn't break after a few hours.

If you followed our n8n Docker Postgres setup, this is the next step.

Why n8n Needs a Proper Reverse Proxy Config

N8n is not a standard HTTP application. It uses persistent websocket connections for the editor UI, and those break silently if your nginx config doesn't handle upgrades. Most tutorials miss this and give you a config that looks like it works - until you leave a workflow running and come back to a disconnected editor.

The other issue is webhooks. If WEBHOOK_URL doesn't match your actual public URL, external services like Stripe or GitHub will send payloads that n8n receives but can't route correctly.

This config handles both.

What You Need

  • A VPS with Docker and Docker Compose installed

  • A domain name pointing to your server's IP

  • Port 80 and 443 open in your firewall

  • n8n already running on port 5678 (or we'll set it up together below)

Option 1: Nginx as a System Service + Certbot

If you're running nginx directly on the host (not in Docker), this is the cleanest approach.

Install Nginx and Certbot

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Get Your SSL Certificate

sudo certbot --nginx -d n8n.yourdomain.com

Certbot will modify your nginx config automatically. But for n8n specifically, you want full control over the config - so let certbot get the certificate, then replace the config with the one below.

The Nginx Config for n8n

Create or edit /etc/nginx/sites-available/n8n:

# WebSocket upgrade map - required for n8n editor
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name n8n.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name n8n.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/n8n.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000" always;
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;

    # Increase timeouts for long-running workflows
    proxy_read_timeout    300s;
    proxy_connect_timeout 75s;

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

        # WebSocket headers - do not remove these
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Standard proxy headers
        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;

        # Required for n8n to generate correct webhook URLs
        proxy_set_header X-Forwarded-Host  $host;

        # Disable buffering for streaming responses
        proxy_buffering off;
    }
}

Enable it and reload:

sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

n8n Environment Variables

N8n needs to know its public URL. Set these in your n8n environment (.env file or docker-compose):

N8N_HOST=n8n.yourdomain.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.yourdomain.com/
N8N_PROXY_HOPS=1

N8N_PROXY_HOPS=1 tells n8n it's sitting behind one proxy layer, so it reads the X-Forwarded-Proto header correctly. Without this, n8n may generate http:// webhook URLs even when running on HTTPS.


Option 2: Full Docker Compose Stack (Nginx + n8n + Postgres + Certbot)

If you want everything containerized, this is the complete setup.

File Structure

/opt/n8n/
├── docker-compose.yml
├── .env
├── nginx/
│   └── n8n.conf
└── certbot/
    ├── conf/
    └── www/

.env File

DOMAIN=n8n.yourdomain.com
CERTBOT_EMAIL=you@yourdomain.com

POSTGRES_USER=n8n
POSTGRES_PASSWORD=change_this_to_something_strong
POSTGRES_DB=n8n

N8N_ENCRYPTION_KEY=generate_with_openssl_rand_hex_32

Generate the encryption key:

openssl rand -hex 32

docker-compose.yml

version: '3.8'

services:

  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
      DB_POSTGRESDB_USER: ${POSTGRES_USER}
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_HOST: ${DOMAIN}
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://${DOMAIN}/
      N8N_PROXY_HOPS: 1
      EXECUTIONS_DATA_PRUNE: true
      EXECUTIONS_DATA_MAX_AGE: 336
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/n8n.conf:/etc/nginx/conf.d/n8n.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - n8n

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  postgres_data:
  n8n_data:

nginx/n8n.conf

This is the nginx config for the containerized version. It handles the ACME challenge for certbot renewal and proxies everything else to n8n:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name n8n.yourdomain.com;

    # Required for certbot domain verification
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name n8n.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/n8n.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.yourdomain.com/privkey.pem;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    add_header Strict-Transport-Security "max-age=31536000" always;

    proxy_read_timeout    300s;
    proxy_connect_timeout 75s;

    location / {
        proxy_pass         http://n8n: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-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;
    }
}

Note: in the Docker version, proxy_pass points to http://n8n:5678 (the service name), not localhost.

Getting Your First Certificate

Nginx won't start if it references certificates that don't exist yet. Handle this with a two-step init:

Step 1 - Start only nginx in HTTP-only mode (comment out the HTTPS server block temporarily), then run certbot:

docker compose up -d nginx
docker compose run --rm certbot certonly --webroot \
  -w /var/www/certbot \
  -d n8n.yourdomain.com \
  --email you@yourdomain.com \
  --agree-tos \
  --no-eff-email

Step 2 - Uncomment the HTTPS block, then bring everything up:

docker compose up -d

Certbot runs on a 12-hour loop in the background and auto-renews before expiry.


Common Issues and Fixes

WebSocket connection drops / editor disconnects randomly

The map $http_upgrade $connection_upgrade block at the top is required. If it's missing, or the proxy_set_header Upgrade and proxy_set_header Connection lines are absent, the editor will disconnect after the first websocket timeout. Verify your config with nginx -t.

Webhooks generating http:// URLs instead of https://

Check two things: N8N_PROXY_HOPS=1 must be set, and N8N_PROTOCOL=https with a matching WEBHOOK_URL. Restart n8n after any env var change.

413 Request Entity Too Large

Workflows that process files or large JSON payloads will fail if nginx's client body size limit is too low. Add to your server block:

client_max_body_size 50m;

502 Bad Gateway on startup

Almost always means n8n hasn't finished starting, or the postgres healthcheck hasn't passed. Wait 10-15 seconds. If it persists:

docker compose logs -f n8n

Look for Editor is now accessible via: - that's when n8n is ready.

Certificate not found on nginx start

If you're using the Docker Compose setup and nginx fails with a certificate-not-found error, you need to do the two-step init above. The SSL server block requires certificates that already exist on disk.


Verifying the Setup

Three checks before you call it done:

  1. Open https://n8n.yourdomain.com - login screen should load with a valid SSL cert (green padlock).
  2. Create a test webhook trigger node. The URL shown should start with https://.
  3. In browser dev tools (Network tab), reload the n8n editor and confirm there's an active wss:// WebSocket connection.

All three passing means the proxy is working correctly.


Need This Done For You?

If you're setting up n8n for a business and want a production-ready deployment without debugging nginx configs, n8n Logic handles self-hosted n8n setup and maintenance for agencies and SMBs. We've done this setup dozens of times and can have a hardened instance running on your infrastructure same-day.


FAQ

Can I use a subdirectory instead of a subdomain (like domain.com/n8n/)?

Technically yes, but it's painful. N8n has assumptions baked in about its base URL, and subdirectory setups require N8N_PATH and N8N_EDITOR_BASE_URL which can conflict with webhook routing. Use a subdomain.

Do I need nginx if I'm using Traefik or Caddy?

No. Both are solid alternatives. Caddy is simpler if you don't have nginx experience - automatic HTTPS, one-file config. Traefik makes sense if you're running multiple services and want label-based routing. Nginx is the most widely understood option.

My n8n instance is on a different machine from nginx. What changes?

Just the proxy_pass address. Instead of localhost:5678 or n8n:5678, use the internal IP: http://192.168.1.x:5678. Make sure port 5678 is accessible from your nginx host.

How do I renew the certificate manually?

System nginx + certbot: sudo certbot renew. Docker Compose: certbot handles it automatically. Force a manual renewal with: docker compose run --rm certbot renew.


n8n Nginx Reverse Proxy Setup with SSL (Copy-Paste Config) | n8nlogic