Run Express on Kubernetes

Khimananda Oli 8 min read Programming and Languages
Run Express on Kubernetes

By Khimananda Oli | Last reviewed: August 2026

Migrating a Node.js API from a VPS or PaaS to a cluster requires more than just wrapping it in a container; you must adapt the runtime to handle orchestration signals and ephemeral storage. When you run Express on Kubernetes, the primary challenges shift from code logic to configuration management, graceful shutdown handling, and resource governance. This guide provides the exact manifests and operational patterns I use to deploy resilient Express APIs in production environments, moving beyond basic tutorials to address real-world stability.

Ingress ControllerExpress DeploymentPod (Replica 1)Pod (Replica 2)Pod (Replica N)ConfigMap / SecretLiveness ProbeTraffic Flow & Configuration Injection
High-level architecture for running Express on Kubernetes with externalized config and health monitoring

How do you containerize Express for Kubernetes correctly?

A common mistake when teams first run Express on Kubernetes is shipping bloated images containing development dependencies, source maps, or even full OS package managers. In production, your container should be immutable, minimal, and secure. I recommend a multi-stage Dockerfile that separates the build environment from the runtime. This reduces the attack surface and ensures that only compiled artifacts and production dependencies exist in the final layer.

Node.js 20+ includes native support for signal handling improvements, but you still need to ensure your base image is patched regularly. Using node:22-alpine or gcr.io/distroless/nodejs22 is standard practice in 2026. Distroless images are preferable for high-security environments because they lack a shell entirely, preventing attackers from executing arbitrary commands if they compromise the application.

# Build Stage
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
    cp -R node_modules prod_node_modules && \
    npm ci
COPY . .
RUN npm run build

# Production Stage
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prod_node_modules ./node_modules
COPY package.json ./
ENV NODE_ENV=production
USER nonroot
CMD ["dist/server.js"]

This pattern ensures that TypeScript compilation or bundling happens in the first stage, while the second stage contains only what is necessary to execute. Note the USER nonroot directive; running as root inside a container is a critical security violation that fails most SOC 2 and ISO 27001 audits. If you are new to securing these workloads, review Kubernetes security pod policies to enforce non-root execution at the namespace level.

What Kubernetes resources are required to run Express reliably?

To run Express on Kubernetes effectively, you need more than a Deployment. A production-grade setup requires coordinating several primitives to handle networking, scaling, and configuration. The core resources include a Deployment for stateless replicas, a Service for internal DNS-based discovery, an Ingress for external HTTP routing, and optionally a HorizontalPodAutoscaler (HPA) for dynamic scaling based on CPU or custom metrics.

  • Deployment: Manages replica sets and rolling updates. Always set strategy.type: RollingUpdate with appropriate surge and unavailable counts to prevent downtime during deploys.
  • Service: Typically ClusterIP. Avoid NodePort in production unless you have specific edge requirements.
  • Ingress: Terminates TLS and routes host/path combinations to your Service. Use cert-manager for automated certificate renewal.
  • ConfigMap & Secret: Decouple environment-specific variables from your container image. Mount them as environment variables or files depending on sensitivity.

Resource requests and limits are non-negotiable. Without them, the scheduler cannot place pods efficiently, and a single memory leak can starve neighboring workloads. For a typical Express API, start with 100m CPU / 256Mi memory requests and 500m CPU / 512Mi memory limits, then tune based on observed usage. Understanding these constraints is vital; see Kubernetes resource limits and requests for a deep dive on avoiding OOMKilled errors and throttling.

KubeletExpress AppActive Requests1. SIGTERM2. Stop accepting new connections3. Wait for in-flight requestsProcess Exit(0)4. Cleanup & close DB connectionsPod TerminatedMax: terminationGracePeriodSeconds
Graceful shutdown sequence preventing dropped requests during Express pod termination

How do you handle graceful shutdowns and zero-downtime deploys?

Express does not handle SIGTERM signals gracefully by default. When Kubernetes terminates a pod, it sends SIGTERM and starts a countdown (default 30s). If your app ignores this signal, it continues accepting new connections until SIGKILL forcefully ends it, causing client errors. To achieve zero-downtime deployments when you run Express on Kubernetes, you must explicitly listen for SIGTERM, stop the HTTP server from accepting new connections, and wait for existing requests to complete before exiting.

const server = app.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});

const gracefulShutdown = (signal) => {
  console.log(`${signal} received. Shutting down gracefully...`);
  server.close(() => {
    console.log('HTTP server closed. Closing DB connections...');
    // Close database pools, message queue consumers, etc.
    db.pool.end().then(() => {
      console.log('All connections closed. Exiting.');
      process.exit(0);
    });
  });

  // Force shutdown after 25s if graceful shutdown stalls
  setTimeout(() => {
    console.error('Forced shutdown due to timeout');
    process.exit(1);
  }, 25000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

Additionally, configure terminationGracePeriodSeconds in your Deployment spec to match your longest expected request duration plus cleanup time. If your app takes longer than this period, Kubernetes will kill it ungracefully. Pair this with a preStop hook that sleeps for 3-5 seconds to allow the kube-proxy/iptables rules to update across the cluster before your app stops accepting traffic. This small delay prevents race conditions where traffic is routed to a terminating pod.

How do you manage secrets and configuration for Express in Kubernetes?

Never bake secrets into your container image. When you run Express on Kubernetes, use ConfigMaps for non-sensitive data (log levels, feature flags) and Secrets for credentials (DB passwords, API keys). However, base64-encoded Secrets in YAML are not encrypted at rest by default. For production compliance, integrate with an external secrets manager like AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault using the External Secrets Operator or CSI drivers.

MethodSecurity LevelComplexityBest For
Env vars via SecretLow (base64 only)LowDev/Staging, non-sensitive config
Volume mount (Secret)MediumMediumCerts, config files, legacy apps
External Secrets OperatorHigh (encrypted at rest)HighProduction, SOC 2/ISO 27001 compliance
Vault Agent InjectorHighest (dynamic secrets)HighestShort-lived creds, multi-cluster

I typically use the External Secrets Operator for teams already on AWS or GCP. It syncs cloud-native secrets into Kubernetes Secrets automatically, allowing your Express app to consume them as standard environment variables without code changes. This approach satisfies audit requirements for secret rotation and access logging. For detailed implementation patterns, refer to Kubernetes secrets management done right.

How do you monitor and scale Express applications on Kubernetes?

Observability is mandatory when you run Express on Kubernetes because debugging distributed failures differs fundamentally from monolithic troubleshooting. You need three pillars: metrics for autoscaling, logs for forensic analysis, and traces for latency breakdown. Start by exposing a /metrics endpoint using prom-client to emit request rate, error rate, and latency histograms. These metrics feed both Grafana dashboards and the HorizontalPodAutoscaler.

Configure HPA to scale based on custom metrics rather than just CPU. Express is often I/O bound, so CPU utilization may remain low while request latency spikes. Use the Prometheus Adapter to expose http_requests_per_second or http_request_duration_seconds_p95 as scaling targets. Set your minReplicas to at least 2 for high availability and maxReplicas based on your tested capacity ceiling.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: express-api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: express-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300

The behavior block above prevents flapping by requiring sustained load changes before scaling. Scale-up reacts faster (60s window) than scale-down (300s window) to avoid premature downsizing during transient dips. Complement this with structured JSON logging piped to a centralized stack; raw stdout is insufficient for production forensics. See structured logging best practices to ensure your logs are queryable and correlate with traces.

Naive DeploymentRoot user in containerNo resource limits definedIgnores SIGTERM signalsSecrets baked into imageCPU-only autoscalingSingle replica, no PDBProduction-ReadyNon-root distroless imageRequests & limits tunedGraceful SIGTERM handlerExternal secrets operatorCustom metric HPA (RPS/P95)Multi-AZ + PodDisruptionBudget
Critical differences between naive and production-grade approaches to running Express on Kubernetes

Deploy Your Express API with Confidence

When you run Express on Kubernetes with proper containerization, graceful shutdown handling, externalized secrets, and metric-driven autoscaling, you transform a fragile Node.js app into a resilient cloud-native service. The patterns outlined here reflect battle-tested configurations from production systems handling millions of requests daily. Start with the multi-stage Dockerfile and SIGTERM handler, then progressively add HPA and external secrets as your traffic grows. If you need help auditing your current Kubernetes setup or designing a compliant deployment pipeline for your Express API, reach out to discuss your infrastructure needs.

Frequently Asked Questions

Yes, Kubernetes 1.32 or later is recommended for stable sidecar container support and improved resource scheduling for Node.js workloads.

Use a multi-stage Dockerfile with Node 22 slim base image, copy only production dependencies, and set non-root user. Build with docker buildx for multi-arch support targeting your cluster node architecture.

Configure Express to bind to 0.0.0.0 on port 3000 or 8080. Never use localhost as it prevents service mesh and kube-proxy traffic routing from reaching the application container correctly.

Implement SIGTERM handlers using process.on to close database connections and stop accepting new requests. Set terminationGracePeriodSeconds to 30 in your deployment spec to allow cleanup before forced SIGKILL termination.

Start with 256Mi memory request and 512Mi limit for typical APIs. Monitor actual usage via metrics-server for two weeks then adjust based on p99 memory consumption patterns observed in production.

Define liveness probes hitting /healthz returning 200 OK within three seconds. Add readiness probes checking downstream dependencies like databases. Set initialDelaySeconds to five to prevent premature restarts during cold starts.

Yes, configure HPA v2 targeting CPU utilization at 70% or custom metrics like requests-per-second. Express scales horizontally well but ensure session state is externalized to Redis before enabling autoscaling policies.

Store credentials in Kubernetes Secrets mounted as environment variables or volume files. Use External Secrets Operator to sync from AWS Secrets Manager or Vault. Never commit .env files to container images or configmaps.

NGINX Ingress Controller handles path-based routing efficiently for Express apps. Configure rewrite-target annotations for API versioning. Consider Gateway API with Envoy for advanced traffic splitting and canary deployments in 2026 clusters.

Check kubectl logs with previous flag to see pre-crash output. Verify memory limits aren't too restrictive causing OOMKill. Inspect events with kubectl describe pod to identify image pull failures or probe misconfigurations.

Only if you need auto-scaling or multi-region deployment. Single-instance Express apps under ten RPS are cheaper on managed platforms like Railway or Fly.io. Kubernetes adds operational overhead justifying cost only above moderate scale.

Let ingress controllers handle TLS using cert-manager with Let's Encrypt ClusterIssuer. Configure Express to trust proxy headers via trust proxy setting. Avoid terminating TLS inside pods as it wastes CPU cycles better spent on application logic.

Output structured JSON logs to stdout using pino or winston transports. Deploy Fluent Bit DaemonSet to forward logs to Elasticsearch or Loki. Never write logs to pod filesystem as they disappear when containers restart or reschedule.

Use RollingUpdate strategy with maxSurge one and maxUnavailable zero. Ensure readiness probes accurately reflect app state. Implement idempotent startup scripts and connection pooling to prevent request drops during pod replacement cycles.

Yes, Karpenter provisions right-sized nodes matching Express workload profiles reducing waste. Combine with spot instances for non-critical environments. Ensure pods have appropriate tolerations and node affinity rules to leverage dynamic scaling effectively.