Quick answer

Run n8n with PostgreSQL on a private Docker network and put HTTPS in front of it. Create the owner account before exposing the service. Pass N8N_ENCRYPTION_KEY explicitly to both main and worker containers. Existing installations must preserve their original key. Back up that key and the database together, then test an isolated restore.

By LK Wood IV · 2026-06-09 · ~11 min read · St. Louis County, MO

Docker topology diagram of a self-hosted n8n stack: HTTPS traffic enters through Nginx Proxy Manager on the proxy network and forwards to the n8n container (n8nio/n8n pinned to a specific tag, port 5678, ~180 MB), which connects over the private n8n-internal network to PostgreSQL 16 for workflows and encrypted credentials, plus optional Redis 7 and an n8n-worker for queue mode; n8n also reaches LAN homelab services like Proxmox and Grafana, with cron pg_dump backups, totaling ~255 MB RAM idle

n8n is a workflow automation tool with 400+ integrations — the self-hosted alternative to Zapier, Make, and IFTTT. The hosted version charges per workflow execution. Self-hosted runs on your own machine with no execution limits, full access to internal services, and no data leaving your network.

This guide sets up n8n with PostgreSQL, persistent storage, HTTPS via Nginx Proxy Manager, and automated backups.

What you’ll have at the end

  • n8n running in Docker with PostgreSQL as the database
  • Accessible at n8n.yourdomain.com with valid HTTPS
  • Automatic backups of the n8n database and workflows
  • Ready to connect to internal homelab services (Proxmox API, Grafana, Home Assistant)

Prerequisites

  • Docker installed and running on your homelab host
  • Nginx Proxy Manager already set up with a wildcard SSL cert (or you’ll use direct IP access)
  • A proxy Docker network (created by the NPM guide): docker network create proxy

Step 1: Docker Compose setup

mkdir -p /opt/stacks/n8n && cd /opt/stacks/n8n

Create the compose file with n8n and PostgreSQL:

# /opt/stacks/n8n/docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: n8n
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ./postgres:/var/lib/postgresql/data
    networks:
      - n8n-internal
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n"]
      interval: 5s
      timeout: 5s
      retries: 5

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      # Database
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}
      N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY:?Preserve the existing key or set a key for a new installation}"
      # Server config
      N8N_HOST: n8n.yourdomain.com
      N8N_PORT: 5678
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://n8n.yourdomain.com/
      # Timezone
      GENERIC_TIMEZONE: America/Chicago
      TZ: America/Chicago
    volumes:
      - ./n8n:/home/node/.n8n
    networks:
      - n8n-internal
      - proxy

networks:
  n8n-internal:
    driver: bridge
  proxy:
    external: true

For a new installation only, create the environment file below. It refuses to overwrite an existing .env or data directory. Run it before creating the data directories. Install OpenSSL first if openssl is unavailable.

(
  set -e
  umask 077
  if [ -e .env ] || [ -e ./n8n ] || [ -e ./postgres ]; then
    printf 'Existing settings or data found. Preserve the current keys; use the migration note below.\n' >&2
    exit 1
  fi
  db_password=$(openssl rand -hex 32)
  encryption_key=$(openssl rand -hex 32)
  set -o noclobber
  printf 'DB_PASSWORD=%s\nN8N_ENCRYPTION_KEY=%s\n' \
    "$db_password" "$encryption_key" > .env
  chmod 600 .env
)

For an existing installation, keep its key. First back up the database, .env, and ./n8n privately. If N8N_ENCRYPTION_KEY is already supplied to the running service, preserve that exact value. Otherwise retain the encryptionKey value from the existing ./n8n/config file. Put that same value in .env using a private editor, preserving the current DB_PASSWORD. Keep keys out of logs. Do not paste them into support requests.

The environment entries above and in the worker block below pass the key into both containers. A Compose .env file supplies interpolation values; it does not inject them into a container by itself. Stop the main process and any workers for the configuration change, then recreate them together with the unchanged key. Check an existing credential afterward. If the original key is lost, generating another cannot decrypt the old credentials.

Create the data directories:

mkdir -p ./postgres ./n8n

Start the stack:

docker compose up -d

# Watch logs to confirm startup
docker compose logs -f n8n

On first startup, n8n runs database migrations which takes 30–60 seconds. You’ll see “Workflow manager is now running” when it’s ready.

Access n8n at http://your-host-ip:5678 or configure NPM to serve it at https://n8n.yourdomain.com.

Create the owner account before anything else can reach it. n8n’s only built-in authentication is its own user management: the first browser to load the instance is offered the owner-account setup form, and until you complete it there is no login wall. The old N8N_BASIC_AUTH_ACTIVE / _USER / _PASSWORD variables are gone — they appear in a lot of older homelab compose files, and on a current image they are silently ignored rather than rejected, so a stack that looks password-protected is not. Keep port 5678 on the LAN, load the UI, set the owner e-mail and password, and only then point a public hostname at it.

Pin the n8n version — before going to production, pin to a specific tag:

    image: n8nio/n8n:2.35.7    # current stable on 2026-08-22; check hub.docker.com/r/n8nio/n8n/tags

