15.1 Container Logging
A focused guide to Container Logging, connecting core concepts with practical Docker and container operations.
Container logging is the mechanism by which Docker captures a container's standard output and standard error streams, the conventions for what an application should and should not write to them, and the configuration choices that determine where that captured output ultimately ends up and how long it remains available for inspection.
The stdout and stderr convention
The standard, container-native logging convention is for an application to write its log output directly to stdout and stderr rather than to a file inside the container, since Docker captures these two streams automatically and makes them available through docker logs without requiring the application to know anything about being containerized at all:
console.log(JSON.stringify({ level: 'info', message: 'Server started', port: 3000 }));
console.error(JSON.stringify({ level: 'error', message: 'Database connection failed' }));
docker logs my-api
An application that instead writes logs only to a file inside its own writable layer loses this automatic capture entirely, and that log data disappears the moment the container is removed, since it was never part of what Docker treats as the container's log stream.
Logging drivers
Docker supports multiple logging drivers, each determining where the captured stdout and stderr output is actually sent and stored:
docker run -d --log-driver=json-file my-api
docker run -d --log-driver=syslog --log-opt syslog-address=udp://logs.example.com:514 my-api
docker run -d --log-driver=journald my-api
json-file, the default, writes log output to a JSON-formatted file on the host per container, which is simple and requires no external infrastructure, but does not scale well across multiple hosts and provides no built-in aggregation across containers or hosts.
Bounding log size to prevent disk exhaustion
The default json-file driver retains log output indefinitely unless explicitly bounded, which can silently fill a host's disk over time, especially during an incident that produces an unusually high volume of error logging:
docker run -d --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 my-api
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Setting this at the daemon level, through daemon.json, applies the limit to every container by default, which is generally safer than relying on every individual docker run invocation or Compose service definition to set it consistently.
Shipping logs off the host
For anything beyond a single, low-traffic host, logs need to be shipped to a centralized system, since docker logs only queries the local daemon and provides no cross-host search, long-term retention, or alerting capability on its own:
services:
api:
logging:
driver: gelf
options:
gelf-address: "udp://logging-host:12201"
docker run -d --log-driver=fluentd --log-opt fluentd-address=fluentd-host:24224 my-api
A common alternative to a native Docker logging driver is running a log collection agent as its own container that reads other containers' log files directly from the host (where the json-file driver stores them) and forwards them onward, which avoids coupling every individual container's configuration to the specific log shipping backend in use.
Structured logging for better aggregation
Once logs reach a centralized aggregation system, structured (typically JSON) log lines are dramatically more useful than free-form text, since the aggregation system can index and query individual fields directly rather than relying only on text search across the full line:
const logger = {
info: (msg, fields = {}) => console.log(JSON.stringify({ level: 'info', message: msg, timestamp: new Date().toISOString(), ...fields })),
};
logger.info('Order processed', { orderId: '12345', amount: 49.99 });
A query like "find every log line where orderId equals a specific value, across every service, in the last hour" is straightforward against structured logs in most aggregation systems, and essentially impossible to do reliably against unstructured text logs at any meaningful scale.
Avoiding sensitive data in logs
Because logs are often retained for extended periods and accessible to a broad set of people with observability tooling access, sensitive values, passwords, tokens, full credit card numbers, should never be written to log output, structured or otherwise:
logger.info('User authenticated', { userId: user.id }); // correct: no password included
logger.info(`Login attempt: ${JSON.stringify(req.body)}`); // risky: may log a password field directly
A log line that captures an entire request body without filtering known-sensitive fields is a common, easy-to-introduce leak path, and is worth explicitly guarding against with a logging middleware that redacts known sensitive field names before anything is written out.
Log levels and verbosity control
Production logging should support adjustable verbosity, so that detailed debug-level output can be enabled temporarily during an investigation without needing a code change and redeploy:
const logger = require('pino')({ level: process.env.LOG_LEVEL || 'info' });
docker run -d -e LOG_LEVEL=debug my-api
Running with debug-level logging permanently in production is usually impractical due to both the volume of output and the cost of storing and indexing it; an environment variable that controls this without a code change makes it practical to temporarily raise verbosity only when actively investigating something.
Inspecting logs interactively during development and debugging
Beyond the production aggregation pipeline, docker logs remains useful directly against a running container for quick, interactive inspection:
docker logs --since 10m --tail 200 my-api
docker logs -f my-api
docker compose logs -f api
These commands are most useful for immediate, single-host debugging; they are not a substitute for centralized aggregation once a deployment spans more than a host or two, or once log retention beyond the container's own lifetime is required.
Common mistakes
- Writing logs to a file inside the container instead of stdout and stderr, losing Docker's automatic capture and the ability to access that output through
docker logs. - Leaving the default logging driver unbounded, risking disk exhaustion on the host during a period of unusually high log volume.
- Logging unstructured, free-form text that is difficult to query effectively once aggregated into a centralized system.
- Capturing full request or response bodies in logs without filtering sensitive fields, creating an avoidable credential or personal data leak.
- Relying solely on
docker logsfor a multi-host or production deployment with no centralized aggregation, losing log history the moment a container or host is removed.
Effective container logging starts with the stdout and stderr convention that lets Docker capture output automatically, applies a bounded, structured format suited to centralized aggregation, and deliberately avoids writing sensitive data into a stream that is often retained and broadly accessible for far longer than the container that produced it.