How to Install Proxmox VE on a Hetzner Dedicated Server with ZFS RAID1, SSH Hardening, Firewall, and SSL

Install Proxmox on Dedicated server

This tutorial shows how to install Proxmox VE on a dedicated Hetzner server using the official Proxmox ISO, Hetzner Rescue Mode, QEMU/VNC, ZFS RAID1 mirror, hardened SSH access, Hetzner firewall rules, and a trusted Let’s Encrypt certificate through Cloudflare DNS.

This guide is written for a server with 2 NVMe drives where you want safety first. The result is a clean Proxmox host with mirrored storage, SSH key-only login, a custom SSH port, DNS hostname access, firewall protection, and trusted HTTPS for the Proxmox web panel.

Proxmox VE is an open-source virtualization platform for managing KVM virtual machines, LXC containers, storage, networking, and clustering from a web interface. Hetzner’s own Proxmox guide confirms that on dedicated servers you can either install Debian first or use the official Proxmox ISO through QEMU from Rescue Mode. (Hetzner Community)

Example Server

CPU: AMD Ryzen 9 3900
RAM: 128 GB ECC
Disks: 2 × 1.92 TB NVMe
Network: 1 Gbit
Storage plan: ZFS RAID1 mirror

Important Placeholders

Never paste real passwords, private keys, API tokens, or server IPs into public places. Replace these placeholders locally:

[SENSITIVE: SERVER_MAIN_IPV4]
[SENSITIVE: SERVER_GATEWAY]
[SENSITIVE: PROXMOX_ROOT_PASSWORD]
[SENSITIVE: ADMIN_EMAIL]
[SENSITIVE: CLOUDFLARE_API_TOKEN]
[SENSITIVE: CLOUDFLARE_ZONE_ID]
example.com
pve1.example.com

1. Boot Hetzner Rescue Mode

In Hetzner Robot:

Server → Rescue → Linux → Keyboard: us → Activate

Then reboot the server using:

Reset → Execute an automatic hardware reset

SSH into Rescue Mode:

ssh root@[SENSITIVE: SERVER_MAIN_IPV4]

Check disks:

lsblk -o NAME,SIZE,TYPE,MODEL

Example:

nvme0n1  1.7T disk
nvme1n1  1.7T disk

Check network information:

ip -br addr
ip route

Write down privately:

[SENSITIVE: SERVER_MAIN_IPV4]/CIDR
[SENSITIVE: SERVER_GATEWAY]

Example CIDR may be /26, /27, /28, etc. Do not assume /24.

Check boot mode:

[ -d /sys/firmware/efi ] && echo "UEFI" || echo "Legacy"

This tutorial uses the Legacy QEMU command. If your server returns UEFI, use the UEFI/OVMF QEMU path from Hetzner’s documentation. Hetzner’s unattended Proxmox guide also shows gathering boot mode, network, disk information, and running the installer ISO through QEMU. (Hetzner Community)


2. Download and Verify the Official Proxmox ISO

Always check the current Proxmox download page before installing. At the time this tutorial was written, Proxmox showed VE 9.2 as the current version. (Proxmox)

Example for Proxmox VE 9.2-1:

cd /root

wget -O proxmox-ve_9.2-1.iso https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso

echo "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c  proxmox-ve_9.2-1.iso" | sha256sum -c

Expected:

proxmox-ve_9.2-1.iso: OK

3. Open a Secure VNC Tunnel

On your local computer, open an SSH tunnel:

ssh -L 5900:127.0.0.1:5900 root@[SENSITIVE: SERVER_MAIN_IPV4]

Keep this terminal open.

The VNC port is bound to 127.0.0.1 on the server, so it is not publicly exposed. You will view it locally through:

127.0.0.1:5900

Use RealVNC Viewer or TigerVNC Viewer. macOS Screen Sharing may hang with QEMU VNC.


4. Start the Proxmox Installer with QEMU

In the Rescue Mode SSH session, run:

