GitHub Actions CI Docker build job fails with "System.IO.IOException: No space left on device"
GitHub Actions CI is stuck red: a Docker build job dies with "System.IO.IOException: No space left on device : '/home/runner/actions-runner/cached/2.337.0/diag/Worker*.log'" and the job log is empty ("log not found" from gh run view --job <id> --log). The failure repeats on every push and never self-heals, even though no Dockerfile or workflow change caused it.
Two compounding causes, one of which makes the red self-sustaining.
Self-sustaining loop. If the workflow caches the built image in a content-addressed registry tag (e.g. ghcr.io/org/repo:deps-<sha256 of Dockerfile+lockfiles>) and pushes it only at the END of a successful build, then a dependency-lock change rotates the key, the cold build dies on disk, nothing is pushed, and the NEXT run misses the same key and dies again. Tell: the failing job runs for ~8-10 min (a real build) instead of ~1-2 min (a registry pull), and the last green run predates the lock commit. Diagnose with
git log -1 -- <lockfiles>versusgh run listtimestamps; recompute the key by hand to confirm it changed.Why the cold build stopped fitting. GitHub-hosted ubuntu-latest has ~72GB of root disk with only ~14GB free; the preinstalled toolchains eat the rest. When the runner root fills, the runner WORKER process is killed writing its own diag log, so the step log is never uploaded — that is why
gh run view --job --logreturns "log not found" and you only see the IOException in run annotations.
Fixes, in order of leverage:
Reclaim the toolchains you never use, before a cache-miss build (~20GB on ubuntu-latest):
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL && df -h /Takes ~60-70s. Do NOT delete all of /opt/hostedtoolcache — setup-python/setup-node live there. Gate it on the cache-miss branch so cache hits stay fast.Check which Dockerfile stage you are actually building.
docker/build-push-actionwith notarget:builds the LAST stage in the file. Appending a heavyweight stage (in our case an "onboard" testbed stage installing bun + claude-code + codex + omp, ~1.5GB) silently changes the default target for every consumer that omitstarget:, includingdocker compose buildservices without atarget:key. Pinningtarget: runtimecut our cold build from 10m28s to 4m58s. If you add a target input, fold it (and the build-args) into the registry cache key, or a DEVEL=no / target=onboard build will collide with the same deps-<hash> tag and serve the wrong image.Drop
cache-to: type=gha,mode=maxfor large images. The Actions cache is capped at 10GB per repo, so a multi-GB image export self-evicts while still spending runner disk on the exported blobs. A content-addressed registry tag is the better cache for this size.
Verification gotcha: if your workflow gates the build job behind a paths filter (dorny/paths-filter), a commit that only touches .github/** will report CI green WITHOUT running the build at all. To actually prove the fix, land a change that touches a filtered path and rotates the cache key (e.g. adding the target to the key hash), so the run is a guaranteed cache miss and exercises the cold-build path.