While building my Traefik reverse proxy (Hostnames and Let’s Encrypt for My Homelab with Traefik), I discovered my Proxmox host was still on an old 8.x release. Proxmox VE 8 reached end of life in August 2026, so the hypervisor pieces (kernel, QEMU, LXC tooling) had stopped getting fixes. It had also just bitten me: Docker refused to run inside an LXC container on that version, which is why Traefik ended up as a native binary.
This post documents the in-place upgrade to Proxmox VE 9.2: the prep, the checker findings, how I avoided typing long commands at the server’s console, the two scary-looking errors that turned out to be harmless, how I verified everything afterwards, and a follow-up fix: converting my Twingate connector from a privileged to an unprivileged container.
This post is anonymized. The domain is
example.comand the IP addresses are illustrative, not my real ones.
My setup, and why it mattered
- Single Proxmox node, booting with BIOS/GRUB, on the free no-subscription repository
- Open vSwitch networking with one physical NIC (
enp0s31f6, the onboard Intel port) - pfSense VM running my VLANs (an internal network plus pen-testing ranges)
- Pi-hole (my primary DNS), Traefik, Kimai (time tracking), Home Assistant and Calibre, all as guests on this one box
- Twingate for remote access
- A Synology NAS mounted in Proxmox over NFS
Because so much depends on this one host, I wrote down the risks before touching anything:
| Risk | Why it matters | Mitigation |
|---|---|---|
| NIC renamed by the new kernel | All bridges, the management IP and every VLAN depend on the NIC name. If it changes, the host goes dark on the network | Monitor and keyboard connected; rescue steps written down in advance |
| Open vSwitch gets upgraded mid-upgrade | The host’s networking can drop briefly while the upgrade is running | Run the upgrade inside tmux, with the console as backup |
| Web UI Shell tab | Proxmox warns it gets cut off during the upgrade | Use SSH from my Mac instead |
| Twingate connector on this host | Remote access dies at the reboot | Do it from home |
| Pi-hole on this host | If the host itself used Pi-hole for DNS, it would lose name resolution once guests stop | Checked: the host uses the router for DNS, which falls back to a backup resolver. No change needed |
| Kimai data | Losing time-tracking records would hurt | Backups first, and a test restore |
Phase 1: Back up everything
Proxmox has a built-in backup tool (vzdump). For a one-off “back up everything”, the easiest route is a backup job you run by hand.
Choosing the target
pvesm status
My options were the root disk (local), two LVM-thin pools, and the Synology over NFS. The NAS was the clear choice:
- LVM-thin pools can’t store backups at all. They only hold VM disks and container volumes, not backup files.
- My guests live on the big thin pool. A backup on the same disk as the originals protects against very little.
- The NAS survives a full rebuild. In the worst case (reinstall the host), recovery is: reinstall, re-add the NFS share, restore.
Check that the target accepts backups:
pvesm status --content backup
Creating and running the job
Datacenter → Backup → Add:
| Field | Setting | Why |
|---|---|---|
| Storage | the NAS | Off the host |
| Schedule | anything, with Enable unticked | It only runs when I click Run now |
| Selection mode | All | Every VM and container |
| Mode | Stop | Each guest is shut down, backed up, then restarted: the most consistent copy |
| Compression | ZSTD | Fast, good compression |
| Retention | Keep all backups | Nothing gets pruned |
| Note template | {{guestname}} pre-PVE9 |
Easy to find later |
Then select the job and click Run now. The CLI equivalent:
vzdump --all --mode stop --compress zstd --storage NAS-STORE --notes-template '{{guestname}} pre-PVE9'
About 465 GB of guest data took 40 minutes over gigabit, much faster than I expected. vzdump skips empty space quickly and ZSTD roughly halves the rest (one 40 GB VM became an 18.7 GB archive). Each guest is only down during its own turn, so the outages were a few minutes each.
Checking the backups
- The task log ends with
Backup job finished successfullyandTASK OK. If any guest fails, it says “finished with errors” andTASK ERRORinstead. - Protect them. In the NAS storage’s Backups view, select each new backup and click Change Protection, so nothing can prune or delete them.
- Test-restore the one that matters most (Kimai, for me): Restore to an unused VM ID with Unique ticked (new MAC address) and Start after restore unticked, so it can’t clash with the live copy. When it finishes with
TASK OK, delete the test copy.
Phase 2: Update to the latest 8.4, then reboot
The upgrade to 9 requires the latest 8.4 packages first:
apt update && apt dist-upgrade
pveversion
pveversion showed 8.4.x, but the running kernel was still the old one. The update had installed a newer 6.8 kernel that wasn’t in use yet. I rebooted before going further, so I’d know the box boots cleanly on fully updated 8.4. If anything broke later, it would be the 9 upgrade’s fault, not this update’s.
Phase 3: The pre-upgrade checker
Proxmox ships a checker that only reports, and changes nothing:
pve8to9 --full
Mine found one failure and two warnings.
FAIL: Resolved node IP '192.168.1.164' not configured
/etc/hosts still mapped the hostname proxmox to an address from an old network layout. Proxmox’s cluster filesystem expects the hostname to resolve to an IP the host actually has; a stale entry can stop /etc/pve from mounting after a reboot. The fix was one line:
cp /etc/hosts /etc/hosts.bak
nano /etc/hosts # change 192.168.1.164 to the host's real IP, keep the names
hostname --ip-address
WARN: running guests
Expected. I shut them down on upgrade day.
WARN: intel-microcode not found
Worth fixing. I added the non-free-firmware component to the new Debian sources during the upgrade and installed the package before the reboot.
Two more things I checked by hand
- Repositories.
cat /etc/apt/sources.list.d/*.listshowed four files, but only the no-subscription Proxmox repo was enabled. The Ceph, enterprise andpvetestlines were all commented out. No Ceph means no Ceph repo is needed on 9. - Network config.
cat /etc/network/interfacesandip -br linkconfirmed a single physical NIC. The only bridge with a physical port was the main one; the pfSense internal switch has no physical port at all. So a NIC rename would mean fixing one name in two lines.
Phase 4: Avoiding typos at the console
I’d been pasting commands into the web UI’s Shell, which Proxmox says not to use during the upgrade, and I didn’t want to retype long commands on the server’s keyboard. The solution:
- Run the upgrade over SSH from my Mac, inside
tmux. Copy-paste works as usual. If the network drops, the upgrade keeps running on the server, and I can reattach from the Mac or from the console withtmux attach -t upgrade. - Save the long commands on the server as a script beforehand, so the most I’d type at the console is a short command.
Install tmux while still on the 8.4 repositories:
apt install -y tmux
The repository-switch script
Pasting this only creates the file. It has a guard against running twice, and it stops if any Debian 12 (bookworm) repository is still listed:
cat > /root/pve9-repos.sh << 'SCRIPT'
#!/bin/bash
# Switch apt repositories from PVE 8 (bookworm) to PVE 9 (trixie)
set -e
if grep -q trixie /etc/apt/sources.list; then
echo "sources.list already mentions trixie - has this already run? Stopping."
exit 1
fi
echo "== Backing up /etc/apt to /root/apt-backup-pve8"
cp -a /etc/apt /root/apt-backup-pve8
echo "== Stopping audit-log noise"
systemctl disable --now systemd-journald-audit.socket || true
echo "== Debian base: bookworm -> trixie, adding non-free-firmware"
sed -i 's/bookworm/trixie/g' /etc/apt/sources.list
sed -i 's/main contrib$/main contrib non-free-firmware/' /etc/apt/sources.list
echo "== Adding Proxmox VE 9 no-subscription repo"
cat > /etc/apt/sources.list.d/proxmox.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
echo "== Moving old bookworm .list files to /root/old-apt-lists"
mkdir -p /root/old-apt-lists
mv /etc/apt/sources.list.d/*.list /root/old-apt-lists/
echo "== Resulting sources"
cat /etc/apt/sources.list /etc/apt/sources.list.d/proxmox.sources
echo "== apt update"
apt update
if apt policy 2>/dev/null | grep -q bookworm; then
echo "!! bookworm repositories are still listed - STOP and check before upgrading."
exit 1
fi
echo
echo "Repositories switched cleanly. Next: apt dist-upgrade"
SCRIPT
chmod +x /root/pve9-repos.sh
bash -n /root/pve9-repos.sh && echo "Script syntax OK"
The cheat sheet
A plain text file with the remaining steps and the rescue notes, so they’re on screen if I end up at the console (cat /root/pve9-notes.txt):
PVE 8 -> 9 UPGRADE CHEAT SHEET
Reattach to the upgrade session: tmux attach -t upgrade
1. bash /root/pve9-repos.sh
2. apt dist-upgrade
/etc/issue = N | lvm.conf = Y | sshd_config = D, then decide
grub = N | chrony.conf = Y | other = D, then N if unsure
changelog = q | STOP if it wants to remove proxmox-ve
3. apt install intel-microcode
4. pve8to9 --full (remove systemd-boot ONLY if it says so)
5. reboot
6. pveversion | ip -br link | ping -c 3 <gateway-ip>
NO NETWORK AFTER REBOOT:
ip -br link (find the NIC's new name, starts with "en")
nano /etc/network/interfaces (replace enp0s31f6 in BOTH places:
its iface line AND vmbr0's ovs_ports line)
ifreload -a
UNDO THE REPO SWITCH (only BEFORE running apt dist-upgrade):
rm /etc/apt/sources.list.d/proxmox.sources
cp /root/apt-backup-pve8/sources.list /etc/apt/sources.list
mv /root/old-apt-lists/*.list /etc/apt/sources.list.d/
apt update
Phase 5: Upgrade day
- Web UI: select the node, Bulk Actions → Bulk Shutdown, all guests.
- Mac Terminal:
ssh root@192.168.10.164, thentmux new -s upgrade. bash /root/pve9-repos.sh: the bookworm check came back empty.apt dist-upgrade
The prompts
| Prompt | Answer |
|---|---|
/etc/issue |
N (keep mine; it’s only the console banner) |
/etc/lvm/lvm.conf |
Y (take the new version) |
/etc/ssh/sshd_config |
D to view the diff first. Y if it only swaps ChallengeResponseAuthentication for KbdInteractiveAuthentication; N if it would undo my own changes |
/etc/default/grub |
N (keep mine) |
/etc/chrony/chrony.conf |
Y, unless I’d customized time servers |
| Anything else | D to view, N if unsure |
| Changelog pager | q |
| “Restart services without asking?” | the default |
The changelog pager surprised me: before installing, apt shows notes about changes in the new package versions (OpenSSH, adduser and so on) in a pager with a : prompt at the bottom. It’s waiting for q, and the upgrade continues once you press it.
The hard stop: if apt ever says it wants to remove proxmox-ve, answer n. It means a repository is misconfigured.
My mistake: pasting the answer guide
When apt finished, the shell printed a series of command not found errors: lvm.conf, sshd_config, grub, STOP, changelog, plus /etc/issue: Permission denied. I’d accidentally pasted the cheat sheet’s answer guide along with a command, while apt was still running. Once apt exited, bash ran each line as a command. Nothing changed, since none of those words are real commands, and the “permission denied” was bash refusing to execute /etc/issue.
The real question was whether apt’s prompts had read some of that pasted text as answers. These checks settled it:
dpkg --audit # prints nothing if every package is fully configured
apt dist-upgrade # should report 0 upgraded, 0 installed, 0 to remove
sshd -t && sshd -T | grep -Ei 'permitrootlogin|passwordauthentication'
find /etc \( -name '*.dpkg-*' -o -name '*.ucf-*' \) -exec ls -l {} \;
sshd -tchecks the SSH config is valid, andsshd -Tshows the settings that will apply. Root login was still allowed, so I’d be able to reconnect after the reboot.- The
findreveals each config-file decision. A.dpkg-oldfile means the new version replaced mine; a.dpkg-distfile means mine was kept.issue,lvm.confandca-certificates.confhad taken new versions, which is what I’d intended, and nothing else had been touched.
Lesson: copy only the command lines, one at a time.
Two errors that the reboot fixed
Next came the microcode and a second checker run:
apt install intel-microcode
pve8to9 --full | grep -E 'WARN|FAIL'
Two warnings remained:
- “a suitable kernel (proxmox-kernel-7.0) is installed, but an unsuitable (6.8…) is booted”: simply a reboot reminder.
- “No (active) time synchronisation daemon detected”: chrony had been running before the upgrade and now wouldn’t start.
Earlier in the upgrade output, postfix had also failed to restart with Transaction contains conflicting jobs 'restart' and 'stop'. And the chrony log showed:
Fatal error : Could not send notification to $NOTIFY_SOCKET
The config was fine: chrony loaded it normally, then failed at the final step, telling systemd “I’ve started” and getting no answer. Both errors have the same cause. After dist-upgrade, the new Debian 13 packages are on disk, but the old systemd (PID 1) keeps running until the reboot. Services restarted in that window, like the new chrony 4.8 or postfix, can fail to talk to it. Neither matters on a single node for the length of a reboot, so:
reboot
Phase 6: After the reboot
pveversion # pve-manager/9.2.x (running kernel: 7.0.x-pve)
ip -br link # enp0s31f6 and vmbr0_mgmt UP
chronyc tracking # reference server and a microsecond-level offset
pve8to9 --full | grep -E 'WARN|FAIL' # WARNINGS: 0, FAILURES: 0
The NIC kept its name, chrony started normally, and the checker was clean. The $NOTIFY_SOCKET error was indeed only the pre-reboot mismatch.
Counting running guests from ip -br link
Proxmox creates a host-side network interface for every running guest, named after the guest’s ID:
| Interface | Meaning |
|---|---|
tap100i0, tap100i1 |
VM 100, network cards 0 and 1 (two NICs fits a firewall with WAN and LAN sides) |
veth201i0 |
Container 201, network card 0 |
fwbr202i0, fwln202o0 |
Helpers Proxmox adds when a guest has its firewall enabled |
So tap = VM, veth = container, then the guest ID, i and the NIC number. These only exist while a guest runs. Counting the distinct IDs gave the same eight guests as before the upgrade, so start-at-boot had brought everything back.
That shows each guest started, not that its service works. The real test was opening, through Traefik:
https://kimai.home.example.com(with my recent entries present)- Pi-hole, Home Assistant, Calibre and pfSense by hostname
- the Proxmox web UI, after a force-reload (⌘ + Shift + R)
All worked, which confirmed DNS, Traefik, the certificate and the guests in one go.
One thing that looks wrong but isn’t
PVE 9 accounts for VM memory differently. Guests that don’t report their own memory usage, including pfSense (FreeBSD), can show over 100% in the UI. It’s only a display change.
A hook to remember
During the upgrade I also noticed Removing subscription nag from UI.... That’s an apt hook from a community post-install script I ran at some point, and it edits the web UI’s JavaScript after every update. It caused no trouble here, but if the web UI ever comes up blank after an update, that hook is the first suspect.
Cleanup (a few days later)
apt autoremove
This removes the old 6.8 and 5.15 kernels and the leftover Debian 12 libraries. I waited a few days first, because the old kernels are the fallback boot option if the new one misbehaves. The most recent 6.8 kernel isn’t on the autoremove list, so one fallback remains.
Optional extras:
apt modernize-sourcesconverts the Debian repo file to the new deb822 format, matching the Proxmox one. It previews the changes and asks before applying./usr/share/pve-manager/migrations/pve-lvm-disable-autoactivationclears the checker’s LVM autoactivation notice. Optional for local storage.- The pre-upgrade backups: keep them protected for a few weeks, then unprotect and delete.
A bonus of being on 9.1+: Docker inside LXC containers works again, if I ever want it.
Bonus: making the Twingate connector unprivileged
The warning that started it
A day after the upgrade, starting my Twingate connector container showed:
WARN: Systemd 249 detected. You may need to enable nesting.
TASK WARNINGS: 1
Nesting lets a container create its own namespaces, the walled-off views of processes, mounts and networks that containers are built from. That’s what lets Docker run inside an LXC, and newer systemd versions use the same mechanism to sandbox individual services inside the container. Without it, some services start slowly or fail. In an unprivileged container, nesting is safe, and Proxmox turns it on by default for new ones.
What the check actually found
To see every container’s features and privilege level at once:
for id in $(pct list | awk 'NR>1{print $1}'); do echo "$id: $(pct config $id | grep -E '^(features|unprivileged)' | tr '\n' ' ')"; done
The connector already had nesting=1, so the warning itself didn’t matter. But the output showed something more important: every container had unprivileged: 1 except the connector. That line only appears in the config of unprivileged containers, so the connector was privileged.
In a privileged container, root inside the container is real root on the host. If it were ever compromised, the attacker would be much closer to owning the Proxmox host. Of all my guests, the Twingate connector is the one that talks to an outside service for remote access, so it’s the one I most want tightly contained.
Converting it with a backup and restore
Proxmox can convert a container to unprivileged while restoring a backup. The plan, done from home since remote access drops during the switch:
- Back up the old container (Stop mode, to the NAS).
- Stop it and turn off Start at boot, so two copies of the same connector never run at once.
- Restore the backup to a new ID with Unprivileged container ticked, leaving Unique unticked so the network settings stay the same.
The restore from the web UI failed:
tar: ./var/spool/postfix/dev/urandom: Cannot mknod: Operation not permitted
tar: ./var/spool/postfix/dev/random: Cannot mknod: Operation not permitted
tar: Exiting with failure status due to previous errors
TASK ERROR: unable to restore CT ...
Those are device files in the mail server’s sandbox (postfix keeps a mini /dev folder). An unprivileged container isn’t allowed to create device files, which is the whole point of it being unprivileged. Tar extracted everything else, then exited with an error because of those two, and Proxmox treated that as a failed restore and cleaned up.
The connector doesn’t need those files, and postfix works without them. The shell version of the restore has an option to accept unpack errors:
pct restore 211 NAS-STORE:backup/vzdump-lxc-210-<date>.tar.zst \
--storage <thin-pool> --unprivileged 1 --ignore-unpack-errors 1
From the shell, pct restore doesn’t print TASK OK. Success is the last line, merging backed-up and given configuration.., followed by the prompt with no error.
Switching over and checking
pct shutdown 210 && pct set 210 --onboot 0 # old, privileged
pct start 211 && pct set 211 --onboot 1 # new, unprivileged
pct config 211 | grep -E '^(features|unprivileged)'
pct exec 211 -- systemctl status twingate-connector
Then three real checks: the connector shows online in Twingate’s admin console, https://kimai.home.example.com loads from my phone with Wi-Fi off and Twingate on, and the service is active (running). All three passed. If they hadn’t, going back would have been one stop and one start.
Two loose ends:
- The connector doesn’t need a mail server, so I turned postfix off:
pct exec 211 -- systemctl disable --now postfix. perl: warning: Setting locale failedappears when runningpct execfrom my Mac. It passes my SSH session’s language setting (en_US.UTF-8) into a container that doesn’t have that locale installed. It’s harmless;locale-gen en_US.UTF-8inside the container silences it.
I’m keeping the old container stopped for a week as a fallback, then deleting it. Now every container on the host is unprivileged.
Lessons learned
- The checker is the plan.
pve8to9 --fullfound the one real problem (the stale/etc/hostsentry) before it could stop the host from booting properly. Run it before and after. - Reboot after the 8.4 update, so problems from the update and from the major upgrade don’t get mixed up.
- Backups on the NAS, not on the thin pool. LVM-thin can’t hold backup files anyway, and a backup beside the originals barely counts.
- Test-restore at least one backup. “TASK OK” on the backup proves it was written; a restore proves it can be read back.
- SSH + tmux beats typing at the console, and a pre-written script plus cheat sheet beats both.
- Paste commands one at a time. Pasting a block while apt is prompting is how I fed the shell my own notes.
- Errors between
dist-upgradeand the reboot can be transitional. New services restarting under the old systemd fail in odd ways; check the logs, and if the config is fine, the reboot clears them. ip -br linktells you which guests are running, at a glance.- Read warnings for what they reveal, not just what they say. A harmless nesting warning led me to the one privileged container on the host, which happened to be the remote-access entry point.