# Hetzner Dedicated Server: Proxmox Setup Guide

This guide covers the manual steps to get a Hetzner dedicated server running
Proxmox VE with SSH access from your workstation. Once SSH works, the
interactive [`proxmox-setup`](proxmox-setup) script handles everything else
(repos, networking, storage, WireGuard, firewall, API tokens).

**Workflow:**
1. **This guide** -- rent a server, install Proxmox via QEMU, get SSH working
2. **`proxmox-setup --host <alias>`** -- configure everything else interactively

---

## 1. Server selection

The [Hetzner Server Auction](https://www.hetzner.com/sb/) offers used
dedicated servers at significant discounts. Look for:

- **CPU**: Intel with VT-x and VT-d (AMD works too, but Intel nested
  virtualization is more proven with Proxmox/IncusOS)
- **Cores**: 32+ (each IncusOS VM wants 4-8 cores)
- **RAM**: 128+ GiB (256 GiB ideal -- 4 VMs at 16 GiB still leaves 192 GiB)
- **Disks**: 2+ NVMe or SSD (ZFS mirror for system, extras for VM storage)
- **Network**: 1 Gbit/s included, single IPv4

Order the server and wait for provisioning (usually minutes for auction
servers). Note the assigned IP address and temporary root password.

---

## 2. Proxmox installation

We use the official Proxmox VE ISO installer running inside QEMU from
the Hetzner rescue system. This gives the full graphical installer with
ZFS support, which is not available via Hetzner's `installimage`. The
method is documented in the
[Hetzner community tutorial](https://community.hetzner.com/tutorials/install-and-configure-proxmox_ve).

### 2.1 Boot the rescue system

In the Hetzner Robot panel, activate the rescue system (Linux 64-bit)
and reboot the server. Then SSH in:

```bash
ssh root@<public-ip>
```

### 2.2 Discover disks

Identify which disks the server has:

```bash
lsblk -d -o NAME,SIZE,MODEL,ROTA,TYPE
```

Example output on a typical auction server with 2 NVMe drives:

```
NAME    SIZE MODEL                ROTA TYPE
nvme0n1 477G Samsung SSD 970 EVO     0 disk
nvme1n1 477G Samsung SSD 970 EVO     0 disk
```

For ZFS RAID1 (mirror), you want two matching disks. NVMe pairs are the
strong favourite -- fast and reliable. Note the device names (`/dev/nvme0n1`,
`/dev/nvme1n1`) for the QEMU command.

If you have additional disks beyond the pair (e.g. large SATA drives),
those can be set up as a separate storage pool later. Only pass the system
disks to QEMU for now -- `proxmox-setup` will detect and configure the
rest.

### 2.3 Check BIOS mode

Determine whether the server boots in UEFI or legacy BIOS mode:

```bash
[ -d "/sys/firmware/efi" ] && echo "UEFI" || echo "BIOS"
```

Most modern Hetzner servers are UEFI. The QEMU command below includes
the OVMF BIOS line for UEFI -- remove it if your server reports BIOS.

### 2.4 Download the Proxmox ISO

Get the latest Proxmox VE ISO from the official download page:

```bash
wget https://enterprise.proxmox.com/iso/proxmox-ve_9.0-1.iso
```

Check [proxmox.com/en/downloads](https://www.proxmox.com/en/downloads)
for the current version and adjust the URL accordingly.

### 2.5 Set up SSH port forwarding for VNC

On your **workstation** (not the server), open an SSH tunnel forwarding
the VNC port:

```bash
ssh -L 5900:localhost:5900 root@<public-ip>
```

This forwards local port 5900 to the server's localhost:5900, where
QEMU will expose its VNC display. Keep this session open.

### 2.6 Start the QEMU installer

In the SSH session on the server, start QEMU with the ISO and the disks
you identified in step 2.2:

```bash
qemu-system-x86_64 \
    -enable-kvm \
    -cpu host \
    -m 16G \
    -boot d \
    -cdrom ./proxmox-ve_9.0-1.iso \
    -drive file=/dev/nvme0n1,format=raw,if=virtio \
    -drive file=/dev/nvme1n1,format=raw,if=virtio \
    -bios /usr/share/OVMF/OVMF_CODE.fd \
    -vnc 127.0.0.1:0
```

**Notes:**
- Remove the `-bios /usr/share/OVMF/OVMF_CODE.fd \` line for legacy
  BIOS servers (step 2.3).
- For SATA drives, use `/dev/sda`, `/dev/sdb` instead of `/dev/nvmeXn1`.
- Disks appear as `/dev/vdX` inside the VM because of the virtio interface
  -- this is normal and expected.
- The `-vnc 127.0.0.1:0` flag binds VNC to localhost only (safe, no
  password needed since it's behind the SSH tunnel).

### 2.7 Connect via VNC and install

Open a VNC client on your workstation and connect to `127.0.0.1:5900`
(or just `127.0.0.1` -- most clients default to port 5900).

The Proxmox graphical installer appears. Walk through it:

1. Accept the EULA.
2. **Target disk**: Select the ZFS RAID1 (mirror) option across both
   drives (`/dev/vda` and `/dev/vdb` in the VM -- these are your NVMe
   drives passed through via virtio).
3. **Country/timezone/keyboard**: Set as appropriate.
4. **Root password and email**: Set a strong root password. This becomes
   `HETZNER_PROXMOX_ROOT_PASSWORD` in your `env` file.
5. **Network**: The installer shows a virtualized NIC. Configure it with
   the server's public IP, gateway, and hostname. This will be corrected
   in the next step since the real NIC name differs.
6. Click **Install** and wait for completion.

The VNC client may disconnect briefly during install -- just reconnect
to `127.0.0.1:5900`.

### 2.8 Predict the real network interface name

After installation completes, stop QEMU with `Ctrl+C` in the SSH
terminal. **Do not reboot yet** -- the network interface name configured
by the installer is wrong (it matches the virtual NIC, not the real one).

Use the Hetzner `predict-check` tool to discover the real interface name:

```bash
predict-check
```

Example output:

```
eth0 -> enp0s31f6
```

Note the predicted name (e.g. `enp0s31f6`). You can also check the
current rescue interface for reference:

```bash
netdata
```

### 2.9 Fix the network config before first real boot

Boot Proxmox again in QEMU, **without** the ISO (no `-cdrom` flag):

```bash
qemu-system-x86_64 \
    -enable-kvm \
    -cpu host \
    -m 16G \
    -boot d \
    -drive file=/dev/nvme0n1,format=raw,if=virtio \
    -drive file=/dev/nvme1n1,format=raw,if=virtio \
    -bios /usr/share/OVMF/OVMF_CODE.fd \
    -vnc 127.0.0.1:0
```

Connect via VNC, log in as root, and edit the network configuration:

```bash
nano /etc/network/interfaces
```

Replace the virtual interface name (e.g. `ens18`) with the predicted
real name (e.g. `enp0s31f6`). A minimal working config:

```
auto lo
iface lo inet loopback

auto enp0s31f6
iface enp0s31f6 inet static
    address <public-ip>/32
    gateway <gateway-ip>
```

Save and shut down the VM (`shutdown -h now` inside the VNC session,
or `Ctrl+C` in the SSH terminal).

### 2.10 Reboot into Proxmox

Exit the rescue system and reboot the server from the Hetzner Robot
panel (or just `reboot` from SSH). The server now boots from disk
into Proxmox with the correct network configuration.

### 2.11 Verify

```bash
ssh root@<public-ip> pvesh get /version
# Should show Proxmox VE version and API info
```

> **Alternative method**: You can also install Debian 13 via Hetzner's
> `installimage` and then upgrade to Proxmox following the
> [official guide](https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_13_Trixie).
> This skips the QEMU/VNC process but does not offer ZFS-on-root from
> the installer.

---

## 3. SSH setup and handoff to proxmox-setup

At this point Proxmox is installed and reachable via its public IP.
The remaining configuration (repos, private networking, storage,
WireGuard, firewall, API tokens) is handled by the `proxmox-setup`
script. You just need SSH key access and an alias.

### 3.1 Copy your SSH key

```bash
ssh-copy-id root@<public-ip>
```

### 3.2 Create an SSH config alias

Add to `~/.ssh/config` on your workstation:

```
Host hetzner-lab
    HostName <public-ip>
    User root
    IdentityFile ~/.ssh/id_ed25519
```

### 3.3 Verify SSH alias works

```bash
ssh hetzner-lab pvesh get /version
```

This must succeed before continuing. If it fails, check your SSH key
and config entry.

### 3.4 Run proxmox-setup

```bash
# Preview what will be configured
hetzner/proxmox-setup --host hetzner-lab --dry-run

# Run for real (interactive, prompts before each step)
hetzner/proxmox-setup --host hetzner-lab
```

The script walks through each step interactively:

1. **Detect** -- reads host info, block devices, existing config
2. **Repos** -- switches to no-subscription repos, removes nag popup
3. **Update** -- `apt update && apt dist-upgrade`
4. **Bridge** -- creates vmbr1 (private bridge, 10.10.0.0/24) with NAT
5. **Storage** -- detects unused disks, creates ZFS pool, registers with PVE
6. **WireGuard** -- installs and configures tunnel, prints client config
7. **Firewall** -- locks down public interface (SSH + WireGuard only)
8. **API token** -- creates role, pool, and token for `incusos-proxmox`

Each step prompts for confirmation. Use `--yes` to skip prompts, or
`--skip-repos`, `--skip-storage`, etc. to skip individual steps.

At the end, the script prints:
- A ready-to-use `proxmox.yaml` config for `envs/hetzner/`
- An `env` file snippet with the API token
- A WireGuard client config to save on your workstation

### 3.5 Connect WireGuard and verify

Save the WireGuard client config printed by the script:

```bash
# Linux
sudo wg-quick up hetzner-lab

# macOS -- import into the WireGuard app
```

Verify access through the tunnel:

```bash
ping 10.10.0.1                          # Proxmox host via WireGuard
curl -sk https://10.10.0.1:8006         # Web UI via WireGuard
```

Optionally add a second SSH alias for access via WireGuard:

```
Host hetzner-lab-wg
    HostName 10.10.0.1
    User root
    IdentityFile ~/.ssh/id_ed25519
```

### 3.6 Test deploy

```bash
# Copy and fill in the connection config
cp envs/hetzner/proxmox.yaml.example envs/hetzner/proxmox.yaml

# Set the API token
export PROXMOX_TOKEN_SECRET="<token from proxmox-setup output>"

# Dry run -- verify correct bridge, IPs, storage
bin/incusos-proxmox --dry-run \
    --proxmox envs/hetzner/proxmox.yaml \
    envs/hetzner/lab-cluster.yaml
```

Confirm the output shows: `vmbr1` bridge, `10.10.0.x` IPs, `local-zfs`
storage, 8 cores per VM.

---

## Network architecture (reference)

Once `proxmox-setup` has completed, the network looks like this:

```
Internet
    |
    | Public IP (e.g. 5.9.x.x)
    v
[vmbr0] ──── Proxmox host ──── [wg0: 10.10.99.1/24]
                  |                       |
                  |                  WireGuard tunnel
                  |                       |
             [vmbr1: 10.10.0.1/24]   Your workstation
                  |                  (10.10.99.2)
            ┌─────┼─────┐
            |     |     |
          VM-01 VM-02 VM-03
          .101  .102  .103
```

| Interface | Purpose |
|-----------|---------|
| vmbr0 | Public (SSH + WireGuard only after firewall) |
| vmbr1 | Private bridge for VMs (10.10.0.0/24, NAT to internet) |
| wg0 | WireGuard tunnel (10.10.99.0/24) |

AllowedIPs uses `10.10.0.0/16` to leave room for future subnets
(OVN overlay at 10.10.10.0/24, etc.) without updating WireGuard config.
