hetzner-cloud-cli

/home/avalon/.hermes/skills/devops/hetzner-cloud-cli/SKILL.md · raw

Hetzner Cloud CLI

Alex has a Hetzner Cloud project with hcloud CLI installed and a working token. Use it instead of asking him to provision things through the web console.

Setup state (as of Crawl4AI deploy)

Common commands

export HOME=/home/avalon  # subagent envs sometimes lack this
export HCLOUD_TOKEN=$(grep HCLOUD_TOKEN ~/.hermes/.env | cut -d= -f2)
HC=~/.local/bin/hcloud

$HC server list
$HC server-type list           # see available sizes & arch
$HC location list              # ID/name/network zone
$HC ssh-key list
$HC server create --name <n> --type cpx11 --image ubuntu-24.04 --location hil --ssh-key avalon-current-vps-astral --user-data-from-file /tmp/cloudinit.yaml
$HC server delete <name>
$HC server poweroff <name>
$HC server reboot <name>
$HC server describe <name>     # full info incl IPv4/IPv6

Pricing reference (monthly cap, hourly billed)

Always verify live with $HC server-type describe <type> -o json: Hetzner US locations (hil, ash) are much more expensive than EU locations for shared CPU cpx* types. Dedicated CPU ccx* types may have little/no region spread, so do not assume moving every server saves money.

See references/hetzner-us-to-eu-migration-notes.md for the Hetzner-first overpayment workflow: compare exact type pricing, remember there is no in-place datacenter swap, recreate in fsn1/nbg1/hel1, migrate data/config, update DNS/firewalls, smoke test, then delete the US source.

See references/freemix-cost-storage-migration.md for the Freemix-derived cost/storage accounting pattern: verify server types, attached volumes, inside-guest mounts, volume pricing, temporary overlap cost, and whether the current EU topology is migration-safe versus final cost-optimized.

See references/freemix-eu-volume-cutover-runbook.md for the follow-on pattern when a large app was staged onto an oversized EU root disk due to volume quota, and you need to prove live storage parity, delete/free the old volume quota if approved, create a new EU volume, and mount it at the live storage path with minimal downtime.

Type Specs hil / US-west price EU price examples
cpx11 2 vCPU x86, 2GB, 40GB ~€20.49/mo ~€5.99/mo
cpx21 3 vCPU x86, 4GB, 80GB ~€37.49/mo ~€10.99/mo
cpx31 4 vCPU x86, 8GB, 160GB ~€73.49/mo ~€20.49/mo
cax11 2 vCPU ARM, 4GB, 40GB not in hil ~€6.99/mo
cax21 4 vCPU ARM, 8GB, 80GB not in hil ~€12.49/mo
cx33 4 vCPU x86, 8GB, 80GB not in hil ~€8.99/mo

Gotchas

Expanding disk on an existing server — volumes (no downtime path)

When a VPS hits >95% root disk full, the cheapest fix is a Hetzner Volume (block storage), not a server resize. Volumes are ~$0.048/GB/mo, live-attached, resizable up, and survive server destroy.

Provision + attach in one command

# --server and --location are MUTUALLY EXCLUSIVE on volume create.
# When attaching to an existing server, OMIT --location — it inherits the server's location.
$HC volume create --name data-100 --size 100 --format ext4 --server ubuntu-8gb-hil-1 --automount
# Output: Volume 105790860 created

--automount writes a /etc/fstab entry via /dev/disk/by-id/scsi-0HC_Volume_<id> with nofail,defaults so a missing volume won't brick boot. Default mount path is /mnt/HC_Volume_<id> — ugly. Rename it:

sudo mkdir -p /data
sudo umount /mnt/HC_Volume_<id>
sudo sed -i "s|/mnt/HC_Volume_<id>|/data|" /etc/fstab
sudo mount -a
sudo chown avalon:avalon /data
df -h /data    # confirm ~98G usable on a 100GB volume
move_dir() {
  local src="$1"; local name="$(basename "$src")"
  [ -L "$src" ] && { echo "skip: $src is symlink"; return; }
  mv "$src" "/data/$name"
  ln -s "/data/$name" "$src"
}
# Safe candidates: static asset vaults, wikis, build artifacts, cache dirs.
# Risky candidates: running app dirs (PM2 cwd) — possible but stop the app first.
move_dir /home/avalon/hermes-media-vault
move_dir /home/avalon/hyperframes

Migrating /var/lib/docker to a volume (BIG win, ~3 min downtime)

Docker data-root is usually the largest single dir on a VPS running containers. To shift it:

# 1. Stop the engine and its socket
sudo systemctl stop docker docker.socket containerd

# 2. rsync with --aHAX to preserve hardlinks/ACLs/xattrs (CRITICAL for docker)
sudo rsync -aHAX --info=progress2 /var/lib/docker/ /data/docker/

# 3. Verify sizes match (rough — docker compacts on next start)
sudo du -sh /var/lib/docker /data/docker

# 4. Move old aside, write daemon.json
sudo mv /var/lib/docker /var/lib/docker.OLD
sudo mkdir -p /etc/docker
echo '{"data-root": "/data/docker"}' | sudo tee /etc/docker/daemon.json

# 5. Start docker, verify
sudo systemctl start docker
sudo docker info | grep "Docker Root Dir"   # → /data/docker
sudo docker ps                              # auto-restart=always containers come back

Restart=no containers won't auto-start after daemon restart

Containers created without --restart unless-stopped (or always) stay Exited after systemctl restart docker. Manually docker start <name> each one. To make them resilient for next time:

sudo docker update --restart unless-stopped <container_name>

Pitfalls (volumes + data-root migration)

Server-resize path (alternative — adds CPU/RAM but requires reboot)

When you need disk + CPU + RAM together, resize the server. Cost-effective tiers from ccx13 (8GB / 80GB / $20):

Type vCPU RAM Disk $/mo (hil)
cpx32 (shared) 4 8GB 160GB ~$18 — cheaper but loses dedicated CPU
ccx23 (dedicated) 4 16GB 160GB ~$40 — clean 2x upgrade
ccx33 (dedicated) 8 32GB 240GB ~$80
$HC server poweroff ubuntu-8gb-hil-1
$HC server change-type ubuntu-8gb-hil-1 ccx23 --upgrade-disk
$HC server poweron ubuntu-8gb-hil-1

--upgrade-disk is irreversible — Hetzner won't let you shrink disk on downgrade later, you'd have to rebuild. Volumes don't have this problem.

Existing servers (do not destroy)

Server access

Use SSH from the main VPS when possible. astral-node-eu-1 (167.233.226.57) and freemix-eu-final (91.99.95.203) accept root@<ip> using the main VPS key. ubuntu-8gb-hil-1 should be inspected locally as avalon (root SSH denied from itself), and ubuntu-2gb-hil-1 may require recovering/updating authorized SSH keys before shell-level inspection. The old hermes-crawl4ai Hetzner server was deleted on 2026-07-01 after migrating Crawl4AI to Hostinger 187.127.70.43.