How to Fix Docker 'No Space Left on Device' Without Losing Your Data
You run a build or start a service with docker compose up, and everything grinds to a halt with this error:
ERROR: failed to solve: failed to register layer: write /var/lib/docker/overlay2/...: no space left on device
Your local disk looks half empty. You open your file manager and see 80 GB of free storage. Yet Docker insists you are out of room. If this is a CI runner, your deployment pipeline is blocked. If it is your local machine, your containers refuse to spin up.
The immediate urge is to run a destructive cleanup command found on Stack Overflow and hope for the best. That works—right until you realize your local Postgres container had uncommitted test migrations and seed data stored in an unmapped volume, now wiped clean.
You can clear dozens of gigabytes from Docker safely. The key is understanding what is eating your space, which assets are safe to delete, and which ones contain active application state.
Where Did All the Space Actually Go?
Docker rarely leaks disk space by accident. It hoards storage because of how container layers, build caching, and volumes work by design:
- BuildKit Cache: Every step in your
Dockerfilecreates an immutable cache layer. Over months of builds, Docker keeps old build stages around so subsequent runs finish faster. This cache easily balloons past 30–50 GB. - Dangling & Intermediate Images: When you rebuild an image with the same tag (like
my-api:latest), the previous image loses its name tag and becomes an untagged "dangling" image (marked as<none>). It still sits on your disk. - Anonymous Volumes: If a Dockerfile specifies a
VOLUME /datainstruction and you spin up a container without giving that volume an explicit name or host bind-mount, Docker assigns it an anonymous 64-character hash. When the container dies, that volume stays on disk indefinitely. - Virtual Disk Allocation (macOS & Windows): On Linux, Docker writes directly to the host filesystem inside
/var/lib/docker. On macOS and Windows, Docker runs inside a lightweight virtual machine backed by an expanding virtual disk image (likeDocker.rawor a WSL2 VHDX file). Even if Docker deletes files internally, the virtual disk image file on the host rarely shrinks automatically. - Uncapped Container Log Files: A runaway Node.js, Python, or Go app spitting out JSON logs can quietly write a 20 GB JSON log file directly inside Docker’s container storage directory.
Step 1: Diagnose What Is Consuming Your Disk
Never run a cleanup command blindly. Docker gives you an exact breakdown of storage consumption using the built-in system inspection tool.
docker system df
You will see output formatted like this:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 6 14.82GB 11.45GB (77%)
Containers 8 2 1.21GB 890MB (73%)
Local Volumes 38 4 32.40GB 28.10GB (86%)
Build Cache 142 0 26.15GB 26.15GB (100%)
Look at the RECLAIMABLE column. In the example above, Docker is holding onto 28 GB of unused volume data and 26 GB of old build cache that is not tied to any running container. That is over 54 GB of safe disk reclamation waiting to happen.
To see granular details—including exact container IDs and image tags—run the verbose flag:
docker system df -v
Sometimes your Linux disk has 50% free megabytes, but you still hit "no space left on device". Docker creates thousands of tiny layer metadata files that can exhaust your disk's inode allocation. Check inode usage on your host machine with df -ih.
Step 1.5: The Safety Net — Back Up Your Critical Volumes First
Before executing any deletion or pruning commands, take 60 seconds to export your valuable container volumes (databases, user uploads, config directories) to a standalone compressed archive on your host machine.
You don't need external backup tools. Spin up a temporary, throwaway Alpine container that mounts your target volume, tars the content, and streams it straight into your current host directory:
1. Back Up a Specific Docker Volume
Replace my_db_volume with your actual volume name (find it via docker volume ls):
docker run --rm \
-v my_db_volume:/volume-data \
-v $(pwd):/backup \
alpine tar -czf /backup/my_db_volume_backup.tar.gz -C /volume-data .
2. Quick Database Dump (If the Container Is Running)
If your database engine is actively running, a clean logical dump is always safer than a raw file copy:
PostgreSQL:
docker exec -t postgres_container pg_dumpall -c -U postgres > ./backup_postgres_$(date +%F).sql
MySQL / MariaDB:
docker exec -t mysql_container mysqldump -u root -p'your_password' --all-databases > ./backup_mysql_$(date +%F).sql
How to Restore If Something Goes Wrong
If you ever accidentally wipe a volume during a cleanup, recreate the volume and extract the archive back into it:
# 1. Recreate the named volume
docker volume create my_db_volume
# 2. Extract the tar backup into the new volume
docker run --rm \
-v my_db_volume:/volume-data \
-v $(pwd):/backup \
alpine sh -c "tar -xzf /backup/my_db_volume_backup.tar.gz -C /volume-data"
Step 2: The Safe Reclaim (Zero Risk to Databases)
If you want to clear space immediately without any risk to your databases, persistent message queues, or project volumes, run this sequence.
<none> image layers from past builds.
1. Clear Out Dead Containers
When a container exits, its writable layer remains on disk until explicitly deleted:
docker container prune -f
2. Wipe the BuildKit Cache
This is often the single biggest disk eater on modern Docker engines using BuildKit. Cleaning it has zero impact on running apps; it simply means your next cold build might take a minute longer to download baseline dependencies.
docker builder prune -f
To wipe all build cache, including cache from older builds that are still referenced:
docker builder prune -a -f
3. Remove Dangling (Untagged) Images
This removes only images that have no tag name. It will not touch named images like postgres:16, redis:alpine, or your project's tagged build artifacts.
docker image prune -f
This three-command routine typically recovers 15–40 GB on an active development workstation with zero data loss.
Step 3: Handling Volumes Without Deleting Active Databases
Volumes store state. They are where your MySQL records, MongoDB collections, Redis dumps, and local MinIO buckets live. This is where engineers get burned.
Do NOT run docker system prune --volumes or docker volume prune -a if you have database containers that are currently stopped. Docker considers any volume not attached to a currently running container as unreferenced, and it will delete it permanently.
| Volume Type | How It Was Created | Is It Safe to Delete? |
|---|---|---|
Named Volumemy-db-data |
Created explicitly via docker volume create or under volumes: in Compose. |
Dangerous: Contains app state. Only delete manually when destroying the project. |
Anonymous Volumea1b2c3d4e5f6... |
Generated automatically from a Dockerfile VOLUME line without a specific name. |
Usually Safe: Often orphaned artifacts from past one-off test runs. |
Host Bind Mount./data:/var/lib/data |
Explicit path mapping on your host disk. | Untouched by Docker: Lives in your local folder; Docker prune commands never delete host directories. |
How to Check What an Unused Volume Actually Holds
Before wiping orphaned volumes, list every unattached volume on your system:
docker volume ls -qf dangling=true
If you see an anonymous hash and you aren't sure what wrote to it, inspect it:
docker volume inspect <VOLUME_NAME_OR_HASH>
To look inside the volume on your host machine without booting a full service:
docker run --rm -it -v <VOLUME_NAME_OR_HASH>:/volume-data alpine ls -la /volume-data
Safely Cleaning Only Dangling Volumes
Once you verify that your critical services are up and running (or their volumes are explicitly named and protected), clear unreferenced volumes:
docker volume prune
Docker will prompt you with a confirmation list showing every volume name and hash scheduled for deletion.
The Big Hammer: When You Want a Clean Slate
If you are working on a secondary development machine, an isolated sandbox, or you already have database backups committed to SQL dumps or remote servers, you can perform a full reset.
The command below stops and wipes all unused containers, networks, all unreferenced images (not just dangling ones), and all unreferenced volumes.
docker system prune -a --volumes
Use this when switching between completely different company tech stacks or when a build environment has become corrupted beyond granular diagnosis.
Docker Desktop Specifics: Reclaiming the Virtual Disk
If you are on macOS or Windows, cleaning files inside Docker might still leave your host hard drive full. This confuses many developers.
Docker Desktop runs a Linux VM inside a virtual disk file:
- macOS:
~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw - Windows (WSL2):
%LOCALAPPDATA%\Docker\wsl\data\ext4.vhdx
When you write 50 GB of images into Docker, this file expands on your host drive. When you delete those images inside Docker, the VM marks the space as free internally, but the host operating system’s .raw or .vhdx file does not shrink automatically.
How to Reclaim the Space on macOS
Modern versions of Docker Desktop on macOS support the Trim command automatically, but you can force an immediate disk compaction from the UI:
- Click the Settings (Gear icon) in Docker Desktop.
- Navigate to Resources > Advanced or Disk usage.
- Click Clean / Purge data, or use the Compact virtual disk option.
How to Reclaim the Space on Windows (WSL2)
For Windows users running Docker through WSL2, you can compact the underlying ext4.vhdx file using diskpart:
# 1. Shut down Docker Desktop and terminate WSL
wsl --shutdown
# 2. Open PowerShell as Administrator and launch diskpart
diskpart
# 3. Inside the diskpart prompt, select and compact the VHDX file:
select vdisk file="C:\Users\<YourUser>\AppData\Local\Docker\wsl\data\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit
This single maintenance step often shrinks a bloated 80 GB VHDX file back down to 10 GB on your Windows drive.
Automating Cleanup on CI/CD Runners (GitHub Actions & GitLab CI)
Self-hosted CI runners (like self-hosted GitHub Actions runners, GitLab Runner VMs, or Jenkins agents) suffer from disk exhaustion faster than developer laptops. When five developers trigger containerized integration tests concurrently, a 100 GB runner disk will fill up in days.
1. Post-Build Pipeline Hooks
In your runner workflow or cron configuration, add a dedicated cleanup job. For GitHub Actions self-hosted runners, add a step that executes after heavy test suites:
- name: Clean Docker Artifacts
if: always()
run: |
docker container prune -f
docker image prune -f
docker builder prune --filter "until=24h" -f
Notice the --filter "until=24h" flag. This is a smart optimization: it deletes build cache older than 24 hours while keeping today's cached layers intact so consecutive runs remain fast.
2. Configure Container Log Limits
By default, Docker keeps JSON container logs indefinitely without rotation. A busy logging service will fill the host disk silently. Set a global log rotation policy by creating or editing your Docker daemon configuration file.
On Linux: /etc/docker/daemon.json (On Docker Desktop: Settings > Docker Engine):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "3"
}
}
After editing, restart the Docker daemon:
sudo systemctl restart docker
This configuration caps every container log file at 20 MB and keeps a maximum of 3 rotated backup files, guaranteeing that container logs will never consume more than 60 MB per container.
Summary: Quick Command Cheat Sheet
| Action | Command | Risk Profile |
|---|---|---|
| Inspect space usage | docker system df |
Read-only (Zero risk) |
| Clean build cache safely | docker builder prune -f |
Safe (Slightly slower next build) |
| Remove stopped containers | docker container prune -f |
Safe (Only touches exited containers) |
| Delete untagged images | docker image prune -f |
Safe (Keeps named images) |
| Delete unattached volumes | docker volume prune |
Medium (Wipes unmapped databases) |
| Nuclear total wipe | docker system prune -a --volumes |
High (Wipes all non-running data) |
Comments