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.
textVirtual 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"]
.dockerignore: What Not to Copy
textnode_modules .next .git .env .env.local *.log README.md .github tests coverage
.envdocker run -e3. 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:
| Object | What it is |
|---|---|
| Pod | The smallest deployable unit — one or more containers |
| Deployment | Manages a set of identical Pods, handles rolling updates |
| Service | Stable network endpoint for a set of Pods |
| Ingress | Routes external HTTP traffic to Services |
| ConfigMap | Non-sensitive configuration data |
| Secret | Sensitive data (passwords, API keys) |
| HPA | Horizontal 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
v1.2.3latestlatest6. 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

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
kubectl describe pod <name>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
git push
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