Skip to content

Instantly share code, notes, and snippets.

@yorickdowne
Last active April 29, 2026 10:35
Show Gist options
  • Select an option

  • Save yorickdowne/ec9e2c6f4f8a2ee93193469d285cd54c to your computer and use it in GitHub Desktop.

Select an option

Save yorickdowne/ec9e2c6f4f8a2ee93193469d285cd54c to your computer and use it in GitHub Desktop.
Debian 12 bookworm upgrade

Debian 12 "Bookworm"

On systems with more than one EFI partition, such as systems set up for RAID with mdadm, grub-efi only upgrades one of the EFI partitions, the one mounted to /boot/efi. Because Debian 12 does not change the grub version from Debian 11, this does not cause boot failures. Discussion here: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1135137
See the Debian 13 gist for a way to verify EFI before rebooting, and upgrading the second copy, if having only one EFI copy upgraded offends your sensibilities

To start, read the official release notes.

If your install fits into "vanilla Debian plus maybe a handful of 3rd-party repos", then this guide for a simple upgrade to Debian 12 "bookworm" from Debian 11 "bullseye" can be helpful. 3rd-party repos are handled with a find command.

If you are on a fork of Debian such as RasPI OS, use their instructions, not this gist.

Note upgrade is only supported from Debian 11 to Debian 12. If you are on Debian 10, upgrade to Debian 11 first and make sure to change the security repo as per the release notes. Then once on Debian 11, you can upgrade to Debian 12.

From Debian 12, you can upgrade to Debian 13.

This guide is only for the OS itself. Applications are as plentiful as sand on the beach, and they may all require additional steps. Plan for that.

  • Check free disk space

df -h

5 GiB free is a conservative amount. sudo apt clean and sudo apt autoremove can be used to free some disk space.

On a server with only docker installed, even 1 GiB free was sufficient. Do err on the side of caution here, however.

  • Identify any 3rd-party repos that may need to be updated. They'll be changed with a find command, below.

ls /etc/apt/sources.list.d

  • Update current distribution

sudo apt update && sudo apt full-upgrade

If this brought in a new kernel, sudo reboot - otherwise continue

  • Optional: Start a screen session

So that an SSH disconnect doesn't stop your upgrade halfway through, you can run in screen:

sudo apt install -y screen && screen
  • Change repo to bookworm, from bullseye.
sudo sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
sudo sed -i '/non-free/ s/$/ non-free-firmware/' /etc/apt/sources.list
  • Change all 3rd-party repos

This assumes the repos have bookworm versions. Run sudo apt update after the change to bookworm to confirm, and deal with any repos that aren't available in bookworm.

sudo find /etc/apt/sources.list.d -type f -exec sed -i 's/bullseye/bookworm/g' {} \;

Run sudo apt update and if it fails on some repos because they don't have bookworm versions, disable them for now, then test again.

  • Update Debian

For the following, say Yes to restarting services, and keep existing config files when prompted. If using msmtp, acknowledge warnings. AppArmor for msmtp worked for me.

sudo apt update && sudo apt full-upgrade

  • Reboot

sudo reboot

  • Clean up old repos

sudo apt autoremove && sudo apt clean

Automated by Ansible

Config ansible.cfg:

[defaults]
interpreter_python = /usr/bin/python3

Playbook bookworm.yml:

---
- name: Upgrade to Debian bookworm
  hosts: all
  serial: 1
  gather_facts: false
  roles:
    - base/upgrade_bookworm

Role base/upgrade_bookworm/tasks/main.yml:

---
- name: Get distribution version
  setup:
    filter: ansible_distribution*
- name: Skip if not Debian 11
  meta: end_host
  when: ansible_distribution != 'Debian' or ansible_distribution_major_version != '11'
- name: apt clean
  apt:
    clean: yes
  become: yes
- name: Get filesystem facts
  setup:
    filter: ansible_mounts
