Skip to main content
RunBook Academy

Docker & ContainersIII · Installation & DaemonLogging

Log drivers at the daemon level

Intermediate⏱ ~28 mindockerjournalctl

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

Not yet marked complete on this device.

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

DriverWhere logs godocker logs reads it?
json-fileLocal JSON file under /var/lib/docker/containers/Yes
localLocal file in a compact custom formatYes
journaldThe systemd journalYes
noneNowhere; logs are discardedNo — nothing to read
syslogA syslog facility on the hostVia the dual-logging cache
fluentdFluentd forward inputVia the dual-logging cache
gelfA GELF endpoint such as Graylog or LogstashVia the dual-logging cache
awslogsAmazon CloudWatch LogsVia the dual-logging cache
gcplogsGoogle Cloud LoggingVia the dual-logging cache
splunkSplunk HTTP Event CollectorVia the dual-logging cache
etwlogsEvent Tracing for WindowsWindows 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

Configuration changedaemon.json
{
"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.

Service impact possiblemigrate
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:

Configuration changerotation
{
"log-driver": "json-file",
"log-opts": {
  "max-size": "10m",
  "max-file": "3",
  "compress": "true"
}
}
OptionDefaultNotes
max-size-1 (unlimited)The whole problem. Always set it.
max-file1Only effective when max-size is also set.
compressfalseCompresses 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:

Configuration changeper container
docker run -d --name web \
--log-driver json-file \
--log-opt max-size=50m \
--log-opt max-file=2 \
nginx:1.27
Read-only / Safefind the space
$ docker system df
TYPE            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.7GB

Illustrative output

A Containers size that large with only nine active is almost always logs held by exited containers.

Read-only / Safebiggest logs
sudo du -h /var/lib/docker/containers/*/*-json.log 2>/dev/null \
| sort -rh | head -10

Delivery 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.

Configuration changenon-blocking
{
"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-optDefaultMeaning
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:

Read-only / Safecache disabled
$ docker logs web
Error response from daemon: configured logging driver does not support reading

Illustrative 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.

Read-only / Safejournalctl
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 today

That 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.

Read-only / Safedaemon default
$ docker info --format 'default driver: {{.LoggingDriver}}'
default driver: journald

Illustrative output

Read-only / Safeaudit
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

  1. Q1. What is the documented default for the json-file driver `max-size` option?

  2. Q2. Which drivers support reading container output with `docker logs` natively, without the dual-logging cache? Select all that apply.

  3. Q3. Setting `"max-file": "5"` in log-opts without also setting `max-size` produces no rotation at all.

  4. 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?

  5. 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?

  6. Q6. Switching to journald removes the risk of silently losing container output.

Passing score: 75%. Answers are checked in this browser.