n8n Server Management
Operational guide for the self-hosted n8n instance backing
https://n8n.hungryhub.com. Covers access, stack layout, common
operations, and known backlog.
Overview
- Purpose: internal workflow automation — Slack messages, partner onboarding webhooks, marketing automation, and ad-hoc integrations (see Restaurant Onboarding for an example consumer).
- Hosting: single Hetzner Cloud VPS (CX-class, Falkenstein) running Ubuntu 22.04.
- Stack: Docker 27 + Compose v2 running the official
n8n-docker-caddylayout (Caddy reverse proxy with Let’s Encrypt, n8n app container). - Storage: two external Docker named volumes —
n8n_data(workflows, credentials, executions) andcaddy_data(TLS certs + Caddy state). - DNS:
n8n.hungryhub.comA record in Cloudflare, orange-cloud proxied (WAF + rate-limit rules apply). Managed inhungryhub-terraform/hh-cloudflare/.
Access control
SSH users (as of 2026-06-08)
| User | Login | Key | Sudo | Use case |
|---|---|---|---|---|
root | n8n-hh (alias for 188.166.239.21) | saiqulhaq key only | n/a (is root) | break-glass / owner-only |
irfan | ssh irfan@n8n-hh | irfan’s key | passwordless | day-to-day ops |
SSH config snippet in ~/.ssh/config:
Host n8n-hh
IdentityFile ~/projects/github.com/saiqulhaq/dotfiles/.skm/work/id_rsa
User root
Hostname 188.166.239.21
(For irfan, override User irfan in a separate Host n8n-hh-irfan
block, or use ssh irfan@n8n-hh directly.)
Add a new operator
The team uses a separate non-root user with passwordless sudo for anyone beyond the owner. To add a teammate:
ssh n8n-hh
# 1. Create user
sudo useradd -m -s /bin/bash <username>
sudo passwd -l <username> # disable password login (key-only)
# 2. Add to sudo group
sudo usermod -aG sudo <username>
# 3. Install their public key
sudo mkdir -p /home/<username>/.ssh
sudo chmod 700 /home/<username>/.ssh
echo "<their-public-key> <username>@hungryhub.com" \
| sudo tee /home/<username>/.ssh/authorized_keys
sudo chmod 600 /home/<username>/.ssh/authorized_keys
sudo chown -R <username>:<username> /home/<username>/.ssh
# 4. Grant passwordless sudo
echo "<username> ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/<username>
sudo chmod 440 /etc/sudoers.d/<username>
sudo visudo -c -f /etc/sudoers.d/<username> # must print "parsed OK"
Have them test from their machine:
ssh <username>@n8n-hh
sudo -i # should drop into a root shell with no password
Remove an operator
sudo userdel -r <username>
sudo rm -f /etc/sudoers.d/<username>
Stack layout
/opt/n8n-docker-caddy/ # compose project root (legacy path)
├── docker-compose.yml # caddy + n8n services
├── .env # public env vars (DOMAIN_NAME, SUBDOMAIN, TZ, SSL_EMAIL)
├── caddy_config/
│ └── Caddyfile # reverse-proxy config
└── local_files/ # bind-mounted into n8n as /files
/root/
├── asd.sh # legacy interactive install script — DO NOT USE
└── update.sh # current upgrade script (see "Upgrade n8n" below)
Docker resources:
$ docker ps
NAMES IMAGE STATUS
n8n-docker-caddy-caddy-1 caddy:latest Up 2 weeks
n8n-docker-caddy-n8n-1 docker.n8n.io/n8nio/n8n:latest Up 2 weeks
$ docker volume ls | grep -E "(n8n_data|caddy_data)"
local n8n_data
local caddy_data
Community nodes (currently installed)
These are baked into the n8n:latest image and must be preserved across
upgrades. If a workflow stops finding one of these nodes, an upgrade
likely dropped it.
| Package | Version | Provides |
|---|---|---|
@apify/n8n-nodes-apify | 0.6.4 | Apify, Apify Trigger |
@brave/n8n-nodes-brave-search | 1.0.25 | Brave Search |
n8n-nodes-powerbi | 2.3.8 | Power BI |
n8n-nodes-serpapi | 0.1.6 | SerpApi Official |
n8n-nodes-tally | 1.2.1 | Tally.so |
Common operations
Upgrade n8n
The legacy approach uses a tiny shell script that pulls the latest upstream image:
ssh n8n-hh
/root/update.sh
This runs docker compose pull && docker compose up -d in
/opt/n8n-docker-caddy/.
Caveat: because the image tag is latest, this jumps to whatever
n8n upstream ships on each run. There is no pinning, no health-gate,
and no automatic rollback. See Hygiene backlog
for the proper replacement.
Add a community node
There is no per-instance install script. To add a community node:
- Open a shell in the running container:
ssh n8n-hh docker exec -u root -it n8n-docker-caddy-n8n-1 bash - Install inside the container:
npm install -g <npm-package-name> - Restart n8n:
docker restart n8n-docker-caddy-n8n-1
Caveat: the package is installed into the container’s node_modules.
The next docker compose pull of latest will rebuild the image and
wipe the install. To make it durable, the image must be rebuilt
with the package baked in. Track this in the Hygiene
backlog.
Remove a community node
ssh n8n-hh
docker exec -u root -it n8n-docker-caddy-n8n-1 bash -c \
'npm uninstall -g <npm-package-name>'
docker restart n8n-docker-caddy-n8n-1
View logs
ssh n8n-hh
docker logs -f n8n-docker-caddy-n8n-1 # n8n app logs
docker logs -f n8n-docker-caddy-caddy-1 # Caddy access + ACME
Restart n8n
ssh n8n-hh
cd /opt/n8n-docker-caddy
docker compose restart n8n
Health check
curl -fsS https://n8n.hungryhub.com/healthz
Open a shell in the n8n container
ssh n8n-hh
docker exec -u root -it n8n-docker-caddy-n8n-1 bash
Backups
Current state: no automated backups. This is tracked in the hygiene backlog.
Manual one-off backup (ad hoc, do NOT rely on this)
ssh n8n-hh
docker run --rm \
-v n8n_data:/src/n8n:ro \
-v caddy_data:/src/caddy:ro \
-v /var/tmp:/dst \
alpine:3.20 \
sh -c 'tar czf /dst/n8n-manual-$(date +%F).tar.gz -C /src .'
# Push to S3 (requires AWS creds with s3:PutObject on the backup bucket)
aws s3 cp /var/tmp/n8n-manual-*.tar.gz s3://hungryhub-prod-n8n-backups/manual/
A Hetzner snapshot of the entire VM is a coarser but simpler safety net (see Disaster recovery).
Disaster recovery
Hetzner snapshot (full VM)
Snapshots live in Hetzner Cloud → Images.
# Via Hetzner CLI
hcloud server list -o columns=id,name,ipv4
hcloud server create-image --type snapshot \
--description "n8n-pre-upgrade-$(date +%F)" <server-id>
Restore: create a new server from the image in Hetzner Cloud Console, update the DNS A record in Cloudflare, redeploy SSH keys.
Volume-only restore
If the n8n container is fine but n8n_data is corrupted, you need a
volume backup (see Backups) — currently we don’t have one.
This is part of the hygiene backlog.
Full rebuild from scratch
If the box is unrecoverable:
- Provision a new Hetzner CX22 in
fsn1with Ubuntu 22.04. - Install Docker + Compose.
- Restore
n8n_dataandcaddy_datavolumes from a backup (or start empty and recreate workflows manually). - Clone the
n8n-docker-caddyrepo:git clone https://github.com/n8n-io/n8n-docker-caddy.git /opt/n8n-docker-caddy cd /opt/n8n-docker-caddy cp .env.example .env # then fill in DOMAIN_NAME, SUBDOMAIN, etc. - Run the install wizard:
sudo /root/asd.sh # ⚠️ see warning below - Reinstall community nodes (see Add a community node).
- Update the Cloudflare A record for
n8n.hungryhub.comto the new server’s IP.
⚠️ /root/asd.sh is the legacy interactive install script. It will
prompt for a domain, email, and timezone. It is left on the box for
disaster-recovery use only. Do not run it on a healthy server.
Hygiene backlog
Items that should be addressed before treating this server as production-grade. Listed in suggested fix order.
| # | Item | Risk | Effort |
|---|---|---|---|
| 1 | Pin the n8n image tag. Currently latest — every update.sh jump is uncontrolled. Bump to a specific version (e.g. 1.74.x). | High — surprise upgrades can break workflows. | 5 min |
| 2 | Pin the Caddy image tag. Same issue. | Low — Caddy is stable, but the principle applies. | 1 min |
| 3 | Move SMTP password out of docker-compose.yml. Currently in plaintext at docker-compose.yml:18 (SG.MVaBQfn-…). | High — secret in a file under /opt. | 10 min (env_file + chmod 600) |
| 4 | Disable debug logging. DEBUG=* and N8N_LOG_LEVEL=debug in compose — fine for dev, noise in prod. | Low | 1 min |
| 5 | Bake community nodes into the image. Today, an update.sh can wipe a manually-installed node. Extend the n8n image with a Dockerfile and rebuild on every version bump. | High — silent loss of node types breaks workflows. | 1–2 hours |
| 6 | Off-box automated backups. Daily tar of n8n_data + caddy_data to S3 (prod account) via cron. 7-day local + 7-day S3 retention. | High — no recovery path if disk dies. | 2–3 hours (including IAM) |
| 7 | Move app config to a versioned repo. A git-tracked docker-compose.yml, Dockerfile, package.json of community nodes, and Makefile-driven deploy — reviewed via PR. | Medium — drift between docs and reality. | 1 day |
| 8 | Terraform for the Hetzner box. hcloud_server, hcloud_firewall, hcloud_volume, cloudflare_record as a module in hungryhub-terraform/. | Medium — server is currently unmanaged infra (no drift detection, no IaC). | 1 day |
| 9 | Migrate SMTP and backup AWS creds to AWS Secrets Manager. Today: plaintext in compose / not in SM. | High | 30 min (plus the IAM work in hungryhub-iam) |
| 10 | Lock down SSH. Disable PermitRootLogin yes once at least one sudo user is verified working. | Low | 5 min |
References
- Restaurant Onboarding System
— example consumer of
n8n.hungryhub.comwebhooks. hungryhub-terraform/hh-cloudflare/— Cloudflare DNS forn8n.hungryhub.comand other HungryHub zones.hungryhub-iam/— IAM roles, Secrets Manager access policies, and GitHub OIDC trust.- n8n self-hosted docs — upstream reference.
n8n-docker-caddy— the compose layout this server uses.- AGENTS.md §CLI Tools —
awsSSO profiles andghprofile conventions used by this team.
Last verified: 2026-06-08 by the AI session that added user irfan
and pinned this document as the canonical n8n management runbook.