This is my personal checklist for upgrading a Proxmox host from Debian Bookworm to Debian Trixie. I keep it short and repeatable.
Step 0: Backup#
personally I prefer lvm snapshot. Soon I am going to automate it via ansible for my homelab, but right at the moment bash script is here
Step 1: Switch APT sources to Trixie and pre-download packages#
chattr -i /etc/apt/sources.list /etc/apt/sources.list.d/* \
; sed -i 's/bookworm/trixie/g' /etc/apt/sources.list \
; sed -i 's/bookworm/trixie/g' /etc/apt/sources.list.d/* \
; apt clean \
; apt update \
; apt-get dist-upgrade --download-only -yyes, I use chattr for sources and even for resolv.conf
Step 2: Run non-interactive dist-upgrade and reboot#
DEBIAN_FRONTEND=noninteractive apt \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" -y dist-upgrade \
; apt clean \
; rebootStep 3: Clean Proxmox repository entries and do the final upgrade pass#
Run this block after the first reboot. I apply repository cleanup conditionally:
- keep
pve-enterpriseif the node has an active enterprise subscription - remove Ceph repository files only if the node is not in a Ceph cluster
# only for no-subscription hosts
rm -f /etc/apt/sources.list.d/pve-enterprise.*
# only if this node is not part of a Ceph cluster
#rm -f /etc/apt/sources.list.d/ceph*
apt clean
apt update
apt dist-upgrade -y
apt autoremove -y
rebootPost-upgrade checks#
cat /etc/debian_version
pveversion -v
apt update
apt full-upgrade -y
systemctl --failedIf systemctl --failed returns no failed units and apt update has no dependency errors, I treat the upgrade as complete.
Optional automation with Ansible#
For repeated upgrades across multiple hosts, I keep a deb822-based Ansible variant. It mirrors the same idea as the manual flow above: normalize repository files, then apply consistent Debian and Proxmox sources.
Important detail for major release migration:
this variable set ties suites to ansible_distribution_release.
For a Bookworm to Trixie migration run, I use an explicit target release variable in inventory and map suites to that target in the role.
Variables example (Debian and Proxmox repos)#
your_role_here_repo_name_debian: "debian"
your_role_here_repo_suites_debian: "{{ ansible_distribution_release }}"
your_role_here_repo_components_debian: "main contrib non-free-firmware"
your_role_here_repo_uris_debian:
- "http://deb.debian.org/debian/"
# in case you have nexus
#- "https://{{ nexus_fqdn }}/repository/debian/"
# debian-updates
your_role_here_repo_name_debian_updates: "{{ your_role_here_repo_name_debian }}-updates"
your_role_here_repo_suites_debian_updates: "{{ your_role_here_repo_suites_debian }}-updates"
your_role_here_repo_components_debian_updates: "{{ your_role_here_repo_components_debian }}"
your_role_here_repo_uris_debian_updates: "{{ your_role_here_repo_uris_debian }}"
# debian-security
your_role_here_repo_name_debian_security: "{{ your_role_here_repo_name_debian }}-security"
your_role_here_repo_suites_debian_security: "{{ your_role_here_repo_suites_debian }}-security"
your_role_here_repo_components_debian_security: "{{ your_role_here_repo_components_debian }}"
your_role_here_repo_uris_debian_security:
- "http://security.debian.org/debian-security/"
# in case you have nexus
#- "https://{{ nexus_fqdn }}/repository/debian-security/"
# pve
your_role_here_repo_pve_name: "pve-install-repo"
your_role_here_repo_pve_gpg: "/usr/share/keyrings/proxmox-archive-keyring.gpg"
your_role_here_repo_pve_suites: "{{ ansible_distribution_release }}"
your_role_here_repo_pve_components: "pve-no-subscription"
your_role_here_repo_pve_uris:
- "http://download.proxmox.com/debian/pve/"
# in case you have nexus
#- "https://{{ nexus_fqdn }}/repository/pve/"
# pbs
your_role_here_repo_pbs_name: "pbs-no-subscribtion"
your_role_here_repo_pbs_gpg: "/usr/share/keyrings/proxmox-archive-keyring.gpg"
your_role_here_repo_pbs_suites: "{{ ansible_distribution_release }}"
your_role_here_repo_pbs_components: "pbs-no-subscription"
your_role_here_repo_pbs_uris:
- "http://download.proxmox.com/debian/pbs/"
# in case you have nexus
#- "https://{{ nexus_fqdn }}/repository/pbs/"Task example (deb822 migration and repo setup)#
- name: cleanup old sources.list
copy:
dest: '/etc/apt/sources.list'
content: "#"
- name: Debian | Remove obsolete repo files
when:
- ansible_distribution == "Debian"
file:
state: absent
path: "{{ item }}"
loop: "{{ your_role_here_repo_obsolete }}"
- name: configure repo | debian
when:
- ansible_distribution == 'Debian'
ansible.builtin.deb822_repository:
types: deb
architectures: amd64
signed_by: "{{ your_role_here_repo_gpg }}"
name: "{{ your_role_here_repo_name_debian }}"
suites: '{{ your_role_here_repo_suites_debian }}'
uris: "{{ your_role_here_repo_uris_debian }}"
components: "{{ your_role_here_repo_components_debian }}"
- name: configure repo | debian-updates
when:
- ansible_distribution == 'Debian'
ansible.builtin.deb822_repository:
types: deb
architectures: amd64
signed_by: "{{ your_role_here_repo_gpg }}"
name: "{{ your_role_here_repo_name_debian_updates }}"
suites: '{{ your_role_here_repo_suites_debian_updates }}'
uris: "{{ your_role_here_repo_uris_debian_updates }}"
components: "{{ your_role_here_repo_components_debian_updates }}"
- name: configure repo | debian-security
when:
- ansible_distribution == 'Debian'
ansible.builtin.deb822_repository:
types: deb
architectures: amd64
signed_by: "{{ your_role_here_repo_gpg }}"
name: "{{ your_role_here_repo_name_debian_security }}"
suites: '{{ your_role_here_repo_suites_debian_security }}'
uris: "{{ your_role_here_repo_uris_debian_security }}"
components: "{{ your_role_here_repo_components_debian_security }}"
- name: Check if /etc/pve exists and is a directory
stat:
path: /etc/pve
register: pve_stat
- name: configure repos | pve
when:
- pve_stat.stat.exists
- pve_stat.stat.isdir
- ansible_distribution == 'Debian'
ansible.builtin.deb822_repository:
types: deb
architectures: amd64
signed_by: "{{ your_role_here_repo_pve_gpg }}"
name: "{{ your_role_here_repo_pve_name }}"
suites: '{{ your_role_here_repo_pve_suites }}'
uris: "{{ your_role_here_repo_pve_uris }}"
components: "{{ your_role_here_repo_pve_components }}"
- name: Check if /etc/proxmox-backup exists and is a directory
stat:
path: /etc/proxmox-backup
register: pbs_stat
- name: configure repos | pbs
when:
- pbs_stat.stat.exists
- pbs_stat.stat.isdir
- ansible_distribution == 'Debian'
- "'pbs' in group_names"
ansible.builtin.deb822_repository:
types: deb
architectures: amd64
signed_by: "{{ your_role_here_repo_pbs_gpg }}"
name: "{{ your_role_here_repo_pbs_name }}"
suites: '{{ your_role_here_repo_pbs_suites }}'
uris: "{{ your_role_here_repo_pbs_uris }}"
components: "{{ your_role_here_repo_pbs_components }}"Notes#
I use the manual commands for one host and emergency maintenance windows. I use the Ansible path when I need the same repository state on multiple Debian or Proxmox nodes.
Why it matters#
This note keeps upgrades repeatable and low stress. The command order is fixed, conditions are explicit, and I can reuse the same process on the next host without rebuilding it from memory.
References#
- Debian 13 (Trixie) release notes, upgrading from Debian 12
- Debian 13.1 release announcement
- APT manual for clean, upgrade, and dist-upgrade behavior
- Proxmox VE package repositories guide
- Proxmox VE 9 release announcement
- Proxmox upgrade path from VE 8 to VE 9
- Ansible deb822_repository module documentation
