Docker & ContainersIII · Installation & DaemonLogging
Log drivers at the daemon level
What you'll learn
- Choose the right log driver for a given host
- Configure rotation that actually applies, at the daemon level and per container
- Explain why `docker logs` fails on some drivers and how dual logging fixes it
- Recognise a container hung by logging back-pressure
- Verify a driver change took effect rather than assuming it did
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
The default log driver is json-file. Every container writes to
/var/lib/docker/containers/<id>/<id>-json.log, and the documented default for
max-size is -1, meaning unlimited. That single default is responsible
for more Docker outages than any other setting on the host.
It is worse than “a container fills the disk”, because the file lives under
/var/lib/docker. When that filesystem fills, the daemon cannot write
container metadata, image layers, or its own state — so you do not lose one
container, you lose the host.
The drivers
| Driver | Where logs go | docker logs reads it? |
|---|---|---|
json-file | Local JSON file under /var/lib/docker/containers/ | Yes |
local | Local file in a compact custom format | Yes |
journald | The systemd journal | Yes |
none | Nowhere; logs are discarded | No — nothing to read |
syslog | A syslog facility on the host | Via the dual-logging cache |
fluentd | Fluentd forward input | Via the dual-logging cache |
gelf | A GELF endpoint such as Graylog or Logstash | Via the dual-logging cache |
awslogs | Amazon CloudWatch Logs | Via the dual-logging cache |
gcplogs | Google Cloud Logging | Via the dual-logging cache |
splunk | Splunk HTTP Event Collector | Via the dual-logging cache |
etwlogs | Event Tracing for Windows | Windows only |
For a Linux host with no central log pipeline, journald and local are both
better defaults than json-file, for different reasons: journald gives you
one place to correlate container output with kernel and unit messages, and
local gives you a compact on-disk format with sane rotation defaults and no
journald configuration to get wrong.
Switching the default driver
{
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}"
}
}Two things about applying this are commonly got wrong.
log-driver is not a reloadable key. It is not on the documented list of
options the daemon applies on SIGHUP. systemctl reload docker will exit 0 and
change nothing. You need systemctl restart docker, and with live-restore
enabled that costs no container downtime.
Existing containers keep their original driver. The driver is recorded in a container’s specification at create time, like everything else in the spec, and restarting a container replays that spec. To migrate a container you must recreate it.
docker ps -a --format '{{.Names}}' | while read -r c; do
drv=$(docker inspect --format '{{.HostConfig.LogConfig.Type}}' "$c")
echo "$c $drv"
done | grep -v ' journald$'docker restart will not do it. Under Compose, docker compose up -d will
recreate containers whose effective configuration changed, which is the
practical migration path for a stack.
Rotation that actually applies
For json-file, both options are needed, and one of them has a trap:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
"compress": "true"
}
}| Option | Default | Notes |
|---|---|---|
max-size | -1 (unlimited) | The whole problem. Always set it. |
max-file | 1 | Only effective when max-size is also set. |
compress | false | Compresses rotated files, not the active one. |
Setting max-file: "5" alone is a config change that looks like it did
something and did nothing at all, because with no max-size the file never
rolls. That is a real audit finding on real fleets.
Budget the worst case honestly: the cap is max-size multiplied by max-file
per container, and the active file counts. Two hundred containers at
10m/3 is roughly 6 GB of log before compression, on the same filesystem the
daemon needs to function.
Per-container overrides use the same names:
docker run -d --name web \
--log-driver json-file \
--log-opt max-size=50m \
--log-opt max-file=2 \
nginx:1.27$ docker system dfTYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 48 9 14.2GB 11.1GB (78%)
Containers 312 9 6.4GB 6.3GB (98%)
Local Volumes 21 7 3.1GB 1.9GB (61%)
Build Cache 190 0 8.7GB 8.7GBIllustrative output
A Containers size that large with only nine active is almost always logs held
by exited containers.
sudo du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null \
| sort -rh | head -10Delivery mode — the failure nobody expects
Docker supports two delivery modes via the mode log option: blocking, which
is the default, and non-blocking. In blocking mode the container’s write
to stdout does not complete until the log driver has accepted the message.
{
"log-driver": "fluentd",
"log-opts": {
"fluentd-address": "logs.example.com:24224",
"mode": "non-blocking",
"max-buffer-size": "4m"
}
}max-buffer-size sets the intermediate ring buffer used in non-blocking mode;
its default is 1m. When the buffer is full, messages are dropped. That is
a deliberate trade: you lose log lines rather than losing the service. For
almost every workload that is the right trade, and it should be a conscious
decision made in advance rather than at 03:00.
Dual logging — how docker logs still works
When you configure a remote driver, Docker also writes to a local local-driver
cache, so docker logs keeps working. That is dual logging, and it is on by
default.
| log-opt | Default | Meaning |
|---|---|---|
cache-disabled | "false" | Set "true" to turn the local cache off entirely |
cache-max-size | "20m" | Size of each cache file before rotation |
cache-max-file | "5" | Number of cache files retained |
cache-compress | "true" | Compress rotated cache files |
Those defaults mean roughly 100 MB per container of local cache on top of
whatever you ship remotely. On a host with hundreds of containers that is worth
budgeting, and cache-disabled is the lever — with the cost being this:
$ docker logs webError response from daemon: configured logging driver does not support readingIllustrative output
That message is unambiguous and worth memorising, because it turns a confusing “why can’t I see the logs” into a one-line answer: this container ships its logs somewhere else, and the local cache is disabled.
journald specifics
journald avoids the unbounded-file problem by handing rotation to the
journal, which is configured in /etc/systemd/journald.conf:
[Journal]
Storage=persistent
SystemMaxUse=2G
SystemKeepFree=4G
MaxFileSec=1month
Docker sets journal fields you can filter on: CONTAINER_ID (truncated to 12
characters), CONTAINER_ID_FULL, CONTAINER_NAME (the name at start time),
CONTAINER_TAG / SYSLOG_IDENTIFIER, and IMAGE_NAME.
CONTAINER=web
# All output from one container, by the name it had when it started.
sudo journalctl CONTAINER_NAME="$CONTAINER"
# A time window, following, with full structured fields.
sudo journalctl CONTAINER_NAME="$CONTAINER" --since "-30 min" -f -o json-pretty
# Everything from one image across every container that ran it.
sudo journalctl IMAGE_NAME=nginx:1.27 --since todayThat last query is the reason to pick journald. Correlating “every container that ran this image” or “container output interleaved with the kernel’s OOM message, in timestamp order” is a single command, and it is not possible at all with per-container JSON files.
Verification that can fail
Reading the config file back proves nothing. Ask the daemon and the container.
$ docker info --format 'default driver: {{.LoggingDriver}}'default driver: journaldIllustrative output
FAIL=0
for c in $(docker ps -q); do
name=$(docker inspect --format '{{.Name}}' "$c")
drv=$(docker inspect --format '{{.HostConfig.LogConfig.Type}}' "$c")
size=$(docker inspect --format '{{index .HostConfig.LogConfig.Config "max-size"}}' "$c")
if [ "$drv" = "json-file" ] && [ -z "$size" ]; then
echo "UNROTATED: $name" >&2
FAIL=1
fi
done
[ "$FAIL" -eq 0 ] && echo "all running containers have bounded logs"
exit "$FAIL"That is verification: it distinguishes a working configuration from a configuration that was written down, and it exits non-zero so a pipeline can gate on it.
Knowledge check
Knowledge check · 6 questions
Q1. What is the documented default for the json-file driver `max-size` option?
Q2. Which drivers support reading container output with `docker logs` natively, without the dual-logging cache? Select all that apply.
Q3. Setting `"max-file": "5"` in log-opts without also setting `max-size` produces no rotation at all.
Q4. A service using the fluentd log driver stops serving requests. CPU is idle, memory flat, no restarts, `docker ps` shows Up 6 days, and its own log file stops at a timestamp an hour ago. What is the most likely cause?
Q5. You change `log-driver` in daemon.json and run `systemctl reload docker`, then restart a container to pick up the change. What is the state afterwards?
Q6. Switching to journald removes the risk of silently losing container output.
Passing score: 75%. Answers are checked in this browser.