Skip to content
+1 (813) 212-3723 support@layeronecloud.com
Self-hosting VPS guides

How to host n8n on a VPS with PostgreSQL and HTTPS

Run a production-minded n8n VPS with Docker Compose, private PostgreSQL, Caddy TLS, execution pruning, exact firewall rules, backups, and upgrades.

7 min read

n8n is a workflow automation platform that connects APIs, schedules jobs, runs webhooks, and stores third-party credentials. Self-hosting n8n gives you control of its runtime and data, but it also makes the VPS a privileged automation system. A compromised owner account or workflow can reach every credential and network destination that n8n can reach.

This guide deploys n8n's stable container with PostgreSQL and Caddy on Ubuntu 24.04 LTS. PostgreSQL stays on the private Compose network, n8n has no public host port, and Caddy is the only public web entry point.

n8n VPS requirements and LayerOne cost

Item Practical starting point
Minimum RAM 2 GB for a small n8n, PostgreSQL, and proxy stack without AI sandbox services
Recommended RAM 4 GB for concurrent executions, upgrades, and moderate binary data
Expected CPU usage Roughly 2–10% of one vCPU while the stack is idle; workflow or Code-node executions can briefly or continuously use every assigned core
Storage requirement 20 GB minimum; 40 GB or more when retaining executions or processing files
Exact LayerOne SKU gc.small — catalog slug layerone-4g, 2 vCPU, 4 GB RAM, 60 GB disk
Expected monthly cost $8.00/month equivalent, metered at $0.0109/hour while the VPS exists

n8n resource use follows workflow design, not just user count. A single large JSON payload, file transformation, unbounded loop, browser task, or parallel AI workflow can exceed this baseline. These CPU figures are operating estimates, not guarantees. Watch docker stats, queue time, execution duration, database growth, and the VPS monitoring guide. Use queue mode, external task runners, and separate workers only after measuring a need and following n8n's scaling documentation.

LayerOne bills hourly and usage can vary. The monthly equivalent is the catalog figure for a continuously existing server, not a fixed invoice guarantee. Check Pricing before ordering.

Before you install

Complete Your first hour on a new server on a fresh Ubuntu 24.04 LTS server. Install Docker Engine and Compose with Running Docker. Point a DNS A record such as automate.example.com to the VPS.

This stack does not include n8n's optional AI Assistant sandbox. Current n8n documentation specifies at least 4 GB and 2 vCPUs for that fuller sandbox stack; size up after measuring if you add it.

1. Set the LayerOne and host firewalls

On the server's LayerOne Networking tab, select inbound DROP and outbound ACCEPT. Allow TCP 22 only from your administration address, plus public TCP 80 and 443. Do not open n8n port 5678 or PostgreSQL port 5432. See The cloud firewall.

Mirror that policy with UFW:

sudo apt update
sudo apt install -y ufw openssl
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from YOUR.ADMIN.IP.ADDRESS to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Docker-published ports can bypass UFW. The upstream LayerOne firewall is a required second boundary. The Compose file below publishes only Caddy's ports.

2. Create secrets and persistent storage

sudo mkdir -p /opt/n8n
sudo chown -R "$USER":"$USER" /opt/n8n
cd /opt/n8n
install -m 600 /dev/null .env
N8N_KEY=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 32)
{
  echo 'N8N_HOST=automate.example.com'
  echo "N8N_ENCRYPTION_KEY=$N8N_KEY"
  echo "POSTGRES_PASSWORD=$DB_PASSWORD"
  echo 'GENERIC_TIMEZONE=America/New_York'
} > .env
unset N8N_KEY DB_PASSWORD

Replace the hostname and timezone in .env. The n8n encryption key protects stored credentials; losing it can make a database backup useless. Keep .env mode 600 and save the key in an off-server password manager.

Create /opt/n8n/compose.yaml:

services:
  postgres:
    image: postgres:16-alpine
    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: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n:stable
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    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_HOST: "${N8N_HOST}"
      N8N_PORT: "5678"
      N8N_PROTOCOL: https
      WEBHOOK_URL: "https://${N8N_HOST}/"
      N8N_PROXY_HOPS: "1"
      N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY}"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
      N8N_RUNNERS_ENABLED: "true"
      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: "336"
      GENERIC_TIMEZONE: "${GENERIC_TIMEZONE}"
      TZ: "${GENERIC_TIMEZONE}"
    volumes:
      - n8n_data:/home/node/.n8n
    expose:
      - "5678"

  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    depends_on:
      - n8n
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config

volumes:
  postgres_data:
  n8n_data:
  caddy_data:
  caddy_config:

Execution pruning is set to 336 hours, or 14 days. Change it to match your audit and troubleshooting needs, but do not retain every successful execution by accident. Binary payloads and database rows can fill a disk quickly.

