Whatever a container writes to standard output and standard error goes to the daemon, which hands each line to the container's logging driver. The default, json-file, appends one JSON object per line to a file the daemon owns, and docker logs reads that file back:
docker run -d --name l3-log1 alpine:3 sh -c 'echo line 1; echo err 1 >&2; sleep 600' >/dev/null
sleep 1; sudo cat "$(docker inspect -f '{{.LogPath}}' l3-log1)"
docker run -d --name l3-log2 --log-driver local alpine:3 sh -c 'echo hi; sleep 600' >/dev/null
docker run -d --name l3-log3 --log-driver none alpine:3 sh -c 'echo lost; sleep 600' >/dev/null
sleep 1; docker logs l3-log2; docker logs l3-log3
docker rm -f l3-log1 l3-log2 l3-log3 >/dev/null{"log":"err 1\n","stream":"stderr","time":"2026-09-25T12:14:53.204897488Z"}
{"log":"line 1\n","stream":"stdout","time":"2026-09-25T12:14:53.205005888Z"}
hi
Error response from daemon: configured logging driver does not support readingEach line carries its stream and a nanosecond timestamp, which is how docker logs keeps standard error apart and how -t and --since work. The file lives in /var/lib/docker/containers/<id>/, readable by root only, and docker rm deletes it with the container. json-file never rotates unless told to; local stores compressed lines and rotates by default (five files of 20 MB), which is why Docker 514 's documentation recommends it. journald and syslog hand lines to the host; fluentd, gelf, splunk, awslogs and gcplogs ship them to a collector (External Log Collectors), and none discards them. A daemon-wide default in daemon.json (daemon.json) affects only containers created afterwards, so set it in BookNest's compose.yaml itself, with an extension field that each of the three services references:
x-logging: &logging
driver: local
options: { max-size: "10m", max-file: "3" }
# ...and under each of web, api and db:
logging: *loggingEach container now keeps at most 30 MB, so one chatty container can no longer fill /var/lib/docker.