Use when provisioning or hardening a Hetzner Cloud VPS: picking the plan/location (CX/CPX/CAX/CCX), bringing it up reproducibly with hcloud + cloud-init, locking down SSH, and wiring the Cloud Firewall to hand off a Docker-ready host. NOT app deploys on Coolify (that is `coolify`), NOT Dockerfiles (that is `docker`), NOT another provider (that is `digitalocean`).
npx skills add https://github.com/ericrisco/rsc-harness --skill hetzner
Hetzner Cloud is chosen for one reason: price/performance. A 2 vCPU / 4 GB AMD
box runs about €7.99/mo, the EU Intel line dips under €4, and EU locations
include 20 TB of egress. The risk is that "cheap" becomes "unhardened and
unmonitored" — a root-SSH box on a public IPv4 with password auth on. Your job
is to make the cheap box safe and reproducible: every server comes up
from a committed cloud-init file and a Cloud Firewall ruleset, never from
click-ops in the console. A box you can't recreate from a file isn't a box, it's
a pet.
Pick the line, then the location. Prices are post-2026-04-01 (a price adjustment
took effect that date); treat exact cents as "verify in the console."
| Line | Chip | When to pick | Price band | EU-only? |
|---|---|---|---|---|
| CX | Intel (shared) | Cost-optimized, tiny EU workloads | ~€3.99/mo (2 vCPU/4 GB/40 GB) | Yes |
| CPX | AMD (shared) | Default for apps; best general value | CPX22 ~€7.99, CPX32 ~€13.99, CPX42 ~€25.49 | No |
| CAX | ARM64 (shared) | Cheapest per-core; ARM-clean workloads | Cheapest per-core | Yes |
| CCX | Dedicated vCPU | Steady CPU load, no noisy-neighbor | CCX13 ~€15.99 (2 vCPU/8 GB) | No |
Rules:
cheaper, but it breaks anything that can't reach IPv6: many container
registries, some package mirrors, CI runners, SSH clients on IPv4-only nets.
If docker pull or apt will run on the box, keep one IPv4 unless you've
confirmed every upstream is dual-stack. Why: a saved €0.50/mo is not worth a
broken docker pull at 2 a.m.
Helsinki) include 20 TB/mo egress; overage ~€1/TB. US (Ashburn, Hillsboro)
and Singapore have far lower included transfer. Why: pick US "for latency,"
serve a video, and the surprise bill is the traffic cap, not the instance.
cost — name it, don't hide it.
Full dated matrix, latency notes, and the no-SLA / no-managed-DB reality:
references/plans-and-locations.md.
Install the official CLI (latest v1.65.0, released 2026-05-21) and create a
context (the token comes from the project's *Security → API tokens*, Read &
Write):
brew install hcloud # or: see github.com/hetznercloud/cli releases
hcloud context create my-project # paste the Read+Write API token when prompted
hcloud server-type list # confirm names/prices before you create
Bring the box up with cloud-init so hardening happens before first login:
hcloud server create \
--name app-01 \
--type cpx22 \
--location fsn1 \
--image debian-12 \
--ssh-key my-laptop \
--firewall app-edge \
--user-data-from-file cloud-init.yaml
Use --location, not --datacenter: the datacenter attribute is **deprecated
and removed after 2026-07-01** for Servers and Primary IPs. Trimmed cloud-init
skeleton (full annotated file in references/cloud-init.md):
#cloud-config
users:
- name: deploy
groups: [sudo]
shell: /bin/bash
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
ssh_authorized_keys:
- ssh-ed25519 AAAA... you@laptop # your real public key, not a placeholder
disable_root: true # no root login at all
ssh_pwauth: false # no password auth, anywhere
package_update: true
packages: [ufw, fail2ban, unattended-upgrades]
runcmd:
- sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
- sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
- ufw default deny incoming && ufw allow 22 && ufw allow 80 && ufw allow 443 && ufw --force enable
- systemctl enable --now fail2ban
- systemctl restart ssh
Rule: never create a box with a bare root password and "I'll harden it later."
Later is a window where the box is reachable as root with password auth on, and
Hetzner IPv4 space is scanned constantly. Bake it into boot.
The Cloud Firewall runs at the network edge — stateful, applied before the
packet reaches the VM, and survives a misconfigured host. Create and apply it
with or before the server:
hcloud firewall create --name app-edge
hcloud firewall add-rule app-edge --direction in --protocol tcp --port 22 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule app-edge --direction in --protocol tcp --port 80 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule app-edge --direction in --protocol tcp --port 443 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall apply-to-resource app-edge --type server --server app-01
Inbound is default-deny — you only describe what you *allow*. Then the host
ufw (set up in cloud-init above) is defense-in-depth: if you ever detach or
fat-finger the Cloud Firewall, the box still isn't wide open. Why both: the edge
firewall is your primary defense, but a single layer is a single point of
failure, and the two are configured through different surfaces (API vs host).
Tighten SSH to your own IP/range once you know it:
hcloud firewall delete-rule app-edge --direction in --protocol tcp --port 22 --source-ips 0.0.0.0/0 --source-ips ::/0
hcloud firewall add-rule app-edge --direction in --protocol tcp --port 22 --source-ips 203.0.113.4/32
PasswordAuthentication no, PubkeyAuthentication yes. Apassword is brute-forceable; an ed25519 key is not.
PermitRootLogin no + a sudo user. Root over SSH is thesingle most-targeted login on the internet.
one port you must expose.
harden anything. Do it if you like clean logs, but never *instead* of key-only
+ a firewall.
"Host ready" means all of the following are true:
ssh root@<ip> is refused; ssh deploy@<ip> works with the key only.sshd -T | grep -E 'permitrootlogin|passwordauthentication' shows both no.ufw status enabled with the same allowlist.unattended-upgrades and fail2ban running.This skill stops at a clean, hardened host. Then route the install/deploy:
coolify for the Coolify install + app deploy flow,
docker for Dockerfiles, compose, and image hardening.
the Backups add-on is automated, rotating, ~20% of the server price. A
snapshot you took once in March is not a backup strategy — for that, see
backups.
back down. Size conservatively or you're stuck paying for it.
and uploads on a volume so a server rebuild doesn't take the data with it.
console/hcloud if the box sends email. Forward DNS records belong to
domains-dns.
product, and phone support only for dedicated-server customers — you run
Postgres yourself, you monitor it yourself. For monitoring and alerting, see
monitoring; treat the box as something you must
watch, not something Hetzner watches for you. Name the trade-off to the user;
don't pretend it's AWS.
| Anti-pattern | Why it's wrong | Do instead |
|---|---|---|
| Root SSH + password auth left on | The most-scanned login on the public internet; bots find it in minutes | PermitRootLogin no, PasswordAuthentication no, key + sudo user |
| Only host ufw, no Cloud Firewall | A host misconfig or reset exposes everything; no edge layer | Cloud Firewall default-deny first, ufw as defense-in-depth |
| --source-ips 0.0.0.0/0 on everything "temporarily" | Temporary rules become permanent; the whole box is exposed | Scope inbound to 80/443 public, SSH to your IP/range |
| Click-ops in the console | Not reproducible — you can't recreate or review the box | cloud-init file + hcloud commands committed to the repo |
| Snapshot treated as backup | One stale manual image, no rotation, no schedule | Enable the Backups add-on or push to off-box storage |
| US location, then surprised by the bill | US/Singapore include far less than EU's 20 TB egress | EU location for egress-heavy apps; check the traffic cap first |
| IPv6-only to save €0.50 | Registry/CI/mirror pulls over IPv4-only break | Keep one IPv4 unless every upstream is confirmed dual-stack |
| Using --datacenter | Deprecated, removed after 2026-07-01 | Use --location |
ssh -o BatchMode=yes root@<ip> # expect: refused / permission denied
ssh deploy@<ip> 'sshd -T | grep -E "permitrootlogin|passwordauthentication"'
# expect: permitrootlogin no / passwordauthentication no
hcloud firewall describe app-edge # expect: only 22 (scoped)/80/443 inbound
ssh deploy@<ip> 'ss -tlnp' # expect: only expected listeners
To lint a cloud-init / hardening file before you ever create the box, run
scripts/verify.sh path/to/cloud-init.yaml — it statically checks for the
must-haves (no root login, no password auth, an SSH key, a firewall step,
fail2ban) with no network calls.
Take ericrisco/hetzner from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference brew.
Without those the skill loads but fails at the first command.