n8n

Self-hosted workflow automation with a Postgres backend.

AutomationbeginnerProduction-ready defaults2 services

The stack

Generated output

docker-compose.yml
services:
  n8n:
    image: docker.n8n.io/n8nio/n8n:1.68.1@sha256:5e8acf8527ddf9d08a4f0e7ac042eb3fb31a6afa071ed2b8be2e47327b0acd49
    restart: unless-stopped
    depends_on:
      - n8n-postgres
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: n8n-postgres
      DB_POSTGRESDB_PORT: "5432"
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_HOST: ${N8N_HOST}
      N8N_PORT: "5678"
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://${N8N_HOST}/
      GENERIC_TIMEZONE: ${TZ}
      N8N_RUNNERS_ENABLED: "true"
      N8N_BLOCK_ENV_ACCESS_IN_NODE: "true"
    volumes:
      - n8n-data:/home/node/.n8n
  n8n-postgres:
    image: postgres:16-alpine@sha256:16bc17c64a573ef34162af9298258d1aec548232985b33ed7b1eac33ba35c229
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: n8n
      POSTGRES_INITDB_ARGS: --data-checksums
    volumes:
      - n8n-postgres-data:/var/lib/postgresql/data
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -U n8n -d n8n
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  n8n-data: {}
  n8n-postgres-data: {}

Replace n8n.example.com with your domain. Generate secrets below.

Environment

.env with generated secrets

.env
# Public hostname n8n is served on. Used for webhook URLs.
# Timezone for cron triggers (IANA tz name).
# PostgreSQL password for the n8n user.
# Encrypts stored credentials. NEVER change this after first boot — all stored credentials become unreadable.

Secrets are generated in your browser via crypto.getRandomValues. Nothing is sent anywhere.

Server-rendered .env template
# Public hostname n8n is served on. Used for webhook URLs.
N8N_HOST=n8n.example.com

# Timezone for cron triggers (IANA tz name).
TZ=UTC

# PostgreSQL password for the n8n user.
DB_PASSWORD=

# Encrypts stored credentials. NEVER change this after first boot — all stored credentials become unreadable.
N8N_ENCRYPTION_KEY=

About

What is n8n?

n8n is an open-source workflow automation tool — Zapier or Make.com that you run yourself. Drag nodes onto a canvas, connect APIs, run on schedule or webhook. The default SQLite backend is fine for trying it out, but anything you actually rely on belongs on Postgres: SQLite locks the entire database during execution writes, which becomes a bottleneck as soon as you have a few concurrent workflows. This stack runs n8n with a dedicated Postgres 16 instance, persistent volumes for both, and a hardened set of environment defaults: encryption key for credentials, runners enabled, and the database not exposed to the host. The reverse proxy block sends `n8n.example.com` to the n8n container on port 5678.

Requirements

Before you start

  • 1 GB RAM minimum, 2 GB recommended.
  • Docker 24+ and Docker Compose v2.
  • A persistent disk — losing the n8n volume means losing all workflows and credentials.
  • Stable system time — cron triggers depend on it.

Deploy

How to deploy

  1. Generate `DB_PASSWORD` and `N8N_ENCRYPTION_KEY` via the "Generate secrets" button.
  2. Back up `N8N_ENCRYPTION_KEY` somewhere outside the host now. You cannot recover credentials without it.
  3. Set `N8N_HOST` to the public hostname (e.g. `n8n.example.com`).
  4. Start the stack: `docker compose up -d`.
  5. Open `https://n8n.example.com` and create the owner account on first visit.

Errors

Common errors & fixes

Webhook URLs in workflows show `http://localhost:5678/...` instead of the public URL.

You did not set `WEBHOOK_URL` (or set `N8N_HOST` wrong). The compose template sets `WEBHOOK_URL=https://${N8N_HOST}/`. Verify your `.env` has the correct `N8N_HOST`.

After restart, all stored credentials show "could not decrypt".

Your `N8N_ENCRYPTION_KEY` changed (likely auto-generated on first boot, then replaced). Restore the original key from backup. If lost, all credentials must be re-entered.

"relation does not exist" errors after upgrading.

n8n runs migrations on startup. If you skipped a major version, jump to the intermediate release first. Check the version-migration notes in n8n docs.

Long-running workflows time out at exactly 5 minutes.

Your reverse proxy timeout is shorter than the workflow. Increase Caddy `request_timeout` or the Traefik `responseHeaderTimeout` for this route.

Limitations

Honest limitations

  • Single-instance only in this compose — n8n queue mode with workers requires Redis and is out of scope here.
  • No external SMTP configured by default — workflows that send email need credentials added via the UI.
  • Postgres is intentionally not exposed to the host. Connect via `docker compose exec` if you need psql.

FAQ

Frequently asked

Why Postgres instead of the default SQLite?+

SQLite is fine for trying n8n, but it locks the database file during writes. With more than a handful of concurrent executions you will see "database is locked" errors. Postgres scales further and is the upstream recommendation for production.

Can I lose my encryption key and still keep my workflows?+

Yes — workflows themselves are stored plaintext. Only stored credentials (API keys, OAuth tokens) need the encryption key. Without the key those become unrecoverable and must be re-entered.

How do I upgrade n8n safely?+

Stop the stack, snapshot both the `n8n-data` and `n8n-postgres-data` volumes, bump the image tag in compose, start the stack. n8n will run migrations on boot. Read the release notes for breaking changes.

Do I need the reverse proxy?+

For real use, yes — n8n issues webhook URLs that must be reachable over HTTPS. The compose file does not expose port 5678 publicly; the proxy block is what makes n8n reachable.

Related

Related stacks