Clean up old kernels on Ubuntu and free /boot
When /boot fills with old linux-image packages, apt stops configuring anything. Find the kernels you can remove safely, and keep the one you booted.
Why apt stops working when /boot fills with old kernels
On Ubuntu, every kernel update writes a new set of files into /boot and leaves the previous ones in place, so a small /boot partition fills up and apt can no longer finish an install. The repair is two steps. Work out which packages on the box are kernels and which one you booted, then remove the rest with apt autoremove --purge.
The order matters. The running kernel is the one package you must not remove, and the box may already be in the state where apt cannot run at all. Diagnose first.
What the failure actually looks like
A kernel version installs two large files into /boot: the compressed kernel (vmlinuz-<version>) and the initramfs (initial RAM filesystem, initrd.img-<version>, the small archive the kernel unpacks before it mounts the real root). The initramfs is built on your machine at install time, which is why the install needs free space and not just download bandwidth. With no room left, the build fails and takes the package down with it.
update-initramfs: Generating /boot/initrd.img-6.8.0-64-generic
gzip: stdout: No space left on device
E: mkinitramfs failure gzip 1
update-initramfs: failed for /boot/initrd.img-6.8.0-64-generic with 1.
dpkg: error processing package linux-image-6.8.0-64-generic (--configure):
installed linux-image-6.8.0-64-generic package post-installation script subprocess returned error exit status 1The version string will be yours. The compressor name comes from COMPRESS= in /etc/initramfs-tools/initramfs.conf, so a recent image may name zstd where an older one names gzip. The two lines that identify this problem are No space left on device and the dpkg: error processing package line under it.
After that, the package sits half-configured. Every later apt run tries to configure it again, fails the same way, and ends with E: Sub-process /usr/bin/dpkg returned an error code (1). This is the part that matters beyond disk space: unattended-upgrades runs on its timer, hits the same error, and stops. The server looks healthy and quietly stops applying security patches. If apt update fails before you get this far, that is a separate problem, often a duplicate entry after the deb822 sources migration.
Check whether /boot is a separate partition
Before deleting anything, find out what you are actually freeing.
findmnt /boot
findmnt -T /boot
df -h /boot /The first command prints a line only if /boot is its own mount point. The second always prints, and names the filesystem that really holds /boot. If they name the same filesystem as /, then /boot is just a directory on the root filesystem and it cannot fill on its own: your root filesystem is full, and old kernels are one contributor among many. In that case sudo apt clean, which empties the downloaded .deb files under /var/cache/apt/archives, buys you room. On a machine with a real /boot partition, apt clean frees nothing there at all, because the cache lives on a different filesystem.
Now get the number you will work against.
df -h /boot
ls -lh /boot/vmlinuz-$(uname -r) /boot/initrd.img-$(uname -r)Compare the Avail column with the size of those two files. The initrd is the larger one. The next kernel update needs room for another pair of roughly that size, so if Avail is smaller than the current initrd, the next update is already going to fail.
Find the kernel you are running
uname -r
cat /var/run/reboot-required.pkgsuname -r prints the release string of the kernel currently in memory. Copy that string somewhere. It is the one version you must not touch.
The second file exists only when a package asked for a reboot. A linux-image line in it means a newer kernel is installed on disk and unused, because the machine has not rebooted since it landed. Reboot before you clean if you can. apt protects the running kernel and the newest one, so cleaning while you run an old kernel keeps one more version pinned than you need.
List the kernel packages and read their states
dpkg --list | grep -E 'linux-(image|modules|headers|tools)' | awk '{print $1, $2}'The first field is dpkg's state code. ii means installed and configured. iF means installed but half-configured, which is exactly what the failed upgrade above leaves behind. rc means removed with its configuration still on disk, which occupies nothing in /boot and is safe to purge.
The second field tells you what kind of package it is. A name with a version inside it, such as linux-image-6.8.0-64-generic, is one specific kernel. A name with no version, such as linux-image-generic, linux-headers-generic or linux-generic, is a meta package. It contains no kernel. Its whole job is to depend on the newest versioned kernel so that apt upgrade pulls new kernels in. Removing a meta package stops the machine receiving kernel updates, and nothing warns you afterwards.
The families divide up like this. linux-image-* holds the compressed kernel in /boot. linux-modules-* and linux-modules-extra-* hold the drivers under /lib/modules. linux-headers-* holds build headers under /usr/src, which means purging headers frees root filesystem space and not /boot space. If your problem is a full /boot partition, the image packages are what you are hunting.
ls -1 /boot/vmlinuz-*
ls -1 /lib/modules/These two listings should line up with each other and with the dpkg --list output. A directory in /lib/modules with no matching installed package is a leftover from someone deleting files by hand.
How apt decides which kernels to keep
apt autoremove will not remove a kernel it considers protected, and the protected set includes the kernel you are running now. The retention policy has changed between Ubuntu releases, so read it off your own machine rather than trusting a number written down anywhere.
apt-config dump | grep -i -e neverautoremove -e versionedkernel
ls -l /etc/apt/apt.conf.d/01autoremove /etc/apt/apt.conf.d/01autoremove-kernelsAPT::NeverAutoRemove is the list of package name patterns that apt autoremove refuses to touch. APT::VersionedKernelPackages is the list of name prefixes that apt treats as versioned kernel packages in the first place. On releases that generate /etc/apt/apt.conf.d/01autoremove-kernels, that file is rewritten by /etc/kernel/postinst.d/apt-auto-removal every time a kernel package is installed, so editing it by hand achieves nothing: the next kernel install overwrites your edit. On releases where the file is absent, apt applies the same protection internally. Either way, apt-config dump shows the rules in force on your box, and that output is the correct answer for your release.
The cleanup that is safe to run
sudo apt update
sudo apt autoremove --purge --dry-run--dry-run changes nothing on disk and prints exactly what the real run would remove. Read the list. Two things should make you stop. A meta package such as linux-generic or linux-image-generic in the removal list means something marked it automatic, and removing it ends your kernel updates. The string from uname -r in the removal list means the running kernel is not protected, which should not happen and needs investigating before you go further.
If the list looks right, run it for real.
sudo apt autoremove --purge
df -h /bootThe --purge half deletes the leftover configuration as well as the package. It frees little extra space, and it keeps dpkg --list free of accumulating rc lines, which makes the next audit readable.
Then confirm the boot menu was rebuilt. Removing a kernel package runs update-grub for you, so the menu should reference only files that still exist.
sudo grep -o 'vmlinuz-[^ ]*' /boot/grub/grub.cfg | sort -u
ls -1 /boot/vmlinuz-*Every version in the first output must appear in the second. A menu entry pointing at a file that is gone is how a working server becomes one that stops at the GRUB prompt. That is one route to a VPS that will not boot after a kernel update, and it is far harder to fix from a rescue console than to avoid here.
Why apt autoremove sometimes removes nothing
apt autoremove only removes packages marked automatic, meaning packages that were installed as a dependency of something else. A kernel you installed yourself, with apt install linux-image-6.8.0-40-generic, is marked manual, and autoremove will never touch it however old it gets.
apt-mark showmanual | grep -E '^linux-'Any versioned kernel in that output is invisible to autoremove. Hand it back, using version strings from your own listing:
sudo apt-mark auto linux-image-6.8.0-40-generic linux-modules-6.8.0-40-generic
sudo apt autoremove --purge --dry-runLeave the meta packages marked manual. They are supposed to be manual, because they are what you asked for.
Remove one named kernel on purpose
Sometimes you want a specific version gone now rather than whenever the policy allows it. Name the image package and let apt work out the rest.
sudo apt purge linux-image-6.8.0-40-genericapt prints a removal list before it does anything, because linux-modules-extra-* depends on the image package and has to go in the same transaction. That printed list is your real safety check, and it is where you catch a meta package being dragged out alongside the version you meant to remove. Answer n if anything unexpected is in it. Follow up with sudo apt autoremove --purge to collect the module and header packages that have now lost their reason to exist.
Why you never remove the running kernel
The kernel already in memory keeps running after its files are deleted, so nothing appears to break at first. What breaks is everything the kernel has not loaded yet. Purging linux-modules-$(uname -r) deletes /lib/modules/$(uname -r)/, so the next module load fails:
modprobe: FATAL: Module nf_tables not found in directory /lib/modules/6.8.0-64-genericA firewall reload fails from that point, and so does mounting a filesystem type this kernel has not touched since boot. Meanwhile /boot/vmlinuz-$(uname -r) is gone, so the boot menu no longer offers the kernel you are running, and the next reboot lands somewhere else. The machine keeps serving traffic and is already unbootable. Check uname -r against the removal list every time.
When /boot is too full for apt to run at all
This is the state that sends people looking for this page. apt autoremove needs dpkg to finish configuring the half-configured kernel package first, and that step rebuilds an initramfs, which needs space in a /boot that has none. Break the loop by hand, once.
uname -r
ls -1 /boot/initrd.img-*Pick one initrd whose version is not the string uname -r gave you, and delete that single file.
sudo rm /boot/initrd.img-6.8.0-40-generic
sudo apt --fix-broken install
sudo apt autoremove --purge
sudo update-grubEach line has a reason. The rm is a deliberate exception that leaves dpkg believing a file exists when it does not. apt --fix-broken install completes the configuration that failed, now that there is room for the initramfs. autoremove --purge then removes the package whose file you deleted along with the other old versions, which puts dpkg back in step with the disk. update-grub rebuilds the menu from the files that actually exist. Do not reboot between the rm and the update-grub, because in that window the menu can still point at the file you just deleted. If dpkg complains that it is interrupted, sudo dpkg --configure -a does the same repair as apt --fix-broken install.
The same job on dnf systems
If your VPS runs Fedora or one of the RHEL rebuilds such as Rocky Linux, the mechanism is inverted. Debian and Ubuntu protect kernels with apt autoremove rules and leave the cleanup for you or for unattended-upgrades to trigger, while dnf enforces a count called installonly_limit and removes the oldest kernel automatically as soon as a new install would exceed it. Read the value in force with grep installonly_limit /etc/dnf/dnf.conf and man 5 dnf.conf, and clear an existing backlog with sudo dnf remove --oldinstallonly. The running kernel is protected there too. For the wider mapping between the two package managers, see the dnf and apt command equivalents.
Stop it happening again
Cleanup that depends on you remembering will fail eventually, so put it in the thing that installs the kernels. Open /etc/apt/apt.conf.d/50unattended-upgrades and look for these keys, which the shipped file already contains as commented lines:
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";Uncomment them rather than appending a second copy at the end. In apt configuration the last assignment of a key wins, so a duplicate makes the file disagree with itself and hides which value is real. Check what the parser ended up with, and watch a run that changes nothing:
apt-config dump | grep -i 'Unattended-Upgrade::Remove'
sudo unattended-upgrade --dry-run --debug
sudo tail -n 40 /var/log/unattended-upgrades/unattended-upgrades.logThe log is the proof. It records each run, so an upgrade that failed for lack of space shows up there long before anyone notices the machine is behind on patches. The rest of that configuration is covered in automatic security updates on Ubuntu.
Before the next kernel lands there is one number to check, and it is the same pair of commands from the start of this guide:
df -h /boot
ls -lh /boot/initrd.img-$(uname -r)If Avail is not comfortably larger than that file, the next kernel will fail exactly as described above, so fix it now rather than during the upgrade. The check is worth a minute alongside your other disk health checks on a VPS. It matters most right before a release upgrade, because moving Ubuntu 24.04 to 26.04 installs a fresh kernel early in the process and do-release-upgrade will refuse to continue when /boot is short of space.
FAQ
Why does Ubuntu keep old kernels instead of deleting them?
Because a kernel that fails to boot leaves you with nothing else to select. Keeping the previous version means a bad update is recoverable from the GRUB menu instead of from a provider rescue console. apt therefore protects a set of kernel packages from automatic removal, always including the one you are running. Run apt-config dump | grep -i neverautoremove to see the exact patterns your release protects, since the policy has changed between releases.
Is apt autoremove --purge safe to run on a production server?
Yes, provided you read the dry run first. Run sudo apt autoremove --purge --dry-run, which writes nothing, and check the printed list. Stop if it contains a meta package such as linux-generic or linux-image-generic, because removing one of those ends future kernel updates. Stop also if it contains the version string that uname -r prints. If neither appears, the removals are old kernels and orphaned dependencies.
apt autoremove removed nothing and /boot is still full. What now?
The old kernels are almost certainly marked manual, and autoremove only touches packages marked automatic. Run apt-mark showmanual | grep -E '^linux-'. Any versioned kernel listed there was installed by hand at some point. Mark it automatic with sudo apt-mark auto linux-image-<version> and run the dry run again, or purge that one version directly with sudo apt purge linux-image-<version>.
Can I delete files from /boot by hand?
Only as a deliberate one-off, when /boot is so full that apt cannot configure the broken kernel package. Delete a single initrd.img-<version> file whose version is not the output of uname -r, then immediately run sudo apt --fix-broken install, sudo apt autoremove --purge and sudo update-grub. Deleting files without those follow-up steps leaves dpkg recording packages whose files are gone and leaves GRUB menu entries pointing at missing files, and the machine fails on its next reboot rather than at the moment you made the mistake.