Docker & ContainersXVII · LoggingDrivers
Logging drivers — json-file, local, journald, and the remote ones
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
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:
| Driver | Destination | Notes |
|---|---|---|
json-file | Host file, one JSON object per line | Default. No rotation unless configured. |
local | Host file, internal binary format | Rotation on by default: 20 MB, 5 files, compressed. |
journald | The systemd journal | Metadata as journal fields. Retention is journald’s problem. |
syslog | A syslog daemon, local socket or remote | TLS options available. |
gelf | Graylog Extended Log Format endpoint | Graylog, and Logstash with a GELF input. |
fluentd | A Fluentd or Fluent Bit forward endpoint | The common self-hosted shipping path. |
awslogs | Amazon CloudWatch Logs | |
gcplogs | Google Cloud Logging | |
splunk | Splunk HTTP Event Collector | |
etwlogs | Event Tracing for Windows | Windows hosts only. |
none | Discarded | docker 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-file | local | |
|---|---|---|
| Format | Text, one JSON object per line | Internal binary, protobuf-based |
| Readable by third-party tools | Yes — this is its whole point | No; the daemon owns these files |
max-size default | -1 (unlimited) | 20m |
max-file default | 1 | 5 |
compress default | false | true |
| Disk and CPU cost per line | Higher — JSON escaping, no compression | Lower |
docker logs | Native | Native |
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.
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:
| Field | Contents |
|---|---|
CONTAINER_ID | Truncated 12-character ID |
CONTAINER_ID_FULL | Full 64-character ID |
CONTAINER_NAME | The name at container start |
CONTAINER_TAG / SYSLOG_IDENTIFIER | The tag log-opt |
IMAGE_NAME | The image the container was created from |
CONTAINER_PARTIAL_MESSAGE | Set on fragments of an over-long line |
Those fields are queryable, which is the actual benefit:
$ journalctl CONTAINER_NAME=api --since '30 min ago' -o short-iso2026-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:
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.2mode=non-blockinginserts a ring buffer between the container and the driver. The application’s write always completes.max-buffer-sizebounds that buffer. The default is1m.- 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,localandjournaldsupport reading natively.docker logsreads 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
localdriver format, anddocker logsreads the cache. This is on by default, sodocker logsworks withfluentd,splunk,awslogsand the rest.
The cache has its own options, which are separate from the driver’s:
| Option | Default |
|---|---|
cache-disabled | "false" |
cache-max-size | "20m" |
cache-max-file | "5" |
cache-compress | "true" |
Verification that can fail
$ 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:
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
Q1. Your Fluentd aggregator becomes unreachable. Containers using the fluentd driver with default settings begin to hang. Why?
Q2. Which of these is NOT a valid value for --log-driver?
Q3. A container was created before you changed the daemon log driver. How do you move it onto the new driver?
Q4. Which drivers read container logs back natively, without relying on the dual-logging local cache? Select all that apply.
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.