Install via docker compose

# docker-compose.yml
services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: n8n
    volumes:
      - pgdata:/var/lib/postgresql/data

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${DB_PASSWORD}

      N8N_HOST: flow.example.com
      N8N_PORT: 5678
      WEBHOOK_URL: https://flow.example.com/
      N8N_PROTOCOL: https
      N8N_ENCRYPTION_KEY: ${ENCRYPTION_KEY}

      # Two-factor authentication on the n8n owner account
      N8N_RUNNERS_ENABLED: true
      N8N_USER_MANAGEMENT_DISABLED: false

      EXECUTIONS_MODE: regular
      GENERIC_TIMEZONE: America/Toronto
    volumes:
      - data:/home/node/.n8n
    depends_on:
      - postgres

volumes:
  pgdata:
  data:
# .env
DB_PASSWORD=$(openssl rand -base64 36 | tr -d '\n')
ENCRYPTION_KEY=$(openssl rand -base64 48 | tr -d '\n')
The encryption key is irrecoverable

N8N_ENCRYPTION_KEY encrypts every saved credential (API keys, OAuth tokens, database passwords). Lose it, and every credential in every workflow becomes unreadable. Back it up with the same care as a database root password.

docker compose up -d
docker compose logs -f n8n

First start writes the schema and exposes the owner-account-setup flow at http://<host>:5678/. Create the owner account; this is the admin user.

Reverse proxy

# Caddy
flow.example.com {
    reverse_proxy 127.0.0.1:5678
}

n8n's web UI uses WebSockets for live updates; Caddy supports them by default. nginx needs the standard Upgrade/Connection headers and a long proxy_read_timeout.

The first workflow

The canonical "hello world": cron trigger → HTTP request → Slack message.

  1. Workflows → Create new.
  2. Add a Schedule Trigger node. Set it to "Every Hour at minute 7."
  3. Add an HTTP Request node. URL: https://api.example.com/status. Method: GET.
  4. Add an IF node. Condition: {{$json["status"]}} equals "down."
  5. From the IF "true" branch, add a Slack node. Connect to a Slack workspace via OAuth. Send to a channel.
  6. Save, then click Active in the top-right.

The workflow now runs every hour: if the upstream is down, Slack gets pinged. The IF node prevents an alert every hour for healthy responses.

Workflow concepts that matter

  • Nodes are functions — each node consumes input items (arrays of JSON), produces output items. Connections wire output to input.
  • Expressions — any field can be templated with {{...}} referencing upstream node outputs, the run context, environment variables, or JavaScript expressions.
  • The Code node — for anything the visual nodes can't do. Standard JavaScript or Python (limited subset in self-hosted) with access to the input items and helpers.
  • Sub-workflows — an Execute Workflow node calls another workflow as a function. The right unit of reuse.
  • Webhooks — the Webhook node generates a URL that triggers the workflow when called. Pair with a public reverse proxy or Cloudflare Tunnel (see that tutorial) to receive events from external SaaS.

LLM integrations

n8n's 2024–25 push has been LangChain-style nodes that integrate cleanly with LLMs:

  • OpenAI / Anthropic / Ollama (local!) nodes — treat any provider with an OpenAI-compatible API as a generic chat completion.
  • AI Agent node — the chat model gets access to other n8n nodes as tools; conversations become workflow triggers.
  • Embeddings + Pinecone / pgvector — build RAG pipelines as visual workflows.

Combined with Ollama, "summarize incoming emails, tag with project, file to Paperless" becomes a workflow that doesn't leave the LAN.

Execution modes

By default n8n runs each workflow execution in-process. For higher throughput:

  • Queue modeEXECUTIONS_MODE=queue + a Redis broker + separate worker containers. The web UI accepts triggers, queues them; workers pick up and execute. Horizontally scalable.
  • External task runnersN8N_RUNNERS_ENABLED=true moves Code-node execution into separate sandboxed processes for security and resource isolation.

Backups

  • The Postgres database — workflows, credentials (encrypted with N8N_ENCRYPTION_KEY), execution history.
  • The data volume (/home/node/.n8n) — the encryption key file (if not set via env), binary file data from past executions.
  • The N8N_ENCRYPTION_KEY itself, stored separately.

For workflow-only backups (no execution history), n8n has CLI export:

docker compose exec n8n n8n export:workflow --all --output=/data/export/workflows.json
docker compose exec n8n n8n export:credentials --all --decrypted --output=/data/export/creds.json

Don't commit decrypted credentials to Git. Encrypted exports re-import on the same n8n instance only; decrypted exports are useful for moving between instances but must be handled like a secrets file.

Versioning workflows in Git

n8n 1.x and later supports Git-backed environments: connect the instance to a Git repository, push workflow definitions to it, deploy them between environments (dev → staging → prod). For single-instance setups, the JSON export above + a manual git push gives the same effect with more friction.

When n8n is the wrong tool

  • For pipelines where the data has to flow at high throughput (millions of events/day), code-first tools (Temporal, Airflow, Kestra) win on observability and durability.
  • For ETL specifically, dbt + Airflow / Dagster is the more honest pick.
  • When the workflow is fully internal and would fit in 50 lines of Python — n8n's canvas overhead isn't worth it; write the script.

Where n8n shines: "connect this SaaS to that SaaS via a transformation that's a little too complex for Zapier, on hardware I own, in a UI my non-engineer collaborators can edit." For that specific shape of problem, it's the best self-hosted tool in 2026.