惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
L
LangChain Blog
云风的 BLOG
云风的 BLOG
M
MIT News - Artificial intelligence
A
About on SuperTechFans
J
Java Code Geeks
量子位
博客园 - 三生石上(FineUI控件)
博客园 - Franky
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
IT之家
IT之家
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
Engineering at Meta
Engineering at Meta
雷峰网
雷峰网
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
Martin Fowler
Martin Fowler
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Docker Ate 56 GB of Disk in a Day: Building a Cleanup Aut...
Mustafa ERBA · 2026-05-17 · via DEV Community

Mustafa ERBAY

"no posts for hours" — the message I got

I noticed it in the evening — my hourly content-generate cron hadn't completed a single successful run since morning. The pipeline-health monitor hadn't fired its state-change email yet (the 4-hour threshold hadn't been hit), but the GitHub Actions panel was bright red.

The last successful run finished at 2026-05-04 12:11 UTC. More than 5 hours had passed. Zero new content. The single most common reason this blog goes down is resource starvation — disk or RAM. I quickly figured out which one.

A line that jumped out at me from the run log:

##[error] System.IO.IOException: No space left on device
  : '/home/github-runner/runner-mustafaerbay/_diag/pages/...log'

Enter fullscreen mode Exit fullscreen mode

The runner couldn't write its own log file — no space on disk. At that point it hadn't even reached the validate step; the runner's own _diag layer was dead. Each cron tick retried and blew up at the exact same place.

SSH into the VPS and see

$ ssh vps 'df -h /'
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        72G   72G   11M 100% /

Enter fullscreen mode Exit fullscreen mode

72 GB disk with 11 MB free. You can't even write a single log line.

The second query was more interesting:

$ ssh vps 'sudo du -hx --max-depth=2 / 2>/dev/null | sort -hr | head -10'
72G  /
54G  /var
39G  /var/lib
15G  /var/www
7.2G /home
5.3G /home/github-runner
4.0G /usr
2.9G /opt
2.6G /usr/lib
2.3G /opt/mustafaerbay

Enter fullscreen mode Exit fullscreen mode

/var/lib was 39 GB. /var/www was 15 GB. This isn't a personal blog VPS — it has 6 different projects on it. My eye went straight to /var/lib because that's where Docker lives.

$ ssh vps 'sudo du -hx --max-depth=1 /var/lib | sort -hr | head'
39G  /var/lib
38G  /var/lib/docker  <- HERE
169M /var/lib/dkms
164M /var/lib/Acronis
140M /var/lib/apt
6.3M /var/lib/mustafaerbay

Enter fullscreen mode Exit fullscreen mode

Docker on its own: 38 GB.

Crack open Docker's internals

$ ssh vps 'sudo docker system df'
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          33        9         27.5GB    23.27GB (84%)
Containers      13        13        1.192MB   0B (0%)
Local Volumes   8         8         387MB     0B (0%)
Build Cache     388       0         7.695GB   7.695GB

Enter fullscreen mode Exit fullscreen mode

This table answers the question directly:

  • 33 images exist, only 9 are active. 24 are "not in use but not deleted." 23.27 GB reclaimable.
  • 388 build cache layers, 0 active. The whole 7.7 GB is up for deletion.
  • Containers and volumes are normal — I don't want to wipe those (postgres data, etc., lives there).

Total reclaimable: ~31 GB. Just freeing that would open up enough space.

See the running containers first, then cut

Don't rush. docker system prune -a deletes everything — you have to know the line between running and not running images. I checked the docker ps output: half a dozen different projects had containers on this VPS — a few of my own side products and some client work. 13 healthy containers in total: postgres, redis, Next.js apps, an Astro SSR service, and a few workers. The only things that can be reclaimed are unanchored images — older image versions not referenced by any running container.

I lined up two safe commands:

# 1. Build cache (old layers, nothing uses them)
sudo docker builder prune -af

# 2. Unused images (the ones not anchored to a running container)
sudo docker image prune -af

Enter fullscreen mode Exit fullscreen mode

The -a flag also removes tagged-but-not-dangling images. Risky? I don't think so — anything anchored to an active container won't be removed anyway (Docker keeps a reference count). Only the "once built, used, then a newer version came along" old images go.

The result:

=== Docker build cache ===
Total reclaimed space: 33.48GB

=== Docker unused images ===
Total reclaimed space: 22.62GB

=== After ===
/dev/sda1   72G   40G   33G  56% /

Enter fullscreen mode Exit fullscreen mode

I reclaimed 56 GB. Disk went from 100% to 56%. 33 GB free. All 13 containers kept running.

Now: let's automate this

This wasn't even the third time it happened — let it be a lesson:

"Once it happens twice, do a manual fix. The third time, automate it."

The disk-cleanup.sh script I wrote is simple but careful. A few principles:

#!/usr/bin/env bash
set -euo pipefail

echo "=== disk-cleanup starting ==="
echo "before: $(df -h / | tail -1)"

# 1) Docker build cache > 72h (newer cache survives)
echo "-- docker builder prune (>72h)"
docker builder prune -af --filter "until=72h"

# 2) Dangling docker images (no -a — tagged-but-unused IS PRESERVED)
echo "-- docker image prune (dangling only)"
docker image prune -f

# 3) journal > 7d
journalctl --vacuum-time=7d

# 4) APT cache (regenerable)
apt-get clean

# 5) mustafaerbay dist-old (deploy backup, regenerated each deploy)
[ -d /opt/mustafaerbay/dist-old ] && rm -rf /opt/mustafaerbay/dist-old

# 6) GitHub runner _diag log files > 14d (files only, LEAVE the directories alone)
find /home/github-runner -path '*/_diag/*' -type f -name '*.log' -mtime +14 -delete

echo "after:  $(df -h / | tail -1)"

Enter fullscreen mode Exit fullscreen mode

Hooked it up to a daily timer running at 03:30 UTC:

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=10m
Persistent=true

Enter fullscreen mode Exit fullscreen mode

RandomizedDelaySec=10m — so it doesn't collide with any other 03:30 cron jobs that might be on the system. Persistent=true — if the VPS rebooted, the run that got skipped will still happen.

⚠️ Be careful with the -a flag

During the manual recovery I used docker image prune -af (with the a flag)
because it was an emergency and I needed space immediately. But for the daily
cron I use prune -f (no a). Don't be too aggressive in an automated runner —
you can get bitten one day. Use -a for manual cleanups, dangling-only for
automation.

Conclusion: the one-line why

My disk filled up because Docker doesn't clean up at all. Every docker-compose build creates a new image; it doesn't keep the old one referenced, but it doesn't delete it either. A few months in, a year in, your disk explodes and you go "wow, did the AI grow this much?"

Nobody's growing it, actually. Docker is a hoarder. If you're not active about it, the disk fire teaches you that.

A two-hour manual recovery + a one-hour systemd timer setup = the guarantee I won't go through this again. That's the real lesson: turn an incident into the foundation for the next one.

Tomorrow disk-cleanup.timer will run for the first time. I'm watching.