All posts
24 Aug 2026

Zero-Downtime Deployments on Kubernetes: Rolling Updates vs. Blue-Green Strategies for Node.js

{

{ “title”: “Zero-Downtime Deployments on Kubernetes: Rolling Updates vs. Blue-Green Strategies for Node.js”, “summary”: “A practical, code-heavy guide to structuring Kubernetes deployment manifests, readiness/liveness probes, and CI/CD pipelines to ensure seamless zero-downtime updates for production Node.js microservices.”, “tags”: [“Kubernetes”, “DevOps”, “Node.js”, “CI/CD”, “Software Architecture”], “body”: “# Zero-Downtime Deployments on Kubernetes: Rolling Updates vs. Blue-Green Strategies for Node.js\n\nDeploying code to production should be boring. However, for many engineering teams running Node.js microservices, deployments are still a source of acute anxiety. Dropped database connections, unhandled promise rejections during SIGTERM, and HTTP 502 Bad Gateway errors are all too common when pods are terminated abruptly.\n\nKubernetes provides native primitives to achieve zero-downtime updates, but they do not work out of the box. Node.js operates on a single-threaded event loop, and Express, Fastify, or NestJS servers often continue accepting requests even after shutdown signals have been sent.\n\nIn this comprehensive guide, we will explore how to architect robust zero-downtime deployment pipelines for Node.js applications on Kubernetes using two primary strategies: Rolling Updates and Blue-Green Deployments.\n\n—\n\n## The Anatomy of a Zero-Downtime Node.js Application\n\nBefore diving into Kubernetes manifests, your Node.js application must be engineered to handle graceful shutdowns. When Kubernetes decides to terminate a pod, it sends a SIGTERM signal to the container process. By default, Node.js processes terminate immediately upon receiving SIGTERM, dropping any in-flight requests currently being processed.\n\nTo prevent this, your application must:\n1. Stop accepting new connections.\n2. Finish processing all active HTTP requests.\n3. Close database connections and clean up resources.\n4. Exit cleanly with code 0.\n\n### Graceful Shutdown Implementation in Express\n\nHere is a production-ready pattern for handling graceful shutdowns in a Node.js Express server:\n\njavascript\nconst express = require('express');\nconst app = express();\n\napp.get('/healthz', (req, res) => {\n res.status(200).send('OK');\n});\n\napp.get('/api/data', async (req, res) => {\n // Simulate async work\n await new Promise((resolve) => setTimeout(resolve, 1000));\n res.json({ message: 'Success' });\n});\n\nconst server = app.listen(3000, () => {\n console.log('Server running on port 3000');\n});\n\n// Track active connections\nlet connections = new Set();\n\nserver.on('connection', (connection) => {\n connections.add(connection);\n connection.on('close', () => {\n connections.delete(connection);\n });\n});\n\n// Handle termination signals\nconst gracefulShutdown = (signal) => {\n console.log(`Received ${signal}. Starting graceful shutdown...`);\n\n // 1. Stop accepting new connections\n server.close(() => {\n console.log('HTTP server closed.');\n \n // 2. Close database pools, Redis clients, etc.\n database.disconnect().then(() => {\n console.log('Database connections closed.');\n process.exit(0);\n });\n });\n\n // 3. Force close remaining connections after a timeout\n setTimeout(() => {\n console.error('Could not close connections in time, forcefully shutting down');\n for (const connection of connections) {\n connection.destroy();\n }\n process.exit(1);\n }, 10000); // 10-second timeout\n};\n\nprocess.on('SIGTERM', () => gracefulShutdown('SIGTERM'));\nprocess.on('SIGINT', () => gracefulShutdown('SIGINT'));\n\n\n—\n\n## Kubernetes Probes: The Gatekeepers of Uptime\n\nKubernetes uses probes to determine the health of your container. For zero-downtime deployments, proper configuration of Liveness, Readiness, and Startup probes is non-negotiable.\n\n### Configuring Probes in Kubernetes\n\nyaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: node-api\n namespace: production\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: node-api\n template:\n metadata:\n labels:\n app: node-api\n spec:\n containers:\n - name: node-api\n image: my-registry.io/node-api:v1.2.0\n ports:\n - containerPort: 3000\n lifecycle:\n preStop:\n exec:\n command: [\"sh\", \"-c\", \"sleep 5\"]\n startupProbe:\n httpGet:\n path: /healthz\n port: 3000\n initialDelaySeconds: 3\n periodSeconds: 5\n failureThreshold: 6\n livenessProbe:\n httpGet:\n path: /healthz\n port: 3000\n periodSeconds: 10\n timeoutSeconds: 3\n failureThreshold: 3\n readinessProbe:\n httpGet:\n path: /healthz\n port: 3000\n periodSeconds: 5\n timeoutSeconds: 2\n successThreshold: 1\n failureThreshold: 2\n\n\n### Why the preStop Hook Matters\nWhen a pod is marked for termination, Kubernetes removes it from all Service endpoints concurrently with sending the SIGTERM signal. Due to network propagation delays, the ingress controller or kube-proxy might still route incoming requests to the terminating pod for a few seconds.\n\nThe preStop hook introduces an artificial delay (sleep 5) before SIGTERM is delivered, giving network routing tables enough time to update so that no incoming traffic hits a pod that is shutting down.\n\n—\n\n## Strategy 1: Rolling Updates\n\nRolling updates are the default deployment strategy in Kubernetes. They replace old pods with new pods incrementally, controlled by maxSurge and maxUnavailable parameters.\n\n### Rolling Update Manifest\n\nyaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: node-api\n namespace: production\nspec:\n replicas: 4\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxSurge: 25% # How many pods can be created above the desired replica count\n maxUnavailable: 0 # Zero downtime requires that NO pods are taken down below desired count\n selector:\n matchLabels:\n app: node-api\n template:\n # ... (pod template specs as shown above)\n\n\n### How It Works During CI/CD\n1. When you trigger an update (kubectl set image deployment/node-api node-api=...), Kubernetes creates 1 new pod (maxSurge: 25% of 4 = 1).\n2. Kubernetes waits for the new pod’s readinessProbe to return HTTP 200.\n3. Once ready, it adds the new pod to the service endpoints and terminates 1 old pod using the preStop hook and SIGTERM.\n4. This cycle repeats until all replicas are updated.\n\n### Pros and Cons of Rolling Updates\n- Pros: Resource efficient (no need to double cluster capacity), native to Kubernetes, simple to configure.\n- Cons: Both old and new versions run concurrently in the cluster. If your database schema undergoes breaking changes, both versions must be fully backwards compatible.\n\n—\n\n## Strategy 2: Blue-Green Deployments\n\nIf your Node.js application involves breaking database migrations or you require instant rollback capabilities, a Blue-Green deployment strategy is preferred. In this pattern, two identical environments (Blue and Green) exist, but only one receives production traffic via a Kubernetes Service selector.\n\n### The Blue Deployment Manifest (deployment-blue.yaml)\n\nyaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: node-api-blue\n namespace: production\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: node-api\n version: blue\n template:\n metadata:\n labels:\n app: node-api\n version: blue\n spec:\n containers:\n - name: node-api\n image: my-registry.io/node-api:v1.0.0\n ports:\n - containerPort: 3000\n readinessProbe:\n httpGet:\n path: /healthz\n port: 3000\n periodSeconds: 5\n\n\n### The Green Deployment Manifest (deployment-green.yaml)\n\nyaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: node-api-green\n namespace: production\nspec:\n replicas: 3\n selector:\n matchLabels:\n app: node-api\n version: green\n template:\n metadata:\n labels:\n app: node-api\n version: green\n spec:\n containers:\n - name: node-api\n image: my-registry.io/node-api:v1.1.0\n ports:\n - containerPort: 3000\n readinessProbe:\n httpGet:\n path: /healthz\n port: 3000\n periodSeconds: 5\n\n\n### The Traffic Router Service (service.yaml)\n\nyaml\napiVersion: v1\nkind: Service\nmetadata:\n name: node-api-service\n namespace: production\nspec:\n selector:\n app: node-api\n version: blue # Swapped to 'green' during promotion\n ports:\n - protocol: TCP\n port: 80\n targetPort: 3000\n\n\n### Executing the Blue-Green Switch\nTo promote the Green environment, you update the Service selector using kubectl or your CI/CD pipeline:\n\nbash\nkubectl patch svc node-api-service -n production --type='json' -p='[\n {"op": "replace", "path": "/spec/selector/version", "value": "green"}\n]'\n\n\n—\n\n## Building a Zero-Downtime GitHub Actions CI/CD Pipeline\n\nLet’s pull everything together into a production-grade GitHub Actions workflow that executes a robust rolling update.\n\nyaml\nname: Production CI/CD Pipeline\n\non:\n push:\n branches:\n - main\n\nenv:\n IMAGE_REGISTRY: my-registry.io\n IMAGE_NAME: node-api\n\njobs:\n build-and-deploy:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout Code\n uses: actions/checkout@v4\n\n - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v3\n\n - name: Authenticate to Container Registry\n uses: docker/login-action@v3\n with:\n registry: ${{ env.IMAGE_REGISTRY }}\n username: ${{ secrets.REGISTRY_USERNAME }}\n password: ${{ secrets.REGISTRY_PASSWORD }}\n\n - name: Build and Push Docker Image\n uses: docker/build-push-action@v5\n with:\n context: .\n push: true\n tags: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}\n cache-from: type=gha\n cache-to: type=gha,mode=max\n\n - name: Set up Kubectl\n uses: azure/setup-kubectl@v3\n with:\n version: 'v1.28.0'\n\n - name: Configure Kubernetes Cluster Access\n uses: azure/k8s-set-context@v4\n with:\n method: kubeconfig\n kubeconfig: ${{ secrets.KUBE_CONFIG }}\n\n - name: Update Kubernetes Deployment Image\n run: |\n kubectl set image deployment/node-api \\\n node-api=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \\\n --namespace=production\n\n - name: Verify Deployment Rollout\n run: |\n kubectl rollout status deployment/node-api \\\n --namespace=production \\\n --timeout=5m\n\n\n—\n\n## Summary Checklist for Node.js Zero-Downtime Deployments\n\n1. Implement Graceful Shutdowns: Listen for SIGTERM, stop the HTTP server with server.close(), flush database connections, and exit cleanly.\n2. Tune Probes: Configure readinessProbe to verify application health continuously and use startupProbe for heavy boot sequences.\n3. Utilize preStop Hooks: Add a brief sleep command to account for ingress endpoint propagation lags.\n4. Choose Your Strategy: \n - Use Rolling Updates (maxUnavailable: 0) for standard microservices.\n - Use Blue-Green Deployments when managing complex database migrations or requiring instant rollbacks.\n\nBy following these architectural patterns, your Node.js microservices will achieve true resilience, allowing your engineering team to ship code rapidly and with absolute confidence.” }

More posts