Kamal's 93 GB Secret: Why Your Docker Cleanup Script Isn't Working
I opened Space Lens this morning and saw 141 GB sitting in ~/Library/Containers/com.docker.docker. My laptop is 1 TB and I was running out of room. I have a cleanup.sh script I wrote two years ago that prunes Docker every week. It clearly wasn't doing its job.
Here's what was actually going on — and the one-line fix that recovered 116 GB.
The old script (what most blog posts tell you to do)
#!/bin/bash
docker container prune -f
docker image prune -a -f
docker volume prune -f
docker network prune -f
This is the canonical Docker cleanup snippet. It's in dozens of Stack Overflow answers. It's also quietly wrong in two places, and if you use Kamal it's wrong in a third.
Step 1: Find what's actually big
Always start here. Don't trust your assumptions:
du -sh ~/Library/Containers/com.docker.docker/Data/* | sort -hr | head
For me, 132 GB out of 141 GB was in a single file: Data/vms/0/data/Docker.raw. That's the disk image for Docker Desktop's Linux VM. So the bloat is inside the VM, not on the macOS side.
Next question: what's inside the VM?
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 3 3 899.4MB 0B (0%)
Containers 3 3 65B 0B (0%)
Local Volumes 28 3 110.3GB 108.8GB (98%)
Build Cache 193 0 8.308GB 8.308GB
110 GB in volumes. 25 of 28 are unused. The cleanup script should be wiping those.
Gotcha #1: docker volume prune skips named volumes
By default, docker volume prune only removes anonymous volumes. Every named volume created by docker-compose (myapp_postgres-data, myapp_redis, etc.) is left alone.
The flag you need is -a:
docker volume prune -a -f
This still preserves any volume attached to a running or stopped container. You won't lose your active databases. It just removes the orphans from projects you've moved on from.
Gotcha #2: Build cache is its own thing
The docker image prune command doesn't touch the build cache. That 8 GB in the table above? Untouched by the canonical snippet.
docker builder prune -a -f
docker buildx prune -a -f
Both, because the default builder and buildx builders maintain separate caches.
Gotcha #3: Kamal's buildx builders are invisible to volume prune
This is the one that cost me 93 GB.
When you run kamal deploy with multi-architecture builds, Kamal creates a buildx builder backed by a Docker container — something like kamal-local-docker-container. That builder stores its cache in a Docker volume named buildx_buildkit_kamal-local-docker-container0_state.
I scanned my volumes by size and found this:
93.4G buildx_buildkit_kamal-local-docker-container0_state/
5.8G advantage-web_ollama_data/
4.0G newsfusion_ollama_data/
3.4G buildx_buildkit_kamal-local-registry-docker-container0_state/
2.3G fitivity-web_ollama_data/
1.8G buildx_buildkit_kamal-engage-multiarch0_state/
93 gigabytes. From a single buildx builder Kamal created the first time I did a multi-arch deploy and never cleaned up. docker volume prune -a won't touch it because the buildx builder container technically still "exists" (just inactive), so the volume is considered in-use.
To get rid of it, you have to remove the builder itself:
docker buildx rm kamal-local-docker-container
Kamal will recreate it on your next deploy. You lose the build cache (one slower deploy), you gain back the disk. Worth it.
The full cleanup script
This is what cleanup.sh looks like now:
#!/bin/bash
# Containers, images, networks
docker container prune -f
docker image prune -a -f
docker network prune -f
# Volumes — note the -a, which is the whole point
docker volume prune -a -f
# Build cache, both flavors
docker builder prune -a -f
docker buildx prune -a -f 2>/dev/null || true
# Optional: nuke inactive *local* buildx builders (Kamal multiarch).
# Recreated automatically on next `kamal deploy`. Remote SSH builders
# are skipped — those don't store data on your machine anyway.
docker buildx ls --format '{{.Name}} {{.DriverEndpoint}} {{.Status}}' 2>/dev/null \
| awk '$2 == "desktop-linux" && $3 == "inactive" {print $1}' \
| xargs -r -n1 docker buildx rm 2>/dev/null
du -sh ~/Library/Containers/com.docker.docker/
The awk filter is the key piece — it only removes inactive builders whose endpoint is desktop-linux (i.e. they live on this machine). Remote SSH builders that point at production servers are left alone.
Does Docker.raw shrink on its own?
Yes — finally, as of recent Docker Desktop versions on Apple Silicon. After I pruned everything inside the VM, the Docker.raw file went from 132 GB to 16 GB on disk within seconds. The logical size still showed 462 GB (it's a sparse file), but actual disk usage dropped immediately because Docker Desktop now uses VirtioFS with TRIM/discard.
If you're on an older version and the file doesn't shrink, the manual reclaim path is:
- Docker Desktop → Settings → Resources → Advanced — there's a disk image size slider. Lower it and apply.
- Or Troubleshoot (bug icon) → "Clean / Purge data" — choose what to wipe and compact.
- Nuclear: Reset to factory defaults. Wipes everything including running containers' data. Only use if you've backed up anything important.
The takeaway
If you use Kamal and Docker Desktop on macOS, your cleanup.sh is almost certainly missing three lines:
-aondocker volume prunedocker builder prune -a -fanddocker buildx prune -a -f- A way to remove inactive local buildx builders
Total time to investigate and fix: about 20 minutes. Space recovered: 116 GB. That's a pretty good return on a Monday morning.
If you want the assist, I did the whole thing through a Claude Code session — pointed it at ~/cleanup.sh, asked it why Docker was eating 141 GB, and it walked through the diagnosis and rewrote the script. The full transcript is what this post is based on.