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.
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.
# 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
node_modules
.next
.git
.env
.env.local
*.log
README.md
.github
tests
coverage
.env files into a Docker image. Environment variables should be injected at runtime via 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.
# 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:
# 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
# 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
# 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
# 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.3), never latest. With latest, you cannot tell what version is running, rollbacks are unreliable, and different nodes might pull different versions.6. Secrets Management
# 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"
# 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
# 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
# .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
# 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> 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
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)
git push — and that changes everything about how fast you can ship.
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