Skip to content

Self-host n8n with Docker, PostgreSQL and WireGuard

This is the setup I run myself: n8n for automations, PostgreSQL as its database, and a WireGuard VPN so that the n8n editor is reachable only from my own devices. Nothing except SSH and the VPN port is open to the internet.

By the end of this guide you will have:

  • n8n running in Docker on your own server
  • PostgreSQL instead of the default SQLite (better for reliability, backups and growth)
  • a private VPN that lets you open n8n from your laptop or phone
  • a simple backup routine

Time needed: about 45 minutes. Cost: a VPS from about €5–11 per month, plus any provider-specific network or backup charges.

Self-hosting vs n8n Cloud. n8n Cloud is the easiest option: no server, no updates, no backups to think about. Self-hosting is cheaper once you run many workflows, and your data stays on your server. If you don’t want to manage a server at all, n8n Cloud is the better choice.

Why PostgreSQL. n8n works out of the box with SQLite, a single file. PostgreSQL is a common choice for a production deployment and supports tools such as pg_dump for backups while the service is running. SQLite can also be backed up; PostgreSQL replication to a second server is possible but requires separate configuration.

Why a VPN instead of a public URL. The n8n editor holds credentials to all your connected services. Keeping it off the public internet removes a whole class of risks: brute-force logins, scanners, zero-day exploits. You connect to the VPN and open n8n like a local app.

  • A VPS with at least 2 CPU cores and 4 GB RAM. I use a Netcup RS 1000 G12 (4 dedicated cores, 8 GB RAM, 256 GB NVMe) — more than enough for n8n plus a few other projects.
  • Ubuntu 24.04 (or Debian 12) installed on the server.
  • SSH access as root or a user with sudo. Windows 10/11, macOS and Linux all have ssh built in.
  • The WireGuard app on the devices you will connect from (Windows, macOS, Android, iOS).

Connect to the server and update it:

Terminal window
ssh root@YOUR_SERVER_IP
apt update && apt upgrade -y

Install Docker with the official script. It also installs the docker compose plugin:

Terminal window
curl -fsSL https://get.docker.com | sh
docker --version
docker compose version

Set up a basic firewall. We only allow SSH and the WireGuard port:

Terminal window
apt install -y ufw
ufw allow 22/tcp
ufw allow 51820/udp
ufw enable

Step 2. Create the project folder and secrets

Section titled “Step 2. Create the project folder and secrets”
Terminal window
mkdir -p /opt/n8n-stack && cd /opt/n8n-stack

Generate two random secrets — a database password and the n8n encryption key:

Terminal window
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
chmod 600 .env
cat .env

Create docker-compose.yml:

Terminal window
nano docker-compose.yml

Paste this:

/opt/n8n-stack/docker-compose.yml
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: n8n
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 5s
timeout: 5s
retries: 10
networks:
- internal
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: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: 10.42.42.10
N8N_PORT: 5678
N8N_PROTOCOL: http
# n8n is reached over plain HTTP inside the VPN (traffic is already encrypted by WireGuard)
N8N_SECURE_COOKIE: "false"
GENERIC_TIMEZONE: Europe/Kyiv
TZ: Europe/Kyiv
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
networks:
internal:
ipv4_address: 10.42.42.10
wireguard:
image: ghcr.io/wg-easy/wg-easy:15
restart: unless-stopped
environment:
# The admin UI is HTTP over an SSH tunnel; required for HTTP cookies in wg-easy v15.
INSECURE: "true"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
ports:
- "51820:51820/udp"
# admin panel: localhost only, opened through an SSH tunnel
- "127.0.0.1:51821:51821/tcp"
volumes:
- wireguard_data:/etc/wireguard
- /lib/modules:/lib/modules:ro
networks:
- internal
networks:
internal:
ipam:
config:
- subnet: 10.42.42.0/24
volumes:
postgres_data:
n8n_data:
wireguard_data:

Change Europe/Kyiv to your own time zone — it controls when scheduled workflows run.

How it fits together: all three containers share one private Docker network, 10.42.42.0/24. n8n always gets the address 10.42.42.10. When you connect to the VPN, the WireGuard container forwards your traffic into this network, so http://10.42.42.10:5678 opens n8n — but only for VPN users.

Start everything:

Terminal window
docker compose up -d
docker compose ps

All three services should show running (PostgreSQL also healthy). If n8n keeps restarting, check its log with docker compose logs n8n.

The wg-easy admin panel listens only on the server’s localhost. To open it from your computer, use an SSH tunnel.

  1. On your computer (not the server), open a terminal and run:

    Terminal window
    ssh -L 51821:127.0.0.1:51821 root@YOUR_SERVER_IP

    Keep this window open.

  2. In your browser, open http://localhost:51821. The wg-easy setup wizard starts.

  3. Create the admin user and password.

  4. When asked for the host, enter your server’s public IP address and keep port 51820.

  5. Add a client for each device — for example laptop and phone.

  6. On the laptop: download the client’s config file and import it into the WireGuard app. On the phone: scan the client’s QR code with the WireGuard app.

Activate the tunnel in the WireGuard app. You can now close the SSH window.

With the VPN active, open:

http://10.42.42.10:5678

n8n asks you to create the owner account. Use a strong password — even behind a VPN, this account controls everything.

Test that it works: create a workflow with a Schedule Trigger set to every minute, activate it, and check the Executions list a couple of minutes later.

Back up three things: the database, the .env file with the encryption key, and the n8n data volume.

A database dump you can run any time — n8n keeps working:

Terminal window
cd /opt/n8n-stack
mkdir -p backups
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/n8n-$(date +%F).sql.gz

Run it automatically every night at 03:00 with cron (crontab -e):

0 3 * * * cd /opt/n8n-stack && docker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/n8n-$(date +\%F).sql.gz

Copy the backups somewhere other than this server. Make sure the account running this cron job can access Docker and write to /opt/n8n-stack/backups (use root’s crontab or configure equivalent permissions). If your provider offers server snapshots, take one before a major change; snapshots complement, but do not replace, off-server database and secrets backups.

Terminal window
cd /opt/n8n-stack
docker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/before-update-$(date +%F).sql.gz
docker compose pull n8n
docker compose up -d n8n

Read the n8n release notes before big version jumps: some versions contain breaking changes. If you prefer controlled updates, replace latest in the image name with a specific version number.

The n8n page doesn’t open. Check that the WireGuard tunnel is active in the app. Then check on the server: docker compose ps — is n8n running?

n8n shows a “secure cookie” warning and won’t log in. Make sure N8N_SECURE_COOKIE: "false" is in the n8n section, then run docker compose up -d again.

The wg-easy page does not load or login fails. Keep the SSH tunnel open, confirm INSECURE: "true" is set for the HTTP-only admin UI, then run docker compose up -d wireguard. Do not publish TCP 51821 to the internet.

wg-easy doesn’t start and mentions modules. Some minimal server images lack the WireGuard kernel module. Run apt install -y linux-modules-extra-$(uname -r) (Ubuntu) or reboot after a kernel update, then docker compose up -d.

Scheduled workflows run at the wrong time. Check GENERIC_TIMEZONE in docker-compose.yml, and the time zone in the workflow settings.

  • Expose only webhooks to the internet while keeping the editor private
  • Add a second server with a PostgreSQL replica for failover
  • Build your first useful workflows — see the Workflows section