# Docker Disk Reclaim

> Use when Docker disk space does not shrink after docker rm/rmi, Docker Desktop C-drive usage stays high, docker_data.vhdx / ext4.vhdx grows, WSL2 virtual disk bloats, or you need to compact a Docker Desktop vhdx on Windows.

- Skill: `koalalive/docker-disk-reclaim` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add koalalive/docker-disk-reclaim`
- Raw SKILL.md: https://api.skillmd.com/api/skills/koalalive/docker-disk-reclaim/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Koalalive (https://skillmd.com/u/koalalive)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/koalalive/docker-disk-reclaim

---


# Docker Disk Space Reclaim

## Overview

Docker Desktop on Windows runs inside a WSL2 VM whose data is a virtual disk (`docker_data.vhdx`). Two facts explain almost every "deleted but space didn't change" report:

1. **Docker refuses to delete `<none>` images that are parent layers of tagged images.** `docker image prune` returning `0B` is often CORRECT — the "dangling" images are base layers still referenced by child images. They are not garbage.
2. **A WSL2 virtual disk never shrinks on its own.** Deleting files inside only frees blocks; the `.vhdx` file keeps its size until explicitly compacted. And `wsl --manage <distro> --set-sparse true` only touches the distro's *main* vhdx (`...\wsl\main\ext4.vhdx`), NOT Docker Desktop's separate data disk (`...\wsl\disk\docker_data.vhdx`).

**Verified against:** Windows 10.0.26200, Docker Desktop 28.3.2, WSL 2.5.10.0. Real case: 92.36GB of "reclaimable" images could NOT be deleted (all parent layers); the actionable space was 55GB reclaimed purely from compacting `docker_data.vhdx` (207.93GB → 152.92GB).

## When to Use

- C: drive usage stays high after `docker rm` / `docker rmi` / `docker system prune -a`
- `docker system df` shows big `RECLAIMABLE` but nothing frees
- `docker data.vhdx` or `ext4.vhdx` files are huge (check `C:\Users\<user>\AppData\Local\Docker\wsl\`)
- Docker Desktop, WSL2, Windows host

**Not for:** Linux-host Docker (just `docker system prune` + `docker volume prune`), macOS (uses a .raw image, different compaction).

## Diagnosis (always do this first)

```powershell
docker system df          # totals: images/containers/cache reclaimable
docker system df -v       # per-image SHARED vs UNIQUE size (key: UNIQUE SIZE)
docker image ls -a --format "{{.Repository}}:{{.Tag}} | {{.ID}} | {{.Size}}"
docker image rm <none-id> # if it says "dependent child images" -> parent layer, NOT garbage
```

Check the actual culprit file:

```powershell
Get-ChildItem "$env:LOCALAPPDATA\Docker\wsl" -Recurse -Include *.vhdx | Select FullName, @{N='SizeGB';E={[math]::Round($_.Length/1GB,2)}}
```

`docker_data.vhdx` = Docker data disk (the big one). `main\ext4.vhdx` = distro OS disk (small).

## Key Traps

| Symptom | Truth |
|---|---|
| `<none>` images (16-25GB each) shown by `docker images -a` | Likely parent/base layers of tagged images. `docker image prune` reclaims 0B. Do NOT force-delete — breaks child images. |
| `docker system df` says 92GB reclaimable | Misleading when images are unused-but-not-dangling; the real reclaim is UNIQUE size of unused images + container writable layers. |
| `docker rm` freed space but C: unchanged | VHDX doesn't auto-shrink. Must compact. |
| `wsl --manage docker-desktop --set-sparse true` "succeeded" but nothing freed | It only converted the small `ext4.vhdx`, not `docker_data.vhdx`. Use diskpart instead. |
| Stopped containers still eating space | `docker rm <id>` needed, not just stop. A single container writable layer can be ~20GB. |

## Reclaim Procedure

### Step 1: Reclaim what Docker can reclaim (optional, ~20-90GB)

```powershell
docker image prune -f          # untagged only (safe)
docker container prune -f      # stopped-only (drops stopped containers)
docker builder prune -af       # build cache
docker volume prune -f         # unused volumes
```

Keep what you need: `docker image prune -a` removes ALL images unused by any container — do not run it if you want images around.

### Step 2: Compact the VHDX (Windows / Docker Desktop)

Prerequisite for diskpart: WSL distro must be **stopped**, Docker Desktop closed.

Either run the bundled `scripts/compact-docker-vhdx.ps1` (auto: kill Docker Desktop → `wsl --shutdown` → elevated diskpart compact → relaunch), or manually:

```powershell
# 1. stop everything
taskkill /F /IM "Docker Desktop.exe" /T
taskkill /F /IM "com.docker.backend.exe" /T
wsl --shutdown
wsl -l -v                        # confirm docker-desktop = Stopped

# 2. compact (needs a UAC elevation prompt)
$script = @"
select vdisk file="C:\Users\29698\AppData\Local\Docker\wsl\disk\docker_data.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
"@
Set-Content -Path "$env:TEMP\compact_disk.txt" -Value $script
Start-Process diskpart -ArgumentList "/s `"$env:TEMP\compact_disk.txt`"" -Verb RunAs -Wait

# 3. verify + relaunch
Get-PSDrive C | Format-List Used,Free
Start-Process "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe"
```

`--set-sparse` alternative (only for distros you created yourself, not Docker Desktop's data disk):
`wsl --manage <distro> --set-sparse true --allow-unsafe` (distro must be stopped).

## Common Mistakes

- Deleting the 6 `<none>` images → `image has dependent child images`; they're shared parent layers, `docker image prune -f` handles garbage correctly (0B is valid output).
- Compacting while distro is running → `WSL_E_DISTRO_NOT_STOPPED`; Docker Desktop auto-restarts the VM, so kill its processes first.
- Checking `Get-ChildItem` `Length` after compact → VHDX reports logical size; use `Get-PSDrive C` (free space) as the truth.
- Killing Docker Desktop via taskkill while it's writing → data risk; prefer graceful `docker desktop stop` first when available.

## Scripts

- `scripts/compact-docker-vhdx.ps1` — one-shot: stop Docker Desktop → `wsl --shutdown` → diskpart compact (elevated) → relaunch → print C: free space.

