Install n8n with Docker Compose on a VPS

8 min read·Matthieu|

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:

  1. 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 .

  2. Backups. Schedule automated backups of the PostgreSQL database and the n8n encryption key. See .

  3. Updates. To update n8n, change N8N_VERSION in .env to the new version, then run docker 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