Docker in Production
Production patterns for Docker: health checks, logging drivers, resource limits, registries, and CI/CD integration.
Health Checks
Docker can monitor container health and restart unhealthy ones:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# docker-compose.yml
services:
api:
image: myapi:1.0
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
docker ps # shows health status: healthy / unhealthy / starting
docker inspect --format='{{.State.Health}}' my-api
Resource Limits
Always set limits to prevent a single container from exhausting the host:
docker run -d \
--memory="512m" \ # hard memory limit
--memory-reservation="256m" \ # soft limit (scheduling hint)
--cpus="1.5" \ # 1.5 CPU cores
--pids-limit=100 \ # max processes (prevent fork bombs)
myapp:1.0
services:
api:
image: myapi:1.0
deploy:
resources:
limits:
memory: 512m
cpus: "1.5"
reservations:
memory: 256m
Logging
# Default driver with rotation
docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
myapp:1.0
# AWS CloudWatch
docker run -d \
--log-driver awslogs \
--log-opt awslogs-region=us-east-1 \
--log-opt awslogs-group=/myapp/production \
myapp:1.0
Configure globally in /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Private Registries
# Docker Hub (private repo)
docker login
docker tag myapp:1.0 myorg/myapp:1.0
docker push myorg/myapp:1.0
# AWS ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
123456789.dkr.ecr.us-east-1.amazonaws.com
docker tag myapp:1.0 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0
# GitHub Container Registry
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
docker push ghcr.io/myorg/myapp:1.0
CI/CD Pipeline
# .github/workflows/docker.yml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Docker Buildx and Multi-Platform Images
# Create a builder with multi-platform support
docker buildx create --name mybuilder --use
# Build for AMD64 and ARM64 (Apple Silicon, AWS Graviton)
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:1.0 \
--push \
.
Restart Policies
docker run -d \
--restart unless-stopped \ # restart on crash, but not if manually stopped
myapp:1.0
| Policy | Behavior |
|---|---|
no | Never restart (default) |
on-failure | Restart only on non-zero exit code |
always | Always restart, including on Docker daemon restart |
unless-stopped | Like always, but not if you manually stopped it |
Zero-Downtime Deployment Pattern
# 1. Pull new image
docker pull myapp:2.0
# 2. Start new container
docker run -d --name myapp-new -p 3001:3000 myapp:2.0
# 3. Wait for health check to pass
until [ "$(docker inspect --format='{{.State.Health.Status}}' myapp-new)" = "healthy" ]; do
sleep 2
done
# 4. Switch load balancer to new container (nginx reload, etc.)
# 5. Stop old container
docker stop myapp-old && docker rm myapp-old
docker rename myapp-new myapp-old
daemon.json Tuning
/etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"default-ulimits": {
"nofile": { "Name": "nofile", "Hard": 64000, "Soft": 64000 }
}
}
live-restore: true keeps containers running when the Docker daemon restarts — critical for production.
Frequently Asked Questions
Should I run Docker in production directly or use Kubernetes?
Docker alone (or with Compose on a single host) is fine for small deployments. When you need multi-host scheduling, auto-scaling, rolling updates, and self-healing across a cluster, move to Kubernetes. Many teams start with Docker on a single VM and migrate when they outgrow it.
How do I do zero-downtime deployments with Docker?
Use a load balancer (nginx, Traefik, AWS ALB) in front of your containers. Start the new container, wait for it to pass health checks, then route traffic to it, then stop the old one. Docker Swarm and Kubernetes handle this natively.
What logging driver should I use in production?
Depends on your stack. json-file is the default — simple but files accumulate on disk. Use awslogs for AWS CloudWatch, splunk for Splunk, or fluentd/fluentbit as a sidecar to ship to any backend. Always set max-size and max-file on json-file to prevent disk exhaustion.