Container Logging Best Practices
Implement container logging best practices in Docker. This lesson covers core concepts, step-by-step setup, hands-on walkthrough, troubleshooting, and next steps for progressive mastery.
Focus: implement container logging best practices
When your containerized application suddenly goes silent or spits out a wall of cryptic JSON, you realize logging isn't just a nice-to-have — it's your eyes and ears inside the black box of production. Implement container logging best practices to avoid the pain of debugging blind. This lesson shows you how to configure structured, centralized logging that makes troubleshooting fast, integrates with your observability stack, and follows Docker’s native patterns.
The problem this lesson solves
Containers are ephemeral by design — they can crash, restart, or be replaced at any moment. Without proper logging, you lose critical context about what happened before a failure. Common anti-patterns include:
- Writing logs inside the container to files like
/var/log/app.log— those files vanish when the container stops. - Using
console.logorprint()with plain text, making it hard to parse or search programmatically. - Having no log rotation, so a single container can fill up its disk and crash.
Pro tip: In Docker, the rule is simple: always write logs to stdout and stderr. Docker captures those streams and makes them available via
docker logs.
Core concept / mental model
Think of Docker logging as a three-layer pipeline:
- Producer — Your application writes messages to stdout (standard output) and stderr (standard error). Docker’s container runtime captures these streams automatically.
- Driver — Docker uses a log driver to route those streams somewhere. By default it’s the
json-filedriver, which stores logs as JSON files on the host. - Consumer — External tools like
docker logs,docker-compose logs, or third-party log collectors (e.g., Fluentd, AWS CloudWatch) read from the driver’s destination.

