Running Docker in development is easy. Running it reliably in production is a different challenge. When containers fail silently or consume all available memory, you need monitoring, logging, and troubleshooting skills. This lesson covers how to keep your production containers healthy, visible, and debuggable — a critical bridge between running Docker locally and migrating to Kubernetes orchestration.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Monitor container resource usage with
docker statsanddocker events - Configure structured logging with Docker log drivers
- Implement health checks for automatic container recovery
- Set CPU and memory limits to prevent resource starvation
- Use
docker execanddocker inspectfor live debugging - Set up cAdvisor and Prometheus for production monitoring
- Troubleshoot common production container failures
2. Why This Matters
Real-world scenario: Your team deploys a Node.js API in a container to production. Two days later, users report timeouts. The container is still running, but memory usage has climbed to 95% and the health check endpoint returns 503. Without monitoring, you wouldn't even know the container is degrading — it's still running, just not serving traffic. Production Docker requires observability: CPU/memory stats, structured logs, health probes, and alerting. These skills are also your foundation for Kubernetes, where Pod health probes and resource limits are mandatory.
3. Core Concepts
Container Resource Monitoring with docker stats
# Live resource usage for all running containers
docker stats
# Stats for specific containers (with no-stream for one-shot)
docker stats --no-stream myapp nginx redis
# Format as JSON for scripting
docker stats --no-stream --format '{{json .}}' myapp
# CPU usage as percentage, memory in MB
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}'
# Watch container events in real time
docker events --filter 'event=oom' --filter 'event=die'
# Historical events (since a timestamp)
docker events --since '2025-02-10T00:00:00' --until '2025-02-11T00:00:00'
Resource Constraints: CPU and Memory Limits
# Hard memory limit (container hits the wall at 512MB)
docker run -d --name myapp --memory=512m myapp:latest
# Soft memory limit (warns before hard limit)
docker run -d --name myapp --memory=512m --memory-reservation=256m myapp:latest
# CPU limits
# --cpus: fractional CPU cores
docker run -d --name myapp --cpus=1.5 myapp:latest
# CPU shares (relative weight, default 1024)
docker run -d --name myapp --cpu-shares=2048 myapp:latest
# Pin to specific CPU cores
docker run -d --name myapp --cpuset-cpus=0,1 myapp:latest
# Combined production run
docker run -d \
--name myapp \
--memory=512m \
--memory-reservation=384m \
--cpus=1.0 \
--restart=unless-stopped \
-p 8080:3000 \
myapp:latest
# Check current limits
docker inspect myapp --format '{{json .HostConfig.Memory}}' | xargs echo "Memory limit bytes:"
docker inspect myapp --format '{{json .HostConfig.NanoCpus}}' | xargs echo "CPU limit (nano-CPUs):"
Health Checks for Automatic Recovery
# HEALTHCHECK instruction in Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Or at runtime (overrides Dockerfile HEALTHCHECK)
docker run -d \
--name myapp \
--health-cmd='curl -f http://localhost:3000/health || exit 1' \
--health-interval=30s \
--health-timeout=3s \
--health-start-period=15s \
--health-retries=3 \
myapp:latest
# Check health status
docker inspect --format '{{.State.Health.Status}}' myapp
docker inspect --format '{{json .State.Health}}' myapp | jq
# View health check logs
docker inspect myapp | jq '.[].State.Health.Log[-1]'
# Auto-restart only on non-healthy state (with restart policy)
docker update --restart=unless-stopped myapp
Structured Logging with Log Drivers
# Default: json-file driver (may fill disk)
docker run -d --name myapp myapp:latest
# Limit log file size and rotation
docker run -d \
--name myapp \
--log-driver=json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp:latest
# Syslog driver (ship to central logging)
docker run -d \
--name myapp \
--log-driver=syslog \
--log-opt syslog-address=tcp://logs.example.com:514 \
--log-opt tag='{{.Name}}/{{.ID}}' \
myapp:latest
# Fluentd driver (stream to EFK/ELK)
docker run -d \
--name myapp \
--log-driver=fluentd \
--log-opt fluentd-address=localhost:24224 \
--log-opt tag='docker.{{.Name}}' \
myapp:latest
# GELF driver (Graylog Extended Log Format)
docker run -d \
--name myapp \
--log-driver=gelf \
--log-opt gelf-address=udp://graylog.example.com:12201 \
myapp:latest
# Configure log driver globally in daemon.json
# /etc/docker/daemon.json
# {
# 'log-driver': 'json-file',
# 'log-opts': {
# 'max-size': '10m',
# 'max-file': '3'
# }
# }
# View logs with filtering and timestamps
docker logs --tail=50 --timestamps myapp
docker logs --since=5m myapp
docker logs --until=2025-02-10T12:00:00 myapp
# Follow logs from multiple containers
docker logs -f myapp redis nginx
cAdvisor: Production Container Monitoring
# Deploy cAdvisor (container advisor) - single host
# From Google: collects CPU, memory, network, and disk metrics
docker run -d \
--name=cadvisor \
--restart=unless-stopped \
-p 8080:8080 \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--volume=/dev/disk/:/dev/disk:ro \
--privileged \
--device=/dev/kmsg \
gcr.io/cadvisor/cadvisor:latest
# Access cAdvisor UI
# http://localhost:8080
# Docker Compose setup with Prometheus
version: '3.8'
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
ports:
- '8080:8080'
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
ports:
- '9090:9090'
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
restart: unless-stopped
# prometheus.yml
scrape_configs:
- job_name: 'cadvisor'
scrape_interval: 15s
static_configs:
- targets: ['cadvisor:8080']
# Access Prometheus: http://localhost:9090
# Query examples in PromQL:
# rate(container_cpu_usage_seconds_total[1m])
# container_memory_usage_bytes
# container_network_receive_bytes_total
Live Debugging and Troubleshooting
# Inspect container configuration
docker inspect myapp
# Check exit code and reason
docker inspect --format '{{.State.Status}}: {{.State.ExitCode}} ({{.State.Error}})' myapp
# View container logs
docker logs --tail=100 myapp
# Execute commands in running container
docker exec -it myapp sh -c 'top -b -n 1'
docker exec myapp sh -c 'ps aux'
docker exec myapp sh -c 'netstat -tulpn'
docker exec myapp sh -c 'df -h'
docker exec myapp sh -c 'free -m'
# Debug with a sidecar container (same network namespace)
docker run -it --rm --network container:myapp nicolaka/netshoot /bin/bash
# Inside netshoot: tcpdump, curl, dig, nmap available
# Copy files from container
docker cp myapp:/var/log/app.log ./app.log
# Save and inspect container filesystem
docker export myapp -o myapp_fs.tar
tar tf myapp_fs.tar | grep error
# Check container resource usage history
docker stats --no-stream --all
4. Hands-On Practice: Production Docker Setup
Set up a production-ready Docker deployment with monitoring, health checks, and proper resource limits. This exercise uses a sample Node.js application with a health endpoint.
# 1. Create a sample app Dockerfile with health check
cat > Dockerfile << 'DOCKERFILE'
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
# Health check endpoint at /health
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
USER node
CMD ['node', 'server.js']
DOCKERFILE
# 2. Simple health endpoint (server.js snippet)
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
const mem = process.memoryUsage();
const uptime = process.uptime();
res.json({ status: 'healthy', uptime, memory: mem });
});
# 3. Build and run with production flags
docker build -t myapp-prod .
docker run -d \
--name myapp-prod \
--memory=256m \
--memory-reservation=192m \
--cpus=0.5 \
--restart=unless-stopped \
-p 3000:3000 \
myapp-prod
# 4. Verify health status
docker inspect --format '{{.State.Health.Status}}' myapp-prod
# 5. Check resource usage
docker stats --no-stream myapp-prod
# 6. Simulate a memory spike (watch for OOM)
docker exec myapp-prod sh -c 'stress --vm 1 --vm-bytes 300M --timeout 10s'
# The container will crash with OOM since limit is 256m
# docker restart will bring it back (restart policy)
# 7. View the crash event
docker events --since '1m' --filter 'container=myapp-prod'
# 8. Deploy cAdvisor for full visibility
docker run -d \
--name=cadvisor \
--restart=unless-stopped \
-p 8080:8080 \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:ro \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--privileged \
gcr.io/cadvisor/cadvisor:latest
# 9. Access cAdvisor
# Open http://localhost:8080 in your browser
# 10. Cleanup
docker stop myapp-prod cadvisor
docker rm myapp-prod cadvisor
5. Common Errors and Solutions
Error 1: OOMKilled (Exit Code 137)
- Cause: Container exceeded its memory limit; kernel OOM killer terminated it
- Fix: Increase
--memoryor optimize the app. Use--memory-reservationas a soft limit. Monitor withdocker events --filter 'event=oom'
Error 2: Container exits immediately (Exit Code 1 or 139)
- Cause: App crashed on startup (code 1) or segfault (code 139)
- Fix: Check
docker logs --tail=50 myappfor the error. Usedocker run -it --rm myapp shto debug interactively. Ensure ENTRYPOINT and CMD are correct
Error 3: Disk full from container logs
- Cause: Default json-file log driver has no rotation; logs fill /var/lib/docker
- Fix: Add
--log-opt max-size=10m --log-opt max-file=3. Truncate existing logs:truncate -s 0 /var/lib/docker/containers/*/*-json.log
Error 4: Health check endpoint unreachable
- Cause: Health check fires before the app is ready (too short
--start-period) - Fix: Increase
--health-start-periodto 30–60s for apps with slow startup. Verify the port is correct in the health check command
Error 5: Container is running but not serving traffic
- Cause: App is alive (not crashing) but stuck or deadlocked (e.g., database connection pool exhausted)
- Fix: Implement a proper health check that tests dependencies. Use
docker execto inspect threads:docker exec myapp sh -c 'kill -3 1'(triggers thread dump in Java, stack trace in Node)
Error 6: Port already allocated
- Cause: Another container or process is using the host port
- Fix: Find the conflict:
netstat -tulpn | grep :8080. Usedocker psto check running containers. Change the host port mapping
6. Summary Checklist
- I can monitor containers with
docker statsanddocker events - I can set CPU and memory limits on containers
- I can add HEALTHCHECK to a Dockerfile or at runtime
- I can configure log rotation and structured log drivers
- I can deploy cAdvisor to monitor container health
- I can debug a crashed container using logs and inspect
- I understand OOMKilled and how to prevent it
- I can troubleshoot port conflicts and disk space issues
7. Practice Exercise
Challenge: Debug and fix a failing production container
- Run
docker run -d --name debug-challenge --memory=64m nginx:alpine - This container has too little memory. Check its health:
docker ps -a— notice it may crash or be unhealthy - Use
docker logs debug-challengeanddocker inspect debug-challengeto diagnose - Fix by running with
--memory=128mand adding a health check that testshttp://localhost:80 - Set up log rotation with max-size=5m and max-file=2
- Add a restart policy of unless-stopped
- Deploy cAdvisor and verify the container appears in its dashboard (http://localhost:8080)
- Bonus: Write a shell script that alerts when a container's health status becomes 'unhealthy' for more than 3 checks
8. Next Steps
You've covered the full lifecycle of Docker in production — from resource limits and health checks to monitoring and troubleshooting. The next category in the DevOps roadmap is Kubernetes: Container Orchestration. Kubernetes builds on everything you've learned here: Pod health probes (liveness, readiness, startup), resource requests and limits, container logging with kubectl logs, and monitoring with Prometheus operator. Your Docker production skills are the direct foundation for K8s operations.
Next lesson in the Docker series: This concludes the Docker series. Continue to the Kubernetes category for container orchestration at scale.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment