Log to stdout, structure as JSON, cap file sizes and know your retention: pragmatic logging for containerized apps without an ELK stack.
The one rule: stdout, unbuffered
The twelve-factor principle remains the foundation of container logging: applications write events to stdout/stderr and treat log routing as the runtime’s job. Never write log files inside a container, they die with it, they hide from docker logs, and inside volumes they grow until the disk fills. Every serious runtime, platform and collector builds on the stdout convention; fighting it buys you nothing.
Unbuffered matters too: Python needs PYTHONUNBUFFERED=1, and any language buffering stdout will show logs minutes late or lose the crucial lines before a crash.
Structure beats prose
The difference between grep-able text and queryable JSON shows up the first time you debug a real incident. JSON lines with a level, timestamp, message and request context turn "search the haystack" into "filter where user_id=X and status=500". Every mainstream logger does this well: pino (Node), zerolog/slog (Go), structlog (Python), Serilog (.NET).
Include a request ID on every line of a request’s lifecycle, correlation is the whole game
Log at boundaries (request in/out, job start/end, external calls) rather than narrating every function
Never log secrets, tokens or full card numbers; add a redaction layer where user data flows
{"level":"error","time":"2026-04-07T10:31:04Z","req_id":"abc123",
"user_id":8841,"route":"/api/checkout","status":500,
"err":"payment provider timeout after 3000ms","duration_ms":3012}
Cap and rotate at the daemon
Docker’s default json-file driver has no size cap, making unbounded logs the number-one cause of mysteriously full Docker hosts. Fix it once, globally:
/etc/docker/daemon.json
{ "log-opts": { "max-size": "20m", "max-file": "3" } }
# 60 MB ceiling per container; restart docker, recreate containers to adopt
Levels and volume discipline
Run production at info level: debug in production drowns signals and inflates costs everywhere downstream. A useful volume heuristic: a healthy request logs 1 to 3 lines, not 30. If a single user action produces a screen of logs, you are narrating rather than reporting, and the noise will hide the one line that matters during an incident.
Do you actually need a log stack?
For single-host and few-host deployments, platform log access, Peon streams live and recent container logs per service in the dashboard, plus daemon-level rotation covers the daily debugging loop: see the error, correlate by request ID, fix. Graduate to Loki or an ELK stack when a concrete need arrives: searching across many servers at once, retention measured in months for compliance, or alerting on log patterns. Adopting that infrastructure before the need is a classic complexity trap; the migration later is easy precisely because everything already logs structured JSON to stdout.
Top comments (0)