This prevents surprise breaking changes from :latest upgrades. Pick whatever the current stable is on the day you install rather than copying the number above — it ages.

Coming from an n8n 1.x install? 2.0 (released 2025-12-08) is a real major, not a point release. It enforces settings-file permissions by default, disables the Execute Command and Local File Trigger nodes unless you opt back in, drops the N8N_CONFIG_FILES variable and the --tunnel option, and removes the in-memory binary-data mode. Back up ./n8n and the database, then read n8n’s release notes before you bump the tag.

Step 2: Configure Nginx Proxy Manager

In NPM → Add Proxy Host:

  • Domain: n8n.yourdomain.com
  • Scheme: http
  • Forward hostname: n8n (container name, on the proxy network)
  • Port: 5678
  • Websockets Support: ON (n8n uses websockets for the editor)
  • SSL: wildcard cert, Force SSL on

Visit https://n8n.yourdomain.com — you should see the n8n sign-in page (or the owner-account setup form, if you skipped it above).

Step 3: First login and owner account

If you did not create the owner account in Step 1, n8n shows the setup form now. The owner is the n8n admin account and the main login wall the instance has, so give it a strong, unique password. n8n does support two-factor authentication with an authenticator app under Settings > Personal, and turning it on is worth the minute it costs. What there is no longer is a separate basic-auth layer to switch on or off — the N8N_BASIC_AUTH_* variables in older guides are ignored by current images. I would put any second layer in NPM, as an access list restricted to your LAN and Tailscale ranges, not in n8n.

Step 4: Connect your first integration

n8n’s integration list is at the bottom-left “Credentials” section. To connect a service:

  1. Click “Add Credential” → search for your service (Gmail, Slack, GitHub, Airtable, etc.)
  2. Follow the OAuth flow or paste the API key
  3. Credentials are stored encrypted in the PostgreSQL database

Connecting to homelab services:

n8n can reach any LAN IP directly. In an HTTP Request node:

  • URL: http://192.168.1.2:8006/api2/json/nodes (Proxmox API)
  • Authentication: Header Auth with Authorization: PVEAPIToken=user@pam!name=token-value (pam is the default Proxmox login backend)

This works because n8n is running inside your LAN — the request goes directly to Proxmox without any internet hop.

Step 5: Example workflows

Workflow 1: Notify when a Proxmox VM stops (via Grafana alert webhook)

  1. Trigger: Webhook (n8n generates a URL you paste into Grafana Alertmanager)
  2. Node: IF → check if alert status is “firing”
  3. Node: Send notification to Discord/Telegram/email

Workflow 2: Daily Homelab health summary

  1. Trigger: Schedule → daily at 8am
  2. HTTP Request: http://uptime-kuma:3001/api/status-page/... → get service status
  3. HTTP Request: Proxmox API → get VM/LXC status
  4. Function: format the data into a summary
  5. Notification: send to Discord/Telegram

Workflow 3: Auto-backup trigger after Immich import

  1. Trigger: Webhook (call from an immich-go post-import script)
  2. Execute Command node: run your restic backup script
  3. Notification: send completion status to Slack

Step 6: Queue mode (optional, for reliability)

Queue mode offloads workflow execution to separate worker processes via Redis. This prevents the main n8n process from being blocked by long-running workflows and recovers from crashes without losing in-progress jobs.

Add Redis to the compose file:

  redis:
    image: redis:7-alpine
    container_name: n8n-redis
    restart: unless-stopped
    networks:
      - n8n-internal
    volumes:
      - ./redis:/data

Update n8n’s environment in the compose file:

      EXECUTIONS_MODE: queue
      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379

Add a worker service:

  n8n-worker:
    image: n8nio/n8n:latest   # same version as n8n
    container_name: n8n-worker
    restart: unless-stopped
    command: worker
    depends_on:
      - n8n
      - redis
      - postgres
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_PORT: 5432
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}
      N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY:?Preserve the existing key or set a key for a new installation}"
      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
    volumes:
      - ./n8n:/home/node/.n8n
    networks:
      - n8n-internal

Redeploy:

docker compose up -d

Step 7: Backups

n8n data lives in two places:

  1. PostgreSQL database — workflows, credentials, execution history
  2. ./n8n volume — local files, SSH keys, custom nodes

Database backup:

# Manual backup
docker exec n8n-postgres pg_dump -U n8n n8n > /mnt/backups/n8n/n8n-$(date +%Y%m%d).sql

# Automated via cron
echo "0 4 * * * root docker exec n8n-postgres pg_dump -U n8n n8n > /mnt/backups/n8n/n8n-\$(date +\%Y\%m\%d).sql" >> /etc/crontab

Credentials in the database need the same encryption key after restoration. Back up the database, .env, and ./n8n/config together with access restricted to you. In this setup the explicitly supplied N8N_ENCRYPTION_KEY is part of the recovery material. A key n8n generated for an older installation may instead be in its config file.

