Mahbubul Alam.
DevOps

Docker and Kubernetes for Node.js Teams: A Practical Starting Point

Jul 20262 min read
Docker and Kubernetes for Node.js Teams: A Practical Starting Point

Start with a Dockerfile that isn't accidentally huge

The most common early mistake is a single-stage Dockerfile that ships the entire node_modules tree, including devDependencies, build tools, and source maps nobody needs in production — often 3-4x larger than necessary. A multi-stage build fixes this directly:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

The build stage has everything needed to compile; the final image only has the production runtime and compiled output. Running as a non-root USER node instead of the default root is a small line that closes off a real category of container escape risk for close to zero effort.

What Kubernetes actually buys you over a single Docker host

A single server running Docker Compose is genuinely fine for a lot of applications — Kubernetes' complexity is a cost you should only pay once you need what it specifically provides: automatic restart of crashed containers, rolling deploys with zero downtime, horizontal scaling based on load, and self-healing when a node dies. If you have one server and no plans to run more, Kubernetes is mostly overhead without benefit. The honest trigger for adopting it is needing at least one of those specific capabilities, not "it's what serious companies use."

Health checks are not optional

Kubernetes can only restart an unhealthy pod if it knows the pod is unhealthy — which means every service needs a real /healthz endpoint that actually checks its dependencies (database connection, not just "the process is running"), wired into liveness and readiness probes:

livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 15
readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  periodSeconds: 5

The distinction between liveness (is the process alive, should it be restarted if not) and readiness (is it ready to receive traffic right now) matters — a pod that's alive but still connecting to its database shouldn't receive traffic yet, but it also shouldn't be killed and restarted for a transient startup delay.

Resource requests and limits, set deliberately

Without explicit CPU/memory requests and limits, Kubernetes schedules pods with no real understanding of what they need, which leads to noisy-neighbor problems (one pod starving another on the same node) or the scheduler over-packing a node. Setting these based on actual observed usage under load — not a guess — is what makes horizontal pod autoscaling behave sensibly instead of scaling reactively to symptoms of misconfiguration.

The realistic adoption path

  1. Containerize with a proper multi-stage Dockerfile, running as non-root
  2. Get comfortable with Docker Compose for local development and small deployments
  3. Move to Kubernetes only once you need a specific capability it provides — rolling deploys, autoscaling, or multi-node resilience
  4. Invest in health checks and resource limits from the first deploy, not retrofitted after an incident
#docker#kubernetes#devops