Homelab
Proof

U1 / Docker foundations

Docker logs grow forever by default: measured caps

Tested on Docker Engine 29.9.2

Test result

With Docker's defaults a noisy container's json-file log grew at a median 99.5 KiB/s with no limit; one set to max-size=1m and max-file=3 never exceeded three files of about 1 MB (2.43 MiB at most in 12 samples over 120 s; the settings allow 2.86 MiB). A limit in daemon.json applied only to containers created after it, so existing containers kept growing until they were recreated.

Line drawing: a long paper strip roll overflowing from a small server, with a pair of scissors cutting the strip into equal lengths.

Set max-size and max-file on every long-running container, or set them once in /etc/docker/daemon.json and then recreate the containers that already exist. The Docker docs list max-size as unlimited (-1) and max-file as 1 for json-file (JSON File driver docs), and docker info on the test box reported json-file as the logging driver; the measurements below show what that costs and what the caps do.

How fast an unlimited log grows

The generator was an Alpine 3.22 container that prints one 100-byte-ish request line in a loop with sleep 0.001. It reached about 594 lines per second (median of three 40 s runs; an earlier run of the same script, since replaced, gave 632 lines per second and 106.0 KiB/s, so the rate varies between runs and machines), and the container’s log file (*-json.log) grew like this:

json-file log growth from a noisy container, and what max-size / max-file change
SetupMeasurementResult
json-file defaults, shell loop (3 runs x 40 s)Log size at 10 / 20 / 30 / 40 s (median of 3)975 KiB / 2,013 KiB / 2,932 KiB / 3,986 KiB
Growth rate99.5 KiB/s (93.1 KiB/s-101.8 KiB/s)
Lines per second (counter at about 40 s / 40)594 (556-607)
Bytes per line on disk (json-file wrapper included)172 (172-172)
Straight-line extrapolation of that rate to 24 h (not measured)8.80 GB (8.24 GB-9.01 GB)
json-file defaults, unthrottled `yes`Log size after 10 s648.21 MiB
max-size=1m, max-file=3 via --log-opt (120 s)Total bytes sampled every 10 s1,003 KiB, 1,955 KiB, 2,000 KiB, 2,070 KiB, 2,113 KiB, 2,171 KiB, 2,235 KiB, 2,307 KiB, 2,355 KiB, 2,356 KiB, 2,425 KiB, 2,492 KiB
Largest total seen / files on disk at end (settings allow at most 3 x 1,000,000 B = 2.86 MiB)2.43 MiB / 3 files: json.log 557,537 B; json.log.1 1,000,000 B; json.log.2 1,000,169 B
Lines emitted vs retained by `docker logs` (container killed first, both read after it stopped)72,768 emitted, 14,794 retained (20.3%)
Same caps via compose `logging:` (40 s)Total bytes / LogConfig reported by inspect2.24 MiB / {"Type":"json-file","Config":{"max-file":"3","max-size":"1m"}}

Alpine 3.22 container printing one ~100-byte line per loop with sleep 0.001. Docker data root on the VM's virtio disk. VM 4 vCPU, 3 GB.

The first rows are the unlimited case. 172 bytes per line on disk is the line plus the JSON wrapper that json-file adds. A straight-line extrapolation of 99.5 KiB/s is 8.80 GB per day; that figure is arithmetic on the measured rate, not a 24 h measurement. A loop with no sleep is much faster: in one 10 s run, yes wrote 648.21 MiB (819.88 MiB in the earlier run).

What max-size and max-file do

With --log-opt max-size=1m --log-opt max-file=3 the log stayed bounded: the largest of 12 samples over 120 s was 2.43 MiB, and the end state was three files:

json.log     557,537 B
json.log.1 1,000,000 B
json.log.2 1,000,169 B

1m rotated at about 1,000,000 bytes, not 1,048,576, in this test. The cost is history: of the 72,768 lines the container had emitted in 120 s, docker logs could return 14,794 (20.3%), because older files are discarded; the container was killed before both numbers were read, and the share falls the longer the container runs and varies with where the rotation is. Size the cap for how far back you need to look, or ship logs elsewhere.

The same options in a compose file produced the same result (2.24 MiB after 40 s, and docker inspect reported "max-file":"3","max-size":"1m"):