Create /opt/n8n/Caddyfile:

automate.example.com {
    encode zstd gzip
    reverse_proxy n8n:5678
}

Make the hostname match .env exactly.

3. Start n8n and create the owner

cd /opt/n8n
docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail=100 postgres n8n caddy

Open https://automate.example.com immediately and create the instance owner. Use a unique password and enable MFA. Do not leave an unclaimed first-run instance exposed.

Verify HTTPS, health, and sockets:

curl -fsS https://automate.example.com/healthz
docker compose ps
sudo ss -lntp | grep -E ':(80|443|5678|5432)[[:space:]]'

Only 80 and 443 should be public listeners. Ports 5678 and 5432 should not appear on 0.0.0.0. Create a manual workflow with a Schedule Trigger and a Set node, run it, activate it, and confirm a scheduled execution. If the service receives external webhooks, use a disposable webhook workflow and verify its production URL uses the HTTPS hostname rather than an internal container name.

4. Control execution and file growth

Review execution settings in the n8n UI. Save failed executions when they help diagnosis, limit successful-execution retention, and avoid storing large binary payloads longer than necessary. Check growth regularly:

docker system df
docker compose exec postgres psql -U n8n -d n8n -c 'SELECT pg_size_pretty(pg_database_size(current_database()));'
df -h /var/lib/docker

Do not use docker system prune --volumes; the named volumes are the database and application state. Apply memory and concurrency limits based on measured workflows, not a guessed global value that kills legitimate jobs.

5. Back up n8n correctly

An n8n recovery needs both its PostgreSQL data and the same N8N_ENCRYPTION_KEY. Workflow JSON exports alone do not preserve usable stored credentials.

Pause n8n so its application volume cannot change while PostgreSQL is dumped, then create private, timestamped archives. The subshell restarts n8n even if a backup command fails, and pg_restore --list rejects a malformed custom dump:

cd /opt/n8n
(
  set -eu
  BACKUP_STAMP=$(date -u +%Y%m%dT%H%M%SZ)
  install -d -m 700 backups
  umask 077
  docker compose stop n8n
  trap 'docker compose start n8n' EXIT
  docker compose exec -T postgres pg_dump -U n8n -d n8n -Fc > "backups/n8n-${BACKUP_STAMP}.dump"
  docker compose exec -T postgres pg_restore --list < "backups/n8n-${BACKUP_STAMP}.dump" >/dev/null
  docker run --rm     -v n8n_n8n_data:/data:ro     -v "$PWD/backups":/backup     alpine sh -c 'umask 077; tar -czf "/backup/n8n-data-${1}.tar.gz" -C /data .' _ "$BACKUP_STAMP"
  sudo chown "$(id -u):$(id -g)" "backups/n8n-data-${BACKUP_STAMP}.tar.gz"
  tar -czf "backups/n8n-config-${BACKUP_STAMP}.tar.gz" .env compose.yaml Caddyfile
)

Compose normally names the volume n8n_n8n_data from the project directory. Confirm with docker volume ls; substitute the actual name if it differs. Encrypt and transfer all three artifacts off the VPS. Restrict the config archive because it contains the database password and encryption key. LayerOne does not create backups; see Backups and snapshots.

To test restoration, create an isolated fresh stack using the saved .env, start PostgreSQL, stop n8n, load the dump with pg_restore, restore the n8n data volume, and start n8n. Confirm workflow definitions, credential decryption, webhook URLs, and a real test execution. Never point two restored copies at the same live webhook or schedule set.

6. Upgrade safely

n8n releases frequently. Read the release and breaking-change notes, take an off-server backup, then update the stable channel:

cd /opt/n8n
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 n8n postgres
curl -fsS https://automate.example.com/healthz

Keep n8n main, workers, and external runners on the same version if you later add those components. Test login, credential access, one manual workflow, one scheduled workflow, and one webhook before pruning old images.

Security pitfalls

  • Treat workflow edit access as code execution and credential access. Grant it only to trusted operators.
  • Review community nodes before installation; they execute inside the n8n environment. Remove unused nodes and credentials.
  • Run n8n's documented security audit periodically: docker compose exec n8n n8n audit.
  • Restrict or disable risky nodes such as Execute Command when the instance does not need them, and follow n8n's runner-hardening guidance for untrusted code.
  • Do not expose PostgreSQL, n8n port 5678, Docker, or a Traefik/Caddy admin API.
  • Rotate any secret that appears in execution data, logs, screenshots, exports, or source control.

Official and LayerOne references

Still stuck

Chat with us from the portal.

Ask the assistant from the Chat bar. During business hours you can ask for a person and a human joins live.