Docker & Kubernetes in Production: A Complete DevOps Guide
Rating
Verdict
Docker is essential for every team regardless of scale. Kubernetes pays off at 5+ services or when you need auto-scaling — for smaller setups, consider Docker Compose or a managed PaaS.
Pros
- Consistent environments from dev to prod
- Horizontal scaling is trivial with K8s
- Rolling updates with zero downtime
- Excellent ecosystem (Helm, ArgoCD, etc.)
- Strong community and documentation
Cons
- Kubernetes has a steep learning curve
- Operational overhead for small teams
- Debugging containers can be tricky
- Resource overhead vs bare metal
Docker & Kubernetes in Production: A Complete DevOps Guide
Containerization has fundamentally changed how we build, ship, and run software. Docker provides the packaging standard while Kubernetes provides the orchestration platform that makes running containers at scale manageable. This guide takes you from writing your first Dockerfile all the way to production-grade Kubernetes deployments with auto-scaling, rolling updates, and observability.
Docker Fundamentals
Docker packages an application and its dependencies into a container — an isolated, reproducible environment that runs identically everywhere. The core components are:
- Dockerfile — Instructions to build a Docker image
- Image — A read-only template created from a Dockerfile
- Container — A running instance of an image
- Registry — Storage for images (Docker Hub, ECR, GCR)
Writing Optimized Dockerfiles
A production Dockerfile should be small, fast to build, and secure. Use multi-stage builds to separate build dependencies from the final image:
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production runner (smallest possible image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=deps /app/node_modules ./node_modules
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
Key optimization rules:
- Use Alpine-based images (
node:20-alpine) — 5x smaller than full Debian - Copy only what's needed in the final stage
- Run as a non-root user for security
- Order layers from least to most frequently changed
- Use
.dockerignoreto exclude node_modules, .git, .env files
# .dockerignore
node_modules
.next
.git
.env*
*.log
coverage
Docker Compose for Local Development
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: builder
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- .:/app
- /app/node_modules
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Kubernetes Architecture
Kubernetes (K8s) is a container orchestration platform that automates deployment, scaling, and management of containerized applications. Key concepts:
- Node — A worker machine (VM or physical) that runs pods
- Pod — The smallest deployable unit, containing one or more containers
- Deployment — Manages a set of identical pods with rolling updates
- Service — Exposes pods as a stable network endpoint
- Ingress — Manages external HTTP/HTTPS access to services
- ConfigMap/Secret — Configuration and sensitive data injection
Writing Kubernetes Manifests
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
labels:
app: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0 # Zero-downtime deploys
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: myregistry/my-app:v1.2.3
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: my-app
topologyKey: kubernetes.io/hostname
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-service
namespace: production
spec:
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
Ingress with TLS
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: my-app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80
Horizontal Pod Autoscaler
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
CI/CD Pipeline with GitHub Actions
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: k8s/
images: ghcr.io/${{ github.repository }}:${{ github.sha }}
Observability: Logs, Metrics & Traces
Production K8s requires comprehensive observability. The standard stack is:
- Metrics: Prometheus + Grafana (kube-prometheus-stack Helm chart)
- Logs: Loki + Grafana or ELK Stack
- Traces: OpenTelemetry + Jaeger or Tempo
# Install kube-prometheus-stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.adminPassword=securepassword
Conclusion
Docker and Kubernetes form the foundation of modern cloud-native infrastructure. Docker gives you portable, reproducible builds. Kubernetes gives you the automation to run them reliably at scale. Start with Docker Compose for local dev, containerize your builds with multi-stage Dockerfiles, and graduate to Kubernetes when you need auto-scaling, rolling deployments, or multi-region resilience.