services:
  noisy:
    image: alpine:3.22
    logging:
      driver: json-file
      options:
        max-size: "1m"
        max-file: "3"

The daemon.json trap

The Docker docs say existing containers do not use new logging configuration automatically. The test confirmed it and shows how far that goes. After writing this file and restarting the daemon:

{"log-driver":"json-file","log-opts":{"max-size":"1m","max-file":"2"}}
daemon.json log defaults (json-file, max-size 1m, max-file 2) only reach containers created after the change
ContainerHostConfig.LogConfig after the changeTotal log bytes
created before daemon.json, ~40 s after daemon restartjson-file {}6.93 MiB
created after daemon.json (no per-container options)json-file {"max-file":"2","max-size":"1m"}1.63 MiB
created after, with --log-opt max-size=10mjson-file {"max-file":"2","max-size":"10m"}4.47 MiB
before (docker restart, 20 s later)json-file {}10.23 MiB
before (removed and recreated, 25 s)json-file {"max-file":"2","max-size":"1m"}1.75 MiB

All three containers ran the same noisy loop. daemon.json was written, then the Docker daemon restarted (the `before` container has restart: always and came back). VM 4 vCPU, 3 GB.

The container created before the change (restart: always, so it came back by itself) still had an empty LogConfig and reached 10.23 MiB after a docker restart. A container created afterward carried max-size=1m, max-file=2 and sat at 1.63 MiB; one that passed its own --log-opt max-size=10m kept its own value. Removing and recreating the old container applied the new defaults. The test did not check whether docker compose up -d recreates a container when only daemon.json changed.

The local driver

The local driver stores logs in its own format and, per the docs, defaults to 20 MB per file, 5 files and compression enabled, about 100 MB per container. On the same fixed workload of 200,000 access-log-style lines:

Same fixed workload (200,000 access-log-style lines) under json-file and local
DriverBytes on disk, 3 runsLines returned by docker logsBytes returned by docker logs
json-file26.50 MiB (26.50 MiB-26.50 MiB)200,00013,807,094
local18.32 MiB (18.32 MiB-18.32 MiB)200,00013,807,094
local / json-file size ratio0.69

Lines are generated with awk (srand 42) so each run writes the same content. Sizes are for a single file set, below the local driver's default rotation size. Under the noisy loop the local driver (defaults) held 1.54 MiB, 3.13 MiB, 4.69 MiB at 20/40/60 s.

local used 0.69 of the space of json-file for identical content, and docker logs returned the same 200,000 lines from both. Under the noisy loop with no options, the local container held 1.54 MiB, 3.13 MiB and 4.69 MiB at 20, 40 and 60 s, so it grows steadily too; its bound comes from the default rotation settings the docs describe, which this 60 s run did not reach. Choosing local is a way to get bounded logs without writing options.

What went wrong

The first run of the script discarded its own data: it named a container y, which Docker rejects (names need at least two characters), and its fixed-workload parser read an extra empty field. Both were fixed and the whole test was rerun; the numbers above are from the rerun.

Reproduce

lab/tests/docker-logs/run.sh
Show the full file
#!/usr/bin/env bash
# docker-logs: json-file log growth with defaults, max-size/max-file, daemon.json scope, and the local driver.
set -uo pipefail
source "$(dirname "$0")/../_lib.sh"
T=docker-logs; OUT="$ROOT/evidence/tests/$T"; RAW="$OUT/raw"; rm -rf "$OUT"; mkdir -p "$RAW"
start_vm a-$T
vm scp-to "$ROOT/lab/tests/$T/guest.sh" guest.sh
gx 'bash guest.sh' > "$RAW/guest-stdout.txt" 2>&1
vm scp-from out "$RAW/"; mv "$RAW/out"/* "$RAW/"; rmdir "$RAW/out"
gx 'cat /etc/docker/daemon.json 2>&1; docker info -f "{{.LoggingDriver}}"' > "$RAW/daemon-after-cleanup.txt" 2>&1
collect_env "$OUT"
python3 "$ROOT/lab/tests/$T/summarize.py" "$RAW" "$OUT"

What this test did not cover

Sources and how this was made

How this page was made: the commands above were run by an AI agent on the test box on November 1, 2026, and the same agent drafted this text from the saved output. The site's owner reviewed the site before publication. Test method and editorial policy explain how results are checked, and what is not. Spotted an error? Tell us.