The accumulation of old kernels until /boot is full, thereby breaking apt, is a bug: LP #1357093, with a fix implemented in all flavors of Ubuntu 16.04 and newer.
Most users who notice this problem installed 'whole-disk encryption', which creates a tiny unencrypted /boot partition. Since it's tiny, the partition fills quickly, and these users notice the problem much sooner than others.
When a new kernel is installed, the /etc/kernel/postinst.d/apt-auto-removal script marks older kernels as eligible for autoremoval...but doesn't run autoremove. The lack of autoremove was originally intended to allow a human to review the list of removed packages.
The bugfix to LP #1357093 works for most users - if it doesn't work for you, then there is another underlying cause preventing old kernel packages from being apt-marked as eligible for autoremove. Seek one-on-one help in the Ubuntu support channels.
In older versions of Ubuntu, you must fix weed old kernels to preserve space manually. How you fix it manually depends upon whether your /boot is full or not. Most users don't notice the problem until /boot is full, and they are getting apt and dpkg no-space-left-on-device errors.
If /boot is not full, and apt works properly, a simple
sudo apt-get autoremove ## Ubuntu 14.04 and older
sudo apt autoremove ## Ubuntu 16.04 and newer
should remove all kernel packages that are eligible for autoremoval.
If /boot is full, and apt actions fail with the dpkg error 'no space left on device', then it is too late to use autoremove.
It's too late because Aptdaemon queues package actions. Autoremove is at the back of the queue, and apt aborts the entire remaining queue when any action fails...including running out of space. (This is arguably a bug in apt/aptdaemon)
The best practice here is to use 'uname' and 'dpkg' to remove one or two old kernels, freeing space for apt to complete it's queued actions. Then autoremove will work.
$ uname -r
3.16.0-36-generic ## This is kernel you MUST NOT remove.
$ dpkg -l | grep linux-image
rc linux-image-3.16.0-23-generic ## 'rc' means already removed
rc linux-image-3.16.0-28-generic ## 'rc' can be safely ignored
ii linux-image-3.16.0-30-generic ## 'ii' means installed. Removable
ii linux-image-3.16.0-31-generic ## Removable
ii linux-image-3.16.0-33-generic ## Removable
ii linux-image-3.16.0-34-generic ## Backup working kernel. Don't remove
ii linux-image-3.16.0-36-generic ## Current kernel. DO NOT REMOVE
## Use dpkg to remove one older kernel, freeing enough space for apt to work
$ sudo dpkg --remove linux-image-3.16.0-30-generic
$ sudo apt-get autoremove ## Ubuntu 14.04 and older
$ sudo apt autoremove ## Ubuntu 16.04 and newer
If you have been ignoring the problem for a long time, then there are second-order effects, like linux-image-generic pointing to the wrong kernel version, and apt dependency errors. There is no single way of fixing all of these at once.
Generally, the easy way to clean up most of these problems is to clean the old packages out of your local package cache, update your package database, and reinstall the offending packages from the Ubuntu repositories (instead of your local cache).
$ sudo apt-get update ## Refresh the package database (14.04 and older)
$ sudo apt update ## (16.04 and newer)
$ sudo apt-get autoclean ## Delete the obsolete packages from your local cache (14.04 and older)
$ sudo apt autoclean ## (16.04 and newer)
$ sudo apt-get install --reinstall <packagename> ## Reinstall the offending package with the latest version in the Ubuntu repositories (14.04 and older)
$ sudo apt install --reinstall <packagename> ## (16.04 and newer)
If you still encounter apt and/or dpkg errors, seek one-on-one help in the Ubuntu support channels.
Removing old kernels is not inherently unsafe, but if you remove all your kernels and reboot, you'll be left at an angry Grub screen. Fixing that takes significant know-how (like that but with an
apt-get install linux-generic at the end).
The first time you do this is quite thrilling but the people looking to clean up their Grub menu or recover some disk space aren't looking for thrills.
The danger comes from users copy-pasting a block of code that —unbeknownst to them and without acknowledging the risks— doesn't apply. There are many examples of detecting old kernels and few are perfect. Even my latest effort still has its pitfalls. And we're talking about an issue that can be fixed; many posts on Ask Ubuntu could lead to permanent data loss if used incorrectly.
We try to safeguard against damage by signposting the risk to make users conscious of potential problems. In the best case scenarios, the user will be prepared and equipped to deal with a problem and in the worst case, at least they can't complain that they weren't warned.
I can see only 1 reason: it does not work flawlessly; there is no clean way to currently decide what the definition of "old kernels" is. "old" does not mean "unused" nor does it mean "unwanted". And any mistake in this will kill a users' machine.
So up to now the manual method is preferred since this puts the power of removing into the users' hands.
Resources for this conclusion: