Created
February 12, 2026 00:34
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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