Self-HostingAI Coding AgentsDocker
n8n with Docker in 2026: Self-Host Workflow Automation and AI Agents (Linux, Windows, Mac)
Before you start
- 16 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
n8n is a workflow automation tool you run yourself: you draw workflows in a browser, connect hundreds of apps, and add AI Agent nodes that use a local or cloud model.
People self-host it to keep their data at home, avoid per-run fees, and mix normal automation with AI. This guide installs the current 2.x version with Postgres in Docker Compose. It also covers what changed since 1.x, how to talk to Ollama, and how to back everything up.
At a glance
- What you get: n8n 2.44.7 plus Postgres 18, running on your machine, with data in
~/docker_volumes/n8n. - Time needed: about 15 minutes, including the image download.
- Difficulty: easy. You copy a few files and run one command.
- You need: Docker with Compose v2, a terminal, and a browser.
- Optional: Ollama for local AI models, a domain for HTTPS.
| What | Value |
|---|---|
| Current release | [email protected], published 2026-10-26 (GitHub releases) |
| Pinned image | docker.n8n.io/n8nio/n8n:2.44.7 (the official image; Docker Hub name n8nio/n8n) |
| Fallback release | 2.44.6 (2026-10-22) |
| Port | 5678 (editor, API and webhooks) |
| Licence | Sustainable Use License: free for your own use, not for reselling n8n as a service |
| Official docs | docs.n8n.io/hosting |
Why 2.44.7? I checked the release on 2026-10-26. n8n itself marks it as stable (same image digest as the 2.44.7 tag), it is a small bug-fix release, and the issue tracker shows no reports of install failures or data loss. If you want a release with a few more days of real-world use, change one line to 2.44.6.
How it fits together#
You and your webhooks reach one n8n container, which keeps its data in Postgres and calls out to AI models and tools.
Only n8n and Postgres are required. Traefik, Ollama, MCP servers and an external task runner are optional add-ons.
Before you start#
You need Docker with Compose v2, a terminal, and a few GB of free RAM.
| System | What you need |
|---|---|
| Linux | Docker Engine with the Compose plugin (Docker Desktop also works), bash, curl, openssl |
| Windows | Docker Desktop with the WSL2 backend, PowerShell 7. For the smoothest run, keep the project inside the WSL filesystem |
| Mac | Docker Desktop for Mac (Apple Silicon or Intel), Terminal |
Check Docker first:
docker --version docker compose version
Checklist:
- [ ] Docker is running and
docker compose versionprints a v2 version. - [ ] Port 5678 is free on your machine.
- [ ] At least 2 GB of free RAM for n8n plus Postgres (more if you run Ollama too).
- [ ] You know where you will store the encryption key backup (a password manager, not the same disk).
- [ ] Only for the Traefik section: a domain, DNS pointing to your server, and ports 80 and 443 open.
Checked against the official docs on 2026-10-26. I did not run the commands in this rewrite; they follow the current official Docker Compose guide, adapted to the ~/docker_volumes layout.
What changed in n8n 2.x#
If you used n8n 1.x, these are the changes that can bite you. Everything is from the official v2.0 breaking-changes list.
| Change in 2.x | What it means for you |
|---|---|
| Task runners are on by default | Code node JavaScript runs in a separate runner process. N8N_RUNNERS_ENABLED is deprecated |
| External runner is its own image | The n8nio/n8n image no longer includes the external-mode runner. Use n8nio/runners if you want one |
| Python in the Code node | The old Pyodide Python node is gone. Native Python needs external runners |
$evaluateExpression() | No longer works in the Code node by default |
| Environment variables in Code nodes | Blocked by default (N8N_BLOCK_ENV_ACCESS_IN_NODE=true). Use credentials instead |
| Execute Command and Local File Trigger nodes | Disabled by default. Re-enable with NODES_EXCLUDE |
| File access | Limited to ~/.n8n-files inside the container by default |
| Databases | MySQL and MariaDB are dropped. Use Postgres (or SQLite for tests) |
| Binary data | In-memory mode removed. Files go to disk by default |
| Activate / Deactivate | Now called Publish / Unpublish. The Start node is removed |
| Image tags | stable and beta replace latest and next |
Coming from 1.x? Do this in order:
- On 1.x, set
N8N_RUNNERS_ENABLED=trueand test your Code nodes. - Replace any Start nodes and check sub-workflow outputs.
- Back up the database and the
.n8nfolder (see "Updates and backups"). - Change the image tag to 2.x and start. The 1.x line (1.123.x) is still maintained if you need more time.
Step 1: Create the folders and secrets#
Make the project folders and generate two random secrets: the encryption key and the database password.
n8n encrypts the credentials you save (API keys, passwords) with N8N_ENCRYPTION_KEY. If you lose the key, those credentials cannot be decrypted. Generate it once and never change it.
Linux#
mkdir -p ~/docker_volumes/n8n/{n8n_data,postgres_data,local_files,backups}
cd ~/docker_volumes/n8n
sudo chown -R 1000:1000 n8n_data local_files
openssl rand -hex 32 # run twice: once for the key, once for the DB password
The n8n container runs as user 1000, so it must own n8n_data and local_files.
Windows (PowerShell)#
$d = "$HOME\docker_volumes\n8n"
New-Item -ItemType Directory -Force "$d\n8n_data","$d\postgres_data","$d\local_files","$d\backups" | Out-Null
Set-Location $d
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Maximum 256) }) # run twice
If bind mounts feel slow or you hit permission errors, run the Linux commands inside your WSL distribution instead and keep the project in the WSL home folder, not under /mnt/c.
Mac (Terminal)#
Same commands as Linux, but skip the chown line (Docker Desktop handles file ownership):
mkdir -p ~/docker_volumes/n8n/{n8n_data,postgres_data,local_files,backups}
cd ~/docker_volumes/n8n
openssl rand -hex 32 # run twice: once for the key, once for the DB password
Check it worked: you have two different 64-character strings and the folder ~/docker_volumes/n8n exists.
Step 2: Write the .env and docker-compose.yml#
Put the secrets in .env and the services in docker-compose.yml. The file has no version: line, because Compose v2 does not need it.
Create .env (use your two generated strings; keep this file private):
N8N_VERSION=2.44.7 POSTGRES_USER=n8n POSTGRES_PASSWORD=<YOUR_DB_PASSWORD> POSTGRES_DB=n8n N8N_ENCRYPTION_KEY=<YOUR_ENCRYPTION_KEY> GENERIC_TIMEZONE=<your/timezone, for example Europe/London> N8N_HOST=localhost WEBHOOK_URL=http://localhost:5678/
Create docker-compose.yml in the same folder:
services:
postgres:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
PGDATA: /var/lib/postgresql/data
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: "5432"
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: ${N8N_HOST}
WEBHOOK_URL: ${WEBHOOK_URL}
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
TZ: ${GENERIC_TIMEZONE}
volumes:
- ./n8n_data:/home/node/.n8n
- ./local_files:/home/node/.n8n-files
depends_on:
postgres:
condition: service_healthy
What the important lines do:
| Line | Why it matters |
|---|---|
PGDATA | Postgres 18 would otherwise write outside the volume and start empty after a recreate |
127.0.0.1:5678:5678 | Only your own machine can reach n8n. Other devices cannot, which is what you want until HTTPS is set up |
N8N_ENCRYPTION_KEY | Encrypts saved credentials. Set it yourself so you can restore later |
WEBHOOK_URL | The public address n8n shows for webhooks. Must match how callers reach you |
GENERIC_TIMEZONE | Schedule nodes use it |
Linux#
Create both files with any editor, for example nano ~/docker_volumes/n8n/.env, then lock the secrets file:
chmod 600 ~/docker_volumes/n8n/.env
Windows (PowerShell)#
Create both files in $HOME\docker_volumes\n8n, for example with Notepad:
notepad "$HOME\docker_volumes\n8n\.env" notepad "$HOME\docker_volumes\n8n\docker-compose.yml"
Make sure Notepad saves the name exactly as .env (choose "All files", not ".txt").
Mac (Terminal)#
Same commands as Linux:
nano ~/docker_volumes/n8n/.env chmod 600 ~/docker_volumes/n8n/.env
Check it worked: docker compose config (run in that folder) prints the merged file with your values filled in and no errors.
Step 3: Start n8n#
Start both containers and wait until the health check passes.
Linux#
cd ~/docker_volumes/n8n docker compose up -d docker compose ps curl -sf http://localhost:5678/healthz
Windows (PowerShell)#
Set-Location "$HOME\docker_volumes\n8n" docker compose up -d docker compose ps Invoke-RestMethod http://localhost:5678/healthz
Mac (Terminal)#
Same commands as Linux:
cd ~/docker_volumes/n8n docker compose up -d docker compose ps curl -sf http://localhost:5678/healthz
Check it worked: docker compose ps lists postgres as healthy and n8n as running. The /healthz endpoint is n8n's official health check and returns a small JSON status (expected: {"status":"ok"}). Potential issue: the first start takes longer while n8n creates its tables.
First run: create your owner account#
Open the editor and create the owner account.
- Browse to
http://localhost:5678. - Fill in the owner form (email, name, a strong password).
- Skip the optional questions, or answer them if you like.
- Create a test workflow with a Manual Trigger and a Set node, then run it.
- Click Publish to turn it on. In 2.x, Publish replaces the old Activate toggle.
Use a real email: the owner account is how you sign in and reset access later.
Add an AI Agent with Ollama#
An AI Agent node needs two things: a chat model and (optionally) tools. Ollama gives you the model on your own hardware.
- Install Ollama (see the Ollama docs) and pull a model that supports tool calling.
- In your workflow, add an AI Agent node.
- Under its model connection, add an Ollama Chat Model node.
- Create an Ollama credential. The base URL depends on where Ollama runs:
| Where Ollama runs | Base URL |
|---|---|
In the same Compose file, service name ollama | http://ollama:11434 |
| On the Docker host (Windows, Mac) | http://host.docker.internal:11434 |
| On the Docker host (Linux) | Add extra_hosts: ["host.docker.internal:host-gateway"] to the n8n service, then use the same URL |
Linux note: if Ollama runs as a system service, it must listen on an address the container can reach (check its OLLAMA_HOST setting).
MCP: n8n has an MCP Server Trigger node (expose your workflows as tools to other AI apps) and an MCP Client Tool node (let an AI Agent call tools from an MCP server). Names can change between releases, so search the node panel for "MCP".
Safety tip: an agent can only do what its tools allow. Start with read-only tools and add write actions one at a time.
Put it behind HTTPS (with or without Traefik)#
Already run Traefik? Use the first route. No Traefik yet and one small server? Use Caddy.
With Traefik#
This section is for a server with a domain. If you only use n8n on your own computer, skip it.
You need Traefik already running with an https entrypoint and a certificate resolver, and a Docker network called proxy. Create the network if it is missing:
docker network create proxy
Change the n8n service like this (remove the ports: block, because Traefik now handles access):
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: unless-stopped
environment:
N8N_HOST: <your-domain>
N8N_PROTOCOL: https
WEBHOOK_URL: https://<your-domain>/
N8N_PROXY_HOPS: "1"
# ...keep the database and key settings from Step 2
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.n8n.rule=Host(`<your-domain>`)
- traefik.http.routers.n8n.entrypoints=https
- traefik.http.routers.n8n.tls=true
- traefik.http.routers.n8n.tls.certresolver=<your-resolver>
- traefik.http.services.n8n.loadbalancer.server.port=5678
networks:
proxy:
external: true
Then run docker compose up -d again. Update N8N_HOST and WEBHOOK_URL in .env too, so they never disagree with the labels. The label names follow Traefik's standard Docker provider; check your Traefik version's entrypoint and resolver names.
Without Traefik: Caddy#
Choose this when you have no Traefik yet and run one small server. Caddy is one program with a short config, and it handles certificates itself (see caddyserver.com/docs, Automatic HTTPS).
| Your case | What Caddy does | What you do |
|---|---|---|
| a) Public domain | Gets and renews a free Let's Encrypt certificate automatically | Point a DNS A record (for example n8n.example.com) at the server and open ports 80 and 443 |
| b) Home only, no domain | tls internal: Caddy makes its own local certificate | Trust Caddy's root certificate on each device (below) |
Caddyfile for n8n, public domain:
n8n.example.com {
reverse_proxy localhost:5678
}
Caddyfile for n8n, home only:
n8n.home.arpa {
tls internal
reverse_proxy localhost:5678
}
In this guide n8n listens on port 5678, published on 127.0.0.1, which is where Caddy reaches it when Caddy runs on the same machine. Set N8N_HOST, N8N_PROTOCOL=https and WEBHOOK_URL=https://n8n.example.com/ in .env (as in the Traefik example above) and keep N8N_PROXY_HOPS=1, then docker compose up -d.
Run Caddy
- Linux: install from the official apt repository (caddyserver.com/docs/install), put the Caddyfile at
/etc/caddy/Caddyfile, thensudo systemctl reload caddy. - Windows (PowerShell):
winget install CaddyServer.Caddy, then in the folder with yourCaddyfileruncaddy run. - Mac:
brew install caddy, then in the folder with yourCaddyfileruncaddy run. - Docker: add this service to the same
docker-compose.yml, so Caddy shares the Docker network with n8n.
caddy:
image: caddy:2.11.7
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./conf:/etc/caddy:ro
- ~/docker_volumes/caddy/data:/data
- ~/docker_volumes/caddy/config:/config
In the Docker route save the Caddyfile as conf/Caddyfile next to this compose file (create the folder first: mkdir -p conf on Linux and Mac, New-Item -ItemType Directory -Force conf on Windows), and write reverse_proxy n8n:5678 in the Caddyfile instead of localhost:5678. If only Caddy should reach n8n, remove its 127.0.0.1 port publish.
Home only: trust Caddy's root certificate (the tls internal certificates are signed by it).
- Caddy machine:
caddy trust. - Linux device: copy
pki/authorities/local/root.crtfrom Caddy's data directory, save it as/usr/local/share/ca-certificates/caddy-root.crt, thensudo update-ca-certificates. - Windows device: copy
root.crt, then in an administrator PowerShellcertutil -addstore -f ROOT root.crt. - Mac device: copy
root.crt, thensudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root.crt.
Make n8n.home.arpa resolve to the server (your router's DNS, or each device's hosts file).
Check it worked: curl -I https://n8n.example.com/healthz answers HTTP/2 200.
Security and testing#
Keep n8n off the open internet unless you need webhooks, and protect the secrets.
- Protect
.env:chmod 600 .envon Linux and Mac. On Windows, remove other users' access in the file's Security tab. - Never commit
.envto git. - Keep the
127.0.0.1port binding until HTTPS is in place. - For remote access without a public site, use a VPN or an SSH tunnel.
- If you need public webhooks only, put them on the HTTPS address and keep the editor behind a login you trust (n8n's owner account, plus your proxy's own login if you have one).
- Save credentials in n8n's credential store, not in Code nodes.
- Back up
N8N_ENCRYPTION_KEYsomewhere separate.
Tests to run:
docker compose ps docker logs n8n --tail 50 curl -I https://<your-domain>/healthz
docker compose psshows both services up.- The logs show n8n is ready, with no database errors.
- The
curl -Iline (only for the Traefik setup) returnsHTTP/2 200and a valid certificate.
Troubleshooting#
Start with the logs: docker logs n8n --tail 100.
| Problem | Solution |
|---|---|
Browser cannot reach localhost:5678 | Run docker compose ps. If n8n is restarting, read the logs. Check nothing else uses port 5678 |
Postgres says database files are incompatible with server | The Postgres major version changed. Restore from a pg_dumpall backup into the new version, as the official upgrade guide says |
| Postgres 18 starts empty after a recreate | The PGDATA line is missing. Put it back as shown in Step 2 |
| Saved credentials stop working after a restore | The N8N_ENCRYPTION_KEY differs from the original. Restore the original key |
| Settings file permission warning on Windows or a mounted drive | Set N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false. The exact wording of the message may differ per version |
Webhook URLs show localhost | Set WEBHOOK_URL to the real address and recreate the container |
| Code node cannot read an environment variable | Expected in 2.x. Use credentials, or set N8N_BLOCK_ENV_ACCESS_IN_NODE=false if you accept the risk |
| A file node refuses a path | Keep files in the local_files folder (~/.n8n-files inside the container), or set N8N_RESTRICT_FILE_ACCESS_TO |
| Execute Command node is missing | It is off by default in 2.x. Re-enable it with NODES_EXCLUDE only if you must |
| Works on Linux but is slow or breaks on Windows | Move the project into the WSL filesystem |
| AI Agent cannot reach Ollama | Check the base URL table in the AI Agent section and Ollama's listen address |
Known issues#
The n8n issue tracker has open reports against the recent 2.44.x releases. These are narrow cases, not install problems:
- #42044: the SFTP node timing out during the handshake with some servers.
- #42070: a daily Schedule Trigger running twice on the night clocks go back.
- #41992: webhook bodies with ISO-8859-1 encoding failing.
I could not confirm exact error strings for every setup, so match your log line against the table above and the tracker.
Diagnostic commands:
docker logs n8n --tail 100
docker compose ps
docker inspect n8n --format '{{.State.Status}}'
Updates and backups#
Back up before every update, and test that the backup restores. A backup you never restored is a guess.
Update#
- Run a backup (below).
- Read the release notes between your version and the target. Move one release at a time.
- Change
N8N_VERSIONin.env. - Run:
cd ~/docker_volumes/n8n docker compose pull docker compose up -d
On Windows use Set-Location "$HOME\docker_volumes\n8n" first, then the same two docker compose commands.
Backup script (Linux and Mac)#
Save as ~/docker_volumes/n8n/backup.sh and run chmod +x backup.sh:
#!/usr/bin/env bash set -euo pipefail cd ~/docker_volumes/n8n d=$(date +%F) mkdir -p backups docker compose exec -T postgres pg_dump -U n8n n8n | gzip > "backups/n8n-$d.sql.gz" tar czf "backups/n8n_data-$d.tgz" n8n_data local_files cp .env "backups/env-$d" && chmod 600 "backups/env-$d" find backups -type f -mtime +14 -delete
Schedule it with cron at 03:00:
(crontab -l 2>/dev/null; echo "0 3 * * * $HOME/docker_volumes/n8n/backup.sh") | crontab -
Backup on Windows#
Run this in PowerShell (the Task Scheduler can run it daily at 03:00):
Set-Location "$HOME\docker_volumes\n8n" $d = Get-Date -Format yyyy-MM-dd docker compose exec -T postgres pg_dump -U n8n n8n | Set-Content -Encoding utf8 "backups\n8n-$d.sql" Compress-Archive -Path n8n_data, local_files -DestinationPath "backups\n8n_data-$d.zip" -Force Copy-Item .env "backups\env-$d"
Restore test#
Do this on a scratch copy, never on the live install.
- Make a new folder. Copy in
docker-compose.yml, the backed-up.env, and unpack then8n_dataarchive there. - In the copy's compose file, change the n8n port to
127.0.0.1:5679:5678. - Start only the database:
docker compose up -d postgres. - Load the dump:
gunzip -c backups/n8n-<date>.sql.gz | docker compose exec -T postgres psql -U n8n n8n. - Start n8n, open port 5679, and open a workflow.
- Test a saved credential. If it works, your encryption key and data match.
Performance and resources#
n8n with Postgres is light; AI models are the heavy part.
- Use Postgres for anything beyond a quick test.
- In 2.x, binary data goes to disk by default, so plan disk space for big files. Queue mode (for scaling to workers) stores it in the database by default.
- Start with 2 vCPUs and 2 to 4 GB of RAM for n8n and Postgres, then measure. The official Compose guide lists 4 GB and 2 vCPUs for its larger stack.
- A 7B to 8B local model needs roughly 8 GB or more of RAM or VRAM. Check your model's page.
- Watch real use with
docker stats. - For many parallel runs, read the official queue-mode and worker docs.
I did not measure RAM for this version, so I give no numbers here.
FAQ#
Which image tag should I use? Pin an exact version such as 2.44.7. The stable tag moves when n8n releases, which is fine for testing but surprising on a server.
Can I move from n8n 1.x? Yes. Test your Code nodes with runners enabled on 1.x, replace Start nodes, back up, then upgrade. See "What changed in n8n 2.x".
Is SQLite good enough? For a quick trial, yes. For anything you rely on, use Postgres as in this guide.
Why does my Code node not see environment variables any more? n8n 2.x blocks it by default for safety. Use n8n credentials instead.
Is n8n really free? The Sustainable Use License lets you use it for your own or your company's work. You cannot sell n8n itself as a hosted service. Read the licence page for your case.
Sources#
Read on 2026-10-26:
- n8n Docker Compose installation guide
- n8n v2.0 breaking changes
- n8n hosting documentation
- n8n GitHub releases (2.44.7)
- n8n image on Docker Hub
- n8n issue tracker
Checked against the official docs on 2026-10-26. Version, image tag and issue search were checked through the public GitHub and Docker Hub APIs the same day. The commands were not run for this rewrite.
- Caddy docs: automatic HTTPS, reverse_proxy, install (caddyserver.com/docs, Caddy v2.11.7, read 2026-10-26)