qemu-system-x86_64 \
  -enable-kvm \
  -m 4096 \
  -smp 4 \
  -boot d \
  -cdrom /root/proxmox-ve_9.2-1.iso \
  -drive file=/dev/nvme0n1,format=raw,media=disk,if=virtio \
  -drive file=/dev/nvme1n1,format=raw,media=disk,if=virtio \
  -vnc 127.0.0.1:0 \
  -k en-us

The terminal will look “stuck.” That is normal. QEMU is running the installer.

Open RealVNC Viewer and connect to:

127.0.0.1:5900

If asked for a password, leave it blank.


5. Proxmox Installer Choices

Choose:

Install Proxmox VE Graphical

If you see a warning about missing KVM acceleration inside QEMU, click OK. The installed Proxmox system will later boot directly on the real hardware.

Use:

Country: United States
Time zone: America/Chicago or UTC
Keyboard: English (US)

Set your root password and admin email:

Password: [SENSITIVE: PROXMOX_ROOT_PASSWORD]
Email: [SENSITIVE: ADMIN_EMAIL]

Target Harddisk

This is the most important part.

Click Options and choose:

Filesystem: ZFS RAID1
Disk 1: /dev/vda
Disk 2: /dev/vdb

Inside QEMU, the real NVMe disks appear as:

/dev/vda = /dev/nvme0n1
/dev/vdb = /dev/nvme1n1

Use:

ashift: 12
compress: on
checksum: on
copies: 1
hdsize: full disk size
ARC max size: use the max the installer allows, or adjust later

The ARC value may look small because the installer only sees the temporary QEMU memory. You can tune ZFS ARC later after Proxmox boots on the real 128 GB RAM server.

Network Screen

Use:

Hostname: pve1.example.com
IP Address: [SENSITIVE: SERVER_MAIN_IPV4]/CIDR
Gateway: [SENSITIVE: SERVER_GATEWAY]
DNS Server: 1.1.1.1

Do not use a temporary QEMU DNS such as 10.0.2.3.

Leave this unchecked:

Pin network interface names: unchecked

Because the installer is running inside QEMU, the network interface shown there is temporary.

Also leave this unchecked:

Automatically reboot after successful installation: unchecked

When installation finishes, do not click reboot.


6. Verify ZFS Before First Boot

Go back to the SSH terminal running QEMU and press:

Ctrl + C

Now you are back in Rescue Mode.

If Rescue Mode does not have ZFS tools, zpool import may ask to temporarily install ZFS support. Accept it.

Run:

zpool import

Expected:

pool: rpool
state: ONLINE

rpool
  mirror-0
    nvme0n1p3 ONLINE
    nvme1n1p3 ONLINE

If you see only one disk online, or one disk still has LVM, stop and clean/reinstall before booting. A correct ZFS RAID1 install must show both NVMe partitions online.

Import and mount the new system:

zpool import -f -R /mnt rpool
zfs list

You should see:

rpool/ROOT/pve-1  /mnt

7. Fix the Network Interface Name

The installer may have saved the temporary QEMU interface, such as ens3. We need to replace it with the real physical NIC.

Find the real NIC name:

udevadm test-builtin net_id /sys/class/net/eth0 2>/dev/null | grep ID_NET_NAME

Example:

ID_NET_NAME_PATH=enp35s0

Check the Proxmox network file:

cat /mnt/etc/network/interfaces

If you see something like:

iface ens3 inet manual

auto vmbr0
iface vmbr0 inet static
    bridge-ports ens3

replace ens3 with the real interface:

cp /mnt/etc/network/interfaces /mnt/etc/network/interfaces.bak-before-first-boot

sed -i 's/ens3/enp35s0/g' /mnt/etc/network/interfaces

cat /mnt/etc/network/interfaces

Expected format:

auto lo
iface lo inet loopback

iface enp35s0 inet manual

auto vmbr0
iface vmbr0 inet static
    address [SENSITIVE: SERVER_MAIN_IPV4]/CIDR
    gateway [SENSITIVE: SERVER_GATEWAY]
    bridge-ports enp35s0
    bridge-stp off
    bridge-fd 0