Key definitions
- Log driver: A plugin that determines how Docker handles logs from a container. Examples include
json-file,syslog,fluentd,gelf,awslogs. - Log rotation: Automatic removal of old log files to manage disk usage. Configured via
--log-opt max-sizeand--log-opt max-file. - Structured logging: Outputting logs in a machine-parseable format (like JSON) instead of plain text.
How it works step by step
1. Application writes to stdout/stderr
The most important rule: never write logs to files inside the container. Instead, use your language’s standard logging library to output to stdout or stderr.
# Python example: logging to stdout
import sys
import logging
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logging.info('Application started')
2. Docker captures the streams
When you run docker run, Docker attaches to the container’s stdout and stderr file descriptors. The log driver then transports these streams.
3. View logs with docker logs
# View last 100 lines, follow new output
docker logs --tail 100 -f my_container
4. Configure log driver and rotation
# Use json-file driver with rotation
docker run --log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
my_image
Hands-on walkthrough
Let’s build a logging demo step by step.
Step 1: Create a simple Python Flask app with proper structured logging
Create app.py:
from flask import Flask, request
import logging
import json
import sys
app = Flask(__name__)
# Configure JSON logger
class JsonFormatter(logging.Formatter):
def format(self, record):
log_entry = {
'timestamp': self.formatTime(record, self.datefmt),
'level': record.levelname,
'message': record.getMessage(),
'module': record.module,
'line': record.lineno
}
return json.dumps(log_entry)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logger = logging.getLogger(__name__)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
@app.route('/')
def home():
logger.info('Home page accessed', extra={'client_ip': request.remote_addr})
return 'Hello, world'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Step 2: Create a Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
requirements.txt:
flask==2.3.3
Step 3: Build and run with logging options
docker build -t logging-demo .
docker run -d --name log-test \
--log-driver json-file \
--log-opt max-size=5m \
--log-opt max-file=3 \
-p 5000:5000 \
logging-demo
Step 4: Generate traffic and view logs
# Hit the endpoint a few times
curl http://localhost:5000
curl http://localhost:5000
# See the structured logs
docker logs log-test
Expected output:
{"timestamp": "2025-03-12 10:30:45,123", "level": "INFO", "message": "Home page accessed", "module": "app", "line": 26}
{"timestamp": "2025-03-12 10:30:46,567", "level": "INFO", "message": "Home page accessed", "module": "app", "line": 26}
Compare options / when to choose what
| Log driver | Use case | Pros | Cons |
|---|---|---|---|
json-file (default) |
Single-node development, simple debugging | Built-in, no extra setup | No centralization; logs lost on node failure |
fluentd |
Centralized logging in Kubernetes or Docker Swarm | Aggregates from multiple containers, forwarding to many backends | Requires Fluentd daemon setup |
awslogs |
AWS ECS or EC2 deployment | Direct integration with CloudWatch Logs | Locked into AWS ecosystem |
syslog |
Enterprise environments with existing syslog infrastructure | Compatible with legacy systems | Less structured than JSON |
gelf |
Graylog users | Full-structured logging with rich search | Graylog license costs can add up |
Pro tip: For production, always use a remote log driver (fluentd, awslogs, or gelf) to avoid losing logs when containers die. Use
json-fileonly for local development.
Troubleshooting & edge cases
Logs are missing or empty
- Symptom:
docker logsreturns nothing. - Cause: Application wrote to a file instead of stdout/stderr.
- Fix: Change your app to log to stdout/stderr. For legacy apps, symlink
/dev/stdoutto a log file inside the container.
# Workaround for apps that can't be changed
RUN ln -sf /dev/stdout /var/log/app.log
Log rotation not working
- Symptom: Docker host disk fills up.
- Cause: Missing
--log-opt max-sizeand--log-opt max-file. - Fix: Always set rotation limits. Example:
docker run --log-opt max-size=10m --log-opt max-file=5 my_image
JSON logs are interleaved or malformed
- Symptom:
docker logsshows lines that look like{"message": "..."}{"message": "..."}without newlines. - Cause: Multiple writes to stdout without proper newline formatting.
- Fix: Ensure each log message ends with a newline. In Python, the logging handler automatically adds one; in bash scripts, use
echo.
# Bad: no newline after JSON
printf '{"level": "info", "msg": "hello"}'
# Good: with newline
printf '{"level": "info", "msg": "hello"}\n'
What you learned & what's next
You now understand how to implement container logging best practices: write to stdout/stderr, use structured logs (preferably JSON), configure log drivers and rotation, and choose the right driver for your environment. You completed a hands-on Flask app that produces parseable JSON logs, and you know how to troubleshoot common issues like missing logs or disk pressure.
Next lesson: Learn how to centralize logs from multiple containers using Docker Compose and Fluentd — the foundation for observability at scale.
Practice recap
Now try this on your own: Create a Dockerized Node.js Express app that logs HTTP requests as JSON. Run it with the json-file driver and rotation (max 5MB per file, up to 3 files). Generate traffic with curl and verify the logs look correct with docker logs. Then change the driver to fluentd and point it to a local Fluentd instance (use Docker Compose). This exercise solidifies the pipeline from app to remote aggregator.
Common mistakes
- Writing logs to files inside the container instead of stdout/stderr — those files disappear when the container stops.
- Not setting log rotation (
max-size,max-file) onjson-filedriver, causing disk to fill up over time. - Using plain text logs instead of structured JSON — makes parsing and searching with tools like
jqor log aggregators harder. - Relying solely on
docker logsin production — logs are lost if the container is removed; use a remote driver likefluentdorawslogs.
Variations
- Use
docker-composewithlogging:section to define driver and rotation across all services in one YAML file. - Attach a sidecar container with a log shipper (e.g., Filebeat) to forward logs from shared volumes — useful when the main app cannot be modified.
- Combine Docker
json-filedriver with a log collector like Logstash to forward logs to Elasticsearch without changing the application.
Real-world use cases
- Production microservice debugging: Centralize JSON logs from 50+ containers via Fluentd to Elasticsearch and Kibana for real-time search.
- Compliance auditing: Send all container stdout logs to AWS CloudWatch Logs and archive them to S3 with retention policies.
- CI/CD pipeline health: Capture structured build logs from Docker containers in Jenkins and parse them to trigger alerts on failure.
Key takeaways
- Always write application logs to stdout and stderr — never to files inside the container.
- Use structured logging (JSON) so logs are machine-parseable and searchable.
- Configure log rotation with
max-sizeandmax-fileto prevent disk exhaustion. - Choose the log driver based on your deployment:
json-filefor dev,fluentdorawslogsfor production. - Set Docker daemon defaults for log drivers and rotation via
/etc/docker/daemon.jsonfor consistency across all containers. - Test logging during development by running
docker logsand verifying output is complete and well-formatted.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.