--- name: docker-wsl2-build-safety description: >- Stop bulk builds from killing Docker Desktop (and don't blame the disk). Docker Desktop on Windows runs its engine inside a WSL2 VM with a fixed memory ceiling. Build a lot of images while a lot of containers are resident and the VM's storage layer can collapse — taking every container down with it. The failure does not look like 'you ran out of memory'. It looks like your disk is dying. --- # docker-wsl2-build-safety — stop bulk builds from killing Docker Desktop (and don't blame the disk) Docker Desktop on Windows runs its engine inside a WSL2 VM with a **fixed memory ceiling**. Build a lot of images while a lot of containers are resident and the VM's storage layer can collapse — taking every container down with it. The failure does not look like "you ran out of memory". It looks like **your disk is dying**. This skill is the guard, and the diagnostic that tells the two apart. > Live-proven on a 125 GB host running ~165 containers (2026-07-25): a 72-image bulk build > followed by a container recreate wave crashed the WSL2 backend. Windows logged **44,051 > `disk` Event-51 errors and 2,266 NTFS delayed-write failures in a single hour**. The drive > was diagnosed as failing. It was not. The correct read was in the *distribution*, not the > count. ## The tell: is it the disk, or the VM? Both produce I/O errors. They differ in **shape over time**. | | failing drive | WSL2 storage collapse | |---|---|---| | distribution | spread over days/weeks, **growing** | one spike, then **decays to zero** | | preceding days | errors present and rising | **zero** | | SMART | reallocated/pending sectors climb | clean / `Healthy` | | correlation | none in particular | starts exactly at a heavy build or recreate wave | | after reboot | errors return under normal use | fine until the next bulk build | Run this **before** condemning hardware: ```powershell Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='disk';Id=51;StartTime=(Get-Date).AddDays(-10)} | Group-Object { $_.TimeCreated.ToString('yyyy-MM-dd') } | Select-Object Count,Name ``` All the errors on one day with nothing before it ⇒ **it is the VM, not the disk.** In the proven case the hourly counts decayed 2,075 → 126 → 65 while SMART stayed `Healthy`, and the drive worked normally afterwards. Inside the VM the signature is a virtual SCSI device being taken offline: ``` sd 0:0:0:3: Device offlined - not ready after error recovery I/O error, dev sdd, sector 1382182208 op 0x0:(READ) containerd: garbage collection failed: input/output error overlayfs: failed to get metacopy (-5) ``` `docker` then reports **"Docker Desktop is unable to start"** and no restart fixes it until the VM is fully shut down (`wsl --shutdown`) and brought back. ## The guard Three rules, all mechanical: **1. Never `docker compose build` a large set with the fleet resident.** Build on a memory-capped `docker-container` builder so a runaway build is OOM-killed *inside* the builder instead of taking the VM down: ```bash docker buildx create --name capped --driver docker-container \ --driver-opt memory=16g --bootstrap docker buildx build --builder capped -t myimage:latest --load . ``` **2. Gate every bulk operation on real VM headroom.** Drop this in and call it first: ```bash headroom_gb() { wsl.exe -d docker-desktop -e sh -c \ "awk '/MemAvailable/{print int(\$2/1048576)}' /proc/meminfo" 2>/dev/null | tr -d '\r' } swap_used_pct() { wsl.exe -d docker-desktop -e sh -c \ "awk '/SwapTotal/{t=\$2} /SwapFree/{f=\$2} END{if(t>0) print int((t-f)*100/t); else print 0}' /proc/meminfo" 2>/dev/null | tr -d '\r' } require_headroom() { # require_headroom