Home DevOps Docker No Space Left on Device: Reclaim Disk
Intermediate 6 min · September 23, 2026

Docker No Space Left on Device: Reclaim Disk

Run df -h and docker system df, prune unused images and build cache, then cap json-file logs.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 11 min
  • A Linux host with Docker installed and sudo access
  • A terminal where you can run df, du, and docker commands
  • Basic familiarity with images, containers, and volumes
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Confirm it's Docker's disk: df -h /var/lib/docker shows the filesystem full, and docker system df shows what Docker itself is holding
  • Reclaim fast and safe: docker container prune, docker image prune -a for dangling and unused images, and docker builder prune for build cache
  • Cap the silent killer: json-file container logs grow forever — set max-size and max-file in daemon.json and rotate them
  • If the disk is just too small, move Docker's data-root to a bigger volume instead of pruning every week
✦ Definition~90s read
What is Docker No Space Left on Device Fix?

Every byte Docker manages lives under its data-root, /var/lib/docker by default: overlay2 holds image and container filesystem layers, volumes holds named volume data, containers holds per-container config plus json-file logs, and buildkit holds the build cache (often the largest single consumer on CI machines). When the filesystem backing that directory hits 100%, the kernel returns ENOSPC to whatever asked for a write — a layer extract, a log append, a container filesystem write — and Docker surfaces it as "no space left on device." The daemon doesn't reserve headroom and won't auto-clean; it fails writes until a human intervenes.

Imagine a workshop where every project leaves offcuts on the floor — lumber, nails, sawdust.

Two readings matter and they answer different questions. df -h tells you the filesystem is full and how full; docker system df tells you how much of it Docker accounts for (images, containers, volumes, build cache) and how much is reclaimable. When those disagree — filesystem full but Docker claims little — the bytes belong to something else on the same filesystem: runaway json-file logs (counted opaquely), another app's data, or deleted-but-held-open files. du -sh /var/lib/docker/* settles it by weighing each subdirectory directly.

What this is NOT: it isn't a registry problem, a permissions problem, or an inode-exhaustion-by-millions-of-tiny-files problem in the usual case (though df -i is worth one glance). It also isn't fixed permanently by one heroic prune — without log caps and scheduled cleanup, the same volume fills again on a schedule set by your build and log volume.

Treat the prune as triage and the caps plus data-root sizing as the cure.

Plain-English First

Imagine a workshop where every project leaves offcuts on the floor — lumber, nails, sawdust. Nobody cleans because every scrap might be useful. One morning you can't open the door. That's /var/lib/docker: old images, stopped containers, orphaned volumes, build cache, and logs piling up for months. The fix isn't a bigger workshop first — it's sweeping: toss what's provably trash, schedule the sawdust (logs), and only then buy more floor space.

It starts as a pull that dies halfway: "failed to register layer: no space left on device." Then builds fail, then running containers can't write, then the daemon itself starts acting strange. Your monitoring said disk was at 62% last week — but that was the root filesystem, and Docker lives on its own volume (or worse, shares /var with logs that exploded overnight). Docker disk exhaustion is a slow leak that becomes a sudden outage, and it always lands during a deploy.

The frustration is that docker system df and df -h disagree, prune commands free less than expected, and the same failure returns a month later. That's because Docker stores five different kinds of bytes — images, containers, volumes, build cache, and logs — and each needs its own cleanup. Pruning images when the problem is 40GB of json-file logs fixes nothing.

This guide maps every byte to its owner: diagnose with df plus docker system df, reclaim with targeted prunes, cap logs with max-size, and move data-root when the volume is genuinely undersized. You'll leave with a cleanup runbook and the guards that keep the floor swept.

Where Docker's Bytes Actually Live

Docker's disk footprint splits into five buckets, and each fills for its own reason. overlay2 stores image layers and container writable layers — it grows with every pulled tag and every file your containers write. volumes holds named volume data, which survives container deletion by design and is the classic "I deleted everything, disk still full" surprise. containers holds per-container state plus json-file logs, which grow without bound unless capped. buildkit holds build cache — intermediate layers from every build, invisible in docker images, often the biggest bucket on CI. And tmp plus network sandboxes round out the remainder.

The data-root (/var/lib/docker unless moved) sits on one filesystem, and that filesystem is shared with whatever else lives on the volume — system logs, other apps, or nothing if you're lucky. df -h reports the filesystem; docker system df reports Docker's accounted portion. The gap between them is the first clue: a big gap means logs, another tenant, or held-open deleted files. Small gap means Docker's own buckets, and docker system df -v names the winner.

Learn the weighing commands before the deleting commands. sudo du -sh /var/lib/docker/ ranks the buckets on disk truthfully, docker builder du isolates build cache, and the find over -json.log ranks log files. Weighing takes a minute and aims every later command. Teams that prune blind delete 11GB of images against an 87GB cache problem; teams that weigh fix it in one shot.

⚠ Volumes Survive Prunes by Design
docker container prune and docker image prune never touch named volumes. If du shows volumes as the hog, only docker volume prune (or explicit docker volume rm) reclaims it — and that deletes database data permanently. Check what's mounted before you prune volumes.
📊 Production Insight
The costliest mistake is pruning the wrong bucket: 11GB of images freed against an 87GB build-cache problem while 63 jobs kept failing. One du command would have aimed the fix correctly.
🎯 Key Takeaway
Five buckets: layers, volumes, container logs, build cache, scratch. Weigh with du and system df before deleting anything.

Diagnose: df, system df, and du in 60 Seconds

Run the triage trio in order and you'll know the bucket within a minute. First df -h /var/lib/docker (or df -h / on shared-root boxes): Use% near 100 confirms ENOSPC territory and names the filesystem. Also glance at df -i — if inodes are exhausted while blocks remain, millions of tiny files (usually runaway logs or caches) are the story, and pruning whole objects beats deleting files. Second, docker system df: TYPE, TOTAL, ACTIVE, SIZE, RECLAIMABLE per bucket, with build cache listed separately on modern versions. RECLAIMABLE is your estimated prize for each prune.

Third, docker system df -v drills into the winners — which images are dangling, which containers stopped weeks ago, which volumes unreferenced. Pair it with sudo du -sh /var/lib/docker/ | sort -rh to catch what Docker under-reports, especially logs. When system df claims 20GB but df shows 95GB used, the du ranking plus the -json.log hunt finds the dark matter: logs, held-open deleted files (lsof +L1), or a non-Docker tenant on the same volume.

Write the three outputs into the incident thread before acting. They take 60 seconds, they make the fix reviewable, and they stop the second-guessing about whether the prune was safe — RECLAIMABLE plus the stopped-container list is your evidence. On Kubernetes nodes the same trio runs against containerd's paths, but the logic is identical: filesystem first, runtime accounting second, directory weights third.

disk-triage-trio.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. Filesystem truth (blocks + inodes)
df -h /var/lib/docker
df -i /var/lib/docker

# 2. Docker's own accounting per bucket
docker system df
docker system df -v 2>/dev/null | head -40

