Contents

Docker Cleanup Tutorial: Reclaim Disk Space with Prune Commands

After using Docker for a while, old images, stopped containers, build cache, and unused volumes can pile up and take tens of gigabytes. This section shows you how to find what's using the space and clean it up safely. All output below comes from commands run in sequence in the same environment.

Check how much space is in use

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          13        3         2.886GB   2.377GB (82%)
Containers      4         1         98.3kB    16.38kB (16%)
Local Volumes   4         1         40.61MB   40.61MB (100%)
Build Cache     151       0         3.554GB   2.954GB
FieldMeaning
TOTALTotal count
ACTIVENumber currently used by containers
SIZESpace used
RECLAIMABLESpace that can be reclaimed (not in use)

This environment has 13 images, of which only 3 are in use. You can reclaim 2.377 GB (82%). Build cache is even larger, with 2.954 GB. Add -v to see details for each image, container, and volume: docker system df -v.

Current images and containers:

$ docker images
IMAGE               ID             DISK USAGE   CONTENT SIZE   EXTRA
adminer:latest      74f29c416e14        164MB         43.8MB
alpine:3.23         85fe1e81d675       26.6MB         8.21MB   U
hello-node:latest   4063457524bb        245MB         62.2MB
myapp:latest        80cb5c41a286       54.9MB         24.8MB   U
mysql:8.4           24989179f588       1.12GB          254MB
nginx:1.30          9bf97bd7714f        242MB         66.5MB
nginx:1.30-alpine   0985e772fb9f       93.7MB           27MB   U
node:24-alpine      ebfe2f904627        242MB           62MB
postgres:18         74935e722416        650MB          169MB
python:3.14-slim    a2b82f3c4855        180MB         45.7MB
redis:8-alpine      3811787313eb        161MB         39.6MB
registry:3          ddf754342cfc       86.9MB         20.7MB
ubuntu:26.04        f144425ff09b        162MB         45.6MB
$ docker ps -a
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS                     PORTS     NAMES
cc43746c9a29   alpine:3.23         "true"                   7 seconds ago   Exited (0) 6 seconds ago             anon1
7e269fff858b   nginx:1.30-alpine   "/docker-entrypoint.…"   8 seconds ago   Up 7 seconds               80/tcp    web
bf5ff318eb63   alpine:3.23         "echo done"              8 seconds ago   Exited (0) 7 seconds ago             old2
b580380c1fd2   myapp               "cat /version.txt"       9 seconds ago   Exited (0) 8 seconds ago             old1

U in the EXTRA column means In Use (a container is using the image). Only web is running; the other three containers have exited, but still take up space.

Clean up individual items

CommandWhat it removes
docker container pruneAll stopped containers
docker image pruneDangling images (unnamed <none> images)
docker image prune -aAll images not used by any container
docker builder pruneBuild cache
docker volume pruneUnused anonymous volumes
docker volume prune -aAll unused volumes (including named volumes)
docker network pruneUnused custom networks

Each command asks for confirmation (Are you sure you want to continue? [y/N]). Add -f to skip confirmation, which is useful in scripts. The examples below all use -f.

Remove stopped containers

$ docker container prune -f
Deleted Containers:
cc43746c9a2993bda0fb21ab3f161b78603353714f396f86fa8c4b5caf90e25a
bf5ff318eb6331a05239ad7fa39420ff96dc1c8f84b9a3548ce38a0f71a2d63a
b580380c1fd2dc6cca2dc6e009bb877c3eb2df1cf388664a32c8932aff8169fa

Total reclaimed space: 16.38kB

The three stopped containers are removed. The running web container is unaffected.

If you use Docker Desktop, you can also select containers on the Containers screen and click Delete:

Select containers on the Docker Desktop Containers screen and click Delete
Docker Desktop: Select containers and click Delete. Image: Docker documentation (Apache-2.0)

Docker 29: Rebuilding no longer leaves <none> images

Previously, when you rebuilt an image with the same name (for example, by running docker build -t myapp . multiple times), the old version became a "dangling image" named <none>, which you had to clean up with docker image prune. Tested with Docker 29:

