Docker and Kubernetes Best Practices for Production Node.js & Go Microservices
Stop shipping bloated, insecure containers. Discover multi-stage Docker builds, non-root user execution, Kubernetes probes, resource requests/limits, and graceful shutdown handlers.
1. The Anatomy of a Flawed Dockerfile
Most container images in early-stage startups suffer from the same set of critical vulnerabilities:
• Running as `root`: If an attacker exploits a remote code execution bug in an npm package, they obtain root access on the host node.
• Massive Bundle Size: Copying test files, build tools (C++ compilers, python), and source code results in 1.4GB images that take minutes to pull across clusters.
• Ignoring Zombie Processes: Node.js does not run as an init process (`PID 1`), causing unhandled SIGTERM signals and orphaned zombie processes during container shutdown.
Adopting production container best practices transforms fragile 1.2GB containers into secure, sub-60MB distroless images that boot in 300ms.
2. Production Multi-Stage Dockerfile for Node.js / TypeScript
Here is an enterprise-grade multi-stage Dockerfile that compiles TypeScript, prunes devDependencies, runs as an unprivileged user, and leverages dumb-init for proper signal handling:
# Stage 1: Build & Compile
FROM node:22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
# Stage 2: Minimal Distroless Production Runner
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN apk add --no-cache dumb-init
# Create unprivileged system user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 appuser
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
USER appuser
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]3. Kubernetes Liveness vs Readiness vs Startup Probes
Misconfigured health checks in Kubernetes cause cascading outages. Understanding the distinct purpose of each probe is essential:
• Startup Probe: Checks if a slow-booting application has finished initializing. Disables liveness and readiness probes until it passes, preventing Kubernetes from prematurely killing pods during boot.
• Readiness Probe: Checks if the pod is currently able to receive user traffic. If a pod's database connection drops temporarily, the readiness probe fails and Kubernetes immediately removes the pod from load balancer routing without restarting the process.
• Liveness Probe: Checks if the container process has completely deadlocked. If it fails, Kubernetes restarts the pod.
4. Graceful Shutdown & Zero-Downtime Rolling Deploys
When Kubernetes terminates a pod during a deployment, it sends a `SIGTERM` signal followed by a `SIGKILL` after `terminationGracePeriodSeconds` (default 30s).
If your application does not catch `SIGTERM`, active customer requests are severed instantly, resulting in 502 Bad Gateway errors.
Proper graceful shutdown in Node.js stops accepting new connections, drains existing HTTP requests, closes database connection pools, and exits cleanly within a 15-second grace window.
Why should I use `dumb-init` with Node.js in Docker?
Node.js was not designed to run as PID 1. It does not forward POSIX signals to child processes or reap zombie processes. `dumb-init` acts as a lightweight init system that handles signal forwarding correctly.
How should I set CPU and Memory requests/limits in Kubernetes?
Set CPU requests based on average utilization and leave CPU limits unset (or generous) to avoid unnecessary CPU throttling. Always set hard Memory limits to prevent a single pod with a memory leak from consuming the entire node.
What is a Distroless container image?
Distroless images contain only your application binary and runtime dependencies, without a Linux shell, package manager, or utility binaries, drastically shrinking the attack surface.