n8n is a node-based workflow automation tool in the spirit of Zapier, except self-hosted, fair-code, and far more capable once you stop dragging boxes and start writing JavaScript. It's the most useful thing you can put on a home server for glue work nobody notices is missing.
What it is
Triggers (webhook, cron, RSS, email) feed into nodes that call APIs, transform data, run shell commands, and hit Postgres. It has 400+ integrations, a usable code node, and sub-workflow support. Most people use it for small glue: notify yourself, sync two databases, generate a report on a schedule.
Before you start
- The encryption key is not optional. n8n encrypts stored credentials with it. Lose it and every saved password in every workflow is unreadable, forever.
- Use Postgres from day one. The default SQLite mode writes the whole database on every save and gets slow and occasionally corrupt under load.
- Understand the webhook split: n8n offers
WEBHOOK_URLfor browser-facing links and a separateWEBHOOK_TEST_URLused while a workflow is in test mode. Point both at the same proxy or testers will get 404s.
1 — Create the directory
mkdir -p ~/services/n8n/data
cd ~/services/n8n
openssl rand -hex 24 # N8N_ENCRYPTION_KEY
2 — Write the compose file
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:5678:5678"
environment:
N8N_HOST: workflow.example.com
N8N_PORT: "5678"
N8N_PROTOCOL: https
WEBHOOK_URL: https://workflow.example.com/
GENERIC_TIMEZONE: UTC
TZ: UTC
N8N_ENCRYPTION_KEY: CHANGE_ME_48_hex_chars
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
N8N_SECURE_COOKIE: "true"
N8N_DIAGNOSTICS_ENABLED: "false"
N8N_RUNNERS_ENABLED: "true"
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: db
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: CHANGE_ME
volumes:
- ./data:/home/node/.n8n
db:
image: postgres:16-alpine
container_name: n8n-db
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: CHANGE_ME
POSTGRES_DB: n8n
volumes:
- ./pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
3 — Start it
docker compose up -d
docker compose logs -f n8n | grep -i "editor is now accessible\|error"
4 — First-run setup
- Create the owner account at
http://yourhost:5678. If open registration is left on, anyone who finds the URL creates an account — turn it off in Settings → General → User management immediately after. - Set a timezone in Settings to match your own, or cron triggers fire at the wrong hour and you waste an evening debugging it.
- Build one trivial workflow to prove the loop: a Schedule Trigger every 5 minutes → a HTTP Request node hitting a public API → a log. Once that fires, everything else is just adding nodes.
- Wire up the credentials you'll actually use, and enable External secrets if you'd rather not store API keys in the database at all.
5 — Operating notes
# export everything, on a schedule
docker exec -u node n8n n8n export:workflow --all --output=/data/backup
# import elsewhere
docker exec -u node n8n n8n import:workflow --separate --input=/data/backup
n8n-nodes-base and official community nodes install inside the container. Your own JavaScript in a Code node can't require() an npm package unless you extend the image — build a derived image, don't mount over the node_modules.