How to find what takes the space: images, stopped containers, volumes and huge logs. What is safe to clean and what is not.
The server runs on Docker, and one day a build fails with "no space left on device" while df -h shows 100%. Docker does not lose space: it keeps images, stopped containers, volumes, build cache and logs, and deletes none of it until you say so. Below is the order of work: how to see in a couple of minutes what has grown, what is safe to clean, where it is easy to lose data and how to keep the disk from filling up again.
In short. See the picture with
docker system df, and check log size separately:sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log | sort -h | tail'. Clean in order:docker container prune, thendocker image prune, and only then think about volumes. A plaindocker volume prunedid not touch named volumes in our test (Docker 29.1.3), butdocker volume prune -adeletes every volume no container is attached to, together with its data. To stop logs from growing forever, set rotation in/etc/docker/daemon.json.
Docker has five sources of growth. Four show up in docker system df, and the fifth does not show up at all - and it is the one that often turns out to be guilty.
What | Where it comes from | Shown in docker system df | How to clean |
|---|---|---|---|
Images | every image download and every build; the old version under the same tag stays behind as an image without a tag | yes, the Images row | docker image prune |
Stopped containers | everything you ran without --rm and did not delete; inside is the data written while it ran | yes, the Containers row | docker container prune |
Volumes | databases and uploads you moved out of the container; they stay after the container is deleted | yes, the Local Volumes row | docker volume prune |
Build cache | intermediate layers of docker build | yes, the Build Cache row | docker builder prune |
Container logs | everything a program prints to stdout and stderr | no | truncate and rotation in daemon.json |
First, see how much of the disk is used overall and compare it with the Docker directories. If the containerd image store is on (as on our test server), images live not in /var/lib/docker but in /var/lib/containerd, so we check both. du -sh prints the total size of a directory, and sudo is needed because the files inside belong to root.
How we got the output. To avoid touching real data we started a separate test Docker daemon with an empty store on a HIP-HOSTING VPS running Ubuntu 24.04 and built a "cluttered" server in it: a few images, two builds under one tag, three stopped containers with data, two volumes and a container that writes to its log without stopping. Your sizes will differ, the order of work is the same. One note on units: du -h counts in multiples of 1024 while docker uses decimal MB and GB, so numbers from the two commands differ a little. Long identifiers in the outputs are shortened. The docker commands here and below are written without sudo: we assume you are root or in the docker group.
8.9 GB of 38 are used, and Docker holds about 2.7 GB of it: 1.3 GB in /var/lib/docker and 1.4 GB in /var/lib/containerd. In our case the images ended up in containerd; with the classic store they usually sit in /var/lib/docker/overlay2. If both directories are small on your server and the disk is full, Docker is not to blame - see the last section.
The docker system df command splits Docker's space into categories. The SIZE column shows how much a category takes, and RECLAIMABLE shows how much of it can be returned because nothing uses it.
Row by row. Images has six images and 1.475 GB; in the RECLAIMABLE column Docker estimated 256.7 MB as returnable (we did not dig into how it counts that; below, image prune actually freed 157.3 MB and image prune -a 313.8 MB). Containers has four stopped containers, 251.7 MB, all of it reclaimable (100%). Local Volumes has two volumes, 335.5 MB together, both "free" because no container is attached to them. The Build Cache row is empty for us: the images were built with the legacy builder, and no cache showed up in this row on the test server. With BuildKit you will most likely see a non-zero value.
The -v flag shows every image, container and volume separately, but its output is very wide, so we do not print it. Let us look at the images with the ordinary command. Note myapp:1.0 and the image without a name of the same 327 MB: we built the image under the tag myapp:1.0 twice with a changed file. The tag went to the new build and the old one lost its name. Such images are called "dangling": nothing uses them, but they take space. In Docker 29 the plain docker images did not list that image, while the filter -f dangling=true shows it as <untagged> (other versions may show <none>):
The SIZE column of docker ps -a --size shows how much space stopped containers hold (we cut the output to four columns with --format). Here are three containers of 83.9 MB each: we ran them so that each wrote 80 MB of data, and they finished but were not deleted.
Remember this line from the output above: Containers ... 251.7MB. It is the size of the writable layers of stopped containers, and logs are not part of it. A log is a file where Docker puts everything a program prints to stdout and stderr. By default it is named <id>-json.log and grows without limit.
The biggest logs can be found with du and sorting. The asterisks in the path stand for all containers at once, sort -h orders by size taking units (M, G) into account, and tail -3 keeps the three biggest. All of it is wrapped in sudo sh -c '...' because the directory is closed to a normal user and the shell expands the asterisks: without the wrapper it expands them as you and finds nothing.
The chatty container is a test program that prints a "request handled" line in a loop. In 25 seconds it produced a 947 MB log. A real application writes slower, but the principle is the same: a chatty service without rotation collects gigabytes over weeks, and docker system df will not show it. In our test 947 MB of the 1.3 GB in /var/lib/docker were that one log:
How to read it: containers in this directory holds container logs and service files, volumes holds volumes. If you have hundreds of megabytes or gigabytes in containers, look for a big *-json.log.
We go from what is almost risk-free to where data can be lost. After each step it is useful to look at df -h /.
docker container prune deletes all stopped containers. Before that, review the list with docker ps -a: everything a program wrote inside a container (not into a volume) disappears with it. The -f flag removes the "are you sure?" question. We use it in the examples so the output comes without a dialogue, but on a production server, until you are used to it, run without it and answer the question by hand.
The command reported 251.7 MB freed, yet the disk got 1.1 GB lighter at once: together with the chatty container its 947 MB log was deleted. Those 947 MB never made it into Docker's report.
docker image prune without flags deletes only "dangling" images, the very ones without a name. It is safe unless you need to roll back to the previous build: an image without a tag can only be run by its ID, and it may be the only copy of the earlier version.
docker builder prune deletes the cache of intermediate layers. It is safe, but the next build will be slower because the cache has to warm up again. We had no cache, and the command returned 0B.
docker image prune -a goes further: it deletes every image that no container uses, tagged ones included. There is no danger to data, but the images will have to be downloaded again, and if the registry is unavailable or you built the image locally without the sources, there will be nothing to restore it from. So for local builds make sure you still have the sources or the Dockerfile.
Volumes hold what Docker was installed for: databases, files uploaded by users, certificates. A container can be recreated, the data of a volume cannot. There are two rules.
First. According to the Docker documentation a plain docker volume prune deletes only anonymous volumes, the ones created automatically without a name. In our test (Docker 29.1.3) named volumes stayed in place after it and it freed nothing (the first command in the output below); we did not test anonymous volumes.
Second. The -a flag changes everything: docker volume prune -a deletes every volume no container is attached to right now. If the database container is already deleted (for example you ran container prune or compose down earlier, and the order in this article is exactly that), its volume counts as "unused" and is deleted. We created the volumes pgdata and old-uploads, and with -a both disappeared, although the first one "played" a database:
So before -a always look at docker volume ls and ask yourself about every volume whose data it is. If in doubt, copy the volume first and clean afterwards. On older Docker versions the volume commands may behave differently, so on an unfamiliar server check docker --version and the volume list first.
docker system prune is "everything at once": it deletes stopped containers, unused networks, images without a tag and build cache. The -a flag adds all unused images, and --volumes adds volumes. We checked on the test daemon what exactly happens. We created a volume keepme with data, a stopped container and an image:
Without flags the command deleted only the stopped container. With -a --volumes the image went away, while the named volume keepme stayed in place: in our Docker version --volumes did not touch the named volume. But do not count on this as protection. On another version or in a script with docker volume prune -a the data will be gone, so on servers with databases we advise cleaning with separate commands, as described above.
If a log ate the space, there are two actions: remove what has piled up and keep it from piling up again.
To empty a log without deleting the container, use truncate -s 0. The command makes the file empty while the file itself stays in place, so Docker usually keeps writing to it. We checked this on a stopped container: before the command the file weighed 766 MB, after it 0. For a running container the method usually works too, but we did not check that. If the log holds something you need to read, save it elsewhere first.
To keep logs from growing, set rotation. Docker stores a log in files, and you can limit the size of one file and the number of files. Create or extend /etc/docker/daemon.json (on the test daemon the file lay in a separate folder, the path in the output below is replaced with the usual one): max-size is the maximum size of one file, max-file is how many files to keep. These settings apply to new containers: already created ones keep writing by the old rules until you recreate them. After editing, restart the Docker service with sudo systemctl restart docker, but note that it stops running containers unless live-restore is on.
We tested the setting: we restarted the test daemon with such a file and started a new "chatty" container for 25 seconds. The log did not grow without limit but split into three files, 23 MB in total:
The same options can be set for one container with the keys --log-opt max-size=10m --log-opt max-file=3 in docker run, or with a logging: block in a Compose file (more on Compose in our article on several services in Docker Compose). Those two ways come from the Docker documentation; on the test server we checked only daemon.json.
--rm so they delete themselves when they finish.docker system df regularly and keep it in your usual server checklist. Growth of the RECLAIMABLE value is a signal to remove the extra.If /var/lib/docker and /var/lib/containerd are small while the disk is full, look elsewhere. This command shows the biggest first-level directories without crossing into other file systems: sudo du -xh --max-depth=1 / | sort -h | tail. The -x flag stops du from leaving one file system, and --max-depth=1 shows only the top level. If bytes are free but files cannot be created, check inodes: see our article on "No space left on device" when there is free space. And if the server is slow along with the disk, start with diagnosing a slow server.
Without flags, according to the documentation, it deletes stopped containers, unused networks, images without a tag and build cache; in our test volumes stayed in place after it. But deleting stopped containers erases what was written in them, so look at docker ps -a first.
Most often because docker system df does not count container logs. Check the size of the *-json.log files in /var/lib/docker/containers.
In our test (Docker 29.1.3) the named volume with data stayed in place after the command with --volumes. But docker volume prune -a deletes all unused volumes, named ones included, so run it only after checking the list.
With sudo truncate -s 0 path_to_the_log. The path is shown by docker inspect -f '{{.LogPath}}' container_name.
No, the settings in daemon.json apply to containers created after it. Old ones have to be recreated.
docker system df, logs are not.sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log | sort -h | tail'; they often turn out to be the culprit.docker volume prune -a deletes all unused volumes together with their data, including the volume of a database whose container is already deleted./etc/docker/daemon.json (max-size, max-file); the setting applies to new containers.du -xh --max-depth=1 / and check inodes.