# 3. Directory weights catch what accounting hides
sudo du -sh /var/lib/docker/* 2>/dev/null | sort -rh | head
sudo find /var/lib/docker/containers -name '*-json.log' -exec du -h {} + 2>/dev/null | sort -rh | head -10

# Deleted-but-held-open files pinning freed space
sudo lsof +L1 2>/dev/null | grep -iE 'docker|containerd' | head
📊 Production Insight
Sixty seconds of weighing beats sixty minutes of blind pruning. The trio (df, system df -v, du) named build cache in the Tuesday incident while image-prune guesses wasted two hours.
🎯 Key Takeaway
df for the filesystem, system df for Docker's buckets, du for ground truth. Paste all three into the incident before pruning.

Prune Safely: Containers, Images, Volumes, Networks

Prune in ascending order of danger. Stopped containers first: docker container prune reclaims their writable layers and is nearly always safe — stopped means nobody needs them, though check docker ps -a for containers you stopped deliberately as checkpoints. Then dangling images: docker image prune drops untagged layers from rebuilds; adding -a also drops tagged images no container uses, which is safe on builders but think twice on deploy hosts mid-release. Volumes last and carefully: docker volume prune deletes every volume no container references, including database data from a compose stack that's merely down rather than gone.

The nuclear option, docker system prune -a --volumes, combines all three plus networks and build cache — powerful and unforgiving. Gate it with filters: --filter 'until=168h' keeps the last week, and label filters protect blessed objects (docker volume ls --filter label=keep). On shared hosts, list candidates first (docker images, docker volume ls) and prune by name rather than by flag. A filter typo with -f is how teams delete the production database volume at 2 AM.

After pruning, re-run docker system df to confirm the reclaim matched the estimate, and retry the failed pull or build. If usage barely moved, you pruned the wrong bucket — go back to du instead of pruning harder. And remember pruning is triage: it buys time while you add the caps and schedules below, not a strategy you repeat monthly by hand.

safe-prune-sequence.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Ascending danger: containers, dangling images, then aged unused images
docker ps -a --format '{{.Names}} {{.Status}}' | head -20
docker container prune -f
docker image prune -f
docker image prune -af --filter 'until=720h'   # unused tags older than 30d

# Volumes: list before you leap (down stacks still own data)
docker volume ls
docker volume prune --filter 'label!=keep'   # drops unreferenced volumes

# Networks rarely matter, but they're free to sweep
docker network prune -f

# Confirm the reclaim, then retry the failed operation
docker system df
docker pull myapp:latest
📊 Production Insight
Volume prune on a host with a down-but-not-gone compose stack deletes live database data. One team lost a staging Postgres this way — list volumes and check compose state before any volume prune.
🎯 Key Takeaway
Containers, then images, then volumes — each with a listing step first. Use until-filters; confirm with system df afterward.

Build Cache: the Bucket Everyone Forgets

BuildKit caches every intermediate layer from every build so rebuilds are fast — and never evicts anything unless configured. On a CI builder running dozens of builds daily, cache grows monotonically for months: 14 months reached 87GB in the Tuesday incident. docker images doesn't show it, docker image prune -a doesn't touch it, and older docker system df versions bury it. The only commands that see it are docker builder du (or docker buildx du) and the build-cache line in modern system df output. If your builders have never run a builder prune, assume tens of gigabytes are sitting there.

Reclaim with docker builder prune, surgically or fully: --keep-storage 20GB retains recent cache for speed while dropping the tail, while -af wipes everything and makes the next build slow but correct. A full wipe during business hours on a busy builder farm can spike build times 3-5x for an hour — schedule it or use keep-storage. For permanence, set a GC ceiling in daemon.json so the daemon evicts automatically: the builder GC policy with keepStorage caps total cache without any cron.

Also shrink what enters the cache. A .dockerignore that excludes .git, node_modules, and test fixtures cuts context and cache churn; multi-stage builds with pinned builder images avoid re-pulling shifting bases. Cache is a performance feature with a storage bill — budget it like one, with a cap in config and a prune in the weekly schedule, and it stops being an incident source.

build-cache-reclaim.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# See the invisible bucket
 docker builder du
# docker buildx du   # equivalent on newer CLIs

# Surgical: keep 20GB of hot cache, drop the tail
docker builder prune --keep-storage 20GB -f

# Full wipe (next builds go slow for a while — schedule wisely)
# docker builder prune -af

# Permanent ceiling in /etc/docker/daemon.json, then restart
# {
#   "builder": { "gc": { "defaultKeepStorage": "20GB", "enabled": true } }
# }
sudo systemctl restart docker
docker builder du   # confirm the ceiling holds
📊 Production Insight
Fourteen months without a builder prune put 87GB of cache on a 100GB builder volume. A 20GB GC ceiling plus a weekly prune ended the entire failure class for that fleet.
🎯 Key Takeaway
builder du to see it, keep-storage prune to trim it, GC ceiling in daemon.json to cap it forever.

Log Caps: Stop json-file From Growing Forever

The default json-file log driver appends every container stdout line to a -json.log file with no rotation unless you configure it. A chatty service at a few MB per hour reaches gigabytes in weeks; a debug-logging incident can do it in a day. These logs live under /var/lib/docker/containers, count opaquely in system df, and survive container restarts — only container removal (or rotation) reclaims them. When the -json.log hunt shows multi-GB files, you've found the leak that pruning images will never fix.

Cap all future containers in daemon.json: log-driver json-file with log-opts max-size (per file) and max-file (rotated files kept). Fifty megabytes times 3 files bounds any container at 150MB — tune to your log volume and retention needs. Restart dockerd after the change, and understand the boundary: caps apply to containers created after the change. Existing containers keep their uncapped files until recreated, so a compose up --force-recreate (or pod rollout) is part of the rollout.

For immediate relief without recreation, truncate the live file: truncate -s caps the size while the container keeps its file handle, unlike rm which leaves the space pinned until restart. Longer term, ship logs to a real backend (local driver with rotation, syslog, or a collector) instead of hoarding them on the node. Node disks are the most expensive log store you own — bounded local files plus centralized aggregation is the production pattern.

log-cap-rollout.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Rank the log files eating the disk
sudo find /var/lib/docker/containers -name '*-json.log' -exec du -h {} + 2>/dev/null | sort -rh | head -10

# Cap future containers: 50MB x 3 files each (daemon.json), then restart
sudo python3 - <<'EOF'
import json
p = '/etc/docker/daemon.json'
try:
    cfg = json.load(open(p))
except FileNotFoundError:
    cfg = {}
cfg['log-driver'] = 'json-file'
cfg['log-opts'] = {'max-size': '50m', 'max-file': '3'}
json.dump(cfg, open(p, 'w'), indent=2)
print(open(p).read())
EOF
sudo systemctl restart docker

# Immediate relief on a live container (keeps the file handle valid)
CID=$(docker ps -q --filter name=myapp | head -1)
sudo truncate -s 100M /var/lib/docker/containers/${CID}/${CID}-json.log

# Recreate so existing containers pick up the caps
docker compose up -d --force-recreate app
📊 Production Insight
Uncapped json-file logs are the slowest leak: MB per hour, GB per month, outage per quarter. The 50m x 3 cap bounds every container at 150MB and ends the class in one daemon.json edit.
🎯 Key Takeaway
max-size plus max-file in daemon.json, restart, recreate existing containers. Truncate live logs for instant relief.

Move data-root and Never Page for This Again

Sometimes the volume is simply too small: Docker shares a 20GB root disk with the OS, or the team outgrew the original sizing. Pruning then becomes a weekly chore that always lands at the worst time. Moving the data-root to a dedicated, sized volume ends the treadmill. Provision a volume (100GB minimum for builders, more for heavy CI), format and mount it at /mnt/docker-data, set data-root in daemon.json, and migrate: stop dockerd, rsync -a /var/lib/docker/ to the new mount, start dockerd, and verify with docker info --format '{{.DockerRootDir}}' plus a hello-world run. Keep the old directory until the next deploy proves the move.

Then automate the sweeping. A weekly cron running image and builder prunes with until-filters, a builder GC ceiling, log caps, and a disk alert at 75% with the runbook attached — together these four convert disk-full from an incident into background hygiene. Alert on rate-of-fill too when you can: 75% reached in a day means a leak worth investigating, while 75% reached over months is routine growth.

Verify the whole stack after any change: docker info names the new root dir, system df shows headroom, a fresh pull and build succeed, and the cron logs its runs somewhere you'll notice failures. Disk is infrastructure with an owner, a size, a growth plan, and a monitor. Give Docker's bytes all four and this error retires to a story you tell new hires.

dataroot-move-harden.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Provision + mount the new volume first (example: /dev/nvme1n1)
sudo mkfs -t ext4 /dev/nvme1n1
sudo mkdir -p /mnt/docker-data
sudo mount /dev/nvme1n1 /mnt/docker-data
df -h /mnt/docker-data

# Point the daemon at it
sudo python3 - <<'EOF'
import json
p = '/etc/docker/daemon.json'
try:
    cfg = json.load(open(p))
except FileNotFoundError:
    cfg = {}
cfg['data-root'] = '/mnt/docker-data'
json.dump(cfg, open(p, 'w'), indent=2)
print(open(p).read())
EOF

# Migrate: stop, copy, start, verify (keep the old dir one deploy)
sudo systemctl stop docker
sudo rsync -a /var/lib/docker/ /mnt/docker-data/
sudo systemctl start docker
docker info --format 'root={{.DockerRootDir}}'
docker run --rm hello-world | head -3

# Weekly hygiene cron
# 0 3 * * 0 /usr/bin/docker image prune -af --filter 'until=168h' && /usr/bin/docker builder prune -f --keep-storage 20GB
💡Keep the Old Directory One Deploy
After a data-root move, leave /var/lib/docker in place until a full deploy cycle succeeds on the new volume. If the mount fails at boot, the daemon falls back to an empty old directory and your images vanish — the backup copy is your rollback.
📊 Production Insight
Builders on shared 20GB root disks page monthly; builders on dedicated 200GB volumes with GC ceilings and weekly prunes page never. Storage sizing plus automation beats heroics every time.
🎯 Key Takeaway
Move data-root to a sized volume, automate weekly prunes, alert at 75% — and verify root dir, headroom, and cron after every change.
● Production incidentPOST-MORTEMseverity: high

87GB of Build Cache Killed 63 Nightly Jobs in 2 Hours

Symptom
At 1:14 AM on a Tuesday, all 4 CI builders started failing every job with "no space left on device" during docker build. Sixty-three jobs failed in 2 hours before anyone paged — nightlies were unmonitored. Day shift assumed a bad base image and retried the matrix twice, adding 2 more hours of red. docker system df on a builder showed 11GB reclaimable images, which made no sense against a 100GB disk showing 100% full.
Assumption
The team assumed images were the hog, because images are the visible artifact — they pruned 11GB of old tags and watched the disk barely move. One engineer suspected a log flood from a debug build, but the json-file logs totaled under 2GB. Nobody looked at the build cache because docker system df on their Docker version lumped it under a line nobody read, and the builders had been accumulating cache for 14 months without a single prune.
Root cause
du -sh /var/lib/docker/* showed buildkit at 87GB: 14 months of layer cache from daily matrix builds, each leaving its intermediate layers behind. The builders used one 100GB volume for everything, so cache growth silently consumed the headroom until a slightly larger Tuesday build tipped it over. Image pruning freed 11GB against an 87GB problem — the wrong byte bucket entirely.
Fix
Immediate: docker builder prune -af freed 87GB in 9 minutes and the retried matrix went green. Same week: capped build cache with a 20GB limit in daemon.json (builder cache GC), added a weekly cron running docker system prune -af --filter until=168h plus builder prune, and split builder storage onto a 200GB volume. That month: added a disk alert at 75% on builder volumes with a runbook link, so nightlies page before they drown.
Key lesson
  • Weigh before you prune. du -sh on the data-root subdirectories names the actual hog in seconds — image prune against a build-cache problem wastes the incident window.
  • Build cache is invisible until it isn't. It grows monotonically on busy builders and appears in no one's mental model. Cap it in daemon.json and prune it on a schedule from day one.
  • Unmonitored nightlies need disk alerts, not just job alerts. A 75% builder-disk page with a runbook link turns a 63-job massacre into a 10-minute prune.
Production debug guideFive symptoms, five exact command sequences — weigh first, prune second, cap third.5 entries
Symptom · 01
Pulls or builds fail with no space left, and you don't know whose bytes these are
Fix
Weigh the filesystem and Docker's own accounting side by side: run df -h /var/lib/docker (or df -h / if Docker shares root) and docker system df plus docker system df -v for the per-object breakdown. If Docker accounts for most of it, prune by the biggest bucket shown. If Docker accounts for little, the bytes are logs or another app — move to du next.
Symptom · 02
Docker's accounting doesn't explain the full disk — something hidden is eating it
Fix
Weigh each subdirectory directly with sudo du -sh /var/lib/docker/ | sort -rh | head, and hunt the biggest log files with sudo find /var/lib/docker/containers -name '-json.log' -exec ls -lh {} \; | sort -k5 -hr | head -10. Deleted-but-open files also hide: sudo lsof +L1 | grep -i docker shows space held by removed files, fixed with a daemon or container restart.
Symptom · 03
Images are the confirmed hog — gigabytes of old tags and dangling layers
Fix
List the worst offenders with docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | sort -k2 -hr | head -15 (sizes sort alphabetically so eyeball the GB entries), then prune: docker image prune -af --filter 'until=720h' keeps 30 days and drops the rest. Never prune by gut on a builder mid-release — check docker ps first so you don't delete a layer a running deploy is about to need.
Symptom · 04
Build cache is the hog — buildkit outweighs images and volumes combined
Fix
Confirm with docker builder du (or docker system df showing build cache dominant), then reclaim with docker builder prune -af, or surgically with docker builder prune --keep-storage 20GB. For permanence, set a cache ceiling in /etc/docker/daemon.json under builders or features buildkit GC policy ("keepStorage": "20GB"), then sudo systemctl restart docker.
Symptom · 05
Container logs are the hog — a few *-json.log files in the gigabytes
Fix
Cap them going forward in /etc/docker/daemon.json with {"log-driver": "json-file", "log-opts": {"max-size": "50m", "max-file": "3"}}, restart the daemon, and note caps apply only to containers created after the change — recreate (not restart) existing ones. For immediate relief, truncate a live log with sudo truncate -s 100M /var/lib/docker/containers/<id>/<id>-json.log instead of deleting it.
Docker Disk Full — Confirm, Fix, Prevent
Root CauseHow to ConfirmFixPrevention
Unused images and stopped containersdocker system df shows high reclaimable images; docker ps -a lists long-stopped containersdocker container prune; docker image prune -af with until-filterWeekly cron prune with until-filters; alert at 75% disk
Bloated build cachedocker builder du or system df shows build cache dominant; du weighs buildkit highestdocker builder prune with keep-storage, or -af for full wipeGC ceiling defaultKeepStorage in daemon.json; weekly builder prune
Uncapped json-file logsfind over *-json.log shows GB files under /var/lib/docker/containersTruncate live logs; set max-size and max-file then recreate containersLog caps in daemon.json from day one; ship logs to centralized backend
Orphaned named volumesdu weighs volumes highest; docker volume ls shows unreferenced data volumesdocker volume prune with label filters, or rm by name after checking mountsLabel protected volumes; never prune volumes on hosts with down compose stacks
Undersized or shared volumedf shows tiny or shared filesystem; prunes free little and it refills fastMove data-root to a dedicated sized volume and migrate with rsyncSize builders 100GB+; separate Docker storage from OS and logs
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
disk-triage-trio.shdf -h /var/lib/dockerDiagnose
safe-prune-sequence.shdocker ps -a --format '{{.Names}} {{.Status}}' | head -20Prune Safely
build-cache-reclaim.shdocker builder duBuild Cache
log-cap-rollout.shsudo find /var/lib/docker/containers -name '*-json.log' -exec du -h {} + 2>/dev/...Log Caps
dataroot-move-harden.shsudo mkfs -t ext4 /dev/nvme1n1Move data-root and Never Page for This Again

Key takeaways

1
Weigh with df, system df -v, and du before pruning
aim the fix at the real bucket.
2
Containers, then images, then volumes
ascending danger, with listing steps first.
3
Build cache is invisible to image prune
builder du to see it, GC ceiling to cap it.
4
json-file logs grow forever
max-size and max-file caps, then recreate containers.
5
Truncate live logs; never delete them while the container holds them open.
6
Move data-root to a sized volume; automate weekly prunes and alert at 75%.

Common mistakes to avoid

6 patterns
×

Pruning images when the cache is the hog

Symptom
11GB freed against an 87GB problem; builds keep failing while the team celebrates a prune that changed nothing.
Fix
Weigh with du and builder du first. Match the prune to the biggest bucket — images, cache, logs, or volumes.
×

Running volume prune on a host with a down stack

Symptom
Staging database gone: the compose stack was down for maintenance, so its data volume looked unreferenced and got deleted.
Fix
List volumes and check compose state first. Label protected volumes and exclude them with filters.
×

Deleting a live json-file log instead of truncating

Symptom
Disk usage doesn't drop — the container holds the deleted file open, so the space stays pinned until restart.
Fix
Use truncate -s on live log files. Delete only after stopping the container, and add max-size caps so it stops recurring.
×

Setting log caps and expecting existing containers to obey

Symptom
Caps configured, logs still growing — rotation applies only to containers created after the daemon change.
Fix
Recreate existing containers (compose up --force-recreate) as part of the rollout, then verify new log files rotate.
×

Full builder wipe at peak hour

Symptom
Disk fixed, builds 4x slower for an hour — the entire hot cache was evicted during the busiest window.
Fix
Use --keep-storage to retain hot cache, or schedule full wipes off-peak. Set a GC ceiling so manual wipes get rare.
×

Treating one prune as the fix

Symptom
Same page 6 weeks later: nothing caps growth, so images, cache, and logs refill on schedule.
Fix
Pair every prune with a permanent guard — GC ceiling, log caps, weekly cron, and a 75% alert with a runbook.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
docker system df and df -h disagree — Docker claims little but the disk ...
Q02JUNIOR
Which prune order do you use on a full host, and why?
Q03SENIOR
Why doesn't docker image prune fix a builder that's full?
Q04SENIOR
You set max-size log caps but logs keep growing. Explain.
Q05SENIOR
Design storage for a fleet of CI builders that pages for disk-full month...
Q01 of 05JUNIOR

docker system df and df -h disagree — Docker claims little but the disk is full. What next?

ANSWER
Look for what Docker under-reports: rank *-json.log files under /var/lib/docker/containers, weigh subdirectories with du -sh, and check lsof +L1 for deleted-but-open files. The gap is usually logs, another tenant on the shared volume, or pinned deleted files.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How much headroom should a Docker volume keep?
02
Is docker system prune -a --volumes safe?
03
Why did pruning free less than RECLAIMABLE promised?
04
Do stopped containers still use disk?
05
Can I cap build cache without cron jobs?
06
Should Docker share the root filesystem?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Docker. Mark it forged?

6 min read · try the examples if you haven't

Previous
OCI Runtime Create Failed Fix
23 / 24 · Docker
Next
Docker Exec Format Error Fix