Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Rotate Docker logs and reclaim disk space

By updated

Improve this page

When a log grows without end or a write fails, the temptation is to delete things at random, and you risk erasing data that is still useful. This lesson shows you how to cap a container's logs, check that the limit really applies, read disk usage by category and free a disposable file without recreating the container. The measurements come from a Debian 13 VM with Docker Engine 29.9.2 and Compose 5.6.1. The gestures that modify the system daemon, such as changing daemon.json, were measured on a throwaway VM: do not replay them on a shared host.

What you will learn

Each gesture is checked against the real state of the engine, not only against a configuration file.

  • You read the active driver, the container's options and the path of its log.
  • You check that the files rotate after a write that exceeds their maximum size.
  • You tell an option set on the container apart from a daemon default value.
  • You measure images, containers, volumes and cache, then compare what docker system df announces with the space actually returned, before choosing what can be deleted.
  • You spot a shortage of bytes or inodes (the entries that allow creating files) and free the space without replacing the container.

Prerequisites

You must know how to start a container and read its configuration. The lesson Configurer le daemon Docker (daemon.json) (in French) explains the role of daemon.json, the daemon options and how to restart it.

The outputs below were collected on November 4, 2026 on the reference VM. The demonstration image is pinned to its digest:

Terminal window
IMAGE="alpine:3.23@sha256:ff7f0c727dd796aa36787bfcd9a64bf35a6ce72a73451b3359fde849b6703d63"
mkdir -p ~/journaux-disque/compose
cd ~/journaux-disque
pwd
/home/ansible/journaux-disque

Run the blocks of this lesson in order and in a single terminal. They reuse the variables IMAGE, R and SPACE_ID, the current directory and, during the cache measurement, DOCKER_HOST. In another terminal these variables do not exist: define them again before continuing.

The ~/journaux-disque folder contains only the files created for this demonstration. The examples expose no port and use names different from those of the two labs.

Which driver writes the logs, and where?

The logging driver decides how Docker keeps a container's standard output and standard error. On this VM, the default driver was json-file, which writes one JSON file per container (see the configuration and the json-file driver). This driver does not cap the logs by default: the following test produced a 172,298-byte file with no rotation option.

Terminal window
docker info --format '{{.LoggingDriver}}'
docker run -d --name c35b-log-open --network none "$IMAGE" \
sh -c 'i=0; while [ "$i" -lt 160 ]; do printf "%05d:" "$i"; printf "%01000d\n" 0; i=$((i+1)); done'
printf 'open_exit='
docker wait c35b-log-open
docker inspect c35b-log-open \
--format 'type={{.HostConfig.LogConfig.Type}} options={{json .HostConfig.LogConfig.Config}} path={{.LogPath}}'
OPEN_LOG=$(docker inspect c35b-log-open --format '{{.LogPath}}')
printf 'open_log_size='
sudo stat -c '%s' "$OPEN_LOG"
printf 'open_log_files='
sudo find "$(dirname "$OPEN_LOG")" -maxdepth 1 -type f -name "$(basename "$OPEN_LOG")*" | wc -l
json-file
open_exit=0
type=json-file options={} path=/var/lib/docker/containers/f4a0128ad251dc71241206adfe3126b3206c5ab9f7dd55ac3710a6d6d9f1716f/f4a0128ad251dc71241206adfe3126b3206c5ab9f7dd55ac3710a6d6d9f1716f-json.log
open_log_size=172298
open_log_files=1

In the output, look at two things. options={} says that no limit is configured, and path= gives the location of the log, in a folder named after the container's identifier. docker inspect reads them from HostConfig.LogConfig and LogPath. The file belongs to root: the measurement read its size with sudo, without changing its permissions.

The local driver is an alternative to json-file. The documentation of the local driver describes its internal storage and its rotation. The test further down only checks that docker logs reads its logs, without measuring its default limits.

How do you check that a rotation is in effect?

Three options control rotation: max-size sets the maximum size of a file, max-file the number of files kept and compress compresses the files that have already rotated. You set them on the container, without touching the daemon's default driver. They are fixed at creation: docker run or docker create receives them, and an existing container does not change them. To modify a limit, you recreate the container.

Terminal window
docker run -d --name c35b-log-capped --network none \
--log-driver=json-file --log-opt max-size=36k --log-opt max-file=4 \
--log-opt compress=true \
"$IMAGE" sh -c 'i=0; while [ "$i" -lt 160 ]; do printf "%05d:" "$i"; printf "%01000d\n" 0; i=$((i+1)); done'
printf 'capped_exit='
docker wait c35b-log-capped
docker inspect c35b-log-capped \
--format 'type={{.HostConfig.LogConfig.Type}} options={{json .HostConfig.LogConfig.Config}}'
CAPPED_LOG=$(docker inspect c35b-log-capped --format '{{.LogPath}}')
printf 'capped_log_files=%s\n' "$(sudo find "$(dirname "$CAPPED_LOG")" -maxdepth 1 -type f -name "$(basename "$CAPPED_LOG")*" | wc -l)"
capped_exit=0
type=json-file options={"compress":"true","max-file":"4","max-size":"36k"}
capped_log_files=4

The test counted four files on the host: the current log and three compressed rotated files. The options displayed by inspect do not prove that the rotation took place, they only say what was requested. You have to exceed the ceiling, then count the files in the folder given by LogPath.

Without privileges, docker logs also gives the proof. docker logs c35b-log-capped | wc -l returned 126 lines out of the 160 written, and the first line read back started with 00034. The first 34 lines disappeared with the oldest file: the limit applies, and whatever exceeds the number of files is lost.

Compose accepts the same options under logging.options, and their values are strings in the YAML file. Save the following service in ~/journaux-disque/compose/compose.yaml. It sets a ceiling different from that of the previous command. docker compose config -q validates it, then up -d runs it.

services:
journal-writer:
image: ${IMAGE}
command:
- sh
- -c
- 'i=0; while [ "$$i" -lt 96 ]; do printf "%05d:" "$$i"; printf "%01000d\n" 0; i=$$((i+1)); done'
logging:
driver: json-file
options:
max-size: "24k"
max-file: "4"
compress: "true"
Terminal window
IMAGE="$IMAGE" docker compose -p c35b-log-compose -f compose/compose.yaml config -q
IMAGE="$IMAGE" docker compose -p c35b-log-compose -f compose/compose.yaml up -d
IMAGE="$IMAGE" docker compose -p c35b-log-compose -f compose/compose.yaml wait journal-writer
COMPOSE_CID=$(IMAGE="$IMAGE" docker compose -p c35b-log-compose -f compose/compose.yaml ps -aq journal-writer)
COMPOSE_LOG=$(docker inspect "$COMPOSE_CID" --format '{{.LogPath}}')
printf 'compose_log_files=%s\n' "$(sudo find "$(dirname "$COMPOSE_LOG")" -maxdepth 1 -type f -name "$(basename "$COMPOSE_LOG")*" | wc -l)"
compose_log_files=4

Four files are therefore present with Compose too. The daemon's default driver stayed json-file after these two configurations, since they only touch their own container. The documentation lists the other available options.

With the local driver, the test checks that docker logs returns the output and that inspect returns an empty LogPath: there is no JSON file to count.

Terminal window
docker run -d --name c35b-log-local --network none --log-driver=local \
"$IMAGE" sh -c 'printf "local-driver-check\n"'
printf 'local_exit='
docker wait c35b-log-local
docker inspect c35b-log-local \
--format 'type={{.HostConfig.LogConfig.Type}} options={{json .HostConfig.LogConfig.Config}} path={{.LogPath}}'
docker logs --tail 1 c35b-log-local
printf 'daemon_default_after_per_container_tests='
docker info --format '{{.LoggingDriver}}'
local_exit=0
type=local options={} path=
local-driver-check
daemon_default_after_per_container_tests=json-file

When do the values in daemon.json apply?

The log-opts values in daemon.json become the default values of new containers, after the daemon restarts. Docker expects strings, including for sizes, file counts and booleans. Here is the configuration that was tested:

{
"log-driver": "json-file",
"log-opts": {
"max-size": "48k",
"max-file": "3",
"compress": "true"
}
}

In the diagram, look at the left branch: restarting the daemon does not modify a container that has already been created. Only a container created after it, or recreated, receives the new values.

After the daemon restarts, a container created before keeps empty options and an unbounded log, while a container created after inherits the log-opts. For an old container to inherit them, you must delete it with docker rm then recreate it with docker run.changes nothingapplymust be recreateddaemon.jsonlog-optsDaemon restartContainer created beforeoptions = {}unbounded logContainer created afterinherits the log-optsdocker rm,then docker runRecreated containerinherits the log-opts
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

The test gave three results:

  • The container created before the change kept options={} after the restart. Started again without being recreated, it wrote about 344,600 bytes into a single file, that is two runs of 172,300 bytes.
  • A new container received the three options and produced three files, including a current log of 26,923 bytes.
  • The old container, deleted then recreated, received a new identifier and the new default values.

For an existing container to inherit the new values, you therefore have to recreate it: restarting it is not enough. The test modified daemon.json, restarted the daemon, then restored the original configuration. On a real host, first read the existing configuration and the restart procedure described in Configurer le daemon Docker (daemon.json) (in French).

What should you measure before reclaiming space?

docker system df breaks down the disk usage by Docker into four categories: images, containers, volumes and build cache. docker system df -v then details the items in each category. The table summarizes what each line groups.

LineResource measured
ImagesLocal images, shared and unique sizes in the detailed view.
ContainersSpace associated with containers, including stopped ones.
Local VolumesLocal volumes and the space they occupy.
Build CacheImage build cache.

The RECLAIMABLE column describes a recovery potential, not a permission to delete without having examined the scope. The documentation of docker system df describes the summary and detailed views.

To measure without touching the system daemon, start an isolated secondary daemon. It has its own socket, its own --data-root and its own containerd namespace (--containerd-namespace and --containerd-plugins-namespace). Without these last two options, it would share the containerd image store with the system daemon and would show its images in docker image ls. You then build an image that leaves some cache, without a network since this daemon has no bridge.

Two precautions frame the launch. sudo -v asks for your password before dockerd goes to the background: without it, sudo could wait for an input you cannot see. Then, the loop waits for the socket for 30 seconds at most, and the script prints a message pointing to the dockerd.log log if the daemon did not start.

Terminal window
R=/tmp/c35b-sec
mkdir -p "$R/ctx"
printf 'FROM %s\nRUN dd if=/dev/urandom of=/blob1 bs=1M count=8\nRUN dd if=/dev/urandom of=/blob2 bs=1M count=8\n' "$IMAGE" > "$R/ctx/Dockerfile"
sudo -v
sudo dockerd --host "unix://$R/d.sock" --data-root "$R/data" --exec-root "$R/exec" \
--pidfile "$R/d.pid" --bridge none --iptables=false --ip-masq=false \
--containerd-namespace c35b-ns --containerd-plugins-namespace c35b-plug \
> "$R/dockerd.log" 2>&1 &
export DOCKER_HOST="unix://$R/d.sock"
for i in $(seq 1 30); do [ -S "$R/d.sock" ] && break; sleep 1; done
[ -S "$R/d.sock" ] || echo "dockerd did not start, read $R/dockerd.log"
sleep 3
sudo -E docker pull -q "$IMAGE"
sudo -E docker build --network none -q -t c35b-cache-img "$R/ctx"
sleep 3
sudo -E docker system df
sudo -E docker system df -v
sudo -E docker builder prune --all --filter until=1s --force
sudo -E docker system df

The useful lines of the measurement, before then after the deletion:

Build Cache 5 0 46.52MB 12.29kB
Total: 46.52MB
Build Cache 0 0 0B 0B

Look at the RECLAIMABLE column. Before the deletion, it showed 12.29 kB, while docker builder prune --all printed Total: 46.52MB. Neither of these two figures measures the space returned to the disk: the Total adds up the cache entries removed, and RECLAIMABLE leaves out everything that is shared. docker system df -v shows this sharing with its SHARED column:

