Hetzner Server Management
Create and manage Hetzner Cloud servers using the hcloud CLI.
Prerequisites
Cloud Firewalls
Reusable firewall profiles applied at server creation. Firewalls can be swapped on running servers โ use apply-to-resource / remove-from-resource.
| Firewall |
Rules |
Use case |
ts-ssh |
UDP 41641 (Tailscale) + TCP 22 (SSH) |
Dev boxes โ initial setup, swap to ts-only after tsonlyssh |
ts-only |
UDP 41641 (Tailscale) |
Tailscale-only access, no public ports |
ts-web |
UDP 41641 (Tailscale) + TCP 80,443 (HTTP/S) |
Servers accepting public web traffic |
Swapping firewalls on a running server
hcloud firewall remove-from-resource ts-ssh --type server --server dev
hcloud firewall apply-to-resource ts-only --type server --server dev
Quick Reference
Create a server
hcloud server create \
--name dev \
--type cax21 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key connorads \
--ssh-key connor@penguin \
--firewall ts-ssh
hcloud server create \
--name dev \
--type cpx21 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key connorads \
--ssh-key connor@penguin \
--firewall ts-ssh
hcloud server create \
--name dev \
--type cax21 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key connorads \
--ssh-key connor@penguin \
--firewall ts-ssh \
--without-ipv4
With user-data (auto-run install script)
hcloud server create \
--name dev \
--type cax21 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key connorads \
--ssh-key connor@penguin \
--firewall ts-ssh \
--user-data-from-file - <<'EOF'
#!/bin/bash
curl -fsSL https://raw.githubusercontent.com/connorads/dotfiles/master/install.sh | bash
EOF
The dotfiles installation takes ~5 minutes. To monitor progress:
ssh connor@$(hcloud server ip dev) "cloud-init status"
ssh connor@$(hcloud server ip dev) "sudo journalctl -u cloud-final -n 50 --no-pager"
ssh connor@$(hcloud server ip dev) "sudo journalctl -u cloud-final -f"
ssh connor@$(hcloud server ip dev) "which zsh mise && echo \$SHELL"
With swap (recommended for production)
Ubuntu cloud images don't include swap by default. Add swap via cloud-init at creation:
hcloud server create \
--name dev \
--type cax33 \
--image ubuntu-24.04 \
--location nbg1 \
--ssh-key connorads \
--ssh-key connor@penguin \
--firewall ts-ssh \
--user-data-from-file - <<'EOF'
#cloud-config
swap:
filename: /swapfile
size: 16G
maxsize: 16G
EOF
Recommended swap sizes:
- 4GB RAM โ 4-8GB swap
- 8GB RAM โ 8GB swap
- 16GB+ RAM โ 16GB swap (1:1 ratio)
Add swap to existing server:
ssh connor@$(hcloud server ip dev) "sudo fallocate -l 16G /swapfile && \
sudo chmod 600 /swapfile && \
sudo mkswap /swapfile && \
sudo swapon /swapfile && \
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab"
ssh connor@$(hcloud server ip dev) "free -h"
Common commands
hcloud server list
hcloud server ip dev
ssh connor@$(hcloud server ip dev)
hcloud server delete dev
hcloud server poweroff dev
hcloud server poweron dev
hcloud server reboot dev
hcloud server rebuild dev --image ubuntu-24.04
Server types (commonly used)
Prices in USD for EU regions (US regions ~20% higher):
| Type |
Arch |
vCPU |
RAM |
Disk |
~USD/mo |
| cax11 |
ARM |
2 |
4GB |
40GB |
$4.50 |
| cax21 |
ARM |
4 |
8GB |
80GB |
$8 |
| cax31 |
ARM |
8 |
16GB |
160GB |
$16 |
| cpx21 |
x86 |
3 |
4GB |
80GB |
$9 |
| cpx31 |
x86 |
4 |
8GB |
160GB |
$18 |
Full list: hcloud server-type list
Locations
| ID |
City |
Country |
| fsn1 |
Falkenstein |
DE |
| nbg1 |
Nuremberg |
DE |
| hel1 |
Helsinki |
FI |
| ash |
Ashburn |
US |
| hil |
Hillsboro |
US |
| sin |
Singapore |
SG |
SSH keys
hcloud ssh-key list
hcloud ssh-key create --name mykey --public-key-from-file ~/.ssh/id_ed25519.pub
Images
hcloud image list --type system
hcloud image list --type system --architecture arm
Cloning GitHub repos (SSH agent forwarding)
Use the <name>-agent SSH host (which has agent forwarding enabled) to clone private repos without copying keys to the server. If you hit host key errors, add GitHub's host key first.
ssh dev "ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null"
ssh dev-agent "ssh-add -l"
ssh dev-agent "mkdir -p ~/git && cd ~/git && git clone [email protected]:you/repo.git"
ssh dev-agent "mkdir -p ~/git && cd ~/git && git clone [email protected]:you/repo.git && cd repo && git checkout branch-name"
ssh dev-agent "cd repo && git push"
For interactive sessions (e.g., lazygit):
ssh dev-agent
Post-creation setup
After creating a server, always clear any old host keys for that IP (Hetzner reuses IPs):
ssh-keygen -R $(hcloud server ip dev) 2>/dev/null
ssh-keyscan $(hcloud server ip dev) >> ~/.ssh/known_hosts 2>/dev/null
Then generate/update SSH config entries:
hcssh
hcssh --dry-run
This creates two Host entries per server inside a managed block (# BEGIN/END hetzner-managed):
<name> โ no agent forwarding (safe for AI agents)
<name>-agent โ with agent forwarding (for git push/pull to GitHub)
Run hcssh again after creating/deleting servers to keep SSH config in sync.
This enables VS Code Remote-SSH to show the server in the dropdown.
Optional: Restrict SSH to Tailscale only
After ts up and confirming SSH works via Tailscale (ts ssh connor@dev), run tsonlyssh on the server to remove public port 22 from UFW. This leaves SSH accessible only via the Tailscale interface.
Fallback: Hetzner Cloud Console VNC if locked out.
Notes
- ARM (cax*) servers are best value for dev work
- IPv6-only saves money but requires Tailscale/cloudflared for access from IPv4 networks
- User-data runs as root on first boot
- The dotfiles install.sh handles creating user
connor, installing Nix, home-manager, and mise tools