n8n requires the main process and workers to share the same key. Restore that key before starting a recovery instance. Check an existing credential in an isolated restore; keeping a new random string in .env is not a substitute for the key that encrypted the backup.

Resource usage

On a Debian 12 LXC with PostgreSQL, n8n idle, 15 active workflows:

ServiceRAM
n8n~180 MB
PostgreSQL~60 MB
Redis (if using queue mode)~15 MB
Total~255 MB

n8n is not resource-heavy at idle. CPU usage spikes during workflow execution proportional to the complexity of the workflow, not the number of workflows defined.


n8n pairs well with Uptime Kuma for monitoring-triggered workflows — the Docker Compose starter stack has both running on the same proxy network. For the broader self-hosted app ecosystem n8n fits into, see the 12 best self-hosted apps guide.

Sources

Frequently asked questions

Why self-host n8n instead of using n8n cloud?
n8n cloud starts at $20/month and caps workflow executions. Self-hosted n8n has no execution limits, no per-step pricing, and runs all workflows locally — useful if your workflows access internal services (Homelab APIs, internal databases, Proxmox), handle sensitive data you don’t want leaving your network, or run frequently enough that cloud pricing becomes significant.
Do I need queue mode for n8n?
Queue mode uses Redis to offload workflow execution to worker processes, enabling parallel execution and recovery from crashes without losing running jobs. For a personal homelab with low-volume automations, the default main mode is fine — it’s simpler and uses less RAM. Queue mode is worth the overhead if you run workflows on a schedule that overlap in time, have long-running workflows (more than a few minutes), or want the reliability guarantees for production-level integrations.
Can n8n access services inside my LAN?
Yes. Because n8n runs on your homelab host, it can reach any LAN service by IP or hostname. HTTP Request nodes can call your Proxmox API, your Grafana instance, your NAS, or any internal service without exposing those services to the internet. This is one of the primary advantages of self-hosted n8n over cloud-based automation tools.
How do I update self-hosted n8n?
Pull the new image and restart the container: ‘docker compose pull && docker compose up -d’. n8n handles database migrations automatically on startup. Back up your n8n data directory before updating — schema changes between major versions occasionally require manual intervention, and a backup means you can roll back.
What database does n8n use?
n8n defaults to SQLite for simple single-user setups. For anything more than light personal use, switch to PostgreSQL — it handles concurrent workflow executions and larger credential/execution histories without the locking issues SQLite has under concurrent access. The guide below uses PostgreSQL.

Evidence ledger

Last updated
Methodology
See our methodology for research and review standards.
Update log
  • 2026-09-08 — Corrected encryption-key recovery advice: .env values were never passed into the n8n container. Main and worker environment blocks now explicitly require N8N_ENCRYPTION_KEY. New-install generation refuses existing settings/data; existing installations must preserve their effective key and database password. Docker environment and n8n shared-key documentation checked 2026-09-08. No real credentials were accessed and no full n8n restore was performed.
  • 2026-08-22 — Version-pin example refreshed from 2.34.6 (dated 2026-08-14) to 2.35.7, published 2026-08-21 per the GitHub releases API, checked 2026-08-22. The surrounding advice is unchanged and still tells you to pin whatever is current on the day you install rather than copying this number.
  • 2026-08-22 — Removed two leftover basic-auth references (Step 2’s closing line and all of Step 3) that survived the 2026-08-14 correction: they told readers to expect a basic-auth challenge and to ‘disable n8n’s built-in basic auth’, although the same page had already established that the N8N_BASIC_AUTH_* variables are gone from n8n’s environment-variable reference and are ignored by current images. Step 3 now describes the owner-account form as the only login wall and points a second layer at NPM access lists.
  • 2026-08-14 — Removed the N8N_BASIC_AUTH_ACTIVE / _USER / _PASSWORD block that this guide shipped under a ‘# Security’ comment, and the two matching .env lines. n8n’s current environment-variable reference (docs.n8n.io/deploy/host-n8n/configure-n8n/basic-configuration/use-environment-variables/deployment, retrieved 2026-08-14) documents no BASIC_AUTH variable at all – the full variable index returns zero matches. A current n8n image ignores those variables silently rather than erroring, so the stack described here looked password-protected and was not, which mattered because the same guide then proxies it to a public hostname. Replaced with n8n’s actual auth model: the owner account created on first load. Also re-pinned the example image tag from 1.87.0 to 2.34.6 (n8n GitHub releases, retrieved 2026-08-14) and added the n8n 2.0 breaking-change note – 2.0 shipped 2025-12-08 and enforces settings-file permissions, disables Execute Command and Local File Trigger by default, drops N8N_CONFIG_FILES and –tunnel, and removes in-memory binary data mode, so a tag bump from a 1.x install is not a drop-in. Version strings removed from the diagram and its alt text so the picture cannot drift from the compose block again.
Corrections
Spotted an error or a stale number? Email hello@techfuelhq.com. Confirmed corrections are added to the update log above.

About the author

Written by Lowell K. Wood IV, who builds and runs TechFuelHQ from St. Louis, Missouri.