Skip to main content
RunBook Academy

Docker & ContainersXVII · LoggingDrivers

Logging drivers — json-file, local, journald, and the remote ones

Intermediate⏱ ~26 mindocker

What you'll learn

  • Name every driver Docker Engine ships and what each one targets
  • Choose between json-file, local and journald for host-side storage
  • Explain blocking versus non-blocking delivery and the failure each one produces
  • Predict whether `docker logs` will work for a given driver
  • Change a driver safely, knowing which containers the change reaches

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 logging driver is the component that receives every line your containers write and decides what to do with it. The default is json-file. Choosing something else is a real decision with three consequences most people only discover in production: what it costs on disk, whether docker logs still works, and whether a failure of the log destination becomes a failure of the application.

Every driver the engine ships

These are the exact strings accepted by --log-driver and by the log-driver key in daemon.json:

DriverDestinationNotes
json-fileHost file, one JSON object per lineDefault. No rotation unless configured.
localHost file, internal binary formatRotation on by default: 20 MB, 5 files, compressed.
journaldThe systemd journalMetadata as journal fields. Retention is journald’s problem.
syslogA syslog daemon, local socket or remoteTLS options available.
gelfGraylog Extended Log Format endpointGraylog, and Logstash with a GELF input.
fluentdA Fluentd or Fluent Bit forward endpointThe common self-hosted shipping path.
awslogsAmazon CloudWatch Logs
gcplogsGoogle Cloud Logging
splunkSplunk HTTP Event Collector
etwlogsEvent Tracing for WindowsWindows hosts only.
noneDiscardeddocker logs returns nothing at all.

There is no fluentbit driver. Fluent Bit implements the Fluentd forward protocol, so you point the fluentd driver at it — a distinction that matters the moment somebody types a driver name that the daemon rejects and the container refuses to start.

The two host-file drivers

json-file and local both write to the host. They are not interchangeable.

json-filelocal
FormatText, one JSON object per lineInternal binary, protobuf-based
Readable by third-party toolsYes — this is its whole pointNo; the daemon owns these files
max-size default-1 (unlimited)20m
max-file default15
compress defaultfalsetrue
Disk and CPU cost per lineHigher — JSON escaping, no compressionLower
docker logsNativeNative

Use json-file when something other than Docker reads the files directly — which is exactly the Fluent Bit or Vector tail-the-directory pattern, and the reason it remains the default.

Use local when nothing does. It is the better choice on a host that ships logs through a driver or an agent that talks to the API, and it is the only one of the two that is safe out of the box.

Configuration changeswitch to local
sudo tee /etc/docker/daemon.json.new > /dev/null <<'EOF'
{
"log-driver": "local",
"log-opts": {
  "max-size": "20m",
  "max-file": "5"
}
}
EOF

sudo python3 -m json.tool /etc/docker/daemon.json.new > /dev/null \
&& sudo mv /etc/docker/daemon.json.new /etc/docker/daemon.json \
&& sudo systemctl reload docker

docker info --format 'driver={{.LoggingDriver}}'

journald

On a host that already runs systemd, journald puts container logs in the same place as everything else on the machine, with structured metadata attached to every entry:

FieldContents
CONTAINER_IDTruncated 12-character ID
CONTAINER_ID_FULLFull 64-character ID
CONTAINER_NAMEThe name at container start
CONTAINER_TAG / SYSLOG_IDENTIFIERThe tag log-opt
IMAGE_NAMEThe image the container was created from
CONTAINER_PARTIAL_MESSAGESet on fragments of an over-long line

Those fields are queryable, which is the actual benefit:

Read-only / Safequery by container
$ journalctl CONTAINER_NAME=api --since '30 min ago' -o short-iso
2026-08-12T09:14:02+0000 host api[2841]: {"level":"info","msg":"listening on :8080"}
2026-08-12T09:14:44+0000 host api[2841]: {"level":"warn","msg":"upstream slow","ms":812}
2026-08-12T09:15:01+0000 host api[2841]: {"level":"error","msg":"upstream timeout"}

