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 clone | Clean install + restore | |
|---|---|---|
| Driver compatibility | Virtio drivers on real hardware — may not match | Native hardware drivers, correct from install |
| Bootloader | Frequently needs manual GRUB repair | Works immediately |
| Network interface naming | Often breaks, needs manual netplan fix | Auto-detected correctly |
| Disk size handling | Manual partition/filesystem resize | Handled naturally by LVM during install |
| Time | ~30–90 min, but unpredictable if something breaks | ~45–90 min, predictable |
| What transfers | Everything, automatically | Only 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-diskCopying 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 useWhen you're done, unmount cleanly:
sudo umount /mnt/vm-disk
# if you used qemu-nbd: sudo qemu-nbd --disconnect /dev/nbd0Also 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.txtCopy 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.sqlCopy 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
- Download the Ubuntu Server 24.04 LTS ISO from ubuntu.com/download/server.
- 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 - Boot the target machine from this USB, in UEFI mode.
- 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
idIf 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 applySSH 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 sshUsers 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 UUIDsStep 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/$USERFor 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/dockerFor 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/mysqlStep 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 -yThen 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 mysqlStep 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 -aIf 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 -dDatabases: 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.sqlStep 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://localhostTest 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 filesystemFinally, 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 caseAnd confirm you have the disk space you expect:
df -h /Checklist: what to actually bring over from the VM
| Category | Paths |
|---|---|
| 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 |
| Databases | Logical 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-lvA 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
vzdumpbackup. 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.

Written by Robin te Hofstee
Technology enthusiast with a deep love for programming. Sharing practical guides on React, Next.js, Linux and self-hosting.
Related Posts
Migrating a Proxmox VM to Bare Metal: The Disk-Clone Method
A step-by-step walkthrough of cloning a Proxmox VM's disk onto a USB drive and transplanting it onto physical hardware, including the pitfalls that trip people up.
Self-Hosting n8n on Proxmox
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.
Securing a Next.js Application in Production
Securing a Next.js app in production means protecting the client bundle, the server, and the data flow. This post shows how to lock down Content‑Secur


