All stuff
4 min read

Cleaning Up Old Kernels on Ubuntu

A simple bash script to safely remove old kernels on Ubuntu when apt autoremove doesn't get the job done.

Cleaning Up Old Kernels on Ubuntu
+

If you've been running an Ubuntu server for a while, you've probably noticed /boot filling up with old kernel images. Ubuntu installs new kernels with each update but doesn't always clean up the old ones.

Why Not Just Use apt autoremove?

The official answer is sudo apt autoremove --purge. In practice, it often leaves old kernels behind. This happens because:

  • Manually installed kernels — if a kernel was installed via apt install linux-image-*, apt considers it explicitly requested and won't auto-remove it.
  • Stale autoremoval config — Ubuntu maintains a protection list in /etc/apt/apt.conf.d/01autoremove-kernels that can get out of sync after partial upgrades or version pinning.
  • Held or pinned packages — anything marked hold or pinned in apt preferences is excluded from autoremoval, even if it's no longer needed.

When autoremove isn't cutting it, a targeted script can take care of the rest.

The Script

1#!/bin/bash
2# Remove old kernels on Ubuntu/Debian systems
3# Run without arguments for a dry run
4# Run with "exec" argument to actually remove old kernels
5# Example: sudo ./remove-old-kernels.sh exec
6 
7set -euo pipefail
8 
9# Ensure we're running on a Debian-based system
10if ! command -v dpkg &>/dev/null; then
11 echo "Error: This script requires dpkg (Debian/Ubuntu systems only)."
12 exit 1
13fi
14 
15CURRENT_KERNEL=$(uname -r)
16CURRENT_VERSION=$(echo "$CURRENT_KERNEL" | sed 's/-[a-z].*$//')
17echo "Currently running kernel: $CURRENT_KERNEL"
18 
19# Get only installed (ii) kernel-related packages, excluding the running kernel
20# and meta-packages (linux-image-generic, linux-headers-generic, etc.)
21OLD_KERNELS=$(
22 dpkg --list |
23 awk '/^ii\s+(linux-image|linux-headers|linux-modules)/ { print $2 }' |
24 grep -v "$CURRENT_VERSION" |
25 grep -E '[0-9]+\.[0-9]+\.[0-9]+' || true
26)
27 
28if [ -z "$OLD_KERNELS" ]; then
29 echo "No old kernels found to remove."
30 exit 0
31fi
32 
33echo ""
34echo "Old kernels to be removed:"
35echo "$OLD_KERNELS"
36echo ""
37 
38if [ "${1:-}" == "exec" ]; then
39 if [ "$EUID" -ne 0 ]; then
40 echo "Error: Must run as root. Use: sudo $0 exec"
41 exit 1
42 fi
43 apt purge -y $OLD_KERNELS
44 echo ""
45 echo "Done. Running 'update-grub' to refresh boot menu..."
46 update-grub
47 echo "Old kernels removed successfully."
48else
49 echo "Dry run complete. To remove these kernels, run:"
50 echo " sudo $0 exec"
51fi
1#!/bin/bash
2# Remove old kernels on Ubuntu/Debian systems
3# Run without arguments for a dry run
4# Run with "exec" argument to actually remove old kernels
5# Example: sudo ./remove-old-kernels.sh exec
6 
7set -euo pipefail
8 
9# Ensure we're running on a Debian-based system
10if ! command -v dpkg &>/dev/null; then
11 echo "Error: This script requires dpkg (Debian/Ubuntu systems only)."
12 exit 1
13fi
14 
15CURRENT_KERNEL=$(uname -r)
16CURRENT_VERSION=$(echo "$CURRENT_KERNEL" | sed 's/-[a-z].*$//')
17echo "Currently running kernel: $CURRENT_KERNEL"
18 
19# Get only installed (ii) kernel-related packages, excluding the running kernel
20# and meta-packages (linux-image-generic, linux-headers-generic, etc.)
21OLD_KERNELS=$(
22 dpkg --list |
23 awk '/^ii\s+(linux-image|linux-headers|linux-modules)/ { print $2 }' |
24 grep -v "$CURRENT_VERSION" |
25 grep -E '[0-9]+\.[0-9]+\.[0-9]+' || true
26)
27 
28if [ -z "$OLD_KERNELS" ]; then
29 echo "No old kernels found to remove."
30 exit 0
31fi
32 
33echo ""
34echo "Old kernels to be removed:"
35echo "$OLD_KERNELS"
36echo ""
37 
38if [ "${1:-}" == "exec" ]; then
39 if [ "$EUID" -ne 0 ]; then
40 echo "Error: Must run as root. Use: sudo $0 exec"
41 exit 1
42 fi
43 apt purge -y $OLD_KERNELS
44 echo ""
45 echo "Done. Running 'update-grub' to refresh boot menu..."
46 update-grub
47 echo "Old kernels removed successfully."
48else
49 echo "Dry run complete. To remove these kernels, run:"
50 echo " sudo $0 exec"
51fi

How It Works

The script has two modes: a dry run (default) that shows what would be removed, and an exec mode that actually purges the packages.

Finding old kernels:

1dpkg --list |
2 awk '/^ii\s+(linux-image|linux-headers|linux-modules)/ { print $2 }' |
3 grep -v "$CURRENT_VERSION" |
4 grep -E '[0-9]+\.[0-9]+\.[0-9]+'
1dpkg --list |
2 awk '/^ii\s+(linux-image|linux-headers|linux-modules)/ { print $2 }' |
3 grep -v "$CURRENT_VERSION" |
4 grep -E '[0-9]+\.[0-9]+\.[0-9]+'

This pipeline does a few important things:

  1. awk '/^ii/' — only matches installed packages. dpkg --list also shows removed and half-configured packages; we don't want those.
  2. grep -v "$CURRENT_VERSION" — excludes anything matching the running kernel's base version (e.g., 6.8.0-106). This protects both the flavor-specific packages like linux-image-6.8.0-106-generic and the common packages like linux-headers-6.8.0-106 that the running kernel depends on.
  3. grep -E '[0-9]+\.[0-9]+\.[0-9]+' — only matches versioned packages. This is the key safety measure: it excludes meta-packages like linux-image-generic and linux-headers-generic. Removing those would break future kernel updates via apt upgrade.

After purging, the script runs update-grub to refresh the boot menu so removed kernels no longer appear at startup.

Usage

First, do a dry run to review what will be removed:

1sudo bash remove-old-kernels.sh
1sudo bash remove-old-kernels.sh

Example output:

1Currently running kernel: 6.8.0-106-generic
2
3Old kernels to be removed:
4linux-headers-6.8.0-101
5linux-headers-6.8.0-101-generic
6linux-image-6.8.0-101-generic
7linux-modules-6.8.0-101-generic
8linux-modules-extra-6.8.0-101-generic
1Currently running kernel: 6.8.0-106-generic
2
3Old kernels to be removed:
4linux-headers-6.8.0-101
5linux-headers-6.8.0-101-generic
6linux-image-6.8.0-101-generic
7linux-modules-6.8.0-101-generic
8linux-modules-extra-6.8.0-101-generic

If the list looks right, run it for real:

1sudo bash remove-old-kernels.sh exec
1sudo bash remove-old-kernels.sh exec

Things to Keep in Mind

  • Always do the dry run first. Review the package list before removing anything.
  • This is for Ubuntu/Debian only. Fedora, Arch, and others handle kernel packaging differently.
  • Try apt autoremove --purge first. Use this script for what autoremove misses.
  • Tested on Ubuntu 24.04 LTS with kernel 6.8.0-106-generic.