Skip to content

Deployment Automation

Deployment is where code meets reality. Whether you’re pushing to a single server or orchestrating across global regions, Claude Code transforms deployment from a nail-biting experience into a confident, automated process. This lesson explores how to leverage AI assistance for modern deployment practices.

Scenario: Your team deploys 20 times a day across 5 environments and 3 regions. Each deployment involves Docker builds, Kubernetes manifests, database migrations, feature flags, and rollback procedures. One mistake can bring down production. Traditional approach: deployment specialists and runbooks. With Claude Code: intelligent automation.

Day 1: Manual Docker Setup
- Write Dockerfile by hand
- Debug build issues one by one
- Manually test container
Day 2-3: Kubernetes Configuration
- Copy-paste YAML manifests
- Manually update versions
- Hope nothing breaks
Day 4-5: CI/CD Pipeline
- Write deployment scripts
- Configure environments
- Test in staging (fingers crossed)
Day 6+: Production Issues
- Scramble to fix problems
- Manual rollbacks
- Update runbooks
> Create a production-ready Dockerfile for our Node.js application:
> - Multi-stage build for minimal size
> - Security hardening
> - Build-time caching optimization
> - Non-root user execution
> - Health checks

Claude generates an optimized Dockerfile:

# Build stage
FROM node:20-alpine AS builder
# Install build dependencies
RUN apk add --no-cache python3 make g++
# Create app directory
WORKDIR /build
# Copy dependency files first (better caching)
COPY package*.json ./
COPY yarn.lock* ./
COPY pnpm-lock.yaml* ./
# Install dependencies based on lockfile
RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
elif [ -f pnpm-lock.yaml ]; then corepack enable && pnpm install --frozen-lockfile; \
else npm ci; fi
# Copy source code
COPY . .
# Build application
RUN npm run build
# Prune dev dependencies
RUN npm prune --production
# Production stage
FROM node:20-alpine AS runtime
# Install runtime dependencies only
RUN apk add --no-cache tini curl
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy built application from builder
COPY --from=builder --chown=nodejs:nodejs /build/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /build/package*.json ./
# Expose port (documentation only)
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Switch to non-root user
USER nodejs
# Use tini for proper signal handling
ENTRYPOINT ["/sbin/tini", "--"]
# Start application
CMD ["node", "dist/index.js"]
> Create a Docker Compose setup for local development with:
> - Application container with hot reload
> - PostgreSQL database
> - Redis cache
> - Nginx reverse proxy
> - Proper networking and volumes
docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: myapp_app
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgresql://user:password@postgres:5432/myapp
REDIS_URL: redis://redis:6379
volumes:
- ./src:/app/src:delegated
- ./public:/app/public:delegated
- node_modules:/app/node_modules
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- myapp_network
command: npm run dev
postgres:
image: postgres:15-alpine
container_name: myapp_postgres
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- myapp_network
redis:
image: redis:7-alpine
container_name: myapp_redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- myapp_network
nginx:
image: nginx:alpine
container_name: myapp_nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- myapp_network
volumes:
node_modules:
postgres_data:
redis_data:
networks:
myapp_network:
driver: bridge
> Generate Kubernetes manifests for our application:
> - Deployment with rolling updates
> - Service for load balancing
> - Ingress with TLS
> - ConfigMap and Secrets
> - HorizontalPodAutoscaler
> - PodDisruptionBudget

Deployment

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
labels:
app: myapp
version: v1.0.0
spec:
replicas: 3
selector:
matchLabels:
app: myapp
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: myapp
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- myapp
topologyKey: kubernetes.io/hostname
containers:
- name: myapp
image: myregistry/myapp:v1.0.0
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
- containerPort: 9090
name: metrics
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
envFrom:
- configMapRef:
name: myapp-config
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
securityContext:
runAsNonRoot: true
runAsUser: 1001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
volumeMounts:
- name: temp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
volumes:
- name: temp
emptyDir: {}
- name: cache
emptyDir: {}
imagePullSecrets:
- name: registry-credentials

Service & Ingress

service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: production
labels:
app: myapp
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: http
selector:
app: myapp
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
namespace: production
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- api.example.com
secretName: myapp-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp
port:
number: 80

Autoscaling & PDB

hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60
---
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: myapp
namespace: production
spec:
minAvailable: 2
selector:
matchLabels:
app: myapp

Config & Secrets

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: production
data:
LOG_LEVEL: info
CACHE_TTL: "3600"
MAX_CONNECTIONS: "100"
FEATURE_FLAGS: |
{
"newUI": true,
"betaFeatures": false
}
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: production
type: Opaque
stringData:
database-url: postgresql://user:password@postgres:5432/myapp
redis-url: redis://:password@redis:6379
api-key: your-api-key-here
jwt-secret: your-jwt-secret-here
> Create a Helm chart for our application with:
> - Parameterized values
> - Environment-specific overrides
> - Dependency management
> - Hook jobs for migrations
Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: postgresql
version: "12.1.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "17.3.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
# values.yaml
replicaCount: 3
image:
repository: myregistry/myapp
pullPolicy: IfNotPresent
tag: "" # Overrides the image tag
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-tls
hosts:
- api.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
postgresql:
enabled: true
auth:
username: myapp
password: changeme
database: myapp
redis:
enabled: true
auth:
enabled: true
password: changeme
# Environment-specific values
environments:
production:
replicaCount: 5
resources:
limits:
cpu: 1000m
memory: 1Gi
staging:
replicaCount: 2
ingress:
hosts:
- host: api-staging.example.com
> Create a complete GitHub Actions workflow for:
> - Building and testing on PR
> - Security scanning
> - Building and pushing Docker images
> - Deploying to staging on merge
> - Production deployment with approval
.github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm run test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
build:
needs: [test, security]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.image.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- 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=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Output image
id: image
run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
deploy-staging:
if: github.ref == 'refs/heads/develop'
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Kubernetes
env:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_STAGING }}
run: |
echo "$KUBE_CONFIG" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
# Update image in deployment
kubectl set image deployment/myapp myapp=${{ needs.build.outputs.image }} -n staging
# Wait for rollout
kubectl rollout status deployment/myapp -n staging --timeout=5m
# Run smoke tests
./scripts/smoke-test.sh staging
deploy-production:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Create release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release v${{ github.run_number }}
draft: false
prerelease: false
- name: Deploy to Production
env:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
run: |
echo "$KUBE_CONFIG" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
# Blue-green deployment
./scripts/blue-green-deploy.sh ${{ needs.build.outputs.image }}
> Create a GitLab CI pipeline with similar features
.gitlab-ci.yml
stages:
- test
- security
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
REGISTRY: registry.gitlab.com
IMAGE_NAME: $CI_PROJECT_PATH
# Test stage
test:unit:
stage: test
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm run lint
- npm run test:ci
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# Security scanning
security:scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy fs --severity HIGH,CRITICAL --exit-code 1 .
allow_failure: true
# Build Docker image
build:image:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .
- docker push $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
- docker tag $REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA $REGISTRY/$IMAGE_NAME:latest
- docker push $REGISTRY/$IMAGE_NAME:latest
only:
- main
- develop
# Deploy to staging
deploy:staging:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: staging
url: https://staging.example.com
script:
- echo "$KUBE_CONFIG_STAGING" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl set image deployment/myapp myapp=$REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA -n staging
- kubectl rollout status deployment/myapp -n staging
only:
- develop
# Deploy to production
deploy:production:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: production
url: https://api.example.com
script:
- echo "$KUBE_CONFIG_PRODUCTION" | base64 -d > /tmp/kubeconfig
- export KUBECONFIG=/tmp/kubeconfig
- kubectl set image deployment/myapp myapp=$REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production
when: manual
only:
- main
> Implement a blue-green deployment strategy:
> - Zero-downtime deployment
> - Instant rollback capability
> - Traffic switching
> - Health validation
scripts/blue-green-deploy.sh
#!/bin/bash
set -e
IMAGE=$1
NAMESPACE=${2:-production}
APP_NAME=${3:-myapp}
echo "Starting blue-green deployment..."
# Determine current active deployment
CURRENT_ACTIVE=$(kubectl get service $APP_NAME -n $NAMESPACE -o jsonpath='{.spec.selector.version}')
if [ "$CURRENT_ACTIVE" = "blue" ]; then
NEW_VERSION="green"
OLD_VERSION="blue"
else
NEW_VERSION="blue"
OLD_VERSION="green"
fi
echo "Current active: $OLD_VERSION, deploying to: $NEW_VERSION"
# Update the inactive deployment
kubectl set image deployment/$APP_NAME-$NEW_VERSION \
$APP_NAME=$IMAGE \
-n $NAMESPACE
# Wait for new deployment to be ready
echo "Waiting for $NEW_VERSION deployment to be ready..."
kubectl rollout status deployment/$APP_NAME-$NEW_VERSION -n $NAMESPACE --timeout=10m
# Run health checks
echo "Running health checks on $NEW_VERSION..."
NEW_PODS=$(kubectl get pods -n $NAMESPACE -l app=$APP_NAME,version=$NEW_VERSION -o jsonpath='{.items[*].metadata.name}')
for pod in $NEW_PODS; do
kubectl exec $pod -n $NAMESPACE -- curl -f http://localhost:3000/health || {
echo "Health check failed for pod $pod"
exit 1
}
done
# Switch traffic to new version
echo "Switching traffic to $NEW_VERSION..."
kubectl patch service $APP_NAME -n $NAMESPACE -p \
'{"spec":{"selector":{"version":"'$NEW_VERSION'"}}}'
# Verify traffic switch
sleep 5
ACTIVE_VERSION=$(kubectl get service $APP_NAME -n $NAMESPACE -o jsonpath='{.spec.selector.version}')
if [ "$ACTIVE_VERSION" != "$NEW_VERSION" ]; then
echo "Failed to switch traffic to $NEW_VERSION"
exit 1
fi
echo "Successfully deployed $NEW_VERSION"
echo "Old version $OLD_VERSION is still running for quick rollback"
# Optional: Scale down old version after successful deployment
# kubectl scale deployment/$APP_NAME-$OLD_VERSION --replicas=0 -n $NAMESPACE
> Set up canary deployment using Istio service mesh:
> - Gradual traffic shifting
> - A/B testing capability
> - Automatic rollback on errors
canary-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-canary
namespace: production
spec:
hosts:
- myapp
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: myapp
subset: canary
weight: 100
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
---
# destinationrule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp-destination
namespace: production
spec:
host: myapp
subsets:
- name: stable
labels:
version: stable
- name: canary
labels:
version: canary
---
# Progressive rollout with Flagger
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
service:
port: 80
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
cmd: "hey -z 1m -q 10 -c 2 http://myapp.production:80/"
> Create AWS ECS task definition and service:
> - Fargate deployment
> - Auto-scaling configuration
> - Load balancer integration
> - CloudWatch logging
task-definition.json
{
"family": "myapp",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "myapp",
"image": "myregistry/myapp:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "NODE_ENV",
"value": "production"
}
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/database-url"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}
# Deploy script
#!/bin/bash
# Register task definition
aws ecs register-task-definition --cli-input-json file://task-definition.json
# Update service
aws ecs update-service \
--cluster production \
--service myapp \
--task-definition myapp:latest \
--desired-count 3 \
--deployment-configuration maximumPercent=200,minimumHealthyPercent=100
# Wait for deployment
aws ecs wait services-stable --cluster production --services myapp
> Deploy to Google Cloud Run with:
> - Automatic scaling
> - Custom domain
> - Environment configuration
service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: myapp
annotations:
run.googleapis.com/ingress: all
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "100"
run.googleapis.com/cpu-throttling: "false"
spec:
containers:
- image: gcr.io/myproject/myapp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
limits:
cpu: "2"
memory: "2Gi"
livenessProbe:
httpGet:
path: /health
initialDelaySeconds: 10
periodSeconds: 10
startupProbe:
httpGet:
path: /ready
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
Terminal window
# Deploy script
gcloud run deploy myapp \
--image gcr.io/myproject/myapp:latest \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars NODE_ENV=production \
--set-secrets DATABASE_URL=database-url:latest \
--min-instances 1 \
--max-instances 100 \
--memory 2Gi \
--cpu 2
> Create a migration strategy that ensures zero downtime:
> - Pre-deployment migrations
> - Backward compatible changes
> - Post-deployment cleanup
scripts/migrate-safe.sh
#!/bin/bash
set -e
PHASE=$1
DATABASE_URL=$2
case $PHASE in
"pre-deploy")
echo "Running pre-deployment migrations..."
# Add new columns (nullable or with defaults)
npx knex migrate:up --env production --specific 001_add_new_columns.js
# Add new tables
npx knex migrate:up --env production --specific 002_create_new_tables.js
# Create new indexes (concurrently)
psql $DATABASE_URL -c "CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email_new ON users(email);"
;;
"post-deploy")
echo "Running post-deployment migrations..."
# Backfill data
npx knex migrate:up --env production --specific 003_backfill_data.js
# Drop old columns
npx knex migrate:up --env production --specific 004_drop_old_columns.js
# Rename indexes
psql $DATABASE_URL -c "DROP INDEX IF EXISTS idx_users_email_old;"
;;
"rollback")
echo "Rolling back migrations..."
npx knex migrate:down --env production
;;
*)
echo "Usage: $0 {pre-deploy|post-deploy|rollback}"
exit 1
;;
esac
> Implement comprehensive health checks:
> - Liveness probe
> - Readiness probe
> - Dependency checks
> - Graceful shutdown
health.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');
const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL);
let isShuttingDown = false;
const dependencies = new Map();
// Liveness probe - is the app running?
app.get('/health', (req, res) => {
if (isShuttingDown) {
return res.status(503).json({ status: 'shutting_down' });
}
res.json({ status: 'healthy', uptime: process.uptime() });
});
// Readiness probe - can the app serve traffic?
app.get('/ready', async (req, res) => {
if (isShuttingDown) {
return res.status(503).json({ status: 'shutting_down' });
}
const checks = await Promise.allSettled([
checkDatabase(),
checkRedis(),
checkExternalAPI()
]);
const allHealthy = checks.every(check => check.status === 'fulfilled' && check.value.healthy);
if (allHealthy) {
res.json({
status: 'ready',
dependencies: Object.fromEntries(dependencies)
});
} else {
res.status(503).json({
status: 'not_ready',
dependencies: Object.fromEntries(dependencies)
});
}
});
async function checkDatabase() {
try {
const result = await pool.query('SELECT 1');
dependencies.set('postgres', { healthy: true });
return { healthy: true };
} catch (error) {
dependencies.set('postgres', { healthy: false, error: error.message });
return { healthy: false };
}
}
async function checkRedis() {
try {
await redis.ping();
dependencies.set('redis', { healthy: true });
return { healthy: true };
} catch (error) {
dependencies.set('redis', { healthy: false, error: error.message });
return { healthy: false };
}
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, starting graceful shutdown...');
isShuttingDown = true;
// Stop accepting new requests
server.close(() => {
console.log('HTTP server closed');
// Close database connections
pool.end(() => {
console.log('Database pool closed');
// Close Redis connection
redis.disconnect();
console.log('Redis disconnected');
process.exit(0);
});
});
// Force shutdown after 30 seconds
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
});
> Create an automated rollback system:
> - Monitor error rates
> - Automatic rollback triggers
> - Notification system
scripts/monitor-and-rollback.sh
#!/bin/bash
NAMESPACE=$1
APP_NAME=$2
ERROR_THRESHOLD=5
CHECK_INTERVAL=30
MAX_CHECKS=10
echo "Monitoring deployment for $APP_NAME in $NAMESPACE..."
for i in $(seq 1 $MAX_CHECKS); do
sleep $CHECK_INTERVAL
# Get current error rate from metrics
ERROR_RATE=$(kubectl exec -n $NAMESPACE deployment/$APP_NAME -- \
curl -s http://localhost:9090/metrics | \
grep 'http_requests_errors_total' | \
awk '{print $2}')
# Get total requests
TOTAL_REQUESTS=$(kubectl exec -n $NAMESPACE deployment/$APP_NAME -- \
curl -s http://localhost:9090/metrics | \
grep 'http_requests_total' | \
awk '{print $2}')
# Calculate error percentage
if [ "$TOTAL_REQUESTS" -gt 0 ]; then
ERROR_PERCENT=$(awk "BEGIN {printf \"%.2f\", ($ERROR_RATE/$TOTAL_REQUESTS)*100}")
echo "Check $i/$MAX_CHECKS - Error rate: $ERROR_PERCENT%"
if (( $(echo "$ERROR_PERCENT > $ERROR_THRESHOLD" | bc -l) )); then
echo "Error rate exceeded threshold! Initiating rollback..."
# Trigger rollback
kubectl rollout undo deployment/$APP_NAME -n $NAMESPACE
# Send notification
curl -X POST $SLACK_WEBHOOK_URL \
-H 'Content-type: application/json' \
--data "{\"text\":\"🚨 Automatic rollback triggered for $APP_NAME due to high error rate: $ERROR_PERCENT%\"}"
exit 1
fi
fi
done
echo "Deployment monitoring complete - all checks passed!"

You’ve learned how to leverage Claude Code for comprehensive deployment automation - from containerization to orchestration to zero-downtime deployments. The key is treating deployment as code that can be generated, optimized, and maintained with AI assistance.

Remember: Great deployments are predictable, repeatable, and reversible. Use Claude Code to encode best practices into your deployment pipeline, ensuring every release is as smooth as possible while maintaining the ability to quickly recover from any issues.