Modern DevOps with Docker and Kubernetes

June 20, 2024
Eric Fisher
12 min read

A comprehensive guide to containerization and orchestration, from Docker basics to advanced Kubernetes deployment strategies.

Modern DevOps with Docker and Kubernetes

The containerization revolution has fundamentally transformed how we develop, deploy, and operate applications. Docker and Kubernetes have emerged as the foundational technologies enabling this transformation. This comprehensive guide explores best practices for implementing modern DevOps workflows using containerization and orchestration.

The Container Advantage

Containers solve the “it works on my machine” problem by packaging applications with all their dependencies into portable, lightweight units. Key benefits include:

  • Consistency: Applications run identically across development, testing, and production
  • Resource efficiency: Containers share the host OS kernel, reducing overhead
  • Rapid deployment: Fast startup times enable quick scaling and updates
  • Isolation: Applications are isolated from each other and the host system

Docker Fundamentals

Efficient Dockerfile Design

A well-designed Dockerfile is crucial for security, performance, and maintainability:

# Use specific, minimal base images
FROM node:18-alpine AS builder

# Create non-root user for security
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# Set working directory
WORKDIR /app

# Copy package files first for better caching
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy source code
COPY --chown=nextjs:nodejs . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

Multi-Stage Builds for Optimization

Multi-stage builds dramatically reduce image size and improve security:

# Build stage
FROM golang:1.21-alpine AS build

WORKDIR /app

# Install dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Production stage
FROM alpine:latest

# Install certificates for HTTPS
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy binary from build stage
COPY --from=build /app/main .

# Create non-root user
RUN adduser -D -s /bin/sh appuser
USER appuser

EXPOSE 8080
CMD ["./main"]

Container Security Best Practices

Security should be built into every layer:

# Use official, minimal base images
FROM alpine:3.18

# Update packages and remove package manager
RUN apk update && \
    apk upgrade && \
    apk add --no-cache ca-certificates && \
    rm -rf /var/cache/apk/*

# Create non-root user
RUN addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

# Set secure permissions
COPY --chown=appuser:appgroup app /usr/local/bin/app
RUN chmod 755 /usr/local/bin/app

# Switch to non-root user
USER appuser

# Use HEALTHCHECK for monitoring
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD /usr/local/bin/app --health-check || exit 1

EXPOSE 8080
CMD ["/usr/local/bin/app"]

Docker Compose for Development

Docker Compose simplifies multi-container development environments:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        - NODE_ENV=development
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - app
    networks:
      - app-network

volumes:
  postgres_data:
  redis_data:

networks:
  app-network:
    driver: bridge

Kubernetes Architecture Overview

Kubernetes provides a robust platform for container orchestration with several key components:

  • Master Node: API server, etcd, scheduler, controller manager
  • Worker Nodes: kubelet, kube-proxy, container runtime
  • Pods: Smallest deployable units containing one or more containers
  • Services: Network abstraction for pod communication
  • Ingress: HTTP/HTTPS traffic routing
  • ConfigMaps & Secrets: Configuration and sensitive data management

Kubernetes Deployment Strategies

Deployment Manifests

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  labels:
    app: web-app
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: myregistry/web-app:v1.2.3
        ports:
        - containerPort: 8080
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: log-level
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 2
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          runAsNonRoot: true
          runAsUser: 1000
          capabilities:
            drop:
              - ALL
      imagePullSecrets:
      - name: registry-secret

Service Definition

apiVersion: v1
kind: Service
metadata:
  name: web-app-service
spec:
  selector:
    app: web-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-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"
spec:
  tls:
  - hosts:
    - myapp.example.com
    secretName: web-app-tls
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: web-app-service
            port:
              number: 80

Configuration Management

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  log-level: "info"
  cache-ttl: "300"
  database-pool-size: "10"
  feature-flags: |
    new-ui: true
    experimental-api: false
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  database-url: cG9zdGdyZXNxbDovL3VzZXI6cGFzc3dvcmRAZGI6NTQzMi9teWFwcA==
  api-key: c3VwZXJfc2VjcmV0X2FwaV9rZXk=
  jwt-secret: and0X3NlY3JldF9mb3Jfc2lnbmluZw==

Advanced Kubernetes Patterns

Horizontal Pod Autoscaling

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-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
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

Network Policies for Security

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-app-netpol
spec:
  podSelector:
    matchLabels:
      app: web-app
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    - podSelector:
        matchLabels:
          app: api-gateway
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432
  - to: []
    ports:
    - protocol: TCP
      port: 443
    - protocol: UDP
      port: 53

CI/CD Pipeline Integration

GitLab CI/CD Example

# .gitlab-ci.yml
stages:
  - test
  - build
  - security-scan
  - deploy-staging
  - deploy-production

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

test:
  stage: test
  image: node:18-alpine
  script:
    - npm ci
    - npm run test:unit
    - npm run test:integration
  coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  only:
    - master
    - develop

security-scan:
  stage: security-scan
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_TAG
  allow_failure: false
  dependencies:
    - build

deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  environment:
    name: staging
    url: https://staging.myapp.example.com
  script:
    - kubectl config use-context staging
    - envsubst < k8s/deployment.yaml | kubectl apply -f -
    - kubectl rollout status deployment/web-app -n staging
    - kubectl get services -n staging
  only:
    - develop

deploy-production:
  stage: deploy-production
  image: bitnami/kubectl:latest
  environment:
    name: production
    url: https://myapp.example.com
  script:
    - kubectl config use-context production
    - envsubst < k8s/deployment.yaml | kubectl apply -f -
    - kubectl rollout status deployment/web-app -n production
  when: manual
  only:
    - master

ArgoCD GitOps Configuration

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/web-app-manifests
    targetRevision: HEAD
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: web-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Monitoring and Observability

Prometheus Configuration

apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
data:
  prometheus.yml: |
    global:
      scrape_interval: 15s
      evaluation_interval: 15s
    
    rule_files:
      - "alert_rules.yml"
    
    scrape_configs:
      - job_name: 'kubernetes-apiservers'
        kubernetes_sd_configs:
        - role: endpoints
        scheme: https
        tls_config:
          ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
        relabel_configs:
        - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
          action: keep
          regex: default;kubernetes;https
      
      - job_name: 'web-app'
        kubernetes_sd_configs:
        - role: pod
        relabel_configs:
        - source_labels: [__meta_kubernetes_pod_label_app]
          action: keep
          regex: web-app
        - source_labels: [__meta_kubernetes_pod_name]
          target_label: instance
        - source_labels: [__meta_kubernetes_namespace]
          target_label: kubernetes_namespace
        - source_labels: [__meta_kubernetes_pod_name]
          target_label: kubernetes_pod_name

Application Metrics

// metrics.go
package main

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status_code"},
    )
    
    httpRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "endpoint"},
    )
    
    activeConnections = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_connections",
            Help: "Number of active connections",
        },
    )
)

// Middleware for metrics collection
func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Wrap response writer to capture status code
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next.ServeHTTP(wrapped, r)
        
        duration := time.Since(start).Seconds()
        
        httpRequestsTotal.WithLabelValues(
            r.Method,
            r.URL.Path,
            fmt.Sprintf("%d", wrapped.statusCode),
        ).Inc()
        
        httpRequestDuration.WithLabelValues(
            r.Method,
            r.URL.Path,
        ).Observe(duration)
    })
}

Best Practices Summary

Container Best Practices

  1. Use minimal base images: Alpine or scratch for production
  2. Multi-stage builds: Separate build and runtime environments
  3. Non-root users: Never run containers as root
  4. Health checks: Implement liveness and readiness probes
  5. Resource limits: Always set CPU and memory limits
  6. Security scanning: Scan images for vulnerabilities

Kubernetes Best Practices

  1. Namespace isolation: Use namespaces for environment separation
  2. Resource quotas: Prevent resource monopolization
  3. Network policies: Implement micro-segmentation
  4. RBAC: Follow principle of least privilege
  5. Pod security policies: Enforce security standards
  6. Regular updates: Keep clusters and images updated

Operations Best Practices

  1. Infrastructure as Code: Version control all configurations
  2. GitOps workflows: Use Git as the source of truth
  3. Monitoring: Implement comprehensive observability
  4. Backup strategies: Regular backups of persistent data
  5. Disaster recovery: Test recovery procedures regularly
  6. Documentation: Maintain runbooks and architecture docs

Conclusion

Modern DevOps practices with Docker and Kubernetes enable organizations to build resilient, scalable applications while maintaining development velocity. Key success factors include:

  1. Start simple: Begin with basic containerization before complex orchestration
  2. Security first: Build security into every layer of the stack
  3. Automate everything: CI/CD pipelines reduce human error and increase reliability
  4. Monitor continuously: Observability enables rapid issue resolution
  5. Iterate and improve: DevOps is a journey of continuous improvement

By following these practices and patterns, teams can leverage the full power of containerization and orchestration while maintaining operational excellence.

References

  1. Gartner, Inc. “Container Management Software Market Guide.” Gartner Research, 2023.
  2. Docker, Inc. “Docker Best Practices Guide.” Docker Documentation, 2024.
  3. Cloud Native Computing Foundation. “Kubernetes Documentation.” https://kubernetes.io/docs/
  4. Fowler, Martin. “Continuous Integration.” Martin Fowler’s Blog, 2006.
  5. Kim, Gene, et al. The DevOps Handbook. IT Revolution Press, 2021.
  6. Arundel, John, and Justin Domingus. Cloud Native DevOps with Kubernetes. O’Reilly Media, 2022.