- name: Fail if free space on / is below 5 GiB
  ansible.builtin.assert:
    that:
      - item.size_available > (5 * 1024 * 1024 * 1024)
    fail_msg: "Free disk space on {{ item.mount }} is below 5 GiB"
  loop: "{{ ansible_mounts }}"
  when: item.mount == "/"
- name: All apt packages up to date
  apt:
    upgrade: dist
    update_cache: yes
  become: yes
- name: apt autoremove
  apt:
    autoremove: yes
  become: yes
- name: apt clean
  apt:
    clean: yes
  become: yes
- name: Check if reboot required
  ansible.builtin.stat:
    path: /run/reboot-required
    get_checksum: no
  register: reboot_required_file
- name: Reboot if required
  ansible.builtin.reboot:
    msg: "Reboot initiated by Ansible"
    connect_timeout: 5
    reboot_timeout: 600
    pre_reboot_delay: 0
    post_reboot_delay: 60
    test_command: whoami
  when: reboot_required_file.stat.exists
  become: true
- name: Switch OS from bullseye to bookworm
  ansible.builtin.replace:
    path: /etc/apt/sources.list
    regexp: 'bullseye'
    replace: 'bookworm'
  become: yes
- name: Add non-free-firmware if non-free is in use
  ansible.builtin.replace:
    path: /etc/apt/sources.list
    regexp: '(.*non-free.*)'
    replace: '\1 non-free-firmware'
  become: yes 
- name: Find all 3rd-party repos
  ansible.builtin.find:
    paths: /etc/apt/sources.list.d
    patterns: '*'
    recurse: no
  register: third_party_repos
- name: Switch 3rd-party repos from bullseye to bookworm
  ansible.builtin.replace:
    path: "{{ item.path }}"
    regexp: 'bullseye'
    replace: 'bookworm'
  loop: "{{ third_party_repos.files }}"
  loop_control:
    label: "{{ item.path }}"
  become: yes 
- name: Use apt to move to bookworm
  apt:
    upgrade: dist
    update_cache: yes
  become: yes
- name: Get distribution version
  setup:
    filter: ansible_distribution*
- name: Fail if not Debian 12
  assert:
    that:
      - ansible_distribution_major_version == '12'
    fail_msg: "Upgrade to Debian 12 failed"
- name: apt autoremove
  apt:
    autoremove: yes
  become: yes
- name: apt clean
  apt:
    clean: yes
  become: yes
- name: Re-enable msmtp apparmor profile
    ansible.builtin.file:
      path: /etc/apparmor.d/disable/usr.bin.msmtp
      state: absent
    become: yes
- name: Restart apparmor service
    ansible.builtin.systemd:
      name: apparmor
      state: restarted
    become: yes
- name: Reboot on bookworm
  ansible.builtin.reboot:
    msg: "Reboot initiated by Ansible"
    connect_timeout: 5
    reboot_timeout: 600
    pre_reboot_delay: 0
    post_reboot_delay: 60
    test_command: whoami
  become: yes
- name: Pause for 5 minutes for staggered upgrades
  pause:
    minutes: 5
@hmoffatt
Copy link
Copy Markdown

hmoffatt commented Nov 11, 2024

Thanks for this. A couple of issues I encountered:

  • In the call to stat of reboot-required, get_md5 should be get_checksum, I don't see the former in the documentation.
  • ansible_python_interpreter needs to be set to /usr/bin/python3; by default ansible seems to discover python3.9, which then disappears after the upgrade to Python 3.11 and the following commands fail.

@grimmute
Copy link
Copy Markdown

Thanks!

This helped me upgrade from antiX 22 to 23.

@yorickdowne
Copy link
Copy Markdown
Author

Thanks @hmoffatt ! Changed to get_checksum.

I didn’t have any trouble with Python interpreters and updated an entire fleet with this. Maybe Ansible behavior changed in the meantime? If so, I’ll find out with Debian 13 next year.

@yorickdowne
Copy link
Copy Markdown
Author

I checked my ansible.cfg. I have that set, but didn't document it. Oop. Added that, thanks for pointing it out @hmoffatt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment