Blue-green deployment maintains two identical production environments: "blue" (current live) and "green" (new version). After deploying and validating the green environment, a single configuration change routes all traffic from blue to green. If anything goes wrong, switching traffic back to blue takes under 5 seconds. Zero downtime, instant rollback, full confidence.
The approach was formalized by Martin Fowler in 2010 and remains the preferred deployment strategy for applications where rollback speed is the highest priority. According to the 2025 DORA report, teams using blue-green deployments recover from failures 4.7x faster than teams using rolling updates.
---
Blue-Green Architecture in Kubernetes
In Kubernetes, blue-green is implemented with two Deployments (one per version) and a Service whose selector determines which Deployment receives traffic. Switching versions means patching the Service selector.
Traffic flow:
User --> Service (selector: slot=blue)
|
+--> Deployment-Blue (label: slot=blue) [LIVE]
|
+--> Deployment-Green (label: slot=green) [STANDBY]
---
Step 1: Create Blue Deployment (Current Version)
<h1>blue-deployment.yaml</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-blue
namespace: production
labels:
app: my-app
slot: blue
spec:
replicas: 3
selector:
matchLabels:
app: my-app
slot: blue
template:
metadata:
labels:
app: my-app
slot: blue
spec:
containers:
- name: app
image: ghcr.io/my-org/my-app:v1.2.0
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
---
Step 2: Create the Service with Blue Selector
<h1>app-service.yaml</h1>
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: production
spec:
selector:
app: my-app
slot: blue
ports:
- port: 80
targetPort: 3000
protocol: TCP
type: ClusterIP
The Service selector includes `slot: blue`, which means only pods with the `slot: blue` label receive traffic. The green Deployment runs alongside but receives zero traffic until the selector changes.
---
Step 3: Deploy Green (New Version)
<h1>green-deployment.yaml</h1>
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app-green
namespace: production
labels:
app: my-app
slot: green
spec:
replicas: 3
selector:
matchLabels:
app: my-app
slot: green
template:
metadata:
labels:
app: my-app
slot: green
spec:
containers:
- name: app
image: ghcr.io/my-org/my-app:v1.3.0
ports:
- containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi
---
Step 4: Validate Green Before Cutover
Before switching traffic, validate the green deployment independently. Port-forward to green pods and run smoke tests.
<h1>Wait for green pods to be ready</h1>
kubectl rollout status deployment/my-app-green -n production --timeout=120s
<h1>Port-forward to green for local testing</h1>
kubectl port-forward deployment/my-app-green 8080:3000 -n production &
<h1>Run smoke tests against green</h1>
curl -f http://localhost:8080/health
curl -f http://localhost:8080/api/status
<h1>Run integration test suite against green</h1>
API_URL=http://localhost:8080 npm run test:integration
<h1>Kill port-forward</h1>
kill %1
| Validation Check | Command | Expected Result |
| Pod readiness | `kubectl get pods -l slot=green` | All pods Running, Ready |
| Health endpoint | `curl /health` | 200 OK |
| API smoke test | `curl /api/status` | 200 with valid JSON |
| Integration tests | `npm run test:integration` | All tests pass |
| Resource usage | `kubectl top pods -l slot=green` | Within resource limits |
---
Step 5: Switch Traffic (The Cutover)
The cutover is a single `kubectl patch` command that changes the Service selector from blue to green. All new connections route to green immediately.
<h1>Switch traffic from blue to green</h1>
kubectl patch service my-app -n production \
-p '{"spec":{"selector":{"slot":"green"}}}'
<h1>Verify traffic is hitting green</h1>
kubectl get endpoints my-app -n production
<h1>Monitor green pods for errors</h1>
kubectl logs -l slot=green -n production --tail=50 -f
The cutover takes effect within 1-2 seconds. Existing connections to blue pods drain naturally (controlled by `terminationGracePeriodSeconds`). New connections immediately route to green pods.
---
Step 6: Instant Rollback
If green shows problems after cutover, switch back to blue immediately:
<h1>Instant rollback: switch traffic back to blue</h1>
kubectl patch service my-app -n production \
-p '{"spec":{"selector":{"slot":"blue"}}}'
<h1>Verify rollback</h1>
kubectl get endpoints my-app -n production
echo "Traffic restored to blue (v1.2.0)"
Rollback time: under 5 seconds. The blue deployment was never touched, so it is guaranteed to be in the exact state that was serving traffic before the cutover.
---
Database Migration Strategy
Database migrations are the hardest part of blue-green deployments because both versions must be compatible with the same database during the cutover window.
The Expand-Contract Pattern
-- Expand migration (run BEFORE deploying green)
-- Add new column alongside old one
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMP;
-- Backfill new column from old data
UPDATE users SET email_verified_at = verified_date
WHERE verified_date IS NOT NULL;
-- Create index on new column
CREATE INDEX CONCURRENTLY idx_users_email_verified
ON users(email_verified_at);
-- Contract migration (run AFTER green is stable)
-- Only after blue is decommissioned
ALTER TABLE users DROP COLUMN verified_date;
| Migration Type | When to Run | Rollback Safe |
| Add column | Before green deploy | Yes (blue ignores it) |
| Add index | Before green deploy | Yes (blue ignores it) |
| Rename column | Never during blue-green | No |
| Drop column | After green is stable | No (point of no return) |
| Change column type | Expand-contract only | Depends on direction |
---
Health Check Configuration
Health checks gate every stage of the blue-green deployment. Kubernetes uses readiness probes to determine when green pods are ready for traffic and liveness probes to detect pods that need restart.
// Health check endpoint implementation
app.get("/health", async (req, res) => {
try {
// Check database connectivity
await db.query("SELECT 1");
// Check cache connectivity
await redis.ping();
res.status(200).json({
status: "healthy",
version: process.env.APP_VERSION,
uptime: process.uptime(),
checks: {
database: "connected",
cache: "connected"
}
});
} catch (error) {
res.status(503).json({
status: "unhealthy",
error: error.message
});
}
});
The health endpoint checks both database and cache connectivity. A 503 response triggers Kubernetes readiness probe failure, preventing traffic from reaching the unhealthy pod. This stops a green deployment with a broken database connection from ever receiving production traffic.
---
Complete Blue-Green Deployment Script
#!/bin/bash
<h1>blue-green-deploy.sh</h1>
set -e
NAMESPACE="production"
APP="my-app"
NEW_IMAGE="ghcr.io/my-org/my-app:$1"
<h1>Determine current active slot</h1>
CURRENT_SLOT=$(kubectl get service $APP -n $NAMESPACE -o jsonpath='{.spec.selector.slot}')
if [ "$CURRENT_SLOT" = "blue" ]; then
NEW_SLOT="green"
else
NEW_SLOT="blue"
fi
echo "Current: $CURRENT_SLOT | Deploying to: $NEW_SLOT"
<h1>Update the inactive deployment with new image</h1>
kubectl set image deployment/${APP}-${NEW_SLOT} app=$NEW_IMAGE -n $NAMESPACE
<h1>Wait for rollout</h1>
echo "Waiting for $NEW_SLOT deployment to be ready..."
kubectl rollout status deployment/${APP}-${NEW_SLOT} -n $NAMESPACE --timeout=180s
<h1>Run smoke tests via port-forward</h1>
kubectl port-forward deployment/${APP}-${NEW_SLOT} 8080:3000 -n $NAMESPACE &
PF_PID=$!
sleep 3
if curl -sf http://localhost:8080/health > /dev/null; then
echo "Health check passed"
else
echo "Health check FAILED. Aborting deployment."
kill $PF_PID 2>/dev/null
exit 1
fi
kill $PF_PID 2>/dev/null
<h1>Cutover: switch traffic to new slot</h1>
kubectl patch service $APP -n $NAMESPACE -p "{"spec":{"selector":{"slot":"$NEW_SLOT"}}}"
echo "Traffic switched to $NEW_SLOT ($NEW_IMAGE)"
<h1>Monitor for 60 seconds</h1>
echo "Monitoring for 60 seconds..."
sleep 60
<h1>Final health check</h1>
HEALTHY=$(kubectl get pods -l app=$APP,slot=$NEW_SLOT -n $NAMESPACE -o json
jq '[.items[].status.containerStatuses[0].ready]
all')
if [ "$HEALTHY" = "true" ]; then
echo "Deployment successful. $NEW_SLOT is live."
else
echo "Post-deploy check failed. Rolling back to $CURRENT_SLOT"
kubectl patch service $APP -n $NAMESPACE -p "{"spec":{"selector":{"slot":"$CURRENT_SLOT"}}}"
exit 1
fi
This script automates the entire blue-green flow: detect current slot, deploy to inactive slot, validate, cutover, monitor, and rollback if needed.
For a more gradual approach to production releases, see our canary deployment best practices. If you are setting up the CI/CD pipeline that feeds into blue-green deployments, read our CI/CD pipeline guide for startups. For GitOps-managed blue-green deployments, check our GitOps for small teams guide.
---
Frequently Asked Questions
How much extra infrastructure does blue-green require?
Blue-green requires 2x the compute resources during the deployment window because both versions run simultaneously. After the cutover stabilizes (typically 10-30 minutes), you can scale down the old deployment to zero or delete it. Some teams keep the old deployment at 1 replica as a warm standby for 24 hours.
Can I use blue-green with a single VPS?
Yes, but with limitations. Run both versions on different ports (e.g., 3000 and 3001) and use nginx upstream switching instead of Kubernetes Service selectors. The approach is identical in concept: deploy new version, validate, switch nginx upstream, monitor, optionally rollback.
How do I handle long-running WebSocket connections during cutover?
WebSocket connections to blue pods remain active until the pod terminates or the client reconnects. Set `terminationGracePeriodSeconds` to 300 (5 minutes) to allow existing connections to drain. Implement reconnection logic in the client that automatically reconnects to the new version.
What about shared resources like queues and caches?
Both blue and green connect to the same Redis, RabbitMQ, or other shared services. Ensure queue message formats are backward-compatible. If green introduces a new message format, it must also handle the old format. This is the same expand-contract principle applied to database migrations.
How often should I clean up inactive deployments?
Scale the inactive deployment to zero replicas after 1-2 hours of stable green operation. Delete it entirely after 24-48 hours when you are confident no rollback is needed. Automated cleanup scripts can handle this, but always verify the active version is healthy before removing the inactive one.