Robin.te.Hofstee

Migrating a Proxmox VM to Bare Metal: The Clean-Install-and-Restore Method

Robin te Hofstee18 min readViews3
Migrating a Proxmox VM to Bare Metal: The Clean-Install-and-Restore Method

A safer alternative to disk cloning: back up a Proxmox VM, install Ubuntu fresh on bare metal, and restore your data and configuration on top.

Why this approach instead of a disk clone

There's another tutorial that covers cloning a Proxmox VM's disk directly onto bare-metal hardware via USB. It works, but it drags along every assumption the disk image made about being a virtual machine — virtio drivers, virtual firmware, a virtual network interface name — and transplants all of that onto real hardware that doesn't match any of it. Fixing the mismatches (GRUB repair, driver regeneration, network renaming) is exactly the kind of troubleshooting that eats an afternoon.

This tutorial takes the slower but far more reliable route instead: back up the VM, install a fresh, native copy of Ubuntu Server on the real hardware, and then restore only your actual data and configuration on top of that clean base. The new install already has the correct drivers for your hardware and a bootloader that works out of the box — nothing to repair.

Disk cloneClean install + restore
Driver compatibilityVirtio drivers on real hardware — may not matchNative hardware drivers, correct from install
BootloaderFrequently needs manual GRUB repairWorks immediately
Network interface namingOften breaks, needs manual netplan fixAuto-detected correctly
Disk size handlingManual partition/filesystem resizeHandled naturally by LVM during install
Time~30–90 min, but unpredictable if something breaks~45–90 min, predictable
What transfersEverything, automaticallyOnly what you explicitly restore

The trade-off is that you have to know what data and configuration matters enough to bring over — nothing comes along by accident. That's arguably a feature: it's also a good opportunity to leave behind cruft you've accumulated in the VM over time.

What you'll need

  • SSH or console access to your Proxmox host, with enough free space to hold a backup of the VM
  • A way to move files off the Proxmox host — a USB drive, a NAS, or just SSH access between machines
  • An Ubuntu Server 24.04 LTS ISO and a USB drive to install from
  • The target bare-metal machine

Step 1: Back up the VM on Proxmox

Before touching anything, take a full snapshot-mode backup of the VM. Snapshot mode means the VM keeps running during the backup — no downtime required:

qm list                # find your VMID, e.g. 100
 
vzdump 100 --mode snapshot --storage local --compress zstd --notes "pre-migration-backup"

This produces a compressed archive at:

/var/lib/vz/dump/vzdump-qemu-100-<timestamp>.vma.zst

Confirm it landed:

ls -lh /var/lib/vz/dump/vzdump-qemu-100-*

Keep this backup around until you've fully verified the new bare-metal machine — it's your safety net the entire way through this process.

Step 2: Extract the backup so you can browse its files

The .vma.zst archive isn't something you can just untar — it needs Proxmox's own extraction tool, and that tool (vma) is bundled specifically with Proxmox's own packages. It is not available in standard Debian or Ubuntu repositories, so this step needs to happen on the Proxmox host itself (or another machine that already has Proxmox's package repos configured) — not on some arbitrary Linux box, which is a mistake that trips people up if they try to do this extraction elsewhere.

On the Proxmox host:

mkdir /tmp/vm-restore
cd /tmp/vm-restore
 
vma extract /var/lib/vz/dump/vzdump-qemu-100-*.vma.zst .

This gives you a raw or qcow2 disk image (typically disk-0.raw or disk-0.qcow2) plus the VM's config file.

Mounting the disk image to browse it

To copy files out of that disk image, mount it. The safest way to do this — rather than manually calculating partition byte-offsets, which is easy to get wrong — is to let losetup figure out the partitions for you:

sudo losetup -fP disk-0.raw
losetup -a   # confirm which /dev/loopN it was assigned
 
# This automatically creates /dev/loopNp1 (EFI) and /dev/loopNp2 (root)
mkdir /mnt/vm-disk
sudo mount /dev/loopNp2 /mnt/vm-disk   # replace loopN with what losetup reported

