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')
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.
- Workflows → Create new.
- Add a Schedule Trigger node. Set it to "Every Hour at minute 7."
- Add an HTTP Request node. URL:
https://api.example.com/status. Method: GET. - Add an IF node. Condition:
{{$json["status"]}}equals "down." - From the IF "true" branch, add a Slack node. Connect to a Slack workspace via OAuth. Send to a channel.
- 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 mode —
EXECUTIONS_MODE=queue+ a Redis broker + separate worker containers. The web UI accepts triggers, queues them; workers pick up and execute. Horizontally scalable. - External task runners —
N8N_RUNNERS_ENABLED=truemoves 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_KEYitself, 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.