CACHE ID CACHE TYPE SIZE CREATED LAST USED USAGE SHARED
nyf6pjznkc80 source.local 8.19kB 4 seconds ago 3 seconds ago 1 false
w4p1wknca9ib source.local 4.1kB 4 seconds ago 3 seconds ago 1 false
k8ydgs149k1x regular 12.9MB 4 seconds ago 3 seconds ago 1 true
1ml2p8gkqeaj regular 16.8MB 3 seconds ago 3 seconds ago 1 true
ccvs2ctqyta1 regular 16.8MB 3 seconds ago 3 seconds ago 1 true

With the containerd store, the three regular layers are shared with the alpine image and with the built image. As long as this image exists, RECLAIMABLE does not count what is shared: it is 12.29 kB, the sum of the two source.local entries. Shared does not mean kept, though. Measured on the throwaway VM with df -B1 --output=used /var/lib/containerd, the prune returned about 26 MB: RECLAIMABLE underestimated the space returned, and the Total overestimated it.

A second order of measurement gives the same direction of gap. When the image is deleted before the prune, the two 16.8 MB layers move to SHARED false and RECLAIMABLE shows 33.59MB. The prune nevertheless returned about 43 MB: here again, RECLAIMABLE announced less than the space actually returned.

Only df settles it, because it measures the disk and not an estimate from the engine. These measurements hold for this daemon and this image. Keep the method rather than the figures: read df before and after, and take neither RECLAIMABLE nor the Total of prune as the space you are going to recover.

The sleep 3 before the measurement matters. Launched right after the build, the until=1s filter removed nothing (Total: 0B), because the cache had been used less than a second earlier. This value only served to make the cache eligible during the test: do not copy it onto a shared host. Choose a threshold according to your needs, after reading docker system df -v. The documentation describes the available filters.

Does a build redone under the same tag leave a dangling image, that is, an image that has lost its name? Not in the case measured. The test below builds c35b-cache-img twice with different content in the secondary daemon, then lists the images, hidden entries included. The last command goes through ctr, the command-line client of containerd, which reads the c35b-ns namespace of the secondary daemon directly.

Terminal window
printf 'FROM %s\nRUN echo un > /f\n' "$IMAGE" > "$R/ctx/Dockerfile"
sudo -E docker build --network none -q -t c35b-cache-img "$R/ctx"
printf 'FROM %s\nRUN echo deux > /f\n' "$IMAGE" > "$R/ctx/Dockerfile"
sudo -E docker build --network none -q -t c35b-cache-img "$R/ctx"
sudo -E docker image ls --filter dangling=true
sudo -E docker image ls -a
sudo ctr -n c35b-ns images ls -q
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
alpine@sha256:ff7f0c727dd796aa36787bfcd9a64bf35a6ce72a73451b3359fde849b6703d63 ff7f0c727dd7 13MB 3.94MB
c35b-cache-img:latest bb1e39de3365 12.9MB 3.86MB
docker.io/library/alpine@sha256:ff7f0c727dd796aa36787bfcd9a64bf35a6ce72a73451b3359fde849b6703d63
docker.io/library/c35b-cache-img:latest

On Docker 29.9.2 with the containerd store, the second build moved the tag without leaving a dangling image: the dangling=true list contains only its header, docker image ls -a shows a single c35b-cache-img, and ctr sees no moby-dangling@ entry. This finding holds for this case, a tag moved by a build. On the system daemon, docker image ls --filter dangling=true remains the command to run before deciding that an image is scrap, and the list was empty there.

The measurement is over. Remove the images from the secondary daemon, then go back to the system daemon with unset DOCKER_HOST: without it, the following commands would keep targeting the secondary daemon. Then stop that daemon and delete its folder.

Its containerd namespaces remain, c35b-ns and c35b-ns_history. They live in the system's containerd, not in the deleted folder, and ctr can remove them. The c35b-plug namespace, reserved for plugins, never appeared in ctr namespaces ls during the measurement: there is nothing to remove. A namespace can only be deleted when empty: the loop below first removes the leases (leases) that hold the layers, then the layers (snapshots) and the blobs (content). Once the loop is finished, ctr namespaces ls must no longer list c35b-ns or c35b-ns_history.

