CertGrid CertGrid
Configuration·Docker

Docker Container Logs and Rotation

Container logs are files on the host and by default they grow without limit. Measure one at 3.3 MB, cap it at 36 KB, and see exactly what rotation costs you in retained history.

Operations and Troubleshooting Guide 31 of 46 Intermediate

Tested on the versions above. Log paths are host-specific and require root to read directly. Sizes vary with the workload.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. Logs are files, and you own the disk they sit on

    The default json-file driver writes every line a container sends to stdout or stderr into a JSON file under /var/lib/docker/containers//. docker logs reads that file. Nothing rotates it by default, which is how a chatty container quietly fills a disk weeks after deployment.

    bash Example session
    docker info --format "default logging driver: {{.LoggingDriver}}"default logging driver: json-filedocker inspect cg-nolog --format "{{.LogPath}}"/var/lib/docker/containers/06f643c8b4b7.../06f643c8b4b7...-json.log

    Expected resultA path under /var/lib/docker. The file is owned by root and readable only by root.

    Success conditionYou can name the file behind docker logs. That file is what fills the disk.

  2. Measure an unbounded log

    Thirty thousand short lines is a trivial amount of output - a busy service produces that in minutes. Here it is 3.3 MB, and nothing will ever remove it except deleting the container.

    bash Example session
    docker run -d --name cg-nolog alpine:3.22 sh -c "yes cg-log-line-padding-padding-padding-padding | head -30000; sleep 90"docker logs cg-nolog | wc -l30000sudo du -h --apparent-size $(docker inspect cg-nolog --format {{.LogPath}}) | cut -f13.3M

    Expected resultEvery line retained, and megabytes on disk.

    Success conditionYou have a number. Multiply it by your real log rate and your uptime - that is the disk this container will consume.

  3. Cap it per container

    --log-opt max-size rotates the file at a size, and max-file keeps that many rotations. The values are per container, so you can apply them to one noisy service without touching the daemon or restarting anything else.

    bash
    docker run -d --name cg-rot --log-opt max-size=10k --log-opt max-file=3 alpine:3.22 sh -c "yes cg-log-line-padding-padding-padding-padding | head -30000; sleep 90"docker inspect cg-rot --format "opts={{.HostConfig.LogConfig.Config}}"opts=map[max-file:3 max-size:10k]

    Expected resultThe options recorded on the container.

    Success conditionThe options are on the container, not just in your command line. A container created before you set them keeps the old behaviour until it is recreated.

  4. See what rotation costs you

    This is the trade nobody mentions. Disk is now bounded - three files, 36 KB total instead of 3.3 MB. But docker logs can only return what is still on disk, so 29,744 of your 30,000 lines are simply gone. Rotation is not archiving.

    bash Example session
    sudo sh -c "ls /var/lib/docker/containers/$(docker inspect cg-rot --format {{.Id}})/ | grep json.log"6037d1fe1b47...-json.log6037d1fe1b47...-json.log.16037d1fe1b47...-json.log.2sudo sh -c "du -ch /var/lib/docker/containers/$(docker inspect cg-rot --format {{.Id}})/*json.log* | tail -1"36K	total

    Expected resultThree files totalling 36 KB, and only 256 lines retrievable.

    Verify it worked

    bash Example session
    docker logs cg-rot | wc -l256docker logs cg-nolog | wc -l30000

    Success conditionYou can state the trade in one sentence: bounded disk, lost history. If you need the history, ship it somewhere before it rotates.

  5. Set the default for every new container

    Per-container options do not help with the containers you forget. The durable fix is a daemon default in /etc/docker/daemon.json, which applies to every container created afterwards. Understand the blast radius first: editing that file and reloading affects the whole host, and a malformed file stops the daemon starting at all.

    bash
    # host-wide change - validate the JSON and understand that a bad file breaks the daemon{  "log-driver": "json-file",  "log-opts": { "max-size": "10m", "max-file": "3" }}docker info --format "{{.LoggingDriver}}"# existing containers keep their old settings - only new ones inherit the default

    Expected resultThe current driver. This step is deliberately not executed here - it changes a shared host.

    Success conditionYou know where the default lives and that it is not retroactive. Recreate existing containers to pick it up.

  6. Other drivers, and when to leave json-file

    docker logs only works with json-file, local and journald. Point a container at syslog, fluentd or a cloud driver and docker logs stops returning anything - which is correct behaviour, not a fault, because the logs now live elsewhere. local is worth knowing: it is more compact than json-file and rotates by default.

    bash Example session
    docker info --format "{{json .Plugins.Log}}"["awslogs","fluentd","gcplogs","gelf","journald","json-file","local","splunk","syslog"]

    Expected resultThe drivers this daemon supports.

    Success conditionYou can list your options. Pick a shipping driver for production and keep json-file with rotation for local work.

  7. Clean up

    Removing the container removes its log files with it. That is also the only way to reclaim the space of an unbounded log short of truncating the file by hand.

    bash
    docker rm -f cg-nolog cg-rotcg-nologcg-rot

    Expected resultBoth names echoed back.

    Success conditiondocker ps -a is clean and the log files are gone with the containers.

Troubleshooting

Official sources