Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-caddy layout (Caddy reverse proxy with Let’s Encrypt, n8n app container).
  • Storage: two external Docker named volumes — n8n_data (workflows, credentials, executions) and caddy_data (TLS certs + Caddy state).
  • DNS: n8n.hungryhub.com A record in Cloudflare, orange-cloud proxied (WAF + rate-limit rules apply). Managed in hungryhub-terraform/hh-cloudflare/.

Access control

SSH users (as of 2026-06-08)

UserLoginKeySudoUse case
rootn8n-hh (alias for 188.166.239.21)saiqulhaq key onlyn/a (is root)break-glass / owner-only
irfanssh irfan@n8n-hhirfan’s keypasswordlessday-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.

PackageVersionProvides
@apify/n8n-nodes-apify0.6.4Apify, Apify Trigger
@brave/n8n-nodes-brave-search1.0.25Brave Search
n8n-nodes-powerbi2.3.8Power BI
n8n-nodes-serpapi0.1.6SerpApi Official
n8n-nodes-tally1.2.1Tally.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:

  1. Open a shell in the running container:
    ssh n8n-hh
    docker exec -u root -it n8n-docker-caddy-n8n-1 bash
    
  2. Install inside the container:
    npm install -g <npm-package-name>
    
  3. 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:

  1. Provision a new Hetzner CX22 in fsn1 with Ubuntu 22.04.
  2. Install Docker + Compose.
  3. Restore n8n_data and caddy_data volumes from a backup (or start empty and recreate workflows manually).
  4. Clone the n8n-docker-caddy repo:
    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.
    
  5. Run the install wizard:
    sudo /root/asd.sh   # ⚠️ see warning below
    
  6. Reinstall community nodes (see Add a community node).
  7. Update the Cloudflare A record for n8n.hungryhub.com to 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.

#ItemRiskEffort
1Pin 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
2Pin the Caddy image tag. Same issue.Low — Caddy is stable, but the principle applies.1 min
3Move 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)
4Disable debug logging. DEBUG=* and N8N_LOG_LEVEL=debug in compose — fine for dev, noise in prod.Low1 min
5Bake 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
6Off-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)
7Move 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
8Terraform 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
9Migrate SMTP and backup AWS creds to AWS Secrets Manager. Today: plaintext in compose / not in SM.High30 min (plus the IAM work in hungryhub-iam)
10Lock down SSH. Disable PermitRootLogin yes once at least one sudo user is verified working.Low5 min

References


Last verified: 2026-06-08 by the AI session that added user irfan and pinned this document as the canonical n8n management runbook.