(If you're working from a qcow2 image instead of raw, use qemu-nbd instead:)

sudo apt install -y qemu-utils
sudo modprobe nbd max_part=8
sudo qemu-nbd --connect=/dev/nbd0 disk-0.qcow2
sudo mount /dev/nbd0p2 /mnt/vm-disk

Copying out what you'll actually need

Now copy the specific directories you'll want to restore later, onto external storage (USB, NAS, or another server over SSH):

rsync -av /mnt/vm-disk/etc/ /mnt/backup/etc/
rsync -av /mnt/vm-disk/home/ /mnt/backup/home/
rsync -av /mnt/vm-disk/var/lib/docker/ /mnt/backup/docker/
rsync -av /mnt/vm-disk/var/lib/postgresql/ /mnt/backup/postgresql/
rsync -av /mnt/vm-disk/var/lib/mysql/ /mnt/backup/mysql/
# add /opt, /srv, /root, or any other custom application directories you use

When you're done, unmount cleanly:

sudo umount /mnt/vm-disk
# if you used qemu-nbd: sudo qemu-nbd --disconnect /dev/nbd0

Also capture your installed package list — correctly

A common mistake here is grepping through dpkg's status file to reconstruct "what packages did I install." That approach pulls in far more than you actually asked for, because it can't distinguish packages you deliberately installed from ones that were pulled in automatically as dependencies.

The correct tool for this is built for exactly this purpose — run it on the original VM, ideally while it's still running, before you go any further:

# Run this ON THE VM ITSELF (via SSH into it), not on the Proxmox host
apt-mark showmanual > packages.txt

Copy packages.txt to your backup location alongside everything else. This gives you a clean list of exactly what you chose to install, without the noise of every dependency that came along for the ride.

Dump your databases too, if you're running any

Mounting /var/lib/postgresql or /var/lib/mysql from a stopped disk image and copying the raw files can work, but it's version- and configuration-sensitive. A logical dump is more portable and far easier to restore cleanly:

# On the VM itself, before migrating:
pg_dumpall -U postgres > /tmp/pg_dump.sql
mysqldump -u root -p --all-databases > /tmp/mysql_dump.sql

Copy these dump files to your backup location too. You'll use them in Step 8 instead of (or alongside) the raw data directories.

Step 3: Install Ubuntu Server fresh on the bare-metal machine

  1. Download the Ubuntu Server 24.04 LTS ISO from ubuntu.com/download/server.
  2. Write it to a USB drive:
    lsblk   # identify the USB device carefully, e.g. /dev/sdX
    sudo dd if=ubuntu-24.04.1-live-server-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
  3. Boot the target machine from this USB, in UEFI mode.
  4. Walk through the installer:
    • Network: DHCP is fine for now — you can set a static IP afterward
    • Storage: choose "use entire disk" with LVM — this matters, because LVM makes it trivial to expand storage later without manual partition math
    • Enable the OpenSSH server option so you can SSH in once it's up
    • Create your admin user — using the same username you had on the VM saves you a headache later, because file ownership (UID/GID) will already match when you restore data
    • Let it reboot when finished

Step 4: Initial setup on the new machine

SSH in (or use the console) and get the basics in place:

sudo apt update && sudo apt upgrade -y
sudo apt install -y rsync curl wget git vim htop net-tools
 
# Optional: match the VM's hostname
sudo hostnamectl set-hostname your-vm-hostname
sudo nano /etc/hosts   # update to match the new hostname
 
# Confirm your UID/GID
id

If the id output shows uid=1000 and that matches what your user had on the VM, ownership will mostly line up automatically once you restore files. If it doesn't match, make a note — you'll need to chown restored files afterward rather than assuming they're already correct.

Step 5: Copy your backed-up data onto the new machine

However you staged your backup (USB, NAS, SSH from the Proxmox host), get it accessible on the new server, then restore it selectively:

# Example if your backup is on a mounted USB drive at /mnt/usb
sudo rsync -av /mnt/usb/home/ /home/
sudo rsync -av /mnt/usb/docker/ /var/lib/docker/

Deliberately avoid restoring /etc wholesale. A fresh Ubuntu install already has a correct, working /etc for this specific hardware and OS version. Overwriting it entirely with the VM's /etc will drag back in configuration for virtual hardware that doesn't exist here, and can genuinely break the system you just installed. Instead, Step 6 walks through restoring only the specific config files that actually matter.

Step 6: Restore configuration selectively, not wholesale

This is the step where being deliberate pays off. Go file by file, restoring only what you know you customized:

Network config — only if you had custom netplan settings beyond DHCP:

sudo cp /mnt/backup/etc/netplan/*.yaml /etc/netplan/
sudo netplan apply

SSH config — only if you changed defaults (custom port, key-only auth, etc.):

sudo cp /mnt/backup/etc/ssh/sshd_config /etc/ssh/
sudo systemctl restart ssh

Users and groups — think carefully before doing this one. If you created your admin user with the same username during install (as recommended in Step 3), your UID/GID already matches and you generally don't need to touch /etc/passwd, /etc/shadow, /etc/group, or /etc/gshadow at all. Copying these over can overwrite the very account you just set up during installation, or create UID collisions with system accounts. Only restore these files if you have additional users from the VM that genuinely need to exist on the new machine, and do it carefully:

# Only if you have extra users to bring over — review before copying wholesale
sudo cp /mnt/backup/etc/passwd /etc/
sudo cp /mnt/backup/etc/group /etc/
sudo cp /mnt/backup/etc/shadow /etc/
sudo cp /mnt/backup/etc/gshadow /etc/

Sudoers, if you had custom sudo rules:

sudo cp /mnt/backup/etc/sudoers /etc/
sudo cp -r /mnt/backup/etc/sudoers.d/ /etc/

Application configs — nginx, Apache, or anything else you hand-configured:

sudo rsync -av /mnt/backup/etc/nginx/ /etc/nginx/
sudo rsync -av /mnt/backup/etc/apache2/ /etc/apache2/

Cron jobs:

sudo cp /mnt/backup/etc/crontab /etc/
sudo rsync -av /mnt/backup/etc/cron.d/ /etc/cron.d/

/etc/fstab — never copy this one directly. It's full of UUIDs that refer to the VM's specific virtual disks, which don't exist on this machine. Instead, check what your new drives are actually called and add any custom mount entries manually:

lsblk -f
sudo nano /etc/fstab   # add entries by hand, using the new UUIDs

Step 7: Fix ownership of what you restored

Now that the right files are in place, make sure they're owned correctly:

sudo chown -R $USER:$USER /home/$USER

For Docker specifically, resist the urge to blanket-chown the whole directory tree. /var/lib/docker is meant to stay root-owned — the Docker daemon runs as root and manages ownership of everything inside that tree internally. Recursively chowning it to some other UID (like 999, a common docker-group GID elsewhere) can actually break the daemon rather than fix a permissions problem:

# Correct: leave /var/lib/docker as root:root
sudo chown -R root:root /var/lib/docker

For databases, ownership does matter and should match the service account:

sudo chown -R postgres:postgres /var/lib/postgresql
sudo chown -R mysql:mysql /var/lib/mysql

Step 8: Reinstall your packages and services

Using the clean apt-mark showmanual list you captured in Step 2 (not the noisy dpkg-status grep), reinstall exactly the packages you actually chose to have:

xargs -a packages.txt sudo apt install -y

Then enable and start the services you need:

sudo systemctl enable --now docker
sudo systemctl enable --now nginx
sudo systemctl enable --now postgresql
sudo systemctl enable --now mysql

Step 9: Bring your containers and databases back up

Docker: if you restored /var/lib/docker completely (with correct root ownership, per Step 7), your containers and images should already be there:

sudo systemctl start docker
docker ps -a

If a container fails to start, docker logs <container> is the first place to look. If you manage things via Compose instead, just bring the stack up from your compose files:

docker-compose up -d

Databases: if you took logical dumps in Step 2 (recommended over raw-copying the data directories), restore from those:

# PostgreSQL
sudo -u postgres psql < /mnt/backup/pg_dump.sql
 
# MySQL/MariaDB
sudo mysql < /mnt/backup/mysql_dump.sql

Step 10: Verify everything and clean up

Check that your core services are actually running, not just installed:

systemctl status docker nginx postgresql mysql
journalctl -xeu docker nginx postgresql mysql
curl http://localhost

Test SSH access, log into your web apps, and confirm your databases have the data you expect.

If your new drive is larger than what you had before, expand storage. Because you installed with LVM back in Step 3, this is a logical-volume operation rather than raw partition math:

sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
sudo resize2fs /dev/ubuntu-vg/ubuntu-lv    # or xfs_growfs, depending on filesystem

Finally, check for any files owned by the wrong UID if your usernames didn't match between the old VM and the new install:

find /home -uid 1001 -exec chown 1000:1000 {} \;   # adjust UIDs to your actual case

And confirm you have the disk space you expect:

df -h /

Checklist: what to actually bring over from the VM

CategoryPaths
System config (selective)/etc/netplan, /etc/ssh, /etc/sudoers*, /etc/cron* (never /etc/fstab directly — rebuild it manually)
Users/home, /root — and /etc/passwd / /etc/group etc. only if you have extra accounts to bring over
Web servers/etc/nginx, /etc/apache2, /var/www
DatabasesLogical dumps (pg_dumpall, mysqldump) preferred over raw /var/lib/postgresql or /var/lib/mysql copies
Docker/var/lib/docker (kept root-owned), or your docker-compose.yml files plus named volumes
Applications/opt, /srv, /usr/local/bin, and any custom app directories
Certificates/etc/ssl, /etc/letsencrypt
Logs (optional)/var/log — usually not worth restoring; a fresh log history is fine

Quick reference

# === ON PROXMOX HOST ===
vzdump <VMID> --mode snapshot --storage local --compress zstd
mkdir /tmp/vm-restore && cd /tmp/vm-restore
vma extract /var/lib/vz/dump/vzdump-qemu-<VMID>-*.vma.zst .
sudo losetup -fP disk-0.raw && losetup -a         # note the /dev/loopN it assigns
sudo mount /dev/loopNp2 /mnt/vm-disk
rsync -av /mnt/vm-disk/etc/ /mnt/backup/etc/
rsync -av /mnt/vm-disk/home/ /mnt/backup/home/
rsync -av /mnt/vm-disk/var/lib/docker/ /mnt/backup/docker/
sudo umount /mnt/vm-disk
 
# === ON THE ORIGINAL VM (before shutting it down) ===
apt-mark showmanual > packages.txt
pg_dumpall -U postgres > pg_dump.sql
mysqldump -u root -p --all-databases > mysql_dump.sql
 
# === ON THE NEW BARE-METAL SERVER ===
# 1. Fresh install: Ubuntu Server 24.04, UEFI, LVM, SSH enabled, same username as before
sudo rsync -av /mnt/backup/home/ /home/
sudo rsync -av /mnt/backup/docker/ /var/lib/docker/
sudo chown -R root:root /var/lib/docker             # keep this root-owned
sudo chown -R $USER:$USER /home/$USER
# restore only specific /etc files (netplan, ssh, nginx, sudoers, cron) — never fstab wholesale
xargs -a packages.txt sudo apt install -y
sudo systemctl enable --now docker nginx postgresql mysql
sudo -u postgres psql < pg_dump.sql
sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv && sudo resize2fs /dev/ubuntu-vg/ubuntu-lv

A few habits worth keeping

  • Test the restore somewhere low-stakes first. Spin up a throwaway VM on Proxmox, restore your backup onto it, and confirm your services actually come up before you touch real hardware.
  • Don't delete the vzdump backup. Keep it until the bare-metal machine has been running successfully for a while — it's your fallback if something surfaces later that you didn't catch during verification.
  • Keep the old VM around, powered off, until you trust the new machine. There's no cost to leaving it stopped on Proxmox for a few weeks as insurance.
  • Write down every config file you restore as you go. Future-you, six months from now, will not remember which three files you hand-copied out of a dozen possible ones.
Share this post
Robin te Hofstee

Written by Robin te Hofstee

Technology enthusiast with a deep love for programming. Sharing practical guides on React, Next.js, Linux and self-hosting.

Self-Hosting n8n on Proxmox

1

Deploying n8n on Proxmox using the Community Scripts LXC installer offers data privacy, unlimited execution, and full control compared to n8n Cloud, ideal for a homelab.