Docker & ContainersXVII Β· LoggingRotation
Log rotation and disk consumption
What you'll learn
- State the shipped defaults for the json-file driver and why they are unbounded
- Locate a container log file on the host and measure it
- Configure rotation at the daemon level and per container, and verify it took effect
- Explain why a daemon-level change leaves existing containers uncapped
- Reclaim space from a running container safely, and know why `rm` does not
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
Dockerβs default logging driver is json-file. Its default max-size is
-1, which means unlimited, and its default max-file is 1, which means
there is nothing to rotate into. Put those two together and you get the
sentence that this whole lesson exists to make unforgettable:
A stock Docker installation writes container logs to disk with no upper bound whatsoever.
Not βa generous boundβ. Not βa bound you should tuneβ. No bound. A container
that logs one kilobyte per second writes 86 MB a day, 2.6 GB a month, and
31 GB a year, into a file nobody is watching, until the filesystem holding
/var/lib/docker is full and every workload on the host starts failing at
once.
This is the most common self-inflicted Docker outage there is, and it is entirely a configuration default.
The defaults, stated exactly
$ docker info --format '{{.LoggingDriver}}'json-fileIllustrative output
| Option | json-file default | local default |
|---|---|---|
max-size | -1 (unlimited) | 20m |
max-file | 1 | 5 |
compress | false | true |
The right-hand column is the interesting one. The local driver β which is
also built in, also stores logs on the host, and is also readable with
docker logs β ships with rotation on by default: five files of 20 MB
each, compressed, for a hard ceiling of 100 MB per container. Same engine,
same maintainers, opposite default.
The reason json-file is unbounded is compatibility. Its on-disk format is a
documented, greppable, one-JSON-object-per-line file that a whole ecosystem of
log shippers reads directly, and capping it by default would have silently
truncated those pipelines on upgrade. That is a defensible engineering
decision and a terrible operational one, and you inherit the consequences.
Where the files actually are
Every json-file log is one file per container, under the containerβs own
directory in the daemonβs state tree:
CONTAINER=web
CID=$(docker inspect --format '{{.Id}}' "$CONTAINER")
LOGFILE="/var/lib/docker/containers/$CID/$CID-json.log"
sudo ls -lh "$LOGFILE"*
sudo du -ch /var/lib/docker/containers/"$CID"/ | tail -1The trailing * matters: it picks up -json.log.1, -json.log.2 and, if
compress is on, -json.log.1.gz. Measuring only the active file on a
rotating container reports a fraction of the real consumption.
There is also a much faster way to find the offender across the whole host, and it is the command worth memorising:
$ sudo du -sh /var/lib/docker/containers/* | sort -h | tail -512M /var/lib/docker/containers/1f2a9c4e8b31...
48M /var/lib/docker/containers/7d3e5a90c2ff...
104M /var/lib/docker/containers/b81c40de77a2...
1.1G /var/lib/docker/containers/44ab9f0e13cd...
37G /var/lib/docker/containers/9c7e2b58af04...Illustrative output
Note what this is not. It is not docker system df. Container log files live
outside the containerβs writable layer, so the Containers row of
docker system df β which counts writable layers only β reports kilobytes for
the container in the last line of that output. Dockerβs own disk accounting
cannot see the 37 GB. You have to look with du.
Configuring rotation
Two places, and you need to understand the difference before you touch either.
Daemon-wide default
/etc/docker/daemon.json sets the default for containers created after
the daemon reloads:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"compress": "true"
}
}
Every value in log-opts is a string, including the numeric ones.
"max-file": 3 without quotes is a type error and the daemon will refuse to
start with it. That is a five-minute outage if you edit this file on a
production host and restart without validating first.
sudo python3 -m json.tool /etc/docker/daemon.json > /dev/null && echo 'JSON ok'
sudo systemctl reload docker
docker info --format '{{.LoggingDriver}}'Per container
A per-container setting overrides the daemon default and is fixed at creation time:
docker run -d --name chatty \
--log-driver json-file \
--log-opt max-size=50m \
--log-opt max-file=2 \
nginx:1.27-alpineIn Compose, the same thing lives under logging::
services:
chatty:
image: nginx:1.27-alpine
logging:
driver: json-file
options:
max-size: "50m"
max-file: "2"
The half-fix that everybody does
Here is the failure that turns a fixed problem back into an outage.
Which is why the verification below matters more than the configuration above.
Verification that can fail
Do not check that you edited a file. Check what the container is actually using, which is a different question with a different answer:
$ docker ps -q | xargs -r docker inspect \
--format '{{.Name}} {{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}'/api json-file map[max-file:3 max-size:10m]
/worker json-file map[max-file:3 max-size:10m]
/legacy-etl json-file map[]
/redis json-file map[max-file:3 max-size:10m]Illustrative output
/legacy-etl is the finding. Its Config map is empty, so it inherited the
daemon default as it stood when the container was created β before you fixed
it. Recreate that one.
This command distinguishes working from broken, which βrun it and seeβ does not. Wire it into a nightly check and the second incident never happens.
CONTAINER=chatty
CID=$(docker inspect --format '{{.Id}}' "$CONTAINER")
LOGFILE="/var/lib/docker/containers/$CID/$CID-json.log"
# Keep the tail as evidence before you destroy it.
sudo tail -c 2M "$LOGFILE" > "/var/tmp/$CONTAINER-last2M.json"
# Release the blocks without breaking the daemon's file descriptor.
sudo truncate -s 0 "$LOGFILE"
df -h /var/lib/dockerPlanning log volume
For a fleet of N containers each capped at C bytes, the worst case on-disk cost is the part you can actually guarantee:
Ceiling = N * max-size * max-file
Fifty containers at max-size=10m, max-file=3 is a hard ceiling of 1.5 GB
regardless of how loudly anything logs. That is the number to put on the
capacity plan, because it is the only one an application team cannot blow
through by shipping a debug log level.
Retention-based sizing β βN containers at L MB/day retained R daysβ β belongs to your central store, not to the host. The hostβs job is a fixed, guaranteed ceiling; the central storeβs job is retention. Confusing the two is how people end up sizing a host partition against a retention policy and then being surprised when a single misbehaving service consumes the whole budget.
Undoing it
Rotation settings have no destructive residue. To back out, remove log-opts
from daemon.json, reload, and recreate. The already-rotated files are gone
permanently β rotation deleted them, and nothing in Docker retains a copy β
which is the one irreversible part of the change and the reason to have
central collection in place before you turn aggressive rotation on.
Knowledge check
Knowledge check Β· 5 questions
Q1. On a stock Docker Engine installation with no log-opts configured, what is the maximum size a container log file can reach?
Q2. With `max-size=10m` and `max-file=3`, what is the per-container ceiling?
Q3. You add max-size and max-file to /etc/docker/daemon.json and reload the daemon. Which containers are now capped?
Q4. A running container has a 30 GB json-file log and the disk is at 100%. Which statements are true? Select all that apply.
Q5. A container configured with the fluentd logging driver still consumes local disk on the Docker host by default.
Passing score: 75%. Answers are checked in this browser.