hetshield

Migrate a Hetzner Cloud VM to Proxmox without downtime

Get a raw disk image out of Hetzner Cloud while it keeps running, import it with qm disk import, and fix cloud-init, networking and boot on first start.

updated 10 minute read by Dominik Geimer

Moving a server from Hetzner Cloud to a Proxmox host is three separate jobs: get a disk image out of Hetzner, import it into Proxmox with the right firmware, and make the machine reachable on its first boot in a network it has never seen. The first job is the one Hetzner gives you no tool for. The other two are where most migrations lose an evening.

This guide does all three, with the server staying online until you decide to switch.

Why people are doing this now

In February 2026 Hetzner announced a 38% increase on cloud instances effective 1 April, citing DRAM and SSD costs; the Hacker News thread reached 553 points, and much of it was people pricing a used server in a rack or a Proxmox box in the office instead. Whether or not that arithmetic works for you, the migration is the same, and a copy that boots on your own hardware is worth having even if you stay.

Getting the image out

Hetzner has no image export or download: the API's image resource offers list, get, update, delete and protection, and nothing else (OpenAPI spec). The long version is in Can you download a Hetzner Cloud snapshot?; the short one is that you read the disk yourself, and there are two ways to do it.

With downtime: rescue mode on the server itself

Enable Hetzner's rescue system, reboot into it, and read the disk while nothing is mounted:

hcloud server enable-rescue web-01     # prints a one-time root password
hcloud server reboot web-01
ssh root@<server-ip> 'set -o pipefail; dd if=/dev/sda bs=4M status=progress | zstd -1 -T0' > server.img.zst   # pipefail: a dd that fails must not exit 0 through zstd
hcloud server reboot web-01

The server is offline for the whole read. On a 40 GB cx23 a full read through zstd -1 took 1:52 and produced a 3.49 GB file, and the transfer to your Proxmox host comes on top when you pipe it over SSH as above.

Without downtime: snapshot, throwaway clone, rescue mode

Snapshot the running server, create a temporary server from that snapshot without letting it boot, put the temporary server into rescue mode, and read its disk instead. The original is only touched by the snapshot call and keeps serving traffic. The idea is simple; the job is in the details - picking a clone type that is in stock and matches the source's architecture and disk, retrying when Hetzner says resource_unavailable, keeping the pipeline's exit code honest, deleting the clone and the snapshot afterwards even when something fails halfway, and reading the firmware off the disk while you have it. Can you download a Hetzner Cloud snapshot? walks through each of those. The image is crash-consistent, the same as any snapshot of a running machine.

This is what hetshield does for $29 per server: the export while the machine keeps running, the firmware detection covered below, and the cleanup. The rest of this article is the same whichever way the image left Hetzner.

Either way the result is a compressed raw image of the whole disk. Decompress it on the Proxmox host:

zstd -d server.img.zst -o server.raw

Which firmware the image needs

Do this before creating the VM. The wrong firmware fails before any operating system appears, and the symptom, a blank screen or a UEFI shell, looks like a broken image.

Hetzner does not use one firmware for everything. In August 2023 the changelog announced that new server types "will use UEFI by default" and that a snapshot used with them must have an ESP (Hetzner Cloud changelog). The x86 cx23 we exported for the measurement above had an EFI system partition. Older or in-place-upgraded servers may still be legacy BIOS. Read it off the file:

fdisk -l server.raw        # or: sgdisk -p server.raw

A partition of type EFI System means UEFI, so the VM gets OVMF. No such partition means legacy BIOS, so the VM keeps SeaBIOS. Proxmox's own migration guide says the same: "If it boots in legacy BIOS mode, it needs to be set to SeaBIOS. If it boots in UEFI mode, set it to OVMF (UEFI)" (Migrate to Proxmox VE). Arm servers (the CAX types) are always UEFI and only run on an Arm host. The firmware section of the docs has the equivalent setting for other hypervisors.

Importing into Proxmox

Proxmox reads a raw file directly, so there is no conversion step. The web UI cannot import a bare raw file, so the import runs in the host shell; everything after it also works in the UI.

# 1. an empty machine - adjust memory, cores and the bridge to taste
qm create 900 --name web-01 --memory 4096 --cores 2 --cpu host --ostype l26 \
    --scsihw virtio-scsi-single --net0 virtio,bridge=vmbr0 --serial0 socket

# 2. UEFI image only: OVMF plus a small EFI disk (skip both lines for BIOS)
qm set 900 --bios ovmf
qm set 900 --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=0

# 3. import - the raw file goes in as it is; the last line prints the new volume name
qm disk import 900 /root/server.raw local-lvm

# 4. attach what came back as "unused0", make it the boot disk, start
qm set 900 --scsi0 local-lvm:vm-900-disk-1,discard=on,iothread=1 --boot order=scsi0
qm start 900

Notes on each step:

  • Replace local-lvm with your storage and check the volume name the import prints. On file-based storages it looks like local:900/vm-900-disk-1.raw instead.
  • qm disk import is the current name; qm importdisk is documented as "an alias for qm disk import" (qm(1)). Proxmox 7.2 and newer can also do it in one go: qm create ... --scsi0 local-lvm:0,import-from=/root/server.raw.
  • pre-enrolled-keys=0 creates the EFI disk without Secure Boot keys, which boots anything. Stock Debian and Ubuntu images ship a signed loader and boot with =1 as well; if the image has a self-built kernel or out-of-tree modules, keep it at 0.
  • The default i440fx machine type boots Linux fine. Proxmox describes q35 as the chipset that "provides a virtual PCIe bus, and thus may be desired if you want to pass through PCIe hardware" (Proxmox VE admin guide).