Illustrative output

Delivery mode: the trap that hangs applications

This is the most important paragraph in the lesson.

Every logging driver has a delivery mode, and the default is blocking. Blocking means exactly what it says: the container’s write to stdout does not complete until the driver has accepted the message.

For json-file, local and journald that is a local write and effectively free. For a remote driver it is a network operation, and if the remote endpoint is slow, unreachable, or applying backpressure, the write blocks the application.

The mitigation is mode=non-blocking, and it is an honest trade rather than a free win:

Configuration changenon-blocking delivery
docker run -d --name api \
--log-driver fluentd \
--log-opt fluentd-address=tcp://198.51.100.20:24224 \
--log-opt fluentd-async=true \
--log-opt mode=non-blocking \
--log-opt max-buffer-size=4m \
myapp:1.4.2
  • mode=non-blocking inserts a ring buffer between the container and the driver. The application’s write always completes.
  • max-buffer-size bounds that buffer. The default is 1m.
  • When the buffer is full, log messages are dropped. Silently, from the application’s point of view.

So the choice is: lose log lines during a logging outage, or lose the application during a logging outage. For anything user-facing, drop the lines. For an audit or compliance path where a missing line is itself the incident, blocking may genuinely be correct — but then the log endpoint is a hard dependency of the service and must be engineered like one, with redundancy and its own alerting.

Size max-buffer-size against your peak line rate and the outage you want to ride out. A service emitting 200 lines/second at ~300 bytes each generates about 60 KB/s, so 4m absorbs roughly a minute of total unavailability before it starts dropping. A minute is enough for an aggregator restart; it is not enough for a network partition.

Can you still read the logs back?

Mid-incident, this is the question that matters, and the answer changed with the introduction of dual logging.

  • json-file, local and journald support reading natively. docker logs reads the driver’s own store.
  • Every other driver has no read path of its own. For those, the daemon keeps a local cache in the local driver format, and docker logs reads the cache. This is on by default, so docker logs works with fluentd, splunk, awslogs and the rest.

The cache has its own options, which are separate from the driver’s:

OptionDefault
cache-disabled"false"
cache-max-size"20m"
cache-max-file"5"
cache-compress"true"

Verification that can fail

Read-only / Safefleet driver audit
$ docker ps -q | xargs -r docker inspect \
--format '{{.Name}} {{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}'
/api        fluentd   map[fluentd-address:tcp://198.51.100.20:24224 max-buffer-size:4m mode:non-blocking]
/worker     fluentd   map[fluentd-address:tcp://198.51.100.20:24224 max-buffer-size:4m mode:non-blocking]
/redis      json-file map[]
/nginx      local     map[]

Illustrative output

Two findings in four lines. /redis is on json-file with an empty options map, so it is uncapped — it predates the daemon change. /nginx is on local and therefore fine, but it is also not shipping anywhere, so it will not appear in the central store and nobody will notice until they search for it.

Then confirm the destination is actually receiving, which the audit above cannot tell you:

Read-only / Safeend-to-end check
MARKER="logcheck-$(date +%s)"

docker run --rm \
--log-driver fluentd \
--log-opt fluentd-address=tcp://198.51.100.20:24224 \
--log-opt fluentd-async=true \
alpine:3.20 echo "$MARKER"

echo "now search the central store for: $MARKER"

If the marker does not arrive, the pipeline is broken regardless of what docker inspect reports about the configuration.

Knowledge check

Knowledge check · 5 questions

  1. Q1. Your Fluentd aggregator becomes unreachable. Containers using the fluentd driver with default settings begin to hang. Why?

  2. Q2. Which of these is NOT a valid value for --log-driver?

  3. Q3. A container was created before you changed the daemon log driver. How do you move it onto the new driver?

  4. Q4. Which drivers read container logs back natively, without relying on the dual-logging local cache? Select all that apply.

  5. Q5. Setting mode=non-blocking guarantees that no log lines are lost during a log-destination outage.

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