Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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.log or print() 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:

  1. Producer — Your application writes messages to stdout (standard output) and stderr (standard error). Docker’s container runtime captures these streams automatically.
  2. Driver — Docker uses a log driver to route those streams somewhere. By default it’s the json-file driver, which stores logs as JSON files on the host.
  3. 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.

Mental model: app → stdout/stderr → Docker log driver → external consumer

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-size and --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-file only for local development.

Troubleshooting & edge cases

Logs are missing or empty

  • Symptom: docker logs returns 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/stdout to 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-size and --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 logs shows 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) on json-file driver, causing disk to fill up over time.
  • Using plain text logs instead of structured JSON — makes parsing and searching with tools like jq or log aggregators harder.
  • Relying solely on docker logs in production — logs are lost if the container is removed; use a remote driver like fluentd or awslogs.

Variations

  1. Use docker-compose with logging: section to define driver and rotation across all services in one YAML file.
  2. 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.
  3. Combine Docker json-file driver 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-size and max-file to prevent disk exhaustion.
  • Choose the log driver based on your deployment: json-file for dev, fluentd or awslogs for production.
  • Set Docker daemon defaults for log drivers and rotation via /etc/docker/daemon.json for consistency across all containers.
  • Test logging during development by running docker logs and verifying output is complete and well-formatted.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.