Terminal window
sudo -E docker image rm c35b-cache-img "$IMAGE"
unset DOCKER_HOST
sudo kill "$(cat "${R:?}/d.pid")"
while [ -S "$R/d.sock" ]; do sleep 1; done
sudo rm -rf "${R:?}"
for ns in c35b-ns c35b-ns_history; do
sudo ctr -n "$ns" leases ls -q | xargs -r -n1 sudo ctr -n "$ns" leases delete >/dev/null
for i in $(seq 1 15); do
[ "$(sudo ctr -n "$ns" snapshots ls | wc -l)" -le 1 ] && break
sudo ctr -n "$ns" snapshots ls | awk 'NR>1{print $1}' | xargs -r -n1 sudo ctr -n "$ns" snapshots rm >/dev/null 2>&1
sleep 1
done
sudo ctr -n "$ns" content ls -q | xargs -r sudo ctr -n "$ns" content rm >/dev/null
sudo ctr namespaces rm "$ns"
done
sudo ctr namespaces ls

How do you delete only the targeted resources?

A stopped container and a detached volume are not necessarily scrap. The demonstration created a stopped container carrying a label, another stopped container without that label and a container that uses a named volume. The filter of docker container prune removed only the container carrying the requested label.

Terminal window
docker volume create c35b-data-ref
docker run -d --name c35b-data-holder --network none \
--mount type=volume,src=c35b-data-ref,dst=/payload \
"$IMAGE" sh -c 'printf "stock=kept\n" > /payload/stock; sleep 7200'
docker run --name c35b-stop-target --label c35b.cleanup=lesson --network none "$IMAGE" true
docker run --name c35b-stop-unrelated --network none "$IMAGE" true
docker container prune --filter label=c35b.cleanup=lesson --force
docker inspect c35b-stop-unrelated \
--format 'unrelated_still_present={{.State.Status}}'
docker inspect c35b-data-holder \
--format 'data_holder_still_present={{.State.Status}} volume={{range .Mounts}}{{.Name}}{{end}}'
printf 'reference_data='
docker exec c35b-data-holder cat /payload/stock
docker volume inspect c35b-data-ref --format 'named_volume={{.Name}}'
Deleted Containers:
3343f432a41bcf8f66fe4a4b3b39c4aa816e7bf98f89ca01439bf5699642bb93
Total reclaimed space: 4.096kB
unrelated_still_present=exited
data_holder_still_present=running volume=c35b-data-ref
reference_data=stock=kept
named_volume=c35b-data-ref

The output lists only one deleted identifier. The stopped container without a label and the named volume, with its data, stayed in place. docker rm followed by a container name is even more precise: it targets only that one. Before widening a filter, read the documentation.

An anonymous volume, created without a name when a container declares a path with no source (or when the image declares a VOLUME instruction), can remain after its container is deleted. The demonstration found it again with dangling=true (volumes that no container uses), then deleted it by its exact name. A named volume is deleted the same way, after checking that no useful container uses it.

Terminal window
docker run --name c35b-anon-owner --network none --volume /cache "$IMAGE" \
sh -c 'printf "discard\n" > /cache/item'
ANON_VOLUME=$(docker inspect c35b-anon-owner \
--format '{{range .Mounts}}{{if eq .Destination "/cache"}}{{.Name}}{{end}}{{end}}')
docker rm c35b-anon-owner
docker volume ls --filter dangling=true
docker volume rm "$ANON_VOLUME"

docker system prune removes, at the scale of the daemon, the stopped containers, the unused networks, the dangling images and the build cache. With -a, it also deletes the images that no container uses, and --volumes adds the unused anonymous volumes. The --volumes option puts data at risk, because an anonymous volume can carry some: an image whose Dockerfile declares a VOLUME creates one for every container, and that volume may hold a database that nobody named. The volumes guide shows how to spot them. This wide scope does not replace an inventory of the data to keep: do not make it a routine gesture. The command was not run during the measurement; the documentation lists the resources concerned.

Where does ENOSPC come from, and how do you free space without recreating?

ENOSPC is the error "No space left on device": a write has no room left. The refusal may come from the container's writable layer, from a tmpfs (an in-memory filesystem with a capped size), from a volume or from the host's filesystem. To find which one, docker inspect shows the mounts and the tmpfs limits, df -h measures the bytes and df -i measures the inodes, that is, the entries needed to create files.

