// DevOps

Installing n8n in Docker with HAProxy: A Clear Guide for Beginners

Published on 2026-09-22

This guide shows how to deploy n8n in Docker and expose it to the outside via HAProxy — a reverse proxy that accepts HTTPS and forwards requests to n8n. The instructions are aimed at beginners: each step is explained. Commands and options are verified against the n8n documentation for the 2.x branch (as of September 2026 the current version is 2.40).

Why HAProxy

You can open the n8n port directly to the internet, but that’s unsafe and inconvenient. HAProxy solves three problems:

  1. HTTPS. The proxy terminates TLS and stores the certificate while n8n runs inside the server over plain HTTP.
  2. Single entry point. All requests to the domain go to HAProxy, which decides which application to forward them to. You can add other services to the same server later.
  3. Flexibility. Routing rules, restrictions and load balancing can be added later without touching n8n. More about HAProxy — see the article “Proxy servers: Part 3 — HAProxy”.

What you’ll need

  • A server running Ubuntu or Debian. For a small setup 2 CPU cores and 2–4 GB RAM are enough; the choice between n8n Cloud and a self-hosted server is discussed in the article “n8n: Part 2 — Cloud version or self-hosted server”.
  • Docker and Docker Compose (the docker compose plugin).
  • A domain name with an A record pointing to the server IP address.
  • SSH access with sudo privileges.

Step 1: Prepare the server and project structure

  1. Update packages:

    bash
    sudo apt update && sudo apt upgrade -y
  2. Check Docker:

    bash
    docker --version
    docker compose version

    If the commands are missing, install Docker following the official guide for your OS.

  3. Create the project directory and subdirectories:

    bash
    mkdir -p ~/n8n-project/haproxy/certs
    cd ~/n8n-project

Step 2: .env file with secrets

In the ~/n8n-project directory create a .env file. Docker Compose will substitute values from it into docker-compose.yml.

env
N8N_DOMAIN=n8n.example.com
POSTGRES_USER=n8n
POSTGRES_PASSWORD=replace-with-a-long-password
POSTGRES_DB=n8n
# encryption key for n8n credentials: generate once and store it
N8N_ENCRYPTION_KEY=replace-with-result-of-openssl-rand-hex-32

Generate the encryption key with openssl rand -hex 32. n8n uses this key to encrypt stored credentials: if you lose it, you won’t be able to restore credentials from backups. Store the key separately from the server.


Step 3: docker-compose.yml

Create ~/n8n-project/docker-compose.yml. The version: line at the top of the file is no longer necessary: modern Docker Compose ignores it.

yaml
services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
      PGDATA: /var/lib/postgresql/data
    volumes:
      - db-storage:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: n8nio/n8n:2.40.5   # pin the version and update deliberately
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"   # port accessible only from the server (localhost)
    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: ${N8N_DOMAIN}
      N8N_PORT: "5678"
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://${N8N_DOMAIN}/
      N8N_PROXY_HOPS: "1"       # n8n is running behind a single reverse proxy
      NODE_ENV: production
    volumes:
      - n8n-data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  haproxy:
    image: haproxy:3.2          # long-term support branch
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./haproxy:/usr/local/etc/haproxy:ro
    depends_on:
      - n8n

volumes:
  db-storage:
  n8n-data:

What’s important here:

  • No basic auth username/password in environment variables. Older guides set N8N_BASIC_AUTH_USER and N8N_BASIC_AUTH_PASSWORD, but basic auth support was removed back in n8n 1.0. Login is now only via the owner account that you create on first opening n8n.
  • PostgreSQL instead of SQLite. For persistent operation n8n documentation recommends PostgreSQL; the PGDATA line ensures Postgres 18 stores data in the attached volume.
  • WEBHOOK_URL and N8N_PROTOCOL. Without them n8n will show webhook addresses like http://localhost:5678/..., and external services won’t be able to reach them.
  • N8N_PROXY_HOPS=1 tells n8n that one proxy is in front of it and to take the client address from the X-Forwarded-For header.
  • The 127.0.0.1:5678 port is not visible from the internet — n8n can only be reached through HAProxy.

Step 4: HAProxy for HTTP

First we run everything over HTTP: there is no certificate yet. Create ~/n8n-project/haproxy/haproxy.cfg:

ini
global
    log stdout format raw local0

defaults
    log     global
    mode    http
    option  httplog
    option  forwardfor
    timeout connect 5s
    timeout client  50s
    timeout server  50s
    timeout tunnel  1h

frontend http_front
    bind :80
    acl host_n8n hdr(host) -i n8n.example.com
    use_backend n8n_backend if host_n8n

