// 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:
- HTTPS. The proxy terminates TLS and stores the certificate while n8n runs inside the server over plain HTTP.
- 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.
- 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 composeplugin). - 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
Update packages:
bashsudo apt update && sudo apt upgrade -yCheck Docker:
bashdocker --version docker compose versionIf the commands are missing, install Docker following the official guide for your OS.
Create the project directory and subdirectories:
bashmkdir -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.
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-32Generate 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.
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_USERandN8N_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
PGDATAline ensures Postgres 18 stores data in the attached volume. WEBHOOK_URLandN8N_PROTOCOL. Without them n8n will show webhook addresses likehttp://localhost:5678/..., and external services won’t be able to reach them.N8N_PROXY_HOPS=1tells n8n that one proxy is in front of it and to take the client address from theX-Forwarded-Forheader.- The
127.0.0.1:5678port 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:
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 checkoption forwardforadds theX-Forwarded-Forheader with the client’s address.timeout tunnel 1his 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 fromdocker-compose.ymland a health check.
Start the containers and check their status:
cd ~/n8n-project
docker compose up -d
docker compose psStep 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.
Install acme.sh and socat (socat is required for standalone mode):
bashsudo apt install -y socat curl https://get.acme.sh | sh -s email=admin@example.com source ~/.bashrcChoose 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 letsencryptIssue 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"Install the certificate for HAProxy. HAProxy expects the private key and the certificate chain in a single file. The command in
--reloadcmdassembles 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:
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 checkCheck the syntax and apply the configuration:
cd ~/n8n-project
docker compose run --rm haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
docker compose restart haproxyStep 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:bashdocker 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), then8n-datavolume, 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
As always, prompt and high-quality! I turn to Mikhail for server issues.
As always, prompt and high-quality! For server-related issues, I turn to Mikhail.
// 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)
Или оставьте заявку здесь:
// Related