Docker logging cheatsheet.

Default: json-file

docker logs web
docker logs -f web
docker logs --tail 100 web
docker logs --since 10m web
docker logs --until 2026-01-15T12:00:00 web
docker logs -t web                          # timestamps

Stored as JSON at /var/lib/docker/containers/<id>/<id>-json.log.

Log rotation

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}

Per-container:

docker run --log-opt max-size=10m --log-opt max-file=3 app

Compose:

services:
  web:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

⚠️ Without rotation, logs grow unbounded → disk full.

Logging drivers

  • json-file: default.
  • local: more efficient than json-file.
  • journald: systemd journal (Linux).
  • syslog: forward to syslog daemon.
  • fluentd: forward to Fluentd.
  • gelf: Graylog.
  • awslogs: CloudWatch.
  • gcplogs: GCP Cloud Logging.
  • splunk.
  • none: discard.

syslog

docker run \
  --log-driver syslog \
  --log-opt syslog-address=udp://logserver:514 \
  --log-opt tag="docker/{{.Name}}" \
  app

fluentd

docker run \
  --log-driver fluentd \
  --log-opt fluentd-address=fluentd:24224 \
  --log-opt tag=docker.{{.Name}} \
  app

Fluentd parses + ships to ES / S3 / Loki.

Loki + Promtail (preferred for K8s)

Promtail/Vector tails Docker logs and ships to Grafana Loki. No driver change needed; reads json-file.

App-side: write to stdout

# DO
print(...)
logger.info(...)

# DON'T
logging.FileHandler("app.log")          # logs go nowhere

12-factor: write to stdout/stderr, let runtime collect.

Structured logs

import structlog

log = structlog.get_logger()
log.info("user_login", user_id=42, ip="1.2.3.4")
# {"level":"info","event":"user_login","user_id":42,"ip":"1.2.3.4","timestamp":"..."}

Easier to query, parse, alert on.

Multi-line logs

Multi-line tracebacks become multiple log entries. Use:

import logging
# Format with timestamp and inline traceback

Or use \n escapes. Some aggregators handle multi-line via patterns.

Sidecar log forwarder

services:
  app:
    image: myapp
    logging:
      driver: json-file
      options:
        max-size: "10m"
  
  logshipper:
    image: grafana/promtail
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail.yml:/etc/promtail/promtail.yml

Promtail / Vector / Fluent Bit on each host.

Discarding logs

services:
  noisy:
    logging:
      driver: none

For containers you don’t care about.

docker events

docker events
docker events --since 1h --filter type=container --filter event=stop

Audit container lifecycle.

In-container log paths

If app writes to a file:

services:
  app:
    volumes:
      - ./logs:/app/logs

Tail from host. Worse than stdout.

Log filtering with grep + jq

docker logs web 2>&1 | grep ERROR
docker logs web 2>&1 | jq 'select(.level=="error")'

Logs with timestamps

docker logs -t --since 1h web

If app already logs timestamps, -t adds Docker’s too (redundant).

Common mistakes

  • No log rotation → disk fills.
  • Writing to file inside container → vanishes on restart.
  • Unstructured logs → can’t filter.
  • Logging healthcheck probes → log spam.
  • Including PII in logs.
  • Forgetting to redirect framework errors to stdout.

Read this next

If you want my structured logging templates, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .