Deploy a Node.js Service to Kubernetes

Khimananda Oli 9 min read Programming and Languages
Deploy a Node.js Service to Kubernetes

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.

Stage 1: BuilderInstall All DepsCompile TypeScriptRun Tests / LintCopy /distStage 2: RuntimeAlpine / Slim BaseProd Deps OnlyNon-root UserFinal Image< 200MBNo Build Tools
Multi-stage Docker build architecture reduces final image size by separating build dependencies from the runtime environment when you deploy a Node.js service to Kubernetes.

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.

Startup ProbeInit & DB ConnectMax 60s GraceLiveness ProbeGET /health/liveRestart if FailReadiness ProbeGET /health/readyRemove from SvcTraffic Routing✓ Ready = Receive Traffic✗ Not Ready = Drain✗ Unhealthy = Restart
Kubernetes probe sequence: Startup probe allows slow initialization, while liveness and readiness probes maintain availability 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.

FeatureConfigMapSecret
Use CaseApp config, feature flags, URLsPasswords, tokens, TLS certs
EncodingPlain textBase64 (not encrypted by default)
Encryption at RestNoRequires etcd encryption config
Access ControlRBACRBAC + stricter policies
Best PracticeVersion control friendlyUse 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.

Old ReplicaSet (v1.4.1)Pod APod BPod CDraining Connections...New ReplicaSet (v1.4.2)Pod DPod EPod FPassing Readiness ✓HPA ControllerCPU > 70% → Scale UpCPU < 30% → Scale DownMin: 3 | Max: 10Service Endpoint UpdateOnly Ready Pods Receive Traffic • Zero Downtime During RolloutGraceful Shutdown Ensures In-Flight Requests Complete Before Termination
Rolling update and HPA interaction ensures continuous availability when you deploy a Node.js service to Kubernetes with autoscaling enabled.

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.

Frequently Asked Questions

Use node:22-alpine for production deployments in 2026. It reduces attack surface and image size significantly compared to Debian-based variants. Always pin specific versions rather than using latest tags to ensure reproducible builds and prevent unexpected runtime failures during cluster rollouts.

Define liveness and readiness probes pointing to dedicated HTTP endpoints like /healthz. Set initialDelaySeconds to match your application startup time to prevent premature restarts. Readiness probes should verify database connections while liveness probes only check if the process responds to requests.

Node.js defaults to using all available container memory without respecting Kubernetes limits. Set the --max-old-space-size flag explicitly to 75% of your memory limit. This reserves headroom for V8 overhead and prevents the kernel from terminating your pod during garbage collection spikes.

Dockerfiles offer more control over dependencies and layer caching for complex Node.js services. Buildpacks simplify CI pipelines but can produce larger images. Most teams prefer multi-stage Dockerfiles in 2026 for predictable, optimized production artifacts that pass security scanning reliably.

Store secrets in Kubernetes Secrets or external vaults like HashiCorp Vault, never in ConfigMaps. Mount them as files or inject via CSI drivers. Avoid passing sensitive values directly as environment variables since they appear in pod specs and crash logs accessible to cluster operators.

Start with 100m CPU and 256Mi memory requests based on load testing. Node.js is single-threaded so CPU requests should reflect actual event loop utilization. Monitor with Prometheus node_exporter and adjust quarterly. Over-provisioning wastes budget while under-provisioning causes throttling and latency spikes.

Listen for SIGTERM signals and stop accepting new connections immediately. Finish processing in-flight requests within your terminationGracePeriodSeconds, typically thirty seconds. Close database pools and flush logs before exiting. Without this handler, Kubernetes kills pods mid-request causing 502 errors during deployments.

Run exactly one Node.js process per pod. Let Kubernetes handle horizontal scaling instead of clustering inside containers. Multiple processes complicate health checks, resource allocation, and debugging. The orchestrator distributes load across replicas more efficiently than internal process managers ever could.

Use kubectl logs with --previous to inspect crashed container output. Run kubectl exec for live inspection or ephemeral debug containers for distroless images. Check events with kubectl describe pod for scheduling failures. Enable structured JSON logging to correlate issues across distributed traces effectively.

NGINX Ingress Controller remains the standard choice in 2026 for Node.js workloads due to WebSocket support and configurable buffering. Traefik offers simpler CRD-based configuration. Configure proxy timeouts matching your longest API response times. Enable connection draining annotations to prevent dropped requests during backend pod rotations.

Minimize dependencies and use esbuild or tsup for bundled production builds. Pre-warm caches during image build stages. Consider snapshotting with V8 startup snapshots for heavy frameworks. Faster startups improve autoscaling responsiveness and reduce rollout duration significantly during high-traffic deployment windows.

Yes, always use npm ci in Dockerfiles. It installs exact versions from package-lock.json without modifying lockfiles or running postinstall scripts unpredictably. This guarantees deterministic builds across environments. Combine with --omit=dev to exclude test dependencies and reduce final image footprint substantially.

Generate source maps during build but exclude them from production images. Upload maps to Sentry or Datadog at deploy time via CI pipeline. Configure error handlers to reference map metadata without bundling files. This keeps images small while maintaining readable stack traces in monitoring dashboards.

Use port 3000 by convention but any unprivileged port above 1024 works. Never bind to localhost; listen on 0.0.0.0 to accept traffic from kube-proxy. Document the port in your Dockerfile EXPOSE directive and match it in service definitions. Consistency simplifies network policy configuration.

Only add Istio or Linkerd when you require mutual TLS, advanced traffic splitting, or observability beyond basic metrics. Service meshes increase latency and operational complexity. For most Node.js deployments in 2026, native Kubernetes networking with proper ingress configuration provides sufficient reliability without mesh overhead.