
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
You have built a Node.js API or microservice, but running it reliably in production requires more than just npm start. To successfully deploy a Node.js service to Kubernetes, you must bridge the gap between application code and cluster orchestration through optimized container images, declarative manifests, and proper lifecycle management. This guide walks you through the exact configuration needed for a stable, secure deployment that handles traffic spikes and failures gracefully.
How do you optimize a Node.js Dockerfile for Kubernetes?
The foundation of any reliable Kubernetes deployment is a lean, secure container image. A common mistake I see in audits is teams shipping 1GB+ Node.js images containing build tools, dev dependencies, and source maps. In production, this increases attack surface, slows down pod scheduling, and wastes cluster resources. You should aim for an image under 200MB using a multi-stage build strategy.
Always use a specific version tag like node:22-alpine rather than latest. Alpine-based images are significantly smaller, though you may occasionally need to install native build tools if your dependencies require them. For most pure JavaScript APIs, Alpine works perfectly. Set the NODE_ENV=production variable during the install step to skip devDependencies, and always run the application as a non-root user to satisfy security benchmarks like CIS and SOC 2.
# Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
FROM node:22-alpine AS runtime
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"] This pattern ensures your final artifact contains only what is necessary to run. If you are managing database connections alongside this deployment, review PostgreSQL administration essentials to ensure your connection pooling aligns with Kubernetes pod lifecycles.
What Kubernetes manifests are required for a production Node.js deployment?
Once your image is built and pushed to a registry, you need to define the desired state in Kubernetes. At minimum, you need a Deployment and a Service. However, a production-grade setup also requires careful attention to resource requests, limits, and labels. When you deploy a Node.js service to Kubernetes, never omit resource specifications; without them, the scheduler cannot make intelligent placement decisions, and a single memory leak can starve neighboring pods on the same node.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-api
labels:
app: node-api
spec:
replicas: 3
selector:
matchLabels:
app: node-api
template:
metadata:
labels:
app: node-api
spec:
containers:
- name: node-api
image: registry.example.com/node-api:v1.4.2
ports:
- containerPort: 3000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
envFrom:
- configMapRef:
name: node-api-config
- secretRef:
name: node-api-secrets Notice the explicit image tag. Never use :latest in production manifests because it makes rollbacks impossible and breaks audit trails. The resources block defines both requests (guaranteed minimum) and limits (hard ceiling). For Node.js, set memory limits at least 20% above your observed peak usage to account for V8 heap overhead. If you are unsure about sizing, read Kubernetes resource limits and requests for a deeper dive into tuning these values based on actual metrics.
Exposing the service internally
The Service object provides a stable DNS name and load balancing across your pods. For most Node.js APIs, a ClusterIP service is sufficient, with an Ingress controller handling external traffic termination.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: node-api-svc
spec:
selector:
app: node-api
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP How do you configure health checks for Node.js in Kubernetes?
Kubernetes relies on probes to determine if your application is alive and ready to serve traffic. Without these, the platform cannot distinguish between a running process and a functioning application. A crashed event loop or a deadlocked database connection will leave the pod in a "Running" state while returning errors to users. Implementing proper probes is non-negotiable when you deploy a Node.js service to Kubernetes.
Distinguish clearly between liveness and readiness. A liveness probe answers "Is the process stuck?" and triggers a restart on failure. A readiness probe answers "Can this pod handle requests right now?" and removes the pod from the Service endpoints if it fails. Do not include downstream dependencies (like databases) in your liveness check; if the DB goes down, restarting all your Node.js pods simultaneously will only worsen the outage. Keep liveness local (e.g., checking the event loop), and put dependency checks in readiness.
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
startupProbe:
httpGet:
path: /health/startup
port: 3000
failureThreshold: 30
periodSeconds: 2 The startupProbe is critical for Node.js applications that take time to warm up caches or establish database pools. It disables liveness and readiness checks until the startup probe succeeds, preventing premature kills during initialization. For more on observing these health states, see the four golden signals of monitoring to correlate probe failures with saturation and error rates.
How do you manage secrets and configuration securely in Kubernetes?
Hardcoding database passwords or API keys in your Dockerfile or manifest is a security vulnerability that will fail any compliance audit. Kubernetes provides ConfigMaps for non-sensitive data and Secrets for credentials. When you deploy a Node.js service to Kubernetes, inject these as environment variables or mounted files, never as hardcoded values.
| Feature | ConfigMap | Secret |
|---|---|---|
| Use Case | App config, feature flags, URLs | Passwords, tokens, TLS certs |
| Encoding | Plain text | Base64 (not encrypted by default) |
| Encryption at Rest | No | Requires etcd encryption config |
| Access Control | RBAC | RBAC + stricter policies |
| Best Practice | Version control friendly | Use external secrets operator or Vault |
In practice, base64 encoding is not encryption. Anyone with read access to the namespace can decode a Secret. For production environments, especially those requiring SOC 2 or ISO 27001 compliance, integrate an external secrets manager. Tools like External Secrets Operator or Sealed Secrets allow you to store encrypted secrets in Git or fetch them dynamically from AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault at runtime. This keeps your Git repository clean and your credentials rotated automatically.
How do you scale and update a Node.js service without downtime?
Kubernetes excels at maintaining availability during changes, but only if configured correctly. The default RollingUpdate strategy replaces pods gradually. To prevent downtime during deployments, ensure your Node.js application handles SIGTERM gracefully. When Kubernetes terminates a pod, it sends SIGTERM and waits for terminationGracePeriodSeconds (default 30s) before sending SIGKILL. Your app must stop accepting new connections and finish in-flight requests within this window.
// Graceful shutdown example
process.on('SIGTERM', () => {
console.log('SIGTERM received. Shutting down gracefully...');
server.close(() => {
console.log('HTTP server closed.');
dbPool.end(() => {
console.log('DB connections closed.');
process.exit(0);
});
});
}); For scaling, combine the Horizontal Pod Autoscaler (HPA) with resource metrics. Node.js is typically CPU-bound for compute-heavy tasks or memory-bound for caching workloads. Configure HPA to scale based on CPU utilization or custom metrics like request queue depth. Avoid scaling solely on memory unless you have identified memory pressure as your primary bottleneck, as V8 garbage collection can cause temporary spikes that trigger unnecessary scaling events.
Set maxSurge and maxUnavailable in your Deployment strategy to control rollout speed. A common safe configuration is maxSurge: 1 and maxUnavailable: 0, which ensures full capacity is maintained throughout the update. For high-traffic services, consider blue-green or canary deployments to validate new versions with a subset of traffic before full promotion.
Deploy a Node.js Service to Kubernetes: Next Steps
Successfully deploying to Kubernetes is iterative. Start with the multi-stage Dockerfile and basic manifests outlined here, then layer in observability, secrets management, and autoscaling as your traffic grows. Monitor your pod restart counts, latency percentiles, and resource utilization continuously. If pods are restarting frequently, check your probe configuration and application logs before increasing resources. Remember that infrastructure is code; version your manifests, review them in pull requests, and automate deployments through CI/CD pipelines. If you need help architecting a production-grade Kubernetes platform or auditing your current Node.js deployments, get in touch to discuss your specific requirements.