backend n8n_backend
    server n8n n8n:5678 check
  • option forwardfor adds the X-Forwarded-For header with the client’s address.
  • timeout tunnel 1h is needed for WebSocket: the n8n editor keeps a persistent connection to the server, and without this parameter HAProxy would drop it after 50 seconds.
  • server n8n n8n:5678 check — the container address by the service name from docker-compose.yml and a health check.

Start the containers and check their status:

bash
cd ~/n8n-project
docker compose up -d
docker compose ps

Step 5: Let’s Encrypt certificate via acme.sh

HAProxy doesn’t serve files from disk, so it’s easier to pass domain validation in standalone mode: acme.sh temporarily runs its own web server on port 80. To free the port, HAProxy is stopped for a short time during issuance and renewal — this is done by the --pre-hook and --post-hook hooks that acme.sh remembers and runs at each renewal.

  1. Install acme.sh and socat (socat is required for standalone mode):

    bash
    sudo apt install -y socat
    curl https://get.acme.sh | sh -s email=admin@example.com
    source ~/.bashrc
  2. Choose the certificate authority. Since version 3.0 acme.sh defaults to ZeroSSL; if you need Let’s Encrypt, set it explicitly:

    bash
    ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
  3. Issue the certificate:

    bash
    ~/.acme.sh/acme.sh --issue --standalone -d n8n.example.com \
        --pre-hook  "docker compose -f $HOME/n8n-project/docker-compose.yml stop haproxy" \
        --post-hook "docker compose -f $HOME/n8n-project/docker-compose.yml start haproxy"
  4. Install the certificate for HAProxy. HAProxy expects the private key and the certificate chain in a single file. The command in --reloadcmd assembles this file and restarts HAProxy on each renewal:

    bash
    ~/.acme.sh/acme.sh --install-cert -d n8n.example.com \
        --key-file       ~/n8n-project/haproxy/certs/privkey.pem \
        --fullchain-file ~/n8n-project/haproxy/certs/fullchain.pem \
        --reloadcmd      "cat ~/n8n-project/haproxy/certs/fullchain.pem ~/n8n-project/haproxy/certs/privkey.pem > ~/n8n-project/haproxy/certs/n8n.pem && docker compose -f ~/n8n-project/docker-compose.yml restart haproxy"

Renewal is handled by a cron job that acme.sh added during installation; you can check it with crontab -l.


Step 6: Enable HTTPS in HAProxy

Update haproxy.cfg to the following:

ini
global
    log stdout format raw local0
    ssl-default-bind-options ssl-min-ver TLSv1.2

defaults
    log     global
    mode    http
    option  httplog
    option  forwardfor
    timeout connect 5s
    timeout client  50s
    timeout server  50s
    timeout tunnel  1h

frontend http_https_front
    bind :80
    bind :443 ssl crt /usr/local/etc/haproxy/certs/n8n.pem alpn h2,http/1.1

    # redirect everyone who came via HTTP to HTTPS
    http-request redirect scheme https code 301 unless { ssl_fc }

    acl host_n8n hdr(host) -i n8n.example.com
    use_backend n8n_backend if host_n8n

backend n8n_backend
    http-request set-header X-Forwarded-Proto https if { ssl_fc }
    server n8n n8n:5678 check

Check the syntax and apply the configuration:

bash
cd ~/n8n-project
docker compose run --rm haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
docker compose restart haproxy

Step 7: Verification

Open https://n8n.example.com. You should see a padlock in the address bar and the owner account setup wizard — complete it immediately before others discover the address. After logging in, open any Webhook node: the webhook URL should start with https://n8n.example.com/.

Maintenance

  • Updating n8n. Read the release notes, make a backup, change the image tag in docker-compose.yml, then:
    bash
    docker compose pull n8n
    docker compose up -d
  • Backups. You need three things: a database dump (docker compose exec postgres pg_dump -U n8n n8n > n8n.sql), the n8n-data volume, and the encryption key from .env.
  • Certificate is renewed automatically; on failure check the log ~/.acme.sh/acme.sh.log.
  • Scaling. When the number of processes grows, n8n is moved to queue mode with separate workers — see the article “n8n: Part 5 — Scaling”.

The same installation without HAProxy, using the FastPanel control panel, is described in the article “Installing n8n on FastPanel with Docker Compose”.

// Reviews

Related reviews

// Contact

Need help?

Get in touch with me and I'll help solve the problem

I reply within one business day (03:00-13:00 GMT)

Или оставьте заявку здесь:

Confirm that you are not a bot.

Write and get a quick reply