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.
WhatValue
Current release[email protected], published 2026-10-26 (GitHub releases)
Pinned imagedocker.n8n.io/n8nio/n8n:2.44.7 (the official image; Docker Hub name n8nio/n8n)
Fallback release2.44.6 (2026-10-22)
Port5678 (editor, API and webhooks)
LicenceSustainable Use License: free for your own use, not for reselling n8n as a service
Official docsdocs.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.

How n8n runs in Docker: the browser and incoming webhooks reach n8n on port 5678, optionally through Traefik with HTTPS; n8n stores workflows in Postgres, can call Ollama for local AI models and MCP servers for tools, can hand Code node work to a separate task runner, and keeps its encryption key and files in folders under docker_volumes

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.

SystemWhat you need
LinuxDocker Engine with the Compose plugin (Docker Desktop also works), bash, curl, openssl
WindowsDocker Desktop with the WSL2 backend, PowerShell 7. For the smoothest run, keep the project inside the WSL filesystem
MacDocker Desktop for Mac (Apple Silicon or Intel), Terminal

Check Docker first:

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] Docker is running and docker compose version prints 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.xWhat it means for you
Task runners are on by defaultCode node JavaScript runs in a separate runner process. N8N_RUNNERS_ENABLED is deprecated
External runner is its own imageThe n8nio/n8n image no longer includes the external-mode runner. Use n8nio/runners if you want one
Python in the Code nodeThe 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 nodesBlocked by default (N8N_BLOCK_ENV_ACCESS_IN_NODE=true). Use credentials instead
Execute Command and Local File Trigger nodesDisabled by default. Re-enable with NODES_EXCLUDE
File accessLimited to ~/.n8n-files inside the container by default
DatabasesMySQL and MariaDB are dropped. Use Postgres (or SQLite for tests)
Binary dataIn-memory mode removed. Files go to disk by default
Activate / DeactivateNow called Publish / Unpublish. The Start node is removed
Image tagsstable and beta replace latest and next

Coming from 1.x? Do this in order:

  1. On 1.x, set N8N_RUNNERS_ENABLED=true and test your Code nodes.
  2. Replace any Start nodes and check sub-workflow outputs.
  3. Back up the database and the .n8n folder (see "Updates and backups").
  4. 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#

Run in terminalbash
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)#

Codepowershell
$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):

Run in terminalbash
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):

Run in terminaltext
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:

File contentyaml
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:

LineWhy it matters
PGDATAPostgres 18 would otherwise write outside the volume and start empty after a recreate
127.0.0.1:5678:5678Only your own machine can reach n8n. Other devices cannot, which is what you want until HTTPS is set up
N8N_ENCRYPTION_KEYEncrypts saved credentials. Set it yourself so you can restore later
WEBHOOK_URLThe public address n8n shows for webhooks. Must match how callers reach you
GENERIC_TIMEZONESchedule nodes use it

Linux#

Create both files with any editor, for example nano ~/docker_volumes/n8n/.env, then lock the secrets file:

Run in terminalbash
chmod 600 ~/docker_volumes/n8n/.env

Windows (PowerShell)#

Create both files in $HOME\docker_volumes\n8n, for example with Notepad:

Run in terminalpowershell
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:

Run in terminalbash
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#

Run in terminalbash
cd ~/docker_volumes/n8n
docker compose up -d
docker compose ps
curl -sf http://localhost:5678/healthz

Windows (PowerShell)#

Codepowershell
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:

Run in terminalbash
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.

  1. Browse to http://localhost:5678.
  2. Fill in the owner form (email, name, a strong password).
  3. Skip the optional questions, or answer them if you like.
  4. Create a test workflow with a Manual Trigger and a Set node, then run it.
  5. 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.

  1. Install Ollama (see the Ollama docs) and pull a model that supports tool calling.
  2. In your workflow, add an AI Agent node.
  3. Under its model connection, add an Ollama Chat Model node.
  4. Create an Ollama credential. The base URL depends on where Ollama runs:
Where Ollama runsBase URL
In the same Compose file, service name ollamahttp://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:

Run in terminalbash
docker network create proxy

Change the n8n service like this (remove the ports: block, because Traefik now handles access):

File contentyaml
  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 caseWhat Caddy doesWhat you do
a) Public domainGets and renews a free Let's Encrypt certificate automaticallyPoint a DNS A record (for example n8n.example.com) at the server and open ports 80 and 443
b) Home only, no domaintls internal: Caddy makes its own local certificateTrust Caddy's root certificate on each device (below)

Caddyfile for n8n, public domain:

File contenttext
n8n.example.com {
    reverse_proxy localhost:5678
}

Caddyfile for n8n, home only:

File contenttext
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, then sudo systemctl reload caddy.
  • Windows (PowerShell): winget install CaddyServer.Caddy, then in the folder with your Caddyfile run caddy run.
  • Mac: brew install caddy, then in the folder with your Caddyfile run caddy run.
  • Docker: add this service to the same docker-compose.yml, so Caddy shares the Docker network with n8n.
File contentyaml
  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.crt from Caddy's data directory, save it as /usr/local/share/ca-certificates/caddy-root.crt, then sudo update-ca-certificates.
  • Windows device: copy root.crt, then in an administrator PowerShell certutil -addstore -f ROOT root.crt.
  • Mac device: copy root.crt, then sudo 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 .env on Linux and Mac. On Windows, remove other users' access in the file's Security tab.
  • Never commit .env to git.
  • Keep the 127.0.0.1 port 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_KEY somewhere separate.

Tests to run:

Run in terminalbash
docker compose ps
docker logs n8n --tail 50
curl -I https://<your-domain>/healthz
  • docker compose ps shows both services up.
  • The logs show n8n is ready, with no database errors.
  • The curl -I line (only for the Traefik setup) returns HTTP/2 200 and a valid certificate.

Troubleshooting#

Start with the logs: docker logs n8n --tail 100.

ProblemSolution
Browser cannot reach localhost:5678Run docker compose ps. If n8n is restarting, read the logs. Check nothing else uses port 5678
Postgres says database files are incompatible with serverThe 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 recreateThe PGDATA line is missing. Put it back as shown in Step 2
Saved credentials stop working after a restoreThe N8N_ENCRYPTION_KEY differs from the original. Restore the original key
Settings file permission warning on Windows or a mounted driveSet N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false. The exact wording of the message may differ per version
Webhook URLs show localhostSet WEBHOOK_URL to the real address and recreate the container
Code node cannot read an environment variableExpected in 2.x. Use credentials, or set N8N_BLOCK_ENV_ACCESS_IN_NODE=false if you accept the risk
A file node refuses a pathKeep files in the local_files folder (~/.n8n-files inside the container), or set N8N_RESTRICT_FILE_ACCESS_TO
Execute Command node is missingIt 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 WindowsMove the project into the WSL filesystem
AI Agent cannot reach OllamaCheck 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:

Run in terminalbash
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#

  1. Run a backup (below).
  2. Read the release notes between your version and the target. Move one release at a time.
  3. Change N8N_VERSION in .env.
  4. Run:
Run in terminalbash
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:

File contentbash
#!/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:

Run in terminalbash
(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):

Codepowershell
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.

  1. Make a new folder. Copy in docker-compose.yml, the backed-up .env, and unpack the n8n_data archive there.
  2. In the copy's compose file, change the n8n port to 127.0.0.1:5679:5678.
  3. Start only the database: docker compose up -d postgres.
  4. Load the dump: gunzip -c backups/n8n-<date>.sql.gz | docker compose exec -T postgres psql -U n8n n8n.
  5. Start n8n, open port 5679, and open a workflow.
  6. 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:

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.