Reclaiming space on Debian and Ubuntu, in order of return

Everyone suggests sudo apt clean. It empties the downloaded package cache, and on a desktop that has been through a few upgrades that is perhaps a gigabyte. The bigger wins on Debian and Ubuntu are old kernels, packages that were removed but never purged, and snaps, which apt cannot see at all.

Measure before you delete anything, so you know which of these is yours:

du -sh /var/cache/apt/archives
dpkg -l 'linux-image-*' | grep '^ii'
du -sh /var/lib/snapd 2>/dev/null
journalctl --disk-usage
du -sh /var/log
Range chart of typical sizes on a two-year-old Ubuntu desktop: package cache 0.5 to 2 GB, old kernels 1 to 3 GB, snaps 2 to 10 GB, journal 0.5 to 4 GB, each labelled with the command that clears it
The package cache is usually the smallest of the four, and apt only reaches two of them.

Old kernels come first

Each installed kernel is an image, an initramfs and a modules tree, 200 to 400 MB together. Ubuntu keeps the one you are running, the newest, and the one before it, and autoremove takes the rest. The catch is that it only takes kernels installed as dependencies. Anything marked as manually installed is skipped, which is how machines end up with eight of them.

uname -r                                    # the one you are running
dpkg -l 'linux-image-*' | grep '^ii'        # everything installed
sudo apt autoremove --purge

If the old ones survive that, mark them as automatic and run it again. The pattern below matches 5.x kernels; current Ubuntu releases ship 6.x, so adjust it to what dpkg -l showed you:

sudo apt-mark auto 'linux-image-5.*'
sudo apt autoremove --purge

Never remove the kernel uname -r reports, and keep one older one as a fallback. A machine with exactly one kernel and a failed update needs a rescue disk.

Two kernels is the right number: the running one and one previous. Ubuntu’s default is already close to that when nothing has been marked manual.

autoremove can also offer to remove something you use. That happens when the package came in as a dependency of something you have since removed and nothing marks it as wanted. sudo apt-mark manual <package> settles it for good.

Remove is not purge

This is where the space hides on long-lived machines. apt remove deletes the program and leaves its configuration in /etc; apt purge deletes both. Packages that were removed but not purged show as rc in dpkg -l:

dpkg -l | awk '/^rc/ { print $2 }'
dpkg -l | awk '/^rc/ { print $2 }' | xargs -r sudo apt purge -y

The configuration files are small. What matters is that some packages keep data directories under /var/lib that only a purge removes, and a database server removed but not purged can leave tens of gigabytes there. That data is gone for good once purged, so look first:

sudo du -sh /var/lib/* 2>/dev/null | sort -h | tail -15

The package cache itself

sudo apt clean          # remove every downloaded .deb
sudo apt autoclean      # remove only those no longer downloadable

clean is safe whenever no other apt operation is running. It only deletes downloaded archives, never installed software, and it leaves the package lists in /var/lib/apt/lists alone, so apt update is unaffected. The only cost is downloading a package again if you reinstall it. I would not bother with autoclean; the distinction rarely matters.

Snaps, which apt knows nothing about

Snap keeps older revisions of every installed snap so that snap revert works. On a desktop the default is two revisions in total, the active one and one disabled, and for a browser or an IDE each revision is hundreds of megabytes.

snap list --all | awk '/disabled/ { print $1, $3 }'

On an Ubuntu desktop this is often the largest item on the list. Snap revisions and how to bound them covers removing the disabled ones.

The rest of the machine

What remains is the same on any distribution: the journal, in bounding the systemd journal; /var/log proper, in what grows there and how logrotate handles it; and Docker, if it is installed, which often outweighs everything above combined.

As one block:

sudo apt clean
sudo apt autoremove --purge
sudo journalctl --vacuum-size=200M
sudo snap set system refresh.retain=2
docker system df

The refresh.retain line only matters if someone raised the setting; 2 is already the desktop default. The last line is a report on purpose. A Docker prune on a machine with running services deserves more thought than a one-liner, and what each prune flag deletes sets that out.

Keep reading