Install n8n with Docker Compose on a VPS
Set up n8n 2.x with Docker Compose and PostgreSQL on a VPS. Production-ready from the start with pinned versions, encryption key backup, health checks, and resource limits.
This guide installs n8n 2.12.3 with Docker Compose and PostgreSQL on a VPS. You will have a working n8n instance in about 15 minutes. The setup uses production defaults from the start: pinned image versions, encrypted credentials, localhost-only port binding, health checks on both containers, and Docker resource limits. No reverse proxy or SSL here. That is covered in .
What do I need before installing n8n?
You need a VPS running Debian 12 or Ubuntu 24.04 with at least 4 GB of RAM. Docker and Docker Compose (the docker compose plugin, not the old docker-compose binary) must be installed. If you have not set up Docker yet, follow Docker Compose for Multi-Service VPS Deployments first.
Verify Docker Compose is available:
docker compose version
Expected output:
Docker Compose version v2.x.x
If this command fails with docker: 'compose' is not a docker command, you have the legacy standalone binary. Install the Docker Compose plugin from the official Docker repository instead.
You also need a non-root user with sudo access. All commands in this guide run as that user, not root.
Why use PostgreSQL instead of SQLite for n8n?
PostgreSQL handles concurrent connections, supports WAL for crash recovery, and works with pg_dump for hot backups while n8n is running. SQLite locks the entire database file on every write. Under concurrent webhook executions, this causes timeouts and data corruption. You cannot safely back up a SQLite database while n8n is running without risking a corrupted copy. For anything beyond local testing, PostgreSQL is the right choice.
| Feature | SQLite | PostgreSQL |
|---|---|---|
| Concurrent writes | Single-writer lock | Full MVCC |
| Hot backups | Unsafe while running | pg_dump anytime |
| Crash recovery | Manual journal replay | Automatic WAL replay |
| Scaling | Single process | Connection pooling |
| n8n env var | DB_TYPE=sqlite |
DB_TYPE=postgresdb |
How do I install n8n with Docker Compose on a VPS?
Create a project directory, write a .env file with your secrets, write the docker-compose.yml, start the stack, and verify everything is healthy. Each step includes a verification check.
Create the project directory
mkdir -p ~/n8n && cd ~/n8n
Create the .env file
All secrets go in a .env file. Never hardcode passwords or keys in docker-compose.yml.
Generate a strong database password and the n8n encryption key:
echo "POSTGRES_PASSWORD=$(openssl rand -base64 32)" >> .env
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
Now add the remaining variables:
cat >> .env << 'EOF'
# PostgreSQL
POSTGRES_USER=n8n
POSTGRES_DB=n8n
# n8n
N8N_VERSION=2.12.3
N8N_HOST=localhost
N8N_PORT=5678
N8N_PROTOCOL=http
N8N_DIAGNOSTICS_ENABLED=false
GENERIC_TIMEZONE=UTC
EOF
Lock down the file permissions. Only your user should read this file:
chmod 600 .env
Verify:
ls -la .env
You should see -rw-------. Anyone else on the server cannot read your database password or encryption key.
How do I generate and back up the n8n encryption key?
The N8N_ENCRYPTION_KEY was generated above with openssl rand -hex 32. This produces a 32-byte (64 hex character) random key. n8n uses this key to encrypt every credential you store: API keys, OAuth tokens, database passwords inside workflows. If you lose this key, every stored credential becomes permanently unreadable. There is no recovery mechanism.
Back up the encryption key now. Copy it to a password manager or offline vault:
grep N8N_ENCRYPTION_KEY .env
Store the output somewhere safe outside this server. Do this before adding your first credential in n8n.
Environment variables reference
| Variable | Purpose | Example value |
|---|---|---|
POSTGRES_USER |
PostgreSQL superuser name | n8n |
POSTGRES_PASSWORD |
PostgreSQL superuser password | (generated, 32+ chars) |
POSTGRES_DB |
Database name | n8n |
N8N_VERSION |
Pinned n8n image tag | 2.12.3 |
N8N_ENCRYPTION_KEY |
Encrypts stored credentials | (generated, 64 hex chars) |
N8N_HOST |
Host for n8n UI | localhost |
N8N_PORT |
Port for n8n UI | 5678 |
N8N_PROTOCOL |
HTTP or HTTPS | http |
N8N_DIAGNOSTICS_ENABLED |
Send telemetry to n8n | false |
GENERIC_TIMEZONE |
Timezone for cron triggers | UTC |
Write the docker-compose.yml
cat > docker-compose.yml << 'COMPOSE'
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: unless-stopped
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_HOST}
N8N_PORT: ${N8N_PORT}
N8N_PROTOCOL: ${N8N_PROTOCOL}
N8N_DIAGNOSTICS_ENABLED: ${N8N_DIAGNOSTICS_ENABLED}
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
postgres_data:
n8n_data:
COMPOSE
A few things to notice in this file:
No version: key. Docker Compose V2 ignores it. Every competitor tutorial still includes version: '3.7' or version: '3.8'. The Compose specification deprecated this field. Including it produces a warning on current Docker versions.
Port bound to 127.0.0.1. The line "127.0.0.1:5678:5678" restricts n8n to localhost. This is a security requirement, not a preference. Docker port mappings bypass iptables and UFW rules entirely. If you write 5678:5678 without the 127.0.0.1 prefix, n8n is accessible from the internet even if your firewall blocks port 5678. A reverse proxy on the same machine will forward traffic to localhost:5678 after you set one up.
security_opt: no-new-privileges:true prevents processes inside the container from gaining additional privileges through setuid or setgid binaries. This is a defense-in-depth measure against container escape attacks.
Log rotation. The logging block limits each container's JSON log to 3 files of 10 MB each (30 MB max per service). Without this, Docker logs grow until your disk fills. On a VPS with limited storage, this matters.
Health checks on both services. PostgreSQL uses pg_isready. n8n uses its /healthz endpoint. The depends_on condition ensures n8n only starts after PostgreSQL passes its health check.
Resource limits. PostgreSQL gets 512 MB RAM and 1 CPU. n8n gets 2 GB RAM and 2 CPUs. These numbers work well on a 4-8 GB VPS. Adjust based on your server size and workflow complexity.
Named volumes. Both postgres_data and n8n_data are Docker-managed named volumes. Docker handles ownership and permissions inside the volume automatically. No need to create host directories or fix permissions manually.
restart: unless-stopped instead of restart: always. Both restart after crashes, but unless-stopped respects manual docker compose stop commands. With restart: always, a manually stopped container restarts if the Docker daemon restarts (e.g., after a system update).
Start the stack
cd ~/n8n
docker compose up -d
Watch the logs during first startup:
docker compose logs -f
Wait until you see n8n output a line containing n8n ready on. Press Ctrl+C to exit the log viewer.
Verify the installation
Check that both containers are running and healthy:
docker compose ps
Expected output:
NAME IMAGE ... STATUS PORTS
n8n-n8n-1 docker.n8n.io/n8nio/n8n:2.12.3 ... Up X minutes (healthy) 127.0.0.1:5678->5678/tcp
n8n-postgres-1 postgres:16-alpine ... Up X minutes (healthy)
Sharp eyes: both containers show (healthy) in the STATUS column. This confirms the health checks are passing. If you see (health: starting), wait 30 seconds and check again.
Test the n8n API from the server:
curl -s http://localhost:5678/healthz
Expected output:
{"status":"ok"}
Verify the port is only listening on localhost, not on all interfaces:
ss -tlnp | grep 5678
You should see 127.0.0.1:5678 in the output. If you see 0.0.0.0:5678, your port binding is wrong. Stop the stack, fix the ports line in docker-compose.yml, and restart.
How do I create the n8n owner account?
Open an SSH tunnel from your local machine to access n8n through your browser:
ssh -L 5678:127.0.0.1:5678 your-user@your-server-ip
Now open http://localhost:5678 in your browser. n8n shows a setup screen on first access. Create your owner account with a strong password. This account has full admin access to n8n.
After creating the account, close the SSH tunnel. Do not leave port 5678 tunneled longer than necessary. Set up a proper reverse proxy with SSL for regular access. See .
How do I verify that n8n is running correctly?
After creating your account and closing the SSH tunnel, run a final set of checks from the server.
Check container health:
docker compose ps --format "table {{.Name}}\t{{.Status}}"
Both services should show (healthy).
Check n8n logs for errors:
docker compose logs n8n --tail 20
Look for any ERROR lines. A clean startup shows database migration messages followed by n8n ready on.
Check PostgreSQL logs:
docker compose logs postgres --tail 10
You should see database system is ready to accept connections.
Check disk usage of your Docker volumes:
docker system df -v | grep -E "n8n|postgres"
This tells you how much space n8n and PostgreSQL are using. Check this periodically on VPS instances with limited storage.
Something went wrong?
Container exits immediately. Check logs with docker compose logs n8n. Common causes: missing .env file, wrong PostgreSQL password, or the encryption key contains special characters that break shell expansion. Regenerate your .env if needed.
Permission denied errors. The n8n container runs as UID 1000. If you switch from named volumes to bind mounts, ensure the host directory is owned by UID 1000: sudo chown -R 1000:1000 ./n8n_data.
Health check failing. n8n needs 20-30 seconds to start on first boot while it runs database migrations. If health checks fail, check docker compose logs n8n for migration errors. The start_period: 30s setting gives n8n time before health checks begin.
Cannot connect to n8n. The port is bound to localhost. You cannot access it from another machine without an SSH tunnel or reverse proxy. This is intentional.
Forgot the encryption key. If you lost N8N_ENCRYPTION_KEY and n8n has stored credentials, those credentials are gone. There is no recovery. This is why the backup step exists.
What should I do after installing n8n?
This install gives you a working n8n instance accessible only from the server itself. For production use, you need three more things:
-
Reverse proxy with SSL. Set up Nginx or Caddy in front of n8n with a TLS certificate. This gives you HTTPS access with a domain name. See .
-
Backups. Schedule automated backups of the PostgreSQL database and the n8n encryption key. See .
-
Updates. To update n8n, change
N8N_VERSIONin.envto the new version, then rundocker compose up -d. Docker pulls the new image and recreates the container. Always read the n8n release notes before updating.
For the parent guide covering workflow automation options on a VPS, see .
For Docker Compose fundamentals and managing multiple services, see Docker Compose for Multi-Service VPS Deployments.
Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.
Ready to try it yourself?
Deploy your own server in seconds. Linux, Windows, or FreeBSD.
See VPS Plans