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”:
| Name | Version | Protocol | Source IP | Destination IP | Source port | Destination port | TCP flags | Action |
|---|---|---|---|---|---|---|---|---|
| Allow SSH 19 | IPv4 | TCP | empty | empty | empty | 19 | empty | accept |
| Allow Proxmox 8006 | IPv4 | TCP | empty | empty | empty | 8006 | empty | accept |
| Allow TCP Replies | IPv4 | TCP | empty | empty | empty | 32768-65535 | ack | accept |
| Allow DNS Replies | IPv4 | UDP | empty | empty | 53 | 32768-65535 | empty | accept |
| Allow Ping | IPv4 | ICMP | empty | empty | empty | empty | empty | accept |
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.