The demonstration attached a 2 MiB tmpfs to /scratch and the named volume to /persistent. It wrote a marker datum into the volume, then inspected the mounts and compared the filesystems.

Terminal window
docker run -d --name c35b-tmpfs-space --network none \
--mount type=volume,src=c35b-data-ref,dst=/persistent \
--tmpfs /scratch:rw,size=2m,nr_inodes=64 \
"$IMAGE" sleep 7200
docker exec c35b-tmpfs-space sh -c 'printf "inventory-line\n" > /persistent/inventory'
SPACE_ID=$(docker inspect c35b-tmpfs-space --format '{{.Id}}')
docker inspect c35b-tmpfs-space \
--format 'container_id={{.Id}} tmpfs={{json .HostConfig.Tmpfs}} mounts={{json .Mounts}}'
docker exec c35b-tmpfs-space df -h / /scratch /persistent
docker exec c35b-tmpfs-space df -i /scratch
DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}')
df -h "$DOCKER_ROOT"
df -i "$DOCKER_ROOT"
Filesystem Size Used Available Use% Mounted on
overlay 19.5G 2.1G 16.5G 11% /
tmpfs 2.0M 0 2.0M 0% /scratch
/dev/vda1 19.5G 2.1G 16.5G 11% /persistent
Filesystem Inodes Used Available Use% Mounted on
tmpfs 64 1 63 2% /scratch
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 20G 2.2G 17G 12% /
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 1302528 51235 1251293 4% /

Read the mounts one by one. /scratch is a 2.0 M tmpfs, with 64 inodes. /persistent shows the same /dev/vda1 disk as the host: a named volume consumes the host's disk here, whereas the tmpfs has its own limit. The host disk was not filled. First identify the full mount, then remove only the file you have confirmed is disposable.

The test wrote into /scratch until it hit the limit exactly. dd returned code 1, while df -i still showed 62 inodes available: the bytes were exhausted, not the inodes.

Terminal window
if docker exec c35b-tmpfs-space sh -c \
'dd if=/dev/zero of=/scratch/discard.bin bs=64k count=40'; then
echo 'fill_exit=0'
else
printf 'fill_exit=%s\n' "$?"
fi
docker exec c35b-tmpfs-space df -h /scratch
docker exec c35b-tmpfs-space df -i /scratch
docker exec c35b-tmpfs-space rm /scratch/discard.bin
docker exec c35b-tmpfs-space df -h /scratch
docker exec c35b-tmpfs-space sh -c \
'dd if=/dev/zero of=/scratch/new-write.bin bs=64k count=1'
docker exec c35b-tmpfs-space stat -c 'new_write_bytes=%s' /scratch/new-write.bin
printf 'useful_volume_data='
docker exec c35b-tmpfs-space cat /persistent/inventory
printf 'same_container_id='
test "$(docker inspect c35b-tmpfs-space --format '{{.Id}}')" = "$SPACE_ID" && echo yes
docker inspect c35b-tmpfs-space --format 'tmpfs_after={{json .HostConfig.Tmpfs}}'
dd: error writing '/scratch/discard.bin': No space left on device
33+0 records in
32+0 records out
2097152 bytes (2.0MB) copied, 0.000495 seconds, 3.9GB/s
fill_exit=1
Filesystem Size Used Available Use% Mounted on
tmpfs 2.0M 2.0M 0 100% /scratch
Filesystem Inodes Used Available Use% Mounted on
tmpfs 64 2 62 3% /scratch
Filesystem Size Used Available Use% Mounted on
tmpfs 2.0M 0 2.0M 0% /scratch
1+0 records in
1+0 records out
65536 bytes (64.0KB) copied, 0.000071 seconds, 880.3MB/s
new_write_bytes=65536
useful_volume_data=inventory-line
same_container_id=yes
tmpfs_after={"/scratch":"rw,size=2m,nr_inodes=64"}

After rm, /scratch went back to 0 bytes used and a new 65,536-byte write succeeded. The container's identifier stayed unchanged, its 2 MiB ceiling did not move and the named volume remained readable. Removing the disposable file was enough, without recreating the container.

