Skip to content

Instantly share code, notes, and snippets.

@lightstrike
Created February 12, 2026 00:34
Show Gist options
  • Select an option

  • Save lightstrike/b8cce9495d43c84fb999e95a8198df7a to your computer and use it in GitHub Desktop.

Select an option

Save lightstrike/b8cce9495d43c84fb999e95a8198df7a to your computer and use it in GitHub Desktop.
Reusable dev machine cleanup script - Docker/Supabase projects, package manager caches, runtime caches
#!/usr/bin/env bash
#
# cleanup.sh - Reclaim disk space from Docker, package managers, and caches
#
# DESCRIPTION
# A reusable script for reclaiming disk space across a multi-project dev
# environment. It handles:
#
# 1. Supabase project management - stop Supabase projects you're not actively
# working on to free their containers, images, and volumes. Only containers
# matching the supabase_* naming convention (created by the Supabase CLI)
# are detected and managed. Non-Supabase Docker containers are never
# stopped by this script.
# 2. Docker cleanup - prune stopped containers, unused images, and dangling
# volumes. These steps apply to ALL Docker resources (not just Supabase),
# but only target resources that are already stopped/unused.
# 3. Package manager caches - npm, Yarn, pnpm store + cache
# 4. Runtime caches - Deno, CocoaPods, TypeScript, Poetry
#
# By default, each destructive step requires confirmation. No Supabase projects
# are auto-stopped unless you explicitly specify which to keep or use
# interactive mode.
#
# USAGE
# ./cleanup.sh [OPTIONS]
#
# OPTIONS
# -i Fully interactive mode. Presents a numbered
# list of all running Supabase projects and
# prompts you to choose which to keep. All
# other cleanup steps also prompt for
# confirmation.
#
# --supabase-keep proj1,proj2 Keep only the named Supabase projects
# running. All other Supabase projects will be
# stopped. Project names are comma-separated,
# matching the suffix in container names (e.g.
# "supabase" for supabase_db_supabase).
#
# --supabase-keep-all Skip the Supabase project stop step
# entirely. No Supabase projects will be
# stopped; only already-stopped containers,
# unused images, and dangling volumes are
# cleaned.
#
# --dry-run Show what would be cleaned without actually
# doing anything. Safe to run anytime.
#
# --force Skip all confirmation prompts. Useful for
# automation or cron jobs.
#
# (no flags) Default mode. Keeps all running Supabase
# projects (does not auto-stop anything).
# Confirms each cleanup step interactively.
#
# EXAMPLES
# # Preview what would be cleaned
# ./cleanup.sh --dry-run
#
# # Interactive: pick which Supabase projects to keep, confirm each step
# ./cleanup.sh -i
#
# # Keep only the "supabase" project, stop everything else, confirm each step
# ./cleanup.sh --supabase-keep supabase
#
# # Keep two projects, skip all confirmations
# ./cleanup.sh --supabase-keep supabase,almero-supabase --force
#
# # Clean caches only, don't touch running Supabase projects
# ./cleanup.sh --supabase-keep-all
#
# WHAT GETS CLEANED
# Supabase projects (opt-in via -i or --supabase-keep):
# - Stops containers for unselected Supabase projects
# - Only affects containers named supabase_*_<project>
# - Non-Supabase Docker containers are never stopped
#
# Docker (applies to ALL Docker resources, with confirmation):
# - Stopped containers (docker container prune)
# - Images not used by running containers (docker image prune -a)
# - Dangling volumes (docker volume prune)
#
# Package managers:
# - ~/.npm (npm cache)
# - ~/Library/Caches/Yarn (Yarn cache)
# - ~/Library/pnpm (pnpm content-addressable store, pruned)
# - ~/Library/Caches/pnpm (pnpm cache)
#
# Runtime/build caches:
# - ~/Library/Caches/deno (re-downloads on next supabase functions serve)
# - ~/Library/Caches/CocoaPods (re-downloads on next pod install)
# - ~/Library/Caches/typescript (tsc cache)
# - ~/Library/Caches/pypoetry (Poetry package cache)
#
# All caches are safe to remove - packages and dependencies re-download
# automatically when needed.
#
# NOTES
# - Stopping a Supabase project does NOT delete its volumes/data. Volumes are
# only removed by the "dangling volumes" prune step, which only targets
# volumes not attached to any container (and still requires confirmation).
# - If you need to preserve local DB data before removing volumes, use pg_dump
# on the project's DB container before running this script.
# - The script reports disk usage before and after so you can see the impact.
#
set -euo pipefail
DRY_RUN=false
FORCE=false
INTERACTIVE=false
SUPABASE_KEEP_ALL=false
SUPABASE_KEEP=()
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run) DRY_RUN=true; shift ;;
--force) FORCE=true; shift ;;
-i) INTERACTIVE=true; shift ;;
--supabase-keep-all) SUPABASE_KEEP_ALL=true; shift ;;
--supabase-keep)
IFS=',' read -ra SUPABASE_KEEP <<< "$2"
shift 2
;;
*) shift ;;
esac
done
header() { echo -e "\n\033[1;34m=== $1 ===\033[0m"; }
info() { echo -e " \033[0;36m$1\033[0m"; }
warn() { echo -e " \033[0;33m$1\033[0m"; }
confirm() {
if $FORCE || $DRY_RUN; then return 0; fi
read -rp " Proceed? [y/N] " ans
[[ "$ans" =~ ^[Yy] ]]
}
# --- Discover running Supabase projects ---
# Extracts unique project names from container names like supabase_db_<project>
discover_supabase_projects() {
docker ps --format '{{.Names}}' 2>/dev/null \
| grep '^supabase_' \
| sed 's/^supabase_[^_]*_//' \
| sort -u
}
# --- Status ---
header "Current Disk Usage"
df -h / | tail -1
echo ""
docker system df 2>/dev/null || true
# --- Docker: Stop unwanted Supabase projects ---
if ! $SUPABASE_KEEP_ALL; then
header "Docker: Supabase Projects"
mapfile -t ALL_PROJECTS < <(discover_supabase_projects)
if [[ ${#ALL_PROJECTS[@]} -eq 0 ]]; then
info "No running Supabase projects found"
else
info "Running projects: ${ALL_PROJECTS[*]}"
# Determine which to keep
KEEP=()
if $INTERACTIVE; then
echo ""
echo " Select projects to KEEP (space-separated numbers, or 'all'):"
for i in "${!ALL_PROJECTS[@]}"; do
echo " $((i+1))) ${ALL_PROJECTS[$i]}"
done
read -rp " Keep: " selection
if [[ "$selection" == "all" ]]; then
KEEP=("${ALL_PROJECTS[@]}")
else
for num in $selection; do
idx=$((num-1))
if [[ $idx -ge 0 && $idx -lt ${#ALL_PROJECTS[@]} ]]; then
KEEP+=("${ALL_PROJECTS[$idx]}")
fi
done
fi
elif [[ ${#SUPABASE_KEEP[@]} -gt 0 ]]; then
KEEP=("${SUPABASE_KEEP[@]}")
else
# Default: keep all (no auto-stopping without explicit instruction)
KEEP=("${ALL_PROJECTS[@]}")
fi
# Build stop list
STOP=()
for proj in "${ALL_PROJECTS[@]}"; do
is_kept=false
for keep in "${KEEP[@]}"; do
if [[ "$proj" == "$keep" ]]; then
is_kept=true
break
fi
done
if ! $is_kept; then
STOP+=("$proj")
fi
done
if [[ ${#STOP[@]} -gt 0 ]]; then
warn "Will stop: ${STOP[*]}"
info "Will keep: ${KEEP[*]}"
if $DRY_RUN; then
info "[dry-run] would stop ${#STOP[@]} projects"
else
if confirm; then
for proj in "${STOP[@]}"; do
info "Stopping $proj..."
docker stop $(docker ps -q --filter "name=_${proj}") 2>/dev/null || true
done
fi
fi
else
info "Keeping all projects"
fi
fi
fi
# --- Docker: stopped containers ---
header "Docker: Prune stopped containers"
STOPPED=$(docker container ls -f status=exited -q | wc -l | tr -d ' ')
info "$STOPPED stopped containers"
if [[ "$STOPPED" -gt 0 ]]; then
if $DRY_RUN; then info "[dry-run] would prune"; else
confirm && docker container prune -f
fi
fi
# --- Docker: dangling/unused images ---
header "Docker: Prune unused images"
docker system df --format '{{.Type}}\t{{.Reclaimable}}' 2>/dev/null | grep Images || true
if $DRY_RUN; then info "[dry-run] would prune"; else
confirm && docker image prune -a -f
fi
# --- Docker: unused volumes ---
header "Docker: Unused volumes"
DANGLING=$(docker volume ls -f dangling=true -q | wc -l | tr -d ' ')
info "$DANGLING dangling volumes"
if [[ "$DANGLING" -gt 0 ]]; then
if $DRY_RUN; then info "[dry-run] would prune"; else
confirm && docker volume prune -f
fi
fi
# --- npm cache ---
header "npm cache"
NPM_SIZE=$(du -sh ~/.npm 2>/dev/null | cut -f1 || echo "0")
info "Size: $NPM_SIZE"
if $DRY_RUN; then info "[dry-run] would clean"; else
confirm && npm cache clean --force
fi
# --- Yarn cache ---
header "Yarn cache"
YARN_SIZE=$(du -sh ~/Library/Caches/Yarn 2>/dev/null | cut -f1 || echo "0")
info "Size: $YARN_SIZE"
if [[ "$YARN_SIZE" != "0" ]]; then
if $DRY_RUN; then info "[dry-run] would remove"; else
confirm && rm -rf ~/Library/Caches/Yarn
fi
fi
# --- pnpm ---
header "pnpm store + cache"
PNPM_STORE=$(du -sh ~/Library/pnpm 2>/dev/null | cut -f1 || echo "0")
PNPM_CACHE=$(du -sh ~/Library/Caches/pnpm 2>/dev/null | cut -f1 || echo "0")
info "Store: $PNPM_STORE Cache: $PNPM_CACHE"
if $DRY_RUN; then info "[dry-run] would prune"; else
confirm && { cd /tmp && pnpm store prune 2>/dev/null || true; rm -rf ~/Library/Caches/pnpm; }
fi
# --- Deno cache ---
header "Deno cache"
DENO_SIZE=$(du -sh ~/Library/Caches/deno 2>/dev/null | cut -f1 || echo "0")
info "Size: $DENO_SIZE"
if [[ "$DENO_SIZE" != "0" ]]; then
if $DRY_RUN; then info "[dry-run] would remove"; else
confirm && rm -rf ~/Library/Caches/deno
fi
fi
# --- Other caches ---
header "Other caches (CocoaPods, TypeScript, Poetry)"
for cache_dir in CocoaPods typescript pypoetry; do
SIZE=$(du -sh ~/Library/Caches/$cache_dir 2>/dev/null | cut -f1 || echo "0")
if [[ "$SIZE" != "0" ]]; then
info "$cache_dir: $SIZE"
fi
done
if $DRY_RUN; then info "[dry-run] would remove"; else
confirm && {
rm -rf ~/Library/Caches/CocoaPods
rm -rf ~/Library/Caches/typescript
rm -rf ~/Library/Caches/pypoetry
}
fi
# --- Final status ---
header "Final Disk Usage"
df -h / | tail -1
echo ""
docker system df 2>/dev/null || true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment