Docker and Microservices Deployment: Production-Ready Strategies
Containerization and microservices have revolutionized how we build and deploy applications. After deploying dozens of microservices architectures in production, I'm sharing the battle-tested strategies, patterns, and tools that ensure reliable, scalable deployments.
Docker Fundamentals for Production
Multi-Stage Builds for Optimized Images
# Dockerfile for Node.js application
# Stage 1: Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY yarn.lock ./
# Install dependencies
RUN yarn install --frozen-lockfile
# Copy source code
COPY . .
# Build application
RUN yarn build
# Stage 2: Production stage
FROM node:18-alpine AS production
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
# Copy built application from builder stage
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start application
CMD ["node", "dist/index.js"]
Docker Compose for Local Development
# docker-compose.yml
version: "3.8"
services:
# API Gateway
api-gateway:
build:
context: ./api-gateway
dockerfile: Dockerfile.dev
ports:
- "8080:8080"
environment:
- NODE_ENV=development
- REDIS_URL=redis://redis:6379
depends_on:
- redis
- user-service
- product-service
volumes:
- ./api-gateway:/app
- /app/node_modules
networks:
- microservices
# User Service
user-service:
build:
context: ./user-service
dockerfile: Dockerfile.dev
ports:
- "3001:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:password@postgres:5432/users
- JWT_SECRET=dev-secret
depends_on:
- postgres
volumes:
- ./user-service:/app
- /app/node_modules
networks:
- microservices
# Product Service
product-service:
build:
context: ./product-service
dockerfile: Dockerfile.dev
ports:
- "3002:3000"
environment:
- NODE_ENV=development
- MONGODB_URL=mongodb://mongo:27017/products
depends_on:
- mongo
volumes:
- ./product-service:/app
- /app/node_modules
networks:
- microservices
# Databases
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=users
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- microservices
mongo:
image: mongo:6-alpine
volumes:
- mongo_data:/data/db
networks:
- microservices
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
networks:
- microservices
# Monitoring
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- microservices
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
networks:
- microservices
volumes:
postgres_data:
mongo_data:
redis_data:
grafana_data:
networks:
microservices:
driver: bridge
Kubernetes Deployment Strategies
Deployment Manifests
# user-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
version: v1
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
version: v1
spec:
containers:
- name: user-service
image: myregistry/user-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: user-service-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: user-service-secrets
key: jwt-secret
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: user-service-config
imagePullSecrets:
- name: registry-secret
---
apiVersion: v1
kind: Service
metadata:
name: user-service
labels:
app: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 3000
protocol: TCP
type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
name: user-service-config
data:
app.yaml: |
server:
port: 3000
cors:
enabled: true
origins: ["https://myapp.com"]
logging:
level: info
format: json
---
apiVersion: v1
kind: Secret
metadata:
name: user-service-secrets
type: Opaque
data:
database-url: <base64-encoded-database-url>
jwt-secret: <base64-encoded-jwt-secret>
Horizontal Pod Autoscaler
# user-service-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
Ingress Configuration
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: microservices-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
tls:
- hosts:
- api.myapp.com
secretName: api-tls-secret
rules:
- host: api.myapp.com
http:
paths:
- path: /api/v1/users(/|$)(.*)
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /api/v1/products(/|$)(.*)
pathType: Prefix
backend:
service:
name: product-service
port:
number: 80
- path: /api/v1/orders(/|$)(.*)
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
Service Mesh with Istio
Istio Configuration
# istio-gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: microservices-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: api-tls-secret
hosts:
- api.myapp.com
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: microservices-vs
spec:
hosts:
- api.myapp.com
gateways:
- microservices-gateway
http:
- match:
- uri:
prefix: /api/v1/users
route:
- destination:
host: user-service
port:
number: 80
fault:
delay:
percentage:
value: 0.1
fixedDelay: 5s
retries:
attempts: 3
perTryTimeout: 2s
- match:
- uri:
prefix: /api/v1/products
route:
- destination:
host: product-service
port:
number: 80
subset: v1
weight: 90
- destination:
host: product-service
port:
number: 80
subset: v2
weight: 10
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: product-service-dr
spec:
host: product-service
trafficPolicy:
circuitBreaker:
consecutiveErrors: 3
interval: 30s
baseEjectionTime: 30s
maxEjectionPercent: 50
loadBalancer:
simple: LEAST_CONN
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
Security Policies
# security-policy.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: user-service-authz
spec:
selector:
matchLabels:
app: user-service
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/api-gateway"]
- to:
- operation:
methods: ["GET", "POST", "PUT", "DELETE"]
paths: ["/api/v1/users/*"]
- when:
- key: request.headers[authorization]
values: ["Bearer *"]
---
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
spec:
mtls:
mode: STRICT
CI/CD Pipeline with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy Microservices
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
service: [user-service, product-service, order-service]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "yarn"
cache-dependency-path: ${{ matrix.service }}/yarn.lock
- name: Install dependencies
run: |
cd ${{ matrix.service }}
yarn install --frozen-lockfile
- name: Run tests
run: |
cd ${{ matrix.service }}
yarn test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ${{ matrix.service }}/coverage/lcov.info
flags: ${{ matrix.service }}
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy:
matrix:
service: [user-service, product-service, order-service]
steps:
- uses: actions/checkout@v3
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: ./${{ matrix.service }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup kubectl
uses: azure/setup-kubectl@v3
with:
version: "v1.28.0"
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to Kubernetes
run: |
export KUBECONFIG=kubeconfig
# Update image tags in deployment files
sed -i "s|image: .*user-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/user-service:${{ github.sha }}|" k8s/user-service-deployment.yaml
sed -i "s|image: .*product-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/product-service:${{ github.sha }}|" k8s/product-service-deployment.yaml
sed -i "s|image: .*order-service:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/order-service:${{ github.sha }}|" k8s/order-service-deployment.yaml
# Apply deployments
kubectl apply -f k8s/
# Wait for rollout
kubectl rollout status deployment/user-service
kubectl rollout status deployment/product-service
kubectl rollout status deployment/order-service
- name: Run smoke tests
run: |
export KUBECONFIG=kubeconfig
# Wait for services to be ready
kubectl wait --for=condition=ready pod -l app=user-service --timeout=300s
kubectl wait --for=condition=ready pod -l app=product-service --timeout=300s
kubectl wait --for=condition=ready pod -l app=order-service --timeout=300s
# Run smoke tests
curl -f https://api.myapp.com/health || exit 1
Monitoring and Observability
Prometheus Configuration
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "user-service"
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_name]
action: keep
regex: user-service
- source_labels: [__meta_kubernetes_endpoint_port_name]
action: keep
regex: metrics
- job_name: "product-service"
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_name]
action: keep
regex: product-service
- job_name: "order-service"
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- source_labels: [__meta_kubernetes_service_name]
action: keep
regex: order-service
- job_name: "istio-mesh"
kubernetes_sd_configs:
- role: endpoints
namespaces:
names:
- istio-system
relabel_configs:
- source_labels:
[__meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: istio-telemetry;prometheus
Application Metrics
// metrics.js - Express.js middleware for metrics
const promClient = require("prom-client")
// Create a Registry
const register = new promClient.Registry()
// Add default metrics
promClient.collectDefaultMetrics({ register })
// Custom metrics
const httpRequestDuration = new promClient.Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status_code"],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
})
const httpRequestTotal = new promClient.Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status_code"],
})
const activeConnections = new promClient.Gauge({
name: "active_connections",
help: "Number of active connections",
})
const databaseQueryDuration = new promClient.Histogram({
name: "database_query_duration_seconds",
help: "Duration of database queries in seconds",
labelNames: ["operation", "table"],
buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3, 5],
})
// Register metrics
register.registerMetric(httpRequestDuration)
register.registerMetric(httpRequestTotal)
register.registerMetric(activeConnections)
register.registerMetric(databaseQueryDuration)
// Middleware to track HTTP metrics
const metricsMiddleware = (req, res, next) => {
const start = Date.now()
res.on("finish", () => {
const duration = (Date.now() - start) / 1000
const route = req.route ? req.route.path : req.path
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration)
httpRequestTotal.labels(req.method, route, res.statusCode).inc()
})
next()
}
// Database query wrapper
const trackDatabaseQuery = async (operation, table, queryFn) => {
const start = Date.now()
try {
const result = await queryFn()
const duration = (Date.now() - start) / 1000
databaseQueryDuration.labels(operation, table).observe(duration)
return result
} catch (error) {
const duration = (Date.now() - start) / 1000
databaseQueryDuration.labels(operation, table).observe(duration)
throw error
}
}
module.exports = {
register,
metricsMiddleware,
trackDatabaseQuery,
metrics: {
httpRequestDuration,
httpRequestTotal,
activeConnections,
databaseQueryDuration,
},
}
Distributed Tracing
// tracing.js - OpenTelemetry setup
const { NodeSDK } = require("@opentelemetry/sdk-node")
const {
getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node")
const { JaegerExporter } = require("@opentelemetry/exporter-jaeger")
const { Resource } = require("@opentelemetry/resources")
const {
SemanticResourceAttributes,
} = require("@opentelemetry/semantic-conventions")
const jaegerExporter = new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT || "http://jaeger:14268/api/traces",
})
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]:
process.env.SERVICE_NAME || "unknown-service",
[SemanticResourceAttributes.SERVICE_VERSION]:
process.env.SERVICE_VERSION || "1.0.0",
}),
traceExporter: jaegerExporter,
instrumentations: [
getNodeAutoInstrumentations({
"@opentelemetry/instrumentation-fs": {
enabled: false,
},
}),
],
})
sdk.start()
// Custom span creation
const { trace } = require("@opentelemetry/api")
const createSpan = (name, fn) => {
const tracer = trace.getTracer("user-service")
return tracer.startActiveSpan(name, async (span) => {
try {
const result = await fn(span)
span.setStatus({ code: 1 }) // OK
return result
} catch (error) {
span.setStatus({ code: 2, message: error.message }) // ERROR
span.recordException(error)
throw error
} finally {
span.end()
}
})
}
module.exports = { createSpan }
Security Best Practices
Container Security
# Secure Dockerfile
FROM node:18-alpine AS base
# Install security updates
RUN apk update && apk upgrade && apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile --production
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
# Switch to non-root user
USER nextjs
# Remove unnecessary packages
RUN yarn cache clean
# Set security headers
ENV NODE_OPTIONS="--max-old-space-size=1024"
EXPOSE 3000
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
Network Policies
# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: user-service-netpol
spec:
podSelector:
matchLabels:
app: user-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
ports:
- protocol: TCP
port: 3000
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
Conclusion
Successful microservices deployment requires careful attention to:
- Container Optimization - Multi-stage builds, security, and resource efficiency
- Orchestration - Kubernetes deployments, services, and scaling
- Service Mesh - Traffic management, security, and observability
- CI/CD Automation - Automated testing, building, and deployment
- Monitoring - Comprehensive observability with metrics, logs, and traces
- Security - Container security, network policies, and access control
The key is to start simple and gradually add complexity as your system grows. Focus on automation, monitoring, and security from the beginning to ensure your microservices architecture can scale reliably.
Remember: microservices introduce complexity, so make sure the benefits outweigh the costs for your specific use case. Start with a modular monolith and extract services as needed.
What challenges have you faced with microservices deployment? Are there specific aspects of containerization or orchestration you'd like me to dive deeper into? I'm here to help with any Docker or Kubernetes questions you might have.