Skip to content
Published on

Docker & Kubernetes Essentials — From Containers to Orchestration

Authors

1. What Are Containers

Virtual Machines vs Containers

Traditional virtual machines (VMs) run an entire guest OS on top of a hypervisor. Each VM includes its own kernel, libraries, and binaries, requiring several GB of disk space and tens of seconds to boot.

Containers share the host OS kernel and provide process-level isolation. Image sizes are typically tens of MB, and startup time is measured in milliseconds.

AspectVirtual MachineContainer
Isolation levelFull OSProcess
Image sizeSeveral GBTens to hundreds of MB
Startup timeTens of secondsMilliseconds
Resource overheadHighLow
PortabilityHypervisor dependentRuns anywhere with matching kernel

Linux Namespaces and cgroups

Container isolation is implemented through two Linux kernel features.

Namespaces limit the scope of system resources that a process can see.

  • PID namespace: The container sees an independent process tree starting from PID 1
  • NET namespace: Independent network interfaces, IP addresses, and routing tables
  • MNT namespace: Independent filesystem mount points
  • UTS namespace: Independent hostname
  • IPC namespace: Independent IPC resources
  • USER namespace: Independent UID/GID mappings

cgroups (Control Groups) limit hardware resource usage such as CPU, memory, and disk I/O.

# Check memory limit via cgroup
cat /sys/fs/cgroup/memory/docker/CONTAINER_ID/memory.limit_in_bytes

Thanks to these two features, containers behave like independent machines while sharing the kernel, keeping them lightweight.


2. Docker Fundamentals

Three Core Concepts

Image is a read-only template. Application code, runtime, system libraries, and configuration files are stored in a layered structure.

Container is a running instance of an image. A writable layer is added on top of the image.

Registry is a service for storing and distributing images. Docker Hub is the most common, with private registries like AWS ECR and GitHub Container Registry also available.

Essential Commands

# Pull an image
docker pull nginx:1.25

# Run a container
docker run -d --name my-nginx -p 8080:80 nginx:1.25

# List running containers
docker ps

# View container logs
docker logs my-nginx

# Access container shell
docker exec -it my-nginx /bin/bash

# Stop and remove container
docker stop my-nginx
docker rm my-nginx

# Build an image
docker build -t my-app:1.0 .

# Push image to registry
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0

Understanding Image Layers

Docker images consist of multiple read-only layers. Each instruction in a Dockerfile creates one layer.

# Inspect image layers
docker history my-app:1.0

Layers are cached. Unchanged layers are not rebuilt, so placing less frequently changed instructions at the top of your Dockerfile is key to optimizing build speed.


3. Dockerfile Best Practices

Basic Dockerfile Structure

# Specify base image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy dependency files first (cache optimization)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Run command
CMD ["node", "server.js"]

Multi-stage Builds

Separate build tools from the runtime environment to dramatically reduce the final image size.

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

devDependencies, source code, and build tools needed only during the build stage are excluded from the final image.

Layer Cache Optimization

# Bad: npm install re-runs every time source changes
COPY . .
RUN npm ci

# Good: leverages cache when package.json hasn't changed
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

.dockerignore File

Prevent unnecessary files from being included in the build context.

node_modules
.git
.env
*.md
dist
.DS_Store
coverage

Security Best Practices

# 1. Run as non-root user
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# 2. Use specific base image versions (never use latest tag)
FROM node:20.11.1-alpine3.19

# 3. Use COPY instead of ADD
COPY ./config /app/config

# 4. Never include sensitive data in the image
# Use build ARGs carefully, ensuring they don't remain in the final image

4. Docker Compose

Multi-container Orchestration

Docker Compose defines and manages multiple containers with a single YAML file.

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src
    networks:
      - backend

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:
    driver: bridge

Core Compose Commands

# Start all services
docker compose up -d

# View logs
docker compose logs -f app

# Rebuild and restart a specific service
docker compose up -d --build app

# Check service status
docker compose ps

# Stop all services and clean up resources
docker compose down -v

Networking and Volumes

Networking: Services within the same Compose file can communicate using service names. In the example above, the app service accesses the database via db:5432.

Volumes: Data persists even when containers are deleted. The named volume postgres_data preserves database files.

Development vs Production Environments

# docker-compose.override.yml (auto-applied for development)
services:
  app:
    build:
      target: development
    volumes:
      - ./src:/app/src
    environment:
      - NODE_ENV=development
    command: npm run dev
# Production deployment without override
docker compose -f docker-compose.yml up -d

5. Kubernetes Architecture

Why Kubernetes

Docker Compose is great for managing multiple containers on a single host, but production environments require:

  • Container deployment across multiple servers
  • Auto-scaling
  • Service discovery and load balancing
  • Rolling updates and rollbacks
  • Self-healing

Kubernetes (K8s) is a container orchestration platform that provides all of this.

Control Plane Components

API Server (kube-apiserver): The central gateway that handles all cluster requests. kubectl commands, internal components, and external clients all go through the API Server.

etcd: A distributed key-value store that holds all cluster state. Information about which Pods run where, which Services exist, and more is stored here.

Scheduler (kube-scheduler): Decides which node to place newly created Pods on. It considers resource requirements, node status, affinity rules, and more.

Controller Manager (kube-controller-manager): Ensures the cluster's current state matches the desired state. Includes the ReplicaSet Controller, Node Controller, Job Controller, and others.

Worker Node Components

kubelet: Runs on each node, executing containers and reporting status as directed by the API Server.

kube-proxy: Manages network rules on each node to handle Service load balancing.

Container Runtime: The software that actually runs containers. containerd is the most widely used.

Control Plane
  +------------------+
  | API Server       |<--- kubectl, clients
  | etcd             |
  | Scheduler        |
  | Controller Mgr   |
  +------------------+
        |
  Worker Node 1          Worker Node 2
  +-----------------+   +-----------------+
  | kubelet         |   | kubelet         |
  | kube-proxy      |   | kube-proxy      |
  | containerd      |   | containerd      |
  | [Pod] [Pod]     |   | [Pod] [Pod]     |
  +-----------------+   +-----------------+

6. Kubernetes Core Objects

Pod

A Pod is the smallest deployable unit in K8s. It contains one or more containers that share the same network and storage.

apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: app
      image: my-app:1.0
      ports:
        - containerPort: 3000
      resources:
        requests:
          memory: "128Mi"
          cpu: "250m"
        limits:
          memory: "256Mi"
          cpu: "500m"
      livenessProbe:
        httpGet:
          path: /healthz
          port: 3000
        initialDelaySeconds: 10
        periodSeconds: 5
      readinessProbe:
        httpGet:
          path: /ready
          port: 3000
        initialDelaySeconds: 5
        periodSeconds: 3

ReplicaSet

Ensures that a specified number of Pod replicas are always running. Typically not used directly, but managed through Deployments.

Deployment

Declaratively manages Pods and ReplicaSets. Supports rolling updates, rollbacks, and scaling.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: my-app:1.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
# Check deployment status
kubectl rollout status deployment/my-app

# Update image (triggers rolling update)
kubectl set image deployment/my-app app=my-app:2.0

# Rollback
kubectl rollout undo deployment/my-app

# Scaling
kubectl scale deployment/my-app --replicas=5

Service

Provides stable network endpoints for Pods. Pods are ephemeral, but a Service's IP and DNS remain fixed.

ClusterIP (default): Only accessible within the cluster.

apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

NodePort: Exposes the service externally through a specific port on each node.

apiVersion: v1
kind: Service
metadata:
  name: my-app-nodeport
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
      nodePort: 30080

LoadBalancer: Automatically provisions a cloud provider's load balancer.

apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

Ingress

Routes HTTP/HTTPS traffic to internal cluster Services. You can define routing rules based on hostnames and paths.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app-svc
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-svc
                port:
                  number: 80
  tls:
    - hosts:
        - app.example.com
      secretName: tls-secret

7. Kubernetes Configuration Management

ConfigMap

Separates environment configuration from container images.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "db-service"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  app.properties: |
    server.port=3000
    cache.ttl=300
# Using ConfigMap in a Pod
spec:
  containers:
    - name: app
      image: my-app:1.0
      envFrom:
        - configMapRef:
            name: app-config
      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: app-config
        items:
          - key: app.properties
            path: app.properties

Secret

Manages sensitive data like passwords and API keys. Stored as base64-encoded values.

# Create a Secret
kubectl create secret generic db-secret \
  --from-literal=username=admin \
  --from-literal=password=s3cret
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  username: YWRtaW4=
  password: czNjcmV0
# Using Secret in a Pod
spec:
  containers:
    - name: app
      env:
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: password

PersistentVolume (PV) and PersistentVolumeClaim (PVC)

Maintains data independently from the Pod lifecycle.

# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: standard
# Using PVC in a Deployment
spec:
  containers:
    - name: postgres
      image: postgres:16-alpine
      volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: postgres-storage
      persistentVolumeClaim:
        claimName: postgres-pvc

8. Helm - The Kubernetes Package Manager

What Is Helm

Helm is a tool that bundles K8s manifests into packages called charts. You can install, upgrade, and rollback multiple YAML files as a single unit.

Chart Structure

my-app-chart/
  Chart.yaml          # Chart metadata
  values.yaml         # Default configuration values
  templates/          # K8s manifest templates
    deployment.yaml
    service.yaml
    ingress.yaml
    configmap.yaml
    _helpers.tpl      # Template helper functions
  charts/             # Dependency charts

values.yaml

# values.yaml
replicaCount: 3

image:
  repository: my-app
  tag: "1.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  hostname: app.example.com

resources:
  requests:
    cpu: 250m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilization: 70

Core Helm Commands

# Install a chart
helm install my-release ./my-app-chart

# Install with custom values
helm install my-release ./my-app-chart -f production-values.yaml

# Upgrade a release
helm upgrade my-release ./my-app-chart --set image.tag=2.0

# List releases
helm list

# Check release status
helm status my-release

# Release history
helm history my-release

# Rollback
helm rollback my-release 1

# Uninstall release
helm uninstall my-release

Managing Environment-specific Values Files

# File structure by environment
values.yaml              # Common defaults
values-dev.yaml          # Development
values-staging.yaml      # Staging
values-prod.yaml         # Production
# Staging deployment
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-staging.yaml \
  --namespace staging

# Production deployment
helm upgrade --install my-app ./my-app-chart \
  -f values.yaml \
  -f values-prod.yaml \
  --namespace production

9. Real-world Deployment - Web App + DB + Redis

Overall Architecture

A complete example of deploying a web application, PostgreSQL database, and Redis cache to Kubernetes.

Create Namespace

kubectl create namespace my-app

PostgreSQL Deployment

# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_DB
              value: mydb
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
      volumes:
        - name: postgres-data
          persistentVolumeClaim:
            claimName: postgres-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: my-app
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Redis Deployment

# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports:
            - containerPort: 6379
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "250m"
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: my-app
spec:
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379

Web Application Deployment

# app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
  namespace: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: my-web-app:1.0
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              value: "postgres://$(DB_USER):$(DB_PASS)@postgres:5432/mydb"
            - name: REDIS_URL
              value: "redis://redis:6379"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: username
            - name: DB_PASS
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
          livenessProbe:
            httpGet:
              path: /healthz
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: web-app
  namespace: my-app
spec:
  type: ClusterIP
  selector:
    app: web-app
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app-ingress
  namespace: my-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80

Deployment Order

# 1. Create Secret
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=secure-password-here \
  -n my-app

# 2. Create PVC
kubectl apply -f postgres-pvc.yaml

# 3. Deploy database
kubectl apply -f postgres-deployment.yaml

# 4. Deploy Redis
kubectl apply -f redis-deployment.yaml

# 5. Deploy web app
kubectl apply -f app-deployment.yaml

# 6. Verify status
kubectl get all -n my-app

10. Troubleshooting

CrashLoopBackOff

The Pod is repeatedly starting and crashing.

# Identify the cause
kubectl describe pod POD_NAME -n my-app
kubectl logs POD_NAME -n my-app --previous

# Common causes:
# - Application errors during startup
# - Incorrect environment variables
# - Failed connection to dependent services
# - livenessProbe failure

Resolution: Check logs to identify the error. Increase the livenessProbe initialDelaySeconds, or verify that environment variables are correct.

ImagePullBackOff

The container image cannot be pulled.

# Identify the cause
kubectl describe pod POD_NAME -n my-app

# Common causes:
# - Typo in image name or tag
# - Private registry authentication not configured
# - Image does not exist

# Configure private registry authentication
kubectl create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=user \
  --docker-password=pass \
  -n my-app

OOMKilled

The container was forcefully terminated for exceeding its memory limit.

# Identify the cause
kubectl describe pod POD_NAME -n my-app

# Check real-time resource usage
kubectl top pod -n my-app

# Resolution: increase memory limits
resources:
  limits:
    memory: "512Mi"  # Increased from 256Mi

General Debugging Commands

# List Pods and their status
kubectl get pods -n my-app -o wide

# Detailed Pod information (including events)
kubectl describe pod POD_NAME -n my-app

# Real-time log streaming
kubectl logs -f POD_NAME -n my-app

# Access Pod shell
kubectl exec -it POD_NAME -n my-app -- /bin/sh

# Check Service endpoints
kubectl get endpoints -n my-app

# DNS resolution within the cluster
kubectl run debug --rm -it --image=busybox -- nslookup web-app.my-app.svc.cluster.local

# Node resource status
kubectl top nodes

Conclusion

Here is a summary of what we covered:

  1. Container fundamentals: Lightweight isolation through namespaces and cgroups
  2. Docker: Image building, multi-stage builds, security practices
  3. Docker Compose: Multi-container management for development environments
  4. K8s architecture: Roles of the Control Plane and Worker Nodes
  5. K8s objects: Using Pod, Deployment, Service, and Ingress
  6. Configuration management: ConfigMap, Secret, PV/PVC
  7. Helm: Release management with charts
  8. Real-world deployment: Web app + DB + Redis 3-tier architecture
  9. Troubleshooting: Handling CrashLoopBackOff, ImagePullBackOff, and OOMKilled

Docker and Kubernetes are the foundation of modern infrastructure. I encourage you to practice the examples in this guide on a local environment such as minikube or kind. Hands-on experience deploying Pods, configuring Services, and troubleshooting issues is the fastest way to learn.