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:
- Docker refuses to delete
<none>images that are parent layers of tagged images.docker image prunereturning0Bis often CORRECT — the "dangling" images are base layers still referenced by child images. They are not garbage. - A WSL2 virtual disk never shrinks on its own. Deleting files inside only frees blocks; the
.vhdxfile keeps its size until explicitly compacted. Andwsl --manage <distro> --set-sparse trueonly 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 dfshows bigRECLAIMABLEbut nothing freesdocker data.vhdxorext4.vhdxfiles are huge (checkC:\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)
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:
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)
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:
# 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 -fhandles 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-ChildItemLengthafter compact → VHDX reports logical size; useGet-PSDrive C(free space) as the truth. - Killing Docker Desktop via taskkill while it's writing → data risk; prefer graceful
docker desktop stopfirst when available.
Scripts
scripts/compact-docker-vhdx.ps1— one-shot: stop Docker Desktop →wsl --shutdown→ diskpart compact (elevated) → relaunch → print C: free space.