How to Fix Docker 'No Space Left on Device' Without Losing Your Data

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 Dockerfile creates 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 /data instruction 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 (like Docker.raw or 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
Check Host Inodes on Linux Hosts

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.

The Portable Backup Trick

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.

Step A Delete Stopped Containers: Frees temporary writable layers from exited containers.
Step B Prune BuildKit Cache: Wipes stale multi-stage build layers while preserving all images.
Step C Remove Dangling Images: Clears untagged <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.

Dangerous Command Warning

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 Volume
my-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 Volume
a1b2c3d4e5f6...
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.

Destructive Action

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:

  1. Click the Settings (Gear icon) in Docker Desktop.
  2. Navigate to Resources > Advanced or Disk usage.
  3. 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)

Frequently Asked Questions

Will running `docker system prune` delete my active database data?
No, running basic `docker system prune` only removes stopped containers, unused networks, and dangling images. It does not touch volumes unless you explicitly pass the `--volumes` flag. However, if your database container is currently stopped, `--volumes` will destroy its data.
Why does `df -h` on my Mac or Windows host show no freed space after pruning?
Docker Desktop operates within a fixed-growth virtual disk file (.raw or .vhdx). Deleting files inside Docker frees space inside the virtual machine, but the host operating system file does not shrink automatically without running a compaction command (like WSL2 diskpart or Docker Desktop's Compact Disk tool).
What is the difference between dangling images and unused images?
Dangling images are untagged layers with no name or version (<none>), usually left behind after rebuilding an image with an existing tag. Unused images are fully tagged, valid images that are simply not currently attached to any active container.
How can I prevent Docker from running out of space in the future?
Configure global log rotation in `/etc/docker/daemon.json` with max-size caps, use multi-stage Dockerfiles to minimize build layer sizes, and run scheduled builder prunes with time filters (`--filter "until=168h"`) in your automated tasks.
Why do I get "no space left on device" during a `docker build` even when `docker system df` shows free gigabytes?
This typically happens when your Linux `/tmp` directory or the Docker root partition `/var/lib/docker` is mounted on a separate, smaller host partition, or when your host system has run out of file system inodes rather than raw megabytes. Check inodes with `df -ih`.

Comments