# Hetzner Server

> Create and manage Hetzner Cloud servers. Use when creating VPS/cloud servers, managing Hetzner infrastructure, or setting up dev/remote servers. Requires hcloud CLI.

- Skill: `connorads/hetzner-server` (Agent Skill)
- Install (CLI): `npx skillmds@latest add connorads/hetzner-server`
- Raw SKILL.md: https://api.skillmd.com/api/skills/connorads/hetzner-server/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: connorads (https://skillmd.com/u/connorads)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/connorads/hetzner-server

---


# Hetzner Server Management

Create and manage Hetzner Cloud servers using the `hcloud` CLI.

## Prerequisites

- `hcloud` CLI installed (via mise: `hcloud = "latest"`)
- Authenticated: `hcloud context create <name>` with API token from <https://console.hetzner.cloud>

## 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

```bash
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

```bash
# Prefer ARM (best value)
hcloud server create \
  --name dev \
  --type cax21 \
  --image ubuntu-24.04 \
  --location nbg1 \
  --ssh-key connorads \
  --ssh-key connor@penguin \
  --firewall ts-ssh

# x86 fallback
hcloud server create \
  --name dev \
  --type cpx21 \
  --image ubuntu-24.04 \
  --location nbg1 \
  --ssh-key connorads \
  --ssh-key connor@penguin \
  --firewall ts-ssh

# IPv6-only (saves ~$0.60/month on IPv4)
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
```

**IPv6-only caveat:** `hcloud server ip <name>` and `hcssh` assume IPv4 -
`server ip` prints nothing useful and hcssh reads only the ipv4 column, so
the managed Host entries come out empty. For a `--without-ipv4` server use
`hcloud server ip --ipv6 <name>` (returns the /64; the host is `...::1`) and
write the SSH Host entry by hand, or just reach it over Tailscale once
`ts up` has run.

### With user-data (auto-run install script)

```bash
# Use heredoc - process substitution <(echo '...') escapes the shebang incorrectly
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:

```bash
# Quick status check
ssh connor@$(hcloud server ip dev) "cloud-init status"

# View recent installation logs
ssh connor@$(hcloud server ip dev) "sudo journalctl -u cloud-final -n 50 --no-pager"

# Follow installation in real-time
ssh connor@$(hcloud server ip dev) "sudo journalctl -u cloud-final -f"

# Check if tools are installed
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:

```bash
# Create server with 16GB swap (1:1 ratio for 16GB RAM server)
hcloud server create \
  --name dev \
  --type cax31 \
  --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:**

```bash
# Create 16GB swap file
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"

# Verify swap is active
ssh connor@$(hcloud server ip dev) "free -h"
```

### Reference lookups

Server types, locations, images, SSH keys, and day-to-day server ops are live queries - look them up, don't memorise:

```bash
hcloud server-type list                              # ARM cax* preferred (best value); cpx* x86 fallback
hcloud location list                                 # nbg1 is the usual default here
hcloud image list --type system --architecture arm
hcloud ssh-key list
hcloud server list|ip|delete|poweroff|poweron|reboot|rebuild   # rebuild keeps the IP
```

## 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.

```bash
# First time only: add GitHub's host key
ssh dev "ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null"

# Confirm forwarded agent is visible
ssh dev-agent "ssh-add -l"

# Clone with agent forwarding (use -agent suffix)
ssh dev-agent "mkdir -p ~/git && cd ~/git && git clone git@github.com:you/repo.git"

# Clone specific branch
ssh dev-agent "mkdir -p ~/git && cd ~/git && git clone git@github.com:you/repo.git && cd repo && git checkout branch-name"

# Push/pull with agent forwarding
ssh dev-agent "cd repo && git push"
```

For interactive sessions (e.g., lazygit):

```bash
ssh dev-agent
# Then on server: git clone/push/pull works with forwarded agent
```

## Post-creation setup

After creating a server, **always** clear any old host keys for that IP (Hetzner reuses IPs):

```bash
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:

```bash
hcssh              # update ~/.ssh/config with all Hetzner servers
hcssh --dry-run    # preview without writing
```

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

- 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