$ docker build -q --build-arg VERSION=4 -t myapp:latest .
sha256:3f33bc33eb36171280e9b90d904e25c58b2dcf509390536580ffacafc35d4595

$ docker images --filter dangling=true
IMAGE   ID             DISK USAGE   CONTENT SIZE   EXTRA

$ docker ps -a --filter name=old1
CONTAINER ID   IMAGE          COMMAND              CREATED          STATUS                      PORTS     NAMES
b580380c1fd2   80cb5c41a286   "cat /version.txt"   26 seconds ago   Exited (0) 26 seconds ago             old1

After building myapp:latest four times in a row, --filter dangling=true returns no results. Docker 29 uses the containerd image store by default. When a new image replaces an old one with the same name, the old version is no longer kept as <none>. For containers still using the old version (such as old1), the IMAGE column shows the image ID instead.

So on Docker 29, docker image prune (without -a) usually has little to clean up. The space is more likely taken up by unused images that still have names and build cache.

Remove anonymous volumes

$ docker volume ls
DRIVER    VOLUME NAME
local     6f6f8ce590c609d4f39b3a676282da88866c8ab20a064132f7746c3cf6717f2c
local     991c93cb6ebeac8570e9c15a89b4225c2bb5e20cbfde2f5bf665fa4dfa23a1aa
local     5396795eeeaab8415295814207a02f75f6ea136d5c34574ba9b16ecdc1b975f9
local     cleanup-demo_pgdata

$ docker volume prune -f
Deleted Volumes:
5396795eeeaab8415295814207a02f75f6ea136d5c34574ba9b16ecdc1b975f9
991c93cb6ebeac8570e9c15a89b4225c2bb5e20cbfde2f5bf665fa4dfa23a1aa
6f6f8ce590c609d4f39b3a676282da88866c8ab20a064132f7746c3cf6717f2c

Total reclaimed space: 0B

$ docker volume ls
DRIVER    VOLUME NAME
local     cleanup-demo_pgdata
  • The first three volumes have long names and are anonymous volumes. For example, the postgres image's Dockerfile declares a VOLUME. Docker automatically creates an anonymous volume even if you don't mount one at runtime. It remains after you remove the container (see Volumes and data persistence).
  • docker volume prune only removes unused anonymous volumes.
  • cleanup-demo_pgdata is a named volume for a Compose project. It is no longer used by any container after docker compose down, but it was not deleted.

Remove build cache

$ docker builder du | tail -n 3
Private:	2.996GB
Reclaimable:	3.596GB
Total:		3.596GB

$ docker builder prune -f --filter until=1h | tail -n 3
lv85kuv5xx5h65k941qjoxecz               	true 	98.69kB   	About an hour ago
kc9tcgw8l1aq9dz1agx096se0               	true 	870kB     	About an hour ago
Total:	1.662GB

$ docker builder prune -f | tail -n 3
6os07ml167zpcfh09ushitk2b               	true 	8.285kB   	38 minutes ago
7ir7u28j6esk2x7pyu6q38kvk               	true 	74.75MB   	38 minutes ago
Total:	1.334GB
  • docker builder du shows 3.596 GB of build cache, all reclaimable.
  • --filter until=1h removes only cache that hasn't been used for more than an hour. It removed 1.662 GB.
  • Without a filter, it removed another 1.334 GB.

The tradeoff for deleting build cache is that the next build must run every step again (for example, npm ci and pip install), so it will take longer. It won't affect any images or containers.

Clean up at once: docker system prune

docker system prune

This removes stopped containers, unused networks, dangling images, and build cache.

docker system prune -a

This also removes all unused images. The next time you need them, they must be downloaded again.

Tested: Does --volumes delete database data?

Many tutorials (including an older version of this tutorial) warn that docker system prune --volumes also deletes database volumes that are "temporarily unused" after docker compose down. Tested with Docker 29:

$ docker system prune --volumes -f
Deleted Containers:
871f1c408d23dd413804105498b5520385dfafc9e558c02447ed940684bbc483

Deleted Volumes:
e5d0caa972c5323d5a64e9baf5fcb21d66ce7fbb4879bdcc7aa068a14fdbbf76

Total reclaimed space: 8.192kB

$ docker volume ls
DRIVER    VOLUME NAME
local     cleanup-demo_pgdata

--volumes removed only one anonymous volume. The named Compose database volume cleanup-demo_pgdata is still there. Starting with Docker 23, volume prune and system prune --volumes only handle anonymous volumes by default.

The command that really removes named volumes is docker volume prune -a:

$ docker volume prune -a -f
Deleted Volumes:
cleanup-demo_pgdata

Total reclaimed space: 40.61MB

$ docker volume ls
DRIVER    VOLUME NAME

This time, cleanup-demo_pgdata was deleted, along with its 40.61 MB of database files. They can't be recovered.

Use docker volume prune -a only when you are sure you don't need any of the data. To remove a specific volume, docker volume rm name is safer. For a Compose project, use docker compose down -v, which removes only that project's volumes. If you're using Docker 20.10 or earlier, --volumes still removes named volumes.

Remove all unused images

$ docker image prune -a -f | grep -E "^untagged|^Total"
untagged: nginx:1.30
untagged: node:24-alpine
untagged: postgres:18
untagged: python:3.14-slim
untagged: myapp:latest
untagged: mysql:8.4
untagged: redis:8-alpine
untagged: registry:3
untagged: ubuntu:26.04
untagged: adminer:latest
untagged: alpine:3.23
untagged: hello-node:latest
Total reclaimed space: 2.665GB

$ docker images
IMAGE               ID             DISK USAGE   CONTENT SIZE   EXTRA
nginx:1.30-alpine   0985e772fb9f       93.7MB           27MB   U

$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          1         1         93.66MB   0B (0%)
Containers      1         1         81.92kB   0B (0%)
Local Volumes   0         0         0B        0B
Build Cache     26        0         599.6MB   506.9MB
  • All 12 images except nginx:1.30-alpine, which is used by the web container, were deleted. This reclaimed 2.665 GB.
  • The final docker system df shows that only 93.66 MB of images remain.

These images will be downloaded automatically the next time you run a container or build. If your network is slow or the images are large (AI model images can easily be several GB), consider using --filter to remove only older images:

docker image prune -a --filter "until=168h"      # Remove only images created more than 7 days ago

Note that until checks the image's creation time (when the image author built it), not when you downloaded it.

Container logs also use disk space

By default, json-file logs have no size limit. Here's a test with a container that continuously writes output (the yes command prints a lot of text every second):

$ docker run -d --name noisy alpine:3.23 sh -c "timeout 3 yes hello-docker-log; sleep 600"
863ccacbc4bcf86a81cc3bf7603cef1d6ecc004553ac4b36dd7ef52172ed3dc6

$ docker inspect --format "{{.LogPath}}" noisy
/var/lib/docker/containers/863ccacbc4bcf86a81cc3bf7603cef1d6ecc004553ac4b36dd7ef52172ed3dc6/863ccacbc4bcf86a81cc3bf7603cef1d6ecc004553ac4b36dd7ef52172ed3dc6-json.log

$ du -h $(docker inspect --format "{{.LogPath}}" noisy)
69.3M	/var/lib/docker/containers/863ccacbc4bcf86a81cc3bf7603cef1d6ecc004553ac4b36dd7ef52172ed3dc6/863ccacbc4bcf86a81cc3bf7603cef1d6ecc004553ac4b36dd7ef52172ed3dc6-json.log

$ docker rm -f noisy
noisy

After only 3 seconds of output, the log file was already 69.3 MB. A long-running container that produces lots of logs can grow to tens of gigabytes, and the log isn't automatically deleted until the container is removed. Log files are in /var/lib/docker/containers/CONTAINER_ID/. On a Linux host, find the largest log files like this:

sudo du -h /var/lib/docker/containers/*/*-json.log | sort -h | tail

Set a limit for a single container

$ docker run -d --name noisy --log-opt max-size=10m --log-opt max-file=3 alpine:3.23 sh -c "timeout 3 yes hello-docker-log; sleep 600"
fab1c34ceecb955dc33c01f0e7aa0a60b4137e68d0fe465d52ce23b49ba66161

$ ls -lh $(dirname $(docker inspect --format "{{.LogPath}}" noisy)) | grep json
-rw-------    1 root     root        3.2K Oct 13 01:07 config.v2.json
-rw-r-----    1 root     root        7.2M Oct 13 01:07 fab1c34ceecb955dc33c01f0e7aa0a60b4137e68d0fe465d52ce23b49ba66161-json.log
-rw-r-----    1 root     root        9.5M Oct 13 01:07 fab1c34ceecb955dc33c01f0e7aa0a60b4137e68d0fe465d52ce23b49ba66161-json.log.1
-rw-r-----    1 root     root        9.5M Oct 13 01:07 fab1c34ceecb955dc33c01f0e7aa0a60b4137e68d0fe465d52ce23b49ba66161-json.log.2
-rw-------    1 root     root        1.5K Oct 13 01:07 hostconfig.json

$ docker inspect --format '{{json .HostConfig.LogConfig}}' noisy
{"Type":"json-file","Config":{"max-file":"3","max-size":"10m"}}

$ docker rm -f noisy
noisy

max-size=10m sets a maximum of 10 MB per file. max-file=3 keeps up to 3 files, rotating older ones to .1 and .2. With the same amount of output, the logs take only about 26 MB total and won't grow beyond that.

In Compose:

services:
  app:
    image: myapp
    logging:
      options:
        max-size: "10m"
        max-file: "3"

Set a global log limit

To apply the limit to all new containers, edit /etc/docker/daemon.json on Linux. In Docker Desktop, edit the same JSON under Settings → Docker Engine:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

After editing on Linux, run sudo systemctl restart docker. In Docker Desktop, click Apply & restart. The setting applies only to new containers. Remove and recreate existing containers (in Compose, use docker compose up -d --force-recreate).

Docker Desktop: disk space hasn't increased after cleanup

On Windows and macOS, all Docker data is stored in a virtual disk file. Deleting images frees space inside the virtual disk, but the file itself may not shrink immediately.

  • Docker Desktop: Go to Settings → Resources → Advanced to view and adjust the virtual disk limit. Clean / Purge data on the Troubleshoot screen deletes all Docker data (all images, containers, and volumes). Use it only as a last resort.
  • Windows (WSL 2): If the .vhdx file is still large, run wsl --shutdown, then use diskpart's compact vdisk command to compact the corresponding .vhdx file.

On a server, you can use cron to run these commands once a week:

docker container prune -f --filter "until=168h"
docker image prune -af --filter "until=168h"
docker builder prune -af --filter "until=168h"

These commands don't touch any volumes, so your database data is safe. Add a global log limit, and disk usage is less likely to get out of control.

Recap

  • Check docker system df first, then decide what to clean up.
  • image prune -a removes all unused images; builder prune clears build cache.
  • volume prune removes only anonymous volumes; -a includes named volumes.
  • Set log limits with max-size and max-file.

Exercises

Exercise 1: Write a set of cleanup commands for cron that won't touch any volumes and will remove only images and build cache older than 7 days.

Answer
docker image prune -af --filter "until=168h"
docker builder prune -af --filter "until=168h"

FAQ

Is docker system prune safe?

Without --volumes, it doesn't affect any volumes. Since Docker 23, even with --volumes, it only removes anonymous volumes. The truly dangerous command is docker volume prune -a.

Why hasn't free disk space increased on Windows after cleanup?

Docker Desktop stores data in a virtual disk file, which may not shrink immediately after images are deleted. See the last section of this chapter.

Can I delete all build cache?

Yes. It won't affect any images or containers, but the next build will take longer.

Updated October 14, 2026