# How to Clean Up Docker: Images, Containers, and Volumes

> Reclaim disk from unused Docker images, stopped containers and orphaned volumes, with the difference between prune and prune -a and how to avoid deleting something you needed.

Source: https://latchkey.dev/learn/optimize-ci/clean-up-docker-images  
Updated: 2026-08-31

Docker never reclaims anything on its own. Every image you pulled, every container you stopped and every volume you forgot is still on the disk, and one command removes almost all of it.

Docker keeps things because it cannot know what you still want. Stopped containers are kept so you can read their logs, images are kept so a rerun does not re-pull them, and anonymous volumes outlive the container that created them.

The result is a disk that fills gradually and then all at once. Here is what is taking the space, and the smallest command that gets it back.

## Find out what is using the space

```
docker system df        # summary by type
docker system df -v     # every image, container and volume, individually
```

> Start here rather than with a prune. The verbose form tells you whether the problem is fifty forgotten images or one enormous volume, and those have different answers.

## The one command most people want

```
# Everything not currently in use, including images no container references
docker system prune -a --volumes
```

> This is destructive in a specific way worth understanding: `-a` removes every image not used by a running or stopped container, so images you pull rarely and want to keep will be re-downloaded. `--volumes` removes unused volumes, which is where database data usually lives on a development machine.

## Or clean one thing at a time

| Command | Removes | Keeps |
| --- | --- | --- |
| `docker container prune` | All stopped containers | Running containers and their images |
| `docker image prune` | Dangling images only, the untagged leftovers of rebuilds | Every tagged image |
| `docker image prune -a` | Every image no container references | Only images in use right now |
| `docker volume prune` | Volumes no container references | Volumes attached to any container, running or not |
| `docker network prune` | Networks with no containers attached | Default networks |

> The distinction between `image prune` and `image prune -a` is the one that catches people. The plain form is safe and usually frees less than you hoped; the `-a` form frees a lot and takes images you may want.

## Filters, for when prune -a is too much

```
# Only images older than a week
docker image prune -a --filter "until=168h"

# Keep anything you have labelled
docker image prune -a --filter "label!=keep=true"

# One specific image
docker rmi ghcr.io/acme/app:old-tag
```

> The `until` filter is the safe default for a scheduled cleanup: it reclaims the long tail without touching what you built this week.

## What this does not cover

- The build cache. It is not an image and prune commands aimed at images will not free it. See clearing the Docker build cache.
- Space inside a running container. That belongs to the container and is only freed when the container is removed.
- On Docker Desktop, the virtual disk does not always shrink after a prune even though the space is free inside it. That is a separate problem with a separate fix.

## Measure before you optimise

Pipeline optimisation usually targets the step people assume is slow. Get the real per-step timings first, because the answer is frequently dependency install or a cold cache rather than the build itself.

```Terminal
# per-job timings for the last 20 runs
gh run list --limit 20 --json databaseId,conclusion,createdAt,updatedAt \
  --jq '.[] | "\(.conclusion)\t\(.createdAt)\t\(.updatedAt)"'

# per-step timing inside one run
gh run view <run-id> --log | grep -E "^\S+\s+.*Run |##\[group\]" | head -40
```

> Compare a cold-cache run against a warm one. If most of the difference is install time, caching is the win; if it is not, caching will change nothing and the build itself needs the attention.

## FAQ

### How do I clean up Docker and free disk space?

Run `docker system df` to see where the space has gone, then `docker system prune -a --volumes` to remove everything not currently in use. If that is too aggressive, prune images, containers and volumes separately, or add `--filter "until=168h"` so only older items are removed.

### What is the difference between docker image prune and docker image prune -a?

The plain form removes only dangling images, the untagged leftovers left behind when you rebuild a tag. The `-a` form removes every image that no container references, including tagged images you may still want. The first is safe and usually frees little; the second frees a lot and can cost you a re-pull.

### Does docker system prune delete my data?

Not by default. It leaves volumes alone unless you pass `--volumes`, and it never touches running containers. With `--volumes` it does remove unused volumes, which is where a development database usually lives, so check `docker volume ls` before adding that flag.

### How do I remove all Docker images at once?

`docker rmi -f $(docker images -aq)` removes every image, forcefully. `docker image prune -a` is the better command in almost every case, because it stops short of images something is actually using.

### Can I automate Docker cleanup?

Yes, and a scheduled `docker system prune -af --filter "until=168h"` is the common form: it reclaims anything untouched for a week and leaves recent work alone. On CI runners that are reused between jobs this is worth setting up before disk-full errors start failing builds.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