source /etc/network/interfaces.d/*

Check hostname files:

cat /mnt/etc/hostname
cat /mnt/etc/hosts

Expected:

pve1
[SENSITIVE: SERVER_MAIN_IPV4] pve1.example.com pve1

Export and reboot:

zpool export rpool
reboot

8. First Proxmox Login

Open:

https://[SENSITIVE: SERVER_MAIN_IPV4]:8006

Login:

User: root
Realm: Linux PAM
Password: [SENSITIVE: PROXMOX_ROOT_PASSWORD]

A browser SSL warning is normal at this stage because Proxmox uses a self-signed certificate before ACME is configured.

Check ZFS:

zpool status

Expected:

rpool ONLINE
mirror-0 ONLINE
nvme0n1p3 ONLINE
nvme1n1p3 ONLINE
errors: No known data errors

9. Fix Proxmox Repositories

Fresh Proxmox installs may show:

You do not have a valid subscription
apt-get update failed code 100
401 Unauthorized

That happens when enterprise repositories are enabled without a Proxmox subscription. The enterprise repository is for subscription users; the no-subscription repository does not need a subscription, but Proxmox notes it is not as heavily tested as enterprise. (Proxmox VE)

Check repos:

grep -R "enterprise.proxmox.com\|download.proxmox.com\|proxmox" \
  /etc/apt/sources.list \
  /etc/apt/sources.list.d/*.list \
  /etc/apt/sources.list.d/*.sources 2>/dev/null

Disable enterprise repos and add no-subscription:

mkdir -p /root/apt-sources-backup

cp -a /etc/apt/sources.list /root/apt-sources-backup/ 2>/dev/null || true
cp -a /etc/apt/sources.list.d /root/apt-sources-backup/

mv /etc/apt/sources.list.d/pve-enterprise.sources /etc/apt/sources.list.d/pve-enterprise.sources.disabled 2>/dev/null || true
mv /etc/apt/sources.list.d/ceph.sources /etc/apt/sources.list.d/ceph.sources.disabled 2>/dev/null || true

cat > /etc/apt/sources.list.d/pve-no-subscription.sources <<'EOF'
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF

apt update
apt dist-upgrade -y
reboot

After reboot:

pveversion
zpool status

10. Configure DNS Hostname

In Cloudflare DNS, create:

Type: A
Name: pve1
Value: [SENSITIVE: SERVER_MAIN_IPV4]
Proxy status: DNS only / gray cloud
TTL: Auto

Keep it DNS only, because Cloudflare’s normal proxy only supports specific HTTP/HTTPS ports and Proxmox uses port 8006. Cloudflare documents the ports it proxies by default; 8006 is not one of the standard proxied HTTPS ports. (Cloudflare Docs)

Test from your computer:

dig +short pve1.example.com

Then access:

https://pve1.example.com:8006

11. Harden SSH

Create a dedicated SSH key on your desktop:

mkdir -p ~/.ssh
chmod 700 ~/.ssh

ssh-keygen -t ed25519 -a 100 -C "desktop1-proxmox-pve1" -f ~/.ssh/pve1_desktop1_ed25519

Add the key to Proxmox:

cat ~/.ssh/pve1_desktop1_ed25519.pub | ssh root@[SENSITIVE: SERVER_MAIN_IPV4] "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Test:

ssh -i ~/.ssh/pve1_desktop1_ed25519 root@[SENSITIVE: SERVER_MAIN_IPV4]

Add a shortcut on your Mac:

nano ~/.ssh/config

Add:

Host proxmox
    HostName pve1.example.com
    User root
    Port 19
    IdentityFile ~/.ssh/pve1_desktop1_ed25519
    IdentitiesOnly yes

Now configure Proxmox SSH to use port 19 and key-only login:

cat > /etc/ssh/sshd_config.d/99-custom-ssh-hardening.conf <<'EOF'
Port 19

PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no

PermitRootLogin prohibit-password
UsePAM yes
EOF

/usr/sbin/sshd -t
systemctl reload ssh
ss -tlnp | grep sshd

Open a new terminal and test:

ssh proxmox

Only after key login works should you confirm port 22 is closed:

ss -tlnp | grep sshd

Expected:

0.0.0.0:19
[::]:19

12. Use Hetzner Firewall First

For the host firewall, use Hetzner Robot firewall first. Hetzner’s dedicated-server firewall is stateless and configured at the switch port. Hetzner warns that enabling it before adding rules can block all incoming traffic. (Hetzner Docs)

In Hetzner Robot Firewall, add these incoming rules before removing “Allow all”:

NameVersionProtocolSource IPDestination IPSource portDestination portTCP flagsAction
Allow SSH 19IPv4TCPemptyemptyempty19emptyaccept
Allow Proxmox 8006IPv4TCPemptyemptyempty8006emptyaccept
Allow TCP RepliesIPv4TCPemptyemptyempty32768-65535ackaccept
Allow DNS RepliesIPv4UDPemptyempty5332768-65535emptyaccept
Allow PingIPv4ICMPemptyemptyemptyemptyemptyaccept

Then remove/disable:

Allow all

Test immediately:

ssh proxmox
apt update

And test the web UI:

https://pve1.example.com:8006

Do not enable Proxmox’s internal firewall until you understand the rule order and have rescue access ready. If you enable it incorrectly, you can block both SSH and the web UI.


13. Add Trusted SSL with Let’s Encrypt and Cloudflare

Proxmox supports ACME/Let’s Encrypt certificates, including DNS challenge plugins. The domain must resolve correctly to the node when using certificate validation. (Proxmox VE)

In Cloudflare, create an API token:

Profile → API Tokens → Create Token → Edit zone DNS template

Cloudflare documents creating API tokens through My Profile → API Tokens → Create Token, including using templates such as “Edit zone DNS.” (Cloudflare Docs)

Use permissions:

Zone → DNS → Edit
Zone → Zone → Read
Scope: Specific zone → example.com

Do not paste the token publicly.

In Proxmox:

Datacenter → ACME → Accounts → Add

Create:

Account Name: letsencrypt
Email: [SENSITIVE: ADMIN_EMAIL]
Directory: Let’s Encrypt V2

Then:

Datacenter → ACME → Challenge Plugins → Add

Use:

Plugin ID: cloudflare-example
DNS API: Cloudflare Managed DNS
API Data:
CF_Token=[SENSITIVE: CLOUDFLARE_API_TOKEN]
CF_Zone_ID=[SENSITIVE: CLOUDFLARE_ZONE_ID]

Then add the ACME domain:

pve1 → System → Certificates → ACME → Add
Challenge Type: DNS
Domain: pve1.example.com
Plugin: cloudflare-example

If the GUI does not show an order button, run from SSH:

pvenode config set --acme account=letsencrypt
pvenode acme cert order

Expected:

All domains validated!
Setting pveproxy certificate and key
Restarting pveproxy
Task OK

Refresh:

https://pve1.example.com:8006

You should now have a trusted SSL certificate.

For wildcard certificates, use Traefik + Cloudflare later inside a Docker VM. Do not force wildcard management through the Proxmox host unless you specifically need it there.


14. Final Health Check

Run:

zpool status
apt update
pveversion
ss -tlnp | grep sshd

Expected:

ZFS: ONLINE, no known data errors
APT: no repository errors
SSH: listening only on 19
Web UI: https://pve1.example.com:8006
SSL: valid certificate

Final Result

You now have:

Proxmox installed from official ISO
ZFS RAID1 mirror across both NVMe drives
Correct physical network interface
Hostname and DNS configured
Repositories fixed
System updated
SSH key-only login
Custom SSH port 19
Hetzner firewall protecting the host
Trusted Let’s Encrypt SSL certificate
Clean base ready for VMs

The next recommended steps are to create VM templates, configure off-server backups, and build your production VMs one at a time.

Leave a Reply

Your email address will not be published. Required fields are marked *