A UEFI machine that lands in a firmware shell

A brand-new VM has an empty UEFI boot list, so the firmware looks for the fallback loader at EFI\BOOT\BOOTX64.EFI; stock Debian and Ubuntu images have one. If yours drops into a shell instead, open the boot menu once, pick the entry under EFI\ubuntu or EFI\debian, and the OS writes a proper entry on the way up.

The first boot

The machine boots, and nothing can reach it. This is the moment people decide the image is broken, and it is not. The server still carries the fixed public address Hetzner gave it, a default gateway of 172.31.1.1 (Hetzner static configuration docs), and a network configuration written for a card called eth0. None of those exist on your Proxmox bridge. Open the console in the Proxmox UI, log in locally, and work through these in order.

1. Find the new name of the network card

ip -br link        # ens18 on Proxmox, enp1s0 on libvirt, enp0s3 on VirtualBox, eth0 on Hyper-V

Hetzner boots its images with the old naming scheme, so everything you brought along talks about eth0 and is ignored the moment the card is called ens18. Proxmox's migration guide warns about this: "The name of the network adapter will most likely have changed" (Migrate to Proxmox VE).

2. Give it an address; DHCP is the quickest way back

Ubuntu, netplan:

# replace the contents of /etc/netplan/50-cloud-init.yaml with:
network:
  version: 2
  ethernets:
    lan:
      match: { name: "e*" }
      dhcp4: true

sudo netplan apply

Debian, /etc/network/interfaces:

# replace the contents of /etc/network/interfaces.d/50-cloud-init (NAME from step 1):
auto NAME
iface NAME inet dhcp

sudo systemctl restart networking

Rocky, Alma and Fedora use NetworkManager; nmtui with IPv4 set to Automatic on the new device does the same job, and the first-boot docs have the nmcli lines.

3. Switch off what only made sense at Hetzner

sudo touch /etc/cloud/cloud-init.disabled                 # stops it looking for Hetzner's metadata service at every boot
sudo apt remove hc-utils    # Debian images; Rocky/Alma: sudo dnf remove hc-utils - Ubuntu images do not have it

cloud-init on a foreign host either disables itself or waits a minute or two for a metadata service that is not there, and on some images it rewrites the network configuration you just fixed. The empty file is cloud-init's supported off-switch. hc-utils is Hetzner's helper for private networks; it "provides a dedicated hc-net-ifup@.service systemd service for each private network interface" and starts a DHCP client on new interfaces (Hetzner network configuration docs), which on Proxmox means it fights the configuration from step 2. Keep qemu-guest-agent, which the images also ship; it is useful on Proxmox.

4. If the boot stops at an emergency prompt

grep HC_Volume /etc/fstab

That is an attached Hetzner volume. Volumes are outside every snapshot (Hetzner FAQ), so no image made this way contains them. Hetzner's own fstab line carries nofail and the machine boots without the volume; a hand-written line without it drops you to the emergency prompt. Comment the line out or add nofail, reboot, then restore the volume's data from its own backup.

5. Only if the copy runs next to the original

sudo rm -f /etc/machine-id /var/lib/dbus/machine-id && sudo systemd-machine-id-setup && sudo dbus-uuidgen --ensure
sudo rm -f /etc/ssh/ssh_host_* && sudo ssh-keygen -A && sudo systemctl restart ssh    # the service is called sshd on Rocky, Alma and Fedora

A migration that replaces the original should keep its identity so every client's known_hosts still matches. A second copy that runs alongside the first for testing should not share a machine-id or SSH host keys with it. Both machine-id files have to go: with only /etc/machine-id removed, systemd copies the old id straight back from the D-Bus one.

The full first-boot list, with the same steps for other hypervisors, is at /docs#restore-firstboot.

The cutover

The image is a point in time. Everything that changed on the Hetzner server after the snapshot, database rows, uploaded files, mail, is not in it. A no-downtime export lets you do the move in two passes, which is the whole point.

  1. Lower DNS TTLs a day ahead. Set the records that point at the server to 300 seconds or less, so the switch propagates in minutes.
  2. Export and import while production keeps running. Everything above. Boot the Proxmox copy, fix the network, confirm every service starts.
  3. Rehearse the data sync. For a database, dump on the old server and restore on the new one, or replicate from old to new. For files, rsync -a. Run it once now to see how long it takes.
  4. Freeze the old server. Maintenance mode, or stop the services that write. This is the only downtime in the whole procedure, and it lasts as long as the final sync.
  5. Final sync, then switch DNS to the Proxmox host's public address, or update the reverse proxy or tunnel in front of it.
  6. Watch the old server's logs go quiet. Leave it frozen until the TTL has expired twice and nothing arrives. Power it off, and delete it only once the new one has survived a reboot and a backup cycle.
  7. Keep the image. It is a bootable copy of the server as it was on the day you left. Store it somewhere that is not the Proxmox host.

Summary

A raw image, the right firmware and a network fix. Hetzner does not provide the image, but a snapshot, a switched-off clone and the rescue system produce it while the server keeps serving, and qm disk import takes it from there. Check the partition table before choosing OVMF or SeaBIOS, and expect the machine to boot without a network the first time.

hetshield produces the image the no-downtime way, records whether the disk is UEFI or BIOS, and hands you the file with the import commands above ready to paste, for $29 per export. It is one option among the ones on this page; the docs describe everything it does in your Hetzner project.

Keep reading