Docker & DevOps

Self-host n8n workflow automation with Docker

Run n8n on your own Linux VPS with Docker Compose and PostgreSQL, then publish the editor over HTTPS behind a reverse proxy.

IntermediateLinux server6 min read

To self-host n8n with Docker you run the official docker.n8n.io/n8nio/n8n image next to a PostgreSQL container, give n8n a fixed encryption key, and put a reverse proxy in front so the editor and webhooks are reachable over HTTPS. This guide walks through the whole stack on a Linux VPS, from the Compose file to the first login, and covers the mistakes that break webhooks or lock you out of your saved credentials.

n8n is a workflow automation tool: you connect triggers and nodes visually, and it runs the flows on a schedule or on incoming webhooks. Self-hosting keeps your credentials and data on your own machine instead of a vendor account.

Before you start

You need:

  • A Linux VPS with at least 2 GB RAM (4 GB is more comfortable if you run many workflows). Ubuntu 22.04/24.04, Debian 12, AlmaLinux 9 and Rocky 9 all work.
  • Root or sudo access over SSH.
  • A domain or subdomain such as n8n.example.com with an A record pointing at your server IP (203.0.113.10).
  • Docker Engine and the Compose plugin installed.

If Docker is not installed yet, follow Deploy a Self-Hosted App with Docker Compose from Scratch first, then come back.

Warning: n8n stores the credentials for every service you connect in an encrypted database. The encryption key lives in your environment. If you lose it, those credentials cannot be decrypted — you will have to re-enter them. Generate it once and keep a copy in your password manager.

Install Docker and prepare the host

Confirm Docker is present:

Bash
docker --version
docker compose version

Create a working directory and a dedicated environment file:

Bash
sudo mkdir -p /opt/n8n
cd /opt/n8n
openssl rand -hex 32

Copy the output — that is your encryption key. Put it in /opt/n8n/.env:

Bash
sudo nano /opt/n8n/.env
INI
N8N_ENCRYPTION_KEY=paste_the_hex_string_here
POSTGRES_PASSWORD=pick_a_long_random_password

Lock the file down so only root can read it:

Bash
sudo chmod 600 /opt/n8n/.env

Tip: Generate the database password with openssl rand -base64 24 as well. Do not reuse the encryption key as a password.

Create the n8n Docker Compose stack

Create /opt/n8n/docker-compose.yml:

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

  n8n:
    image: docker.n8n.io/n8nio/n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      - N8N_HOST=n8n.example.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - N8N_PROXY_HOPS=1
      - N8N_SECURE_COOKIE=true
      - WEBHOOK_URL=https://n8n.example.com/
      - GENERIC_TIMEZONE=Europe/London
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_RUNNERS_ENABLED=true
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
  n8n_data:

A few things worth understanding here:

  • PostgreSQL instead of SQLite. SQLite is the default and fine for a quick test, but Postgres handles concurrent executions and backups better. The DB_TYPE=postgresdb block switches n8n over.
  • 127.0.0.1:5678:5678. The editor is bound to loopback only, so nobody can reach port 5678 directly from the internet. The reverse proxy is the only way in.
  • depends_on with condition: service_healthy. n8n waits until Postgres accepts connections before starting. Without the healthcheck you get connection-refused errors on first boot.
  • WEBHOOK_URL. Webhook nodes print this URL to the outside world. If it is wrong, every webhook you publish points at the wrong host.

Warning: Do not change N8N_ENCRYPTION_KEY after the first start. n8n will report mismatching encryption keys and your stored credentials will not open.

Start the stack and create the owner account

Bring it up:

Bash
cd /opt/n8n
sudo docker compose up -d

Watch the logs until n8n reports that the editor is ready:

Bash
sudo docker compose logs -f n8n

Press Ctrl+C to stop following. Then check both containers:

Bash
sudo docker compose ps

Both should show running, and postgres should show healthy.

Publish the editor over HTTPS

n8n must be served over HTTPS. Two common options:

  1. Nginx Proxy Manager — a web UI for proxy hosts and Let's Encrypt certificates. See Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps.
  2. Plain Nginx with Certbot — if you prefer config files.

For the second option, install Nginx and Certbot, then create /etc/nginx/sites-available/n8n:

Nginx
server {
    listen 80;
    server_name n8n.example.com;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header 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_read_timeout 3600;
    }
}

Enable it and get a certificate:

Bash
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/n8n
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d n8n.example.com

The Upgrade and Connection headers matter: n8n pushes execution progress to the browser over a WebSocket. Without them the editor loads but runs appear to hang.

Tip: If you keep the editor private, restrict access at the proxy with an allowlist of your office IP, or put it behind a VPN. Webhook endpoints must stay publicly reachable, so exempt the /webhook/ path from any IP restriction.

Check it works

  1. Open https://n8n.example.com in a browser. You should see the n8n setup screen.
  2. Create the owner account with your email, first name, last name and a strong password. This account is the only way into the editor, so store it in your password manager.
  3. Build a trivial workflow: add a Manual Trigger, then a Set node, and click Test workflow. The node should turn green and show output data.
  4. Confirm the database is actually in use:
Bash
sudo docker compose exec postgres psql -U n8n -d n8n -c "\dt"

You should see n8n tables such as workflow_entity and credentials_entity.

  1. Confirm the encryption key is set inside the container:
Bash
sudo docker compose exec n8n printenv N8N_ENCRYPTION_KEY

If that returns nothing, n8n generated a key on first boot and stored it in the volume — fine, but back the volume up before you ever move the stack.

Troubleshooting

  • Editor loads but executions never finish, or the UI shows a connection error. The proxy is not forwarding WebSocket upgrades. Add proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade and proxy_set_header Connection "upgrade" to the location block, then reload Nginx.
  • "Mismatching encryption keys" on startup. The N8N_ENCRYPTION_KEY value changed, or the .env file is not being read. Restore the original key from your password manager and restart with sudo docker compose up -d.
  • ECONNREFUSED to postgres on boot. The database container is not ready or the password in .env differs from the one already stored in the postgres_data volume. Check sudo docker compose logs postgres; if you changed the password after the first start, either revert it or remove the volume and start fresh.
  • Webhook URLs point to localhost or the wrong domain. WEBHOOK_URL is missing or has a trailing slash problem. Set it to https://n8n.example.com/ and restart the container.
  • 502 Bad Gateway from the proxy. The n8n container is down or not listening on 5678. Run sudo docker compose ps and sudo docker compose logs n8n, and make sure the port mapping is still 127.0.0.1:5678:5678.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.