-
Star
(231)
You must be signed in to star a gist -
Fork
(109)
You must be signed in to fork a gist
-
-
Save Iman/8c4605b2b3ce8226b08a to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash | |
# Ubuntu Server or VM Cleaner. Safe by default; aggressive when asked. | |
# Example safe: sudo ./clean.sh | |
# Example aggressive: sudo JOURNAL_DAYS=3 AGGRESSIVE=1 ./clean.sh | |
# Enable Docker image prune (images only): sudo ./clean.sh --docker-images | |
# Tested on Ubuntu 20.04, 22.04, 24.04 (server/VM images). | |
set -Eeuo pipefail | |
trap 'rc=$?; echo "Error on line $LINENO: $BASH_COMMAND (exit $rc)"; exit $rc' ERR | |
IFS=$'\n\t' | |
PATH=/usr/sbin:/usr/bin:/sbin:/bin | |
############################################################################### | |
# DISCLAIMER – READ BEFORE RUNNING | |
# | |
# This script truncates logs, removes caches, purges orphaned/old packages, | |
# and can purge older kernels. It is intended for headless/minimal Ubuntu | |
# servers and VMs where reclaiming space is the priority. | |
# Not for desktops unless you knowingly opt out of the guard. | |
# | |
# No warranty. Use at your own risk. Snapshot first. | |
############################################################################### | |
# Config via env or flags | |
JOURNAL_DAYS="${JOURNAL_DAYS:-7}" # journal retention in days | |
KEEP_KERNELS="${KEEP_KERNELS:-2}" # number of newest kernels to keep (plus current) | |
SYSPREP="${SYSPREP:-0}" # scrub cloud-init state/logs for golden images | |
AGGRESSIVE="${AGGRESSIVE:-0}" # remove man pages, docs, APT lists, dev-tool caches | |
PRUNE_SNAPS="${PRUNE_SNAPS:-1}" # prune old snap revisions | |
PRUNE_DOCKER_IMAGES="${PRUNE_DOCKER_IMAGES:-0}" # prune unused Docker images only (opt-in) | |
DESKTOP_GUARD="${DESKTOP_GUARD:-1}" # abort if desktop detected | |
UPGRADE="${UPGRADE:-0}" # optionally run full-upgrade at the end | |
usage() { | |
cat <<EOF | |
Usage: sudo $(basename "$0") [options] | |
Options: | |
--journal-days N Keep only last N days of systemd journals [${JOURNAL_DAYS}] | |
--keep-kernels N Keep N newest kernels (plus current) [${KEEP_KERNELS}] | |
--aggressive Aggressive cleanup (man/docs/APT lists + dev-tool caches) | |
--sysprep Cloud-init scrub for golden image | |
--docker-images Prune unused Docker images (images only) | |
--no-snaps Skip Snap old-revision prune | |
--no-desktop-guard Do not abort on desktop systems | |
--upgrade Run apt full-upgrade at the end (optional) | |
-h, --help Show help | |
EOF | |
} | |
# Parse flags | |
while [[ "${1:-}" =~ ^- ]]; do | |
case "$1" in | |
--journal-days) JOURNAL_DAYS="$2"; shift 2;; | |
--keep-kernels) KEEP_KERNELS="$2"; shift 2;; | |
--aggressive) AGGRESSIVE=1; shift;; | |
--sysprep) SYSPREP=1; shift;; | |
--docker-images) PRUNE_DOCKER_IMAGES=1; shift;; | |
--no-snaps) PRUNE_SNAPS=0; shift;; | |
--no-desktop-guard) DESKTOP_GUARD=0; shift;; | |
--upgrade) UPGRADE=1; shift;; | |
-h|--help) usage; exit 0;; | |
*) echo "Unknown option: $1"; usage; exit 1;; | |
esac | |
done | |
# Safety | |
[[ $EUID -eq 0 ]] || { echo "Must be run as root."; exit 1; } | |
is_cmd(){ command -v "$1" >/dev/null 2>&1; } | |
detect_desktop(){ | |
dpkg -l | grep -qE '^(ii)\s+(ubuntu-desktop|xubuntu-desktop|kubuntu-desktop|ubuntustudio-desktop|gnome-shell)\b' | |
} | |
if [[ "$DESKTOP_GUARD" -eq 1 ]] && detect_desktop; then | |
echo "Desktop detected. Intended for servers/VMs. Use --no-desktop-guard to proceed."; exit 1 | |
fi | |
echo "== Ubuntu Server or VM Cleaner starting ==" | |
apt_housekeeping() { | |
echo "-> APT: clean caches and remove unused packages" | |
export DEBIAN_FRONTEND=noninteractive | |
apt-get -y update || true | |
apt-get -y autoremove --purge | |
apt-get -y autoclean | |
apt-get -y clean | |
# Purge residual config packages (state 'rc') | |
rc_pkgs=$(dpkg -l | awk '/^rc/{print $2}') | |
if [[ -n "${rc_pkgs}" ]]; then | |
echo " Purging residual configs:" | |
xargs -r apt-get -y purge <<<"${rc_pkgs}" || true | |
fi | |
if [[ "$AGGRESSIVE" -eq 1 ]]; then | |
echo " Aggressive: removing /var/lib/apt/lists and package caches" | |
rm -rf /var/lib/apt/lists/* /var/lib/apt/lists/partial \ | |
/var/cache/apt/pkgcache.bin /var/cache/apt/srcpkgcache.bin || true | |
fi | |
} | |
orphan_purge() { | |
echo "-> Orphaned packages" | |
if is_cmd deborphan; then | |
deborphan --guess-data --guess-multi --guess-all 2>/dev/null | xargs -r apt-get -y purge | |
else | |
echo " deborphan not installed; skipping." | |
fi | |
} | |
# Kernel purge (handles unsigned images; keeps meta packages) | |
kernel_purge_manual() { | |
echo "-> Kernel purge: keep ${KEEP_KERNELS} newest versions + running kernel and meta-packages" | |
current="$(uname -r)" | |
mapfile -t pkgs < <( | |
dpkg -l | awk ' | |
/^ii/ && $2 ~ /^(linux-(image(-unsigned)?|headers|modules|modules-extra)-[0-9])/ {print $2} | |
' | sort -V | |
) | |
normver() { echo "$1" | sed -E 's/^linux-(image(-unsigned)?|headers|modules|modules-extra)-//' | sed -E "s/-(generic|lowlatency)$//"; } | |
mapfile -t vers < <( | |
printf "%s\n" "${pkgs[@]}" | while read -r p; do normver "$p"; done | sort -Vr | uniq | |
) | |
keep_versions=("${vers[@]:0:${KEEP_KERNELS}}") | |
keep_versions+=("$(echo "$current" | sed -E 's/-[[:alnum:]]+$//')") | |
meta_keep='^(linux-(image|headers)?-generic|linux-virtual|linux-generic)$' | |
to_purge=() | |
for p in "${pkgs[@]}"; do | |
[[ "$p" =~ $meta_keep ]] && continue | |
ver="$(normver "$p")" | |
keep=0 | |
for kv in "${keep_versions[@]}"; do | |
[[ "$ver" == "$kv" ]] && keep=1 && break | |
done | |
(( keep == 0 )) && to_purge+=("$p") | |
done | |
if (( ${#to_purge[@]} > 0 )); then | |
echo " Purging:" | |
printf ' %s\n' "${to_purge[@]}" | |
apt-get -y purge "${to_purge[@]}" || true | |
apt-get -y autoremove --purge || true | |
else | |
echo " Nothing to purge." | |
fi | |
} | |
journal_vacuum() { | |
if is_cmd journalctl; then | |
echo "-> Journald: rotate and vacuum to ${JOURNAL_DAYS} days" | |
journalctl --rotate || true | |
journalctl --vacuum-time="${JOURNAL_DAYS}d" || true | |
journalctl --vacuum-size=200M || true | |
fi | |
} | |
# Truncate broad log set; avoid journald binaries; clear crash dumps | |
log_clean() { | |
echo "-> Logs: truncate active logs; remove rotated/compressed and crashes" | |
find /var/log -type f \ | |
! -name "*.gz" ! -name "*.xz" ! -regex '.*\.[0-9]$' \ | |
! -name "*.journal" ! -name "*.journal~" \ | |
-exec truncate -s 0 {} + || true | |
: > /var/log/wtmp || true | |
: > /var/log/btmp || true | |
: > /var/log/lastlog || true | |
find /var/log -type f -regex '.*\.[0-9]$' -delete || true | |
find /var/log -type f -name '*.gz' -delete || true | |
find /var/log -type f -name '*.xz' -delete || true | |
find /var/crash -type f -delete || true | |
find /var/lib/systemd/coredump -type f -delete 2>/dev/null || true | |
} | |
tmp_clean() { | |
echo "-> Temp: cleaning /tmp and /var/tmp" | |
find /tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true | |
find /var/tmp -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true | |
# Clean temp according to tmpfiles.d policies if present | |
is_cmd systemd-tmpfiles && systemd-tmpfiles --clean || true | |
# Kill old systemd-private-* temp dirs that linger | |
find /tmp -maxdepth 1 -type d -name 'systemd-private-*' -mtime +1 -exec rm -rf {} + 2>/dev/null || true | |
} | |
user_caches() { | |
echo "-> User caches: ~/.cache and Trash" | |
rm -rf /home/*/.cache/* 2>/dev/null || true | |
rm -rf /root/.cache/* 2>/dev/null || true | |
rm -rf /home/*/.local/share/Trash/*/** 2>/dev/null || true | |
rm -rf /root/.local/share/Trash/*/** 2>/dev/null || true | |
# motd-news cache | |
rm -rf /var/cache/motd-news/* 2>/dev/null || true | |
} | |
docs_prune() { | |
if [[ "$AGGRESSIVE" -eq 1 ]]; then | |
echo "-> Aggressive: remove man pages and docs" | |
rm -rf /usr/share/man/?? /usr/share/man/??_* /usr/share/doc/* /usr/share/info/* /var/cache/man/* || true | |
fi | |
} | |
# Extra dev-tool rubbish (only in aggressive mode) | |
dev_tool_caches() { | |
if [[ "$AGGRESSIVE" -ne 1 ]]; then return 0; fi | |
echo "-> Aggressive: remove common dev-tool caches" | |
# Snap cache | |
rm -rf /var/lib/snapd/cache/* 2>/dev/null || true | |
# Per-user caches (root + /home/*) | |
for uhome in /root /home/*; do | |
[[ -d "$uhome" ]] || continue | |
rm -rf "$uhome/.cache/pip" "$uhome/.cache/pip3" 2>/dev/null || true | |
rm -rf "$uhome/.npm" "$uhome/.cache/npm" 2>/dev/null || true | |
rm -rf "$uhome/.yarn" "$uhome/.cache/yarn" 2>/dev/null || true | |
rm -rf "$uhome/.composer/cache" 2>/dev/null || true | |
rm -rf "$uhome/.cargo/registry" "$uhome/.cargo/git" 2>/dev/null || true | |
rm -rf "$uhome/.cache/go-build" "$uhome/go/pkg/mod/cache" 2>/dev/null || true | |
rm -rf "$uhome/.gem/specs" 2>/dev/null || true | |
done | |
# System-wide gem cache if present | |
rm -rf /var/lib/gems/*/cache/* 2>/dev/null || true | |
} | |
snap_prune() { | |
if [[ "$PRUNE_SNAPS" -eq 1 ]] && is_cmd snap; then | |
echo "-> Snap: prune disabled old revisions" | |
snap list --all | awk '/disabled/{printf "%s:%s\n",$1,$3}' | \ | |
while IFS=: read -r pkg rev; do | |
snap remove "$pkg" --revision="$rev" || true | |
done | |
[[ "$AGGRESSIVE" -eq 1 ]] && snap set system refresh.retain=2 || true | |
fi | |
} | |
docker_images_prune() { | |
if [[ "$PRUNE_DOCKER_IMAGES" -eq 1 ]] && is_cmd docker; then | |
echo "-> Docker: prune unused images only (keeps images used by containers)" | |
docker image prune -af || true | |
fi | |
} | |
cloud_init_cleanup() { | |
if [[ "$SYSPREP" -eq 1 ]] && is_cmd cloud-init; then | |
echo "-> Cloud-init: scrub logs and instance state for golden image" | |
cloud-init clean --logs || true | |
rm -rf /var/lib/cloud/* || true | |
rm -f /etc/machine-id || true | |
systemd-machine-id-setup || true | |
# Optional: reset SSH host keys so they regenerate on first boot | |
rm -f /etc/ssh/ssh_host_* 2>/dev/null || true | |
fi | |
} | |
apt_trash() { | |
echo "-> APT archives: ensure cleared" | |
rm -rf /var/cache/apt/archives/* /var/cache/apt/archives/partial/* || true | |
} | |
lib_list_cleanup() { | |
if [[ "$AGGRESSIVE" -eq 1 ]]; then | |
echo "-> Aggressive: remove APT lists" | |
rm -rf /var/lib/apt/lists/* /var/lib/apt/lists/partial 2>/dev/null || true | |
fi | |
} | |
# Discard free space to the hypervisor without creating junk files | |
fstrim_fs() { | |
if is_cmd fstrim; then | |
echo "-> fstrim: TRIM all supported filesystems" | |
fstrim -av || true | |
fi | |
} | |
# Clean up any leftover zero-fill files from previous runs | |
remove_leftover_zero_files() { | |
echo "-> Cleanup: remove any stray /EMPTY* files from past zero-fill runs" | |
rm -f /EMPTY /EMPTY.* 2>/dev/null || true | |
} | |
maybe_upgrade() { | |
if [[ "$UPGRADE" -eq 1 ]]; then | |
echo "-> Optional: apt full-upgrade" | |
export DEBIAN_FRONTEND=noninteractive | |
apt-get -y full-upgrade || true | |
fi | |
} | |
# Execute | |
journal_vacuum | |
log_clean | |
tmp_clean | |
user_caches | |
apt_housekeeping | |
orphan_purge | |
kernel_purge_manual | |
snap_prune | |
apt_trash | |
docs_prune | |
dev_tool_caches | |
lib_list_cleanup | |
cloud_init_cleanup | |
docker_images_prune | |
remove_leftover_zero_files | |
fstrim_fs | |
maybe_upgrade | |
echo "== Cleaning completed ==" |
Thanks man! Ubuntu 20.04
Worked very well for the multiple minimal install VMs I have for both 20.04 and 18.04! Thank you! 😄
Do be careful with every script you copy from the internet, and inspect the commands within until you understand what they're doing.
Thank you! I am new and I was lost space and didn't find how. This help me to do all the task I was reading about in one shot (sorry my english). Ubuntu 20.04.
BE CAREFUL DO NOT USE THIS SCRIPT, YOU WILL HARM YOUR SYSTEM
The following packages will be REMOVED:
apturl* gnome-software* gnome-software-plugin-snap* nautilus-share* software-properties-common* software-properties-gtk* ubuntu-desktop* ubuntu-desktop-minimal*
0 upgraded, 0 newly installed, 8 to remove and 0 not upgraded.
After this operation, 8.035 kB disk space will be freed.
(Reading database ... 242150 files and directories currently installed.)
Removing nautilus-share (0.7.3-2ubuntu3) ...
Removing apturl (0.5.2ubuntu19) ...
Removing gnome-software-plugin-snap (3.36.1-0ubuntu0.20.04.0) ...
Removing gnome-software (3.36.1-0ubuntu0.20.04.0) ...
Removing ubuntu-desktop (1.450.1) ...
Removing ubuntu-desktop-minimal (1.450.1) ...
Removing software-properties-gtk (0.98.9.1) ...
Removing software-properties-common (0.98.9.1) ...
Perhaps those packages are outdated and there's no need to maintain them on your machine, hence you've been prompted to allow deletion and make more space.
Excelente! TKS
thanks :)
Nice work
It worked on Ubuntu 18.04 on DigitalOcean, ty.
nice!
Ubuntu 18.04.4 works fine
Brilliant!!
Running with 18.04 LTS. I have apps like KiCAD and PyCharm on a 34GB root file-system. Down to 6GB left clean.sh found another 5GB to clean out to give me 11GB space left.
Thanks Iman!
It worked on Ubuntu 20.04. Nearly 20GB cleaned.
thumbs up
Great work @Iman!
Never thought the cleanup is super easy with this script.
All happened in a jiffy!
Skip the step
apt-get remove --purge -y software-properties-common
Otherwise some handy steps and commands for tidying out excessive logs thanks !
Cool! Thx!
Thanks a lot
Thanks, Great work!
Thanks!
it cleaned 67.5G , don't know why. but Great cleaning
System ubuntu 20.10 development branch