The opposite case exists: a filesystem can run out of inodes while bytes remain free. In a second 8 MiB tmpfs limited to eight inodes, seven files were created, since the root directory of the tmpfs itself consumes an inode; the eighth failed, while df -h still showed 8 MiB available.

Terminal window
docker run -d --name c35b-tmpfs-inodes --network none \
--tmpfs /small:rw,size=8m,nr_inodes=8 \
"$IMAGE" sleep 7200
docker exec c35b-tmpfs-inodes sh -c \
'i=0; while [ "$i" -lt 32 ]; do if ! touch "/small/f$i"; then printf "create_failed_at=%s\n" "$i"; break; fi; i=$((i+1)); done; printf "files_created=%s\n" "$i"; df -h /small; df -i /small'
touch: /small/f7: No space left on device
create_failed_at=7
files_created=7
Filesystem Size Used Available Use% Mounted on
tmpfs 8.0M 0 8.0M 0% /small
Filesystem Inodes Used Available Use% Mounted on
tmpfs 8 8 0 100% /small

In the output, df -h announces 8.0 M available, and df -i shows 8 inodes used out of 8. When df -h shows free space but creating a file fails, look at df -i too. Never fill a host partition to reproduce a full disk: use a bounded filesystem.

When the host itself is full, the approach does not change, but the tools are those of Linux. The guide Check disk space and inodes on Linux details df, du and how to read inodes. There is also a trap that this lesson did not measure: a deleted file stays on the disk as long as a process keeps it open, so that du counts fewer bytes than df. The guide Control disk capacity and quotas shows how to find that process.

How do you clean up the demonstration?

First remove the Docker resources created for the lesson. Check that the ~/journaux-disque folder contains only the files of this demonstration before deleting its content. The secondary daemon has already been stopped since the section on measuring space.

Terminal window
IMAGE="alpine:3.23@sha256:ff7f0c727dd796aa36787bfcd9a64bf35a6ce72a73451b3359fde849b6703d63"
IMAGE="$IMAGE" docker compose -p c35b-log-compose \
-f "$HOME/journaux-disque/compose/compose.yaml" down
for name in c35b-log-open c35b-log-capped c35b-log-local c35b-data-holder \
c35b-stop-unrelated c35b-tmpfs-space c35b-tmpfs-inodes; do
docker rm -f "$name"
done
docker volume rm c35b-data-ref
docker image rm "$IMAGE"
cd ~
find "$HOME/journaux-disque" -depth -delete

This sequence deleted only the containers, the volume, the image and the folder created for the lesson. The resources that existed before the lesson stay in place: each command targets a name, never an entire category.

Put it into practice

The first lab asks you to cap the log of an identified service. Its tests check the driver and the declared options, then the effective rotation: the oldest lines must have been discarded and the volume kept must fit within the budget these bounds define. The service must also stay up. It uses values and a container different from those of the demonstration.

The second lab has you diagnose a bounded space and remove a disposable file. Its tests check that the original container and the useful data stay in place, that the bounded space remains a 16 MiB tmpfs without having been enlarged, that the disposable file is gone, then that a new write succeeds.

Key points

A reliable recovery starts by identifying the full resource and its owner.

  • You check the rotation on the host's files, not only in HostConfig.LogConfig, and you know that the lines beyond max-file are lost.
  • Log options are set at container creation, and the log-opts of daemon.json only apply to new containers: you recreate a container so that it inherits them.
  • You inspect the categories with docker system df before any deletion. Neither RECLAIMABLE nor the Total of prune gives the space returned: the first ignores the shared layers, the second adds up entries. Only df, read before and after, measures it.
  • You filter the stopped containers and you delete a volume by its name only after checking its data. docker system prune acts on the whole daemon, and --volumes can erase an anonymous volume that carries data.
  • You read df -h and df -i together before concluding that a filesystem is full: bytes and inodes run out separately.
  • You free a disposable file without recreating the container when its ceiling stays unchanged.

Knowledge check

Check that the essentials of this lesson have sunk in. The questions cover configuration choices, diagnosing a full disk and the scope of cleanups.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn