![]()
So every now and then when working with some system there are times it just works so good you kind of forget about it and in those times when looking back at it it’s already lagging behind the times, getting cluttered with the digital equivalent of dust bunnies? Yeah that happens to me too.
The useful distinction I make now is between four different jobs: ordinary cleanup, repairing a broken kernel/package transaction, an Ubuntu release upgrade, and a Debian release upgrade.
Before a release upgrade I verify backups, free space, remote console access, third-party repositories, and the supported upgrade path. I do one release at a time and keep the current SSH session open until the machine has rebooted successfully.
Ordinary Package and Kernel Cleanup
Kernel images are linux-image-*; headers are used to build modules; DKMS rebuilds third-party modules for new kernels. I inspect before removing anything:
uname -r
dpkg --list 'linux-image*' 'linux-headers*' | awk '$1 == "ii" {print $2, $3}'
dkms status
sudo apt update
sudo apt full-upgrade
sudo apt autoremove --purge
apt autoremove understands which kernel packages were installed automatically. I no longer use broad globs such as linux-image-4.7*; they are easy to mistype and can remove more than intended. I keep the running kernel and at least one known-good fallback until after a successful reboot.
Repair a Broken Kernel or Package Transaction
These original recovery commands remain useful:
sudo dpkg --configure -a
sudo apt --fix-broken install
sudo apt reinstall "linux-image-$(uname -r)" "linux-headers-$(uname -r)"
sudo update-initramfs -u -k all
sudo update-grub
dkms status
For one failed DKMS module I rebuild the exact module/version rather than every module blindly:
sudo dkms build -m MODULE_NAME -v MODULE_VERSION -k "$(uname -r)"
sudo dkms install -m MODULE_NAME -v MODULE_VERSION -k "$(uname -r)"
I inspect the error from journalctl, /var/log/apt/term.log, and the DKMS build log before repeating package operations.
Ubuntu Release Upgrade
Ubuntu has a release upgrader that changes sources, checks the upgrade path, and handles prompts:
sudo apt update
sudo apt full-upgrade
sudo reboot
sudo do-release-upgrade
For LTS-to-LTS upgrades I leave /etc/update-manager/release-upgrades at Prompt=lts. I do not manually replace Ubuntu codenames or jump over an intermediate supported release. Canonical maintains the current server upgrade procedure.
Debian Release Upgrade
Debian’s process is deliberately manual and release-specific. I first read both the old and target release notes, remove or disable third-party repositories, and fully update the current release. For the Debian 12 (bookworm) to Debian 13 (trixie) example, a deb822 source looks like:
Types: deb
URIs: https://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: https://security.debian.org/debian-security
Suites: trixie-security
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
After reviewing /etc/apt/sources.list and /etc/apt/sources.list.d/, I use the staged sequence from the target release notes:
export LC_ALL=C
sudo apt update
sudo apt upgrade --without-new-pkgs
sudo apt full-upgrade
sudo reboot
The exact suites, components, known issues, and minimum starting release change over time, so the target’s official Debian release notes are part of the command sequence, not optional background reading. Debian also documents the deb822 sources format.
The Raw Route I Used in 2024
The newer process above has more guardrails, but I do not want it to erase the method that got me here. When I first wrote this note, I approached the machine more directly: inspect what is installed, name the old pieces, change the Debian release codename, and let apt work through the consequences. It was not merely theoretical. It worked on the system I was upgrading.
Kernel cleanup followed that same hands-on logic. I listed the installed images, checked the running kernel, and removed a specific old image and its matching headers:
dpkg --list | grep linux-image
uname -r
sudo apt remove --purge linux-image-OLD-VERSION linux-headers-OLD-VERSION
My original example used a broad version glob such as linux-image-4.7*. I now spell out the exact package names instead. The manual route remains useful when apt autoremove does not select a known obsolete kernel, but I still keep the running kernel and at least one tested fallback.
The original Debian release upgrade was equally direct. For the move from Debian 11 (bullseye) to Debian 12 (bookworm), I changed the codenames in the older .list files and then ran the upgrade:
sudo sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
sudo sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list.d/*.list
export LC_ALL=C
sudo apt-mark showhold
sudo apt update
sudo apt upgrade
sudo apt full-upgrade
sudo apt autoremove
sudo reboot
That compact sequence shows the machinery clearly, but it also hides assumptions. It only covers the older .list format, a search-and-replace can touch third-party repositories, and it assumes that bullseye to bookworm is the supported next step. The apt autoremove at the end was part of my original routine; today I defer it until after the reboot checks below.
So I keep this route as a field note rather than a competing recommendation. It records what worked, while the release-specific procedure above remains the path I would follow on the next machine.
Verify After Reboot
cat /etc/os-release
uname -r
systemctl --failed
dkms status
sudo apt update
sudo apt --fix-broken install
Only after that check do I run another apt autoremove --purge. This keeps the simple cleanup commands from my original note while separating them from the much more consequential release-upgrade work.
Buy Me a Coffee