Self-host n8n for workflow automation
n8n is an open-source, fair-code workflow automation tool that lets you connect apps, trigger actions, and build complex pipelines — without writing code. Self-hosting puts you in control: no per-workflow pricing, no execution limits, and your credentials stay on your own server. Paired with an AI agent like Hermes or OpenClaw, n8n becomes a powerful orchestration layer.
Common n8n use cases
- Sync data between CRM, spreadsheets, and databases
- Trigger AI agent tasks from Slack messages or webhook events
- Automate email responses, form submissions, and Notion updates
- Build CI/CD notification pipelines and incident alerts
Step 1 — Install Docker
n8n has an official Docker image maintained by the n8n team. Install Docker Engine on Ubuntu:
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
# Allow your user to run Docker without sudo
sudo usermod -aG docker $USER
newgrp dockerStep 2 — Run n8n with Docker
Use a named Docker volume so your workflows and credentials survive container restarts and upgrades:
docker run -d \
--name n8n \
--restart unless-stopped \
-p 127.0.0.1:5678:5678 \
-e N8N_HOST=n8n.yourdomain.com \
-e WEBHOOK_URL=https://n8n.yourdomain.com/ \
-e N8N_BASIC_AUTH_ACTIVE=true \
-e N8N_BASIC_AUTH_USER=admin \
-e N8N_BASIC_AUTH_PASSWORD=your_strong_password \
-e N8N_ENCRYPTION_KEY=a_random_32_char_string_here \
-v n8n_data:/home/node/.n8n \
n8nio/n8n127.0.0.1:5678 means n8n only accepts connections from localhost — the reverse proxy in the next step handles external traffic. Never expose port 5678 directly to the internet.Step 3 — HTTPS with Caddy
Caddy automatically provisions and renews TLS certificates. Install it and create a Caddyfile:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddyGet DNS right before the first request
Caddy asks Let's Encrypt for a certificate the first time someone hits the hostname, and Let's Encrypt has to resolve that hostname back to this server to issue it. Point the A record first. Issuance itself is fast — in our own provisioning runs Caddy obtained a certificate for a brand-new subdomain in about seven seconds, including registering an ACME account.
The trap is querying the name too early. A “does not exist” answer gets cached, and Cloudflare-hosted zones publish a 30-minute negative-cache TTL by default — so one premature lookup can make the record look missing for half an hour after it is live. If that happens, sudo resolvectl flush-caches or wait it out.
Step 4 — Open the firewall
sudo ufw allow 80/tcp comment 'HTTP (Caddy)'
sudo ufw allow 443/tcp comment 'HTTPS (Caddy)'
# Do NOT open 5678 externally — Caddy handles it
sudo ufw reloadVisit https://n8n.yourdomain.com — you should see the n8n login screen. Use the basic auth credentials you set in the Docker run command.
Step 5 — Updates and backups
Updating n8n
docker pull n8nio/n8n
# rm -f, not "stop && rm": if the container is already stopped or gone,
# "stop" fails, && short-circuits, and rm never runs — leaving the name
# taken and the next run refusing to start. || true keeps it safe to re-run.
docker rm -f n8n >/dev/null 2>&1 || true
# Re-run the docker run command from Step 2 (same volume, new image)
docker run -d ... n8nio/n8nYour workflows live in the n8n_data volume, not the container, so removing the container is safe — that is the whole point of Step 2 using a named volume.
Back up your workflows
The n8n_data named volume holds all workflows, credentials, and settings. Back it up daily:
# Add to crontab: crontab -e
0 2 * * * docker run --rm -v n8n_data:/data -v /var/backups:/backup alpine \
tar -czf /backup/n8n-$(date +%F).tar.gz /dataTroubleshooting — what actually goes wrong
We provision this exact stack — Docker container behind Caddy — on fresh, disposable VPS instances continuously to test our own platform. These are the failures we hit repeatedly, with the error text each produces.
“The container name /n8n is already in use”
docker: Error response from daemon: Conflict. The container name "/n8n"
is already in use by container "4b7f56e64bf4…". You have to remove
(or rename) that container to be able to reuse that name.This is the single most confusing Docker failure, because it hides the real one. When docker run fails after creating the container — a port already bound, a bad environment variable, an image that exits immediately — the container still exists in a stopped state and keeps the name. Every retry then fails on the name conflict instead of the original cause, so the error you keep reading is not the error you have. Clear it and look at what actually happened:
docker ps -a | grep n8n # it is there, stopped
docker logs n8n # the REAL failure is in here
docker rm -f n8n # then retry the runPut docker rm -f n8n >/dev/null 2>&1 || true in front of any scripted docker run --name, as in Step 5. A named container is not safe to run twice unless you make it so.
Port already bound
docker: Error response from daemon: failed to set up container networking:
driver failed programming external connectivity on endpoint n8n:
failed to bind host port 0.0.0.0:80/tcp: address already in useCaddy owns ports 80 and 443 on this server, and whatever binds a port first wins. A container that tries to publish -p 80:80 alongside Caddy will always lose. This is exactly why Step 2 publishes n8n on 127.0.0.1:5678 and lets Caddy route to it — keep it that way, and check the owner before assuming Docker is at fault:
sudo ss -ltnp | grep -E ':80|:443|:5678'502 Bad Gateway right after setup
dial tcp 127.0.0.1:5678: connect: connection refusedCaddy is working — TLS terminated, request routed — and nothing is listening on the other side yet. On a first boot this is usually just n8n still starting; give it a minute. If it persists, the container is not running or not bound where Caddy expects:
docker ps # is the container actually up?
docker logs --tail 50 n8n # did it crash on startup?
curl -I http://127.0.0.1:5678 # does it answer locally at all?A common cause is a mismatch between the port in your Caddyfile and the one in the docker run command — they have to agree, and nothing warns you when they do not. Worth stressing: a 502 here is almost never a certificate problem. TLS is the fast part.
“docker: command not found”
Obvious in isolation, easy to miss in practice: most VPS base images ship without Docker, and a provider that advertises “Docker support” often means the kernel supports it, not that the binary is installed. If you skipped Step 1 or scripted the install on a fresh image, this surfaces four layers deep in an install log rather than as an obvious missing dependency. Confirm before anything else with docker --version.
Prefer not to manage it yourself?
AgentOcean VPS plans give you a hardened base server with Docker pre-installed — ready for n8n or any other self-hosted app.