Docker & Kubernetes: From Zero to Production
DevOps9 min read

Docker & Kubernetes: From Zero to Production

Vighnesh Salunkhe

Vighnesh Salunkhe

Full Stack Developer

Published

April 29, 2026

Docker & Kubernetes: From Zero to Production

Docker & Kubernetes: From Zero to Production

Every developer has heard "it works on my machine." Docker exists to make that sentence obsolete. Kubernetes exists to make sure it keeps working when you have 50 machines and one of them catches fire.

This guide takes you from writing your first Dockerfile to deploying a production-grade application on Kubernetes — with real configs you can adapt and use today.

"Shipping is a feature. A product that doesn't ship is just a hobby." — Joel Spolsky


1. Docker Fundamentals: Containers vs VMs

A container is not a virtual machine. A VM virtualizes hardware — it runs a full OS. A container virtualizes the OS — it shares the host kernel but isolates the filesystem, processes, and network.

text
Virtual Machines:          Containers:
┌─────────────────┐        ┌─────────────────┐
│   App A  App B  │        │   App A  App B  │
│   Libs   Libs   │        │   Libs   Libs   │
│   OS     OS     │        ├─────────────────┤
│   Hypervisor    │        │   Container RT  │
│   Host OS       │        │   Host OS       │
│   Hardware      │        │   Hardware      │
└─────────────────┘        └─────────────────┘
~GBs, minutes to start     ~MBs, milliseconds to start

The result: containers start in milliseconds, use megabytes of RAM, and pack dozens onto a single server where VMs would pack only a handful.


2. Writing a Production Dockerfile

Most Dockerfiles you find online are wrong for production. Here is the right way.

text
# Multi-stage build — keeps the final image small and secure
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app

# Copy package files first — Docker layer caching
# If package.json hasn't changed, this layer is cached
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production image — minimal, no dev tools
FROM node:20-alpine AS runner
WORKDIR /app

# Security: don't run as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy only what's needed to run
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

USER nextjs

EXPOSE 3000
ENV PORT 3000
ENV NODE_ENV production

CMD ["node", "server.js"]
Multi-stage builds are the single most impactful Dockerfile optimization. A naive single-stage Next.js image is ~1.5GB. A multi-stage image is ~150MB. Smaller images mean faster pulls, less attack surface, and lower storage costs.

.dockerignore: What Not to Copy

text
node_modules
.next
.git
.env
.env.local
*.log
README.md
.github
tests
coverage
Never copy
text
.env
files into a Docker image. Environment variables should be injected at runtime via
text
docker run -e
or Kubernetes Secrets — not baked into the image. Anyone who pulls your image would have your secrets.

3. Docker Compose: Local Development

Docker Compose orchestrates multiple containers locally. Your app, database, cache, and queue — all with one command.

text
# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: builder  # Use builder stage for dev (includes dev deps)
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    volumes:
      - .:/app                    # Mount source for hot reload
      - /app/node_modules         # Don't override node_modules
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: npm run dev

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data  # Persist data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data

  queue:
    image: confluentinc/cp-kafka:latest
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://queue:9092
    depends_on:
      - zookeeper

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181

volumes:
  postgres_data:
  redis_data:
text
# Start everything
docker compose up -d

# View logs
docker compose logs -f app

# Run a command inside a container
docker compose exec app npm run db:migrate

# Stop and remove containers (keep volumes)
docker compose down

# Stop and remove everything including volumes
docker compose down -v

4. Kubernetes Core Concepts

Kubernetes (K8s) is a container orchestrator. It takes your containers and decides where to run them, restarts them when they crash, scales them up under load, and rolls out updates without downtime.

The key objects:

ObjectWhat it is
PodThe smallest deployable unit — one or more containers
DeploymentManages a set of identical Pods, handles rolling updates
ServiceStable network endpoint for a set of Pods
IngressRoutes external HTTP traffic to Services
ConfigMapNon-sensitive configuration data
SecretSensitive data (passwords, API keys)
HPAHorizontal Pod Autoscaler — scales Pods based on metrics

5. Your First Kubernetes Deployment

text
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: portfolio-app
  labels:
    app: portfolio
spec:
  replicas: 3                    # Run 3 instances
  selector:
    matchLabels:
      app: portfolio
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1                # Add 1 new pod before removing old
      maxUnavailable: 0          # Never have fewer than desired replicas
  template:
    metadata:
      labels:
        app: portfolio
    spec:
      containers:
        - name: app
          image: yourregistry/portfolio:v1.2.3   # Always use specific tags, never 'latest'
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:           # Pull from Secret, not hardcoded
                  name: app-secrets
                  key: database-url
          resources:
            requests:
              memory: "256Mi"           # Minimum guaranteed
              cpu: "250m"               # 0.25 CPU cores
            limits:
              memory: "512Mi"           # Maximum allowed
              cpu: "500m"
          readinessProbe:               # Only send traffic when ready
            httpGet:
              path: /api/health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:                # Restart if unhealthy
            httpGet:
              path: /api/health
              port: 3000
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
text
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: portfolio-service
spec:
  selector:
    app: portfolio              # Routes to pods with this label
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP               # Internal only — Ingress handles external traffic
text
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: portfolio-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"   # Auto TLS
    nginx.ingress.kubernetes.io/rate-limit: "100"         # Rate limiting
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - yourname.dev
      secretName: portfolio-tls
  rules:
    - host: yourname.dev
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: portfolio-service
                port:
                  number: 80
Always use specific image tags in production (
text
v1.2.3
), never
text
latest
. With
text
latest
, you cannot tell what version is running, rollbacks are unreliable, and different nodes might pull different versions.

6. Secrets Management

text
# k8s/secrets.yaml — DO NOT commit this file with real values
# Use sealed-secrets, Vault, or external-secrets-operator in production
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  database-url: "postgresql://user:password@db:5432/myapp"
  redis-url: "redis://cache:6379"
  jwt-secret: "your-super-secret-jwt-key-min-32-chars"
text
# Apply secrets from a .env file (never commit the .env)
kubectl create secret generic app-secrets \
  --from-env-file=.env.production \
  --dry-run=client -o yaml | kubectl apply -f -

7. Autoscaling

text
# k8s/hpa.yaml — scale based on CPU usage
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: portfolio-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: portfolio-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70    # Scale up when CPU > 70%
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60    # Wait 60s before scaling up again
    scaleDown:
      stabilizationWindowSeconds: 300   # Wait 5min before scaling down

8. CI/CD with GitHub Actions

text
# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm run test -- --run

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=,suffix=,format=short

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v3

      - name: Configure kubeconfig
        run: echo "${{ secrets.KUBECONFIG }}" | base64 -d > kubeconfig.yaml

      - name: Deploy to Kubernetes
        env:
          KUBECONFIG: kubeconfig.yaml
          IMAGE_TAG: ${{ needs.build-and-push.outputs.image-tag }}
        run: |
          kubectl set image deployment/portfolio-app \
            app=$IMAGE_TAG \
            --record

          # Wait for rollout to complete
          kubectl rollout status deployment/portfolio-app --timeout=5m

9. Watch: Docker and Kubernetes Full Course

Video thumbnail
Watch on YouTube

10. Essential kubectl Commands

text
# Get everything running
kubectl get pods,services,deployments -n production

# Watch pods in real time
kubectl get pods -w

# View logs
kubectl logs -f deployment/portfolio-app --tail=100

# Execute a command inside a pod
kubectl exec -it pod/portfolio-app-abc123 -- sh

# Describe a pod (great for debugging CrashLoopBackOff)
kubectl describe pod portfolio-app-abc123

# Rolling restart (picks up new ConfigMap/Secret values)
kubectl rollout restart deployment/portfolio-app

# Rollback to previous version
kubectl rollout undo deployment/portfolio-app

# Scale manually
kubectl scale deployment/portfolio-app --replicas=5

# Port-forward for local debugging
kubectl port-forward service/portfolio-service 3000:80
text
kubectl describe pod <name>
is your best friend when a pod won't start. It shows the exact error — image pull failures, resource limits exceeded, failed health checks — everything you need to diagnose the problem.

The Production Readiness Checklist

Before going live with Kubernetes:

  • Multi-replica deployment (minimum 2 for zero-downtime updates)
  • Resource requests and limits on every container
  • Readiness and liveness probes configured
  • Secrets stored in Kubernetes Secrets, not ConfigMaps or env vars in YAML
  • Specific image tags, never
    text
    latest
  • HPA configured for autoscaling
  • Ingress with TLS termination
  • CI/CD pipeline that runs tests before deploying
  • Monitoring and alerting (Prometheus + Grafana or Datadog)
  • Log aggregation (ELK stack or Loki)

Containers and Kubernetes have a learning curve. But once you have this infrastructure in place, deploying becomes a

text
git push
— and that changes everything about how fast you can ship.

#Docker#Kubernetes#DevOps#CI/CD#Containers#Helm#GitHub Actions
Vighnesh Salunkhe
Written by

Vighnesh Salunkhe

"Passionate about building scalable web applications and exploring the intersection of AI and human creativity."

Join the Conversation

Share your thoughts or ask a question

Share this article