Containers are ephemeral. When one exits, its filesystem — including any log files — disappears. Observability means capturing logs, metrics, and traces outside the container, before it's gone.

1. The Observability Challenge

Traditional servers are long-lived. You SSH in, tail a log file, and the data is always there. Containers throw that model away: a container can be replaced in seconds, scaled to dozens of replicas, or killed by a health-check failure. Any data written inside the container is gone with it.

This forces a discipline shift. Instead of checking one server, you need:

  • Centralised log aggregation — all container output flows to one place
  • Time-series metrics — numeric snapshots scraped from every replica
  • Distributed traces — a request fingerprint that follows work across many containers
Logs What happened? stdout / stderr Metrics How much / how fast? counters / gauges Traces Where did it go? spans / context Observability Platform Grafana / Datadog / EFK / Jaeger

2. Logging: stdout/stderr Is the Contract

The Twelve-Factor App (Factor XI) is unambiguous: write logs to stdout. Docker captures every byte written to stdout and stderr, stores it via the configured log driver, and makes it accessible with docker logs. Writing to a file inside the container breaks this chain.

# ✅ View live logs from a container
docker logs -f my-api

# ✅ Last 100 lines with timestamps
docker logs --tail 100 --timestamps my-api

# ❌ Don't do this inside your container
echo "error" >> /var/log/app/error.log   # lost on container removal
Why stdout?

stdout is the universal, language-agnostic, driver-agnostic log sink. Your app doesn't need to know whether logs are going to a file, Splunk, CloudWatch, or nowhere. Docker — or the container runtime — handles routing. This decouples the app from its operational environment.

3. Log Drivers

Docker's log driver controls where stdout/stderr goes after Docker captures it. The default is json-file — fine for development, not for production at scale.

DriverDestinationBest For
json-fileLocal JSON file on hostDev / single-host testing
syslogSystem syslog daemonTraditional Linux infra
journaldsystemd journalsystemd hosts, Podman
fluentdFluentd / Fluent BitEFK stack, Kubernetes
awslogsAWS CloudWatch LogsECS / EC2 on AWS
gcplogsGoogle Cloud LoggingGKE / Cloud Run
splunkSplunk HTTP Event CollectorEnterprise Splunk deployments

Configure the driver per container in Compose or globally in /etc/docker/daemon.json:

# docker-compose.yml — per service
services:
  api:
    image: my-api:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"    # rotate at 10 MB
        max-file: "5"      # keep 5 rotated files

# /etc/docker/daemon.json — global default
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}
Container stdout/stderr Log Driver fluentd / awslogs Aggregator Fluent Bit / Logstash Storage Elasticsearch / S3 Dashboard Kibana / Grafana

4. Structured Logging

Plain text logs are hard to query at scale. Structured logging means emitting JSON objects so every log line is machine-parseable. An aggregator can then index individual fields without regex.

# Unstructured — hard to query
2024-01-15T10:23:01Z ERROR payment service: charge failed for user 4892 amount=99.99

# Structured JSON — indexable, filterable
{
  "timestamp": "2024-01-15T10:23:01Z",
  "level":     "error",
  "service":   "payment",
  "event":     "charge_failed",
  "user_id":   4892,
  "amount":    99.99,
  "trace_id":  "abc123def456"
}

Key fields every log line should carry:

  • timestamp — ISO 8601, always UTC
  • level — debug / info / warn / error
  • service — which container / microservice emitted it
  • trace_id — links logs to the distributed trace for that request
  • message — human-readable description

The EFK stack (Elasticsearch + Fluent Bit + Kibana) is a popular open-source choice: Fluent Bit ships logs from each container to Elasticsearch, and Kibana provides a search and visualisation UI.

5. Container Metrics

Metrics answer "how is the container behaving right now?" Docker exposes real-time stats via the kernel's cgroups interface, which limits and tracks resource usage per container.

# Live stats for all running containers
docker stats

# One-shot snapshot (no streaming)
docker stats --no-stream

# Output
CONTAINER ID   NAME       CPU %   MEM USAGE / LIMIT    NET I/O        BLOCK I/O
a1b2c3d4e5f6   api        1.23%   128MiB / 512MiB      10MB / 2MB     0B / 4MB

Key metrics to watch in production:

  • CPU % — sustained >80% → scale out or profile
  • Memory usage / limit — approaching the limit triggers OOM kills
  • Network I/O — unexpected spikes may indicate a loop or attack
  • Block I/O — high write volume may mean you need a volume, not the container layer
  • Restart count — a container that keeps restarting is a red flag

cAdvisor (Container Advisor) is a Google-maintained daemon that scrapes cgroup metrics from every container on a host and exposes them in Prometheus format — the standard bridge between Docker and the Prometheus ecosystem.

6. Prometheus & Grafana Pattern

The most common open-source metrics stack for containers is Prometheus + Grafana:

  1. Your app (or cAdvisor) exposes a /metrics HTTP endpoint in Prometheus text format
  2. Prometheus scrapes that endpoint on a configured interval (e.g. every 15 s)
  3. Grafana queries Prometheus and renders dashboards, graphs, and alerts
# prometheus.yml — minimal scrape config
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]

  - job_name: "my-api"
    static_configs:
      - targets: ["api:9090"]  # app exposes /metrics here

7. Distributed Tracing

In a microservices architecture, a single user request may pass through a gateway, three services, a cache, and a database — each in a different container. Distributed tracing links all that work into a single timeline called a trace, made of spans.

OpenTelemetry (OTel) is the CNCF-standard SDK for instrumenting apps. It produces traces (and metrics, and logs) in a vendor-neutral format that backends like Jaeger and Zipkin can store and display.

# docker-compose.yml — Jaeger all-in-one for dev tracing
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"   # Jaeger UI
      - "4317:4317"     # OTLP gRPC receiver
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  api:
    image: my-api:latest
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
      - OTEL_SERVICE_NAME=my-api

The key mechanism is context propagation: when service A calls service B, it injects a traceparent HTTP header. Service B reads it and creates a child span under the same trace ID. The result is a waterfall view in the Jaeger UI showing exactly where latency lives.

8. The Three Pillars

Logs, metrics, and traces are complementary, not redundant. Use them together:

PillarQuestion AnsweredExample ToolRetention
Logs What happened and why did it fail? EFK, Loki, CloudWatch Days–weeks (can be large)
Metrics Is the system healthy right now? Prometheus, Datadog Months (compact numbers)
Traces Where did this request spend its time? Jaeger, Zipkin, Tempo Hours–days (sampled)
🏭 Industry: Managed Observability Platforms

Datadog, New Relic, and Grafana Cloud unify all three pillars with a single agent. Install the agent as a DaemonSet (Kubernetes) or sidecar, and logs, metrics, and traces all flow to a SaaS backend with pre-built container dashboards. The tradeoff is cost — these platforms charge on ingestion volume, so structured logging and metric cardinality discipline pays off directly in your bill.

🔁 Not Just Docker

Kubernetes runs a logging DaemonSet (Fluent Bit or Fluentd) on every node, scraping container stdout from the node's log directory — same principle, automated at cluster scale. Podman on systemd hosts defaults to the journald log driver; use journalctl CONTAINER_NAME=my-app to query it. The stdout contract holds across all runtimes.

Hands-On Task: Prometheus + Grafana + Sample App

🛠 Observe a running app with Prometheus and Grafana

Goal: Stand up a sample app, cAdvisor, Prometheus, and Grafana in one Compose file, then explore container metrics in a live dashboard.

Step 1 — Create the Compose file:

# compose.yml
services:
  app:
    image: nginx:alpine          # sample workload
    ports: ["8080:80"]

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    ports: ["8081:8080"]

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports: ["9090:9090"]

  grafana:
    image: grafana/grafana:latest
    ports: ["3000:3000"]
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Step 2 — Create prometheus.yml:

global:
  scrape_interval: 10s
scrape_configs:
  - job_name: cadvisor
    static_configs:
      - targets: ["cadvisor:8080"]

Step 3 — Start everything:

docker compose up -d

Step 4 — Explore:

  • Prometheus at http://localhost:9090 → query container_cpu_usage_seconds_total
  • Grafana at http://localhost:3000 (admin / admin) → add Prometheus data source (http://prometheus:9090) → import dashboard ID 14282 (cAdvisor)
  • Generate load: for i in $(seq 50); do curl -s http://localhost:8080 >/dev/null; done
  • Watch CPU and network metrics climb in real time

Tear down: docker compose down

Interactive Quizzes

Why should containers write logs to stdout rather than to a file inside the container?

  • Files inside containers are encrypted and can't be read by Docker
  • The container filesystem is ephemeral — files are lost when the container is removed, and Docker log drivers can only capture stdout/stderr
  • stdout is faster than disk I/O on all platforms
  • Log files inside containers cause port conflicts

Your team deploys to AWS ECS and wants container logs in CloudWatch for alerting. Which log driver should you configure?

  • json-file — the default driver stores JSON locally
  • fluentd — the standard Kubernetes log driver
  • journald — systemd journal integration
  • awslogs — sends container output directly to AWS CloudWatch Logs

An engineer sees high latency in their microservices system. Metrics show everything looks fine. Which observability pillar should they reach for next to pinpoint the slow component?

  • Logs — search for ERROR entries in all services
  • Metrics — add more dashboards and wait
  • Traces — a distributed trace shows a waterfall of spans and reveals which service or database call is slow
  • Restart the containers — metrics will reset and the latency will resolve

Key Takeaways

  • Containers are ephemeral — capture logs and metrics externally or lose them forever
  • stdout is the contract — write all log output to stdout; Docker log drivers handle the rest
  • Log drivers (json-file, fluentd, awslogs, gcplogs) route output to the right sink; always configure rotation in production
  • Structured (JSON) logs are indexable — include timestamp, level, service, and trace_id on every line
  • docker stats gives instant metrics; cAdvisor exposes them in Prometheus format for persistent collection
  • Prometheus + Grafana is the dominant open-source pattern: scrape /metrics, store time-series, visualise dashboards
  • Distributed tracing (OpenTelemetry + Jaeger/Zipkin) links work across containers via a shared trace ID — essential for debugging microservices latency
  • Three pillars: Logs (what), Metrics (how much), Traces (where) — use all three; they answer different questions
  • Managed platforms (Datadog, New Relic, Grafana Cloud) unify all three pillars — worth the cost for teams that want to skip operating the stack themselves