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.
Why this setup
Section titled “Why this setup”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.
What you need
Section titled “What you need”- 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 havesshbuilt in. - The WireGuard app on the devices you will connect from (Windows, macOS, Android, iOS).
Step 1. Prepare the server
Section titled “Step 1. Prepare the server”Connect to the server and update it:
ssh root@YOUR_SERVER_IPapt update && apt upgrade -yInstall Docker with the official script. It also installs the docker compose plugin:
curl -fsSL https://get.docker.com | shdocker --versiondocker compose versionSet up a basic firewall. We only allow SSH and the WireGuard port:
apt install -y ufwufw allow 22/tcpufw allow 51820/udpufw enableStep 2. Create the project folder and secrets
Section titled “Step 2. Create the project folder and secrets”mkdir -p /opt/n8n-stack && cd /opt/n8n-stackGenerate two random secrets — a database password and the n8n encryption key:
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .envecho "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .envchmod 600 .envcat .envStep 3. Write the Docker Compose file
Section titled “Step 3. Write the Docker Compose file”Create docker-compose.yml:
nano docker-compose.ymlPaste this:
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:
docker compose up -ddocker compose psAll three services should show running (PostgreSQL also healthy). If n8n keeps restarting, check its log with docker compose logs n8n.
Step 4. Set up the VPN
Section titled “Step 4. Set up the VPN”The wg-easy admin panel listens only on the server’s localhost. To open it from your computer, use an SSH tunnel.
-
On your computer (not the server), open a terminal and run:
Terminal window ssh -L 51821:127.0.0.1:51821 root@YOUR_SERVER_IPKeep this window open.
-
In your browser, open
http://localhost:51821. The wg-easy setup wizard starts. -
Create the admin user and password.
-
When asked for the host, enter your server’s public IP address and keep port
51820. -
Add a client for each device — for example
laptopandphone. -
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.
Step 5. Open n8n
Section titled “Step 5. Open n8n”With the VPN active, open:
http://10.42.42.10:5678n8n 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.
Step 6. Backups
Section titled “Step 6. Backups”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:
cd /opt/n8n-stackmkdir -p backupsdocker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/n8n-$(date +%F).sql.gzRun 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.gzCopy 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.
Step 7. Updating n8n
Section titled “Step 7. Updating n8n”cd /opt/n8n-stackdocker compose exec -T postgres pg_dump -U n8n n8n | gzip > backups/before-update-$(date +%F).sql.gzdocker compose pull n8ndocker compose up -d n8nRead 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.
Troubleshooting
Section titled “Troubleshooting”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.
What’s next
Section titled “What’s next”- 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