Why Containerize?
Containers give you reproducible environments from local dev to production. "Works on my machine" becomes a non-issue. With Docker and a solid CI/CD pipeline, every commit is a potential production release.
Writing a Production Dockerfile
Use multi-stage builds to keep the final image small. The builder stage installs dev dependencies and compiles TypeScript; the runner stage only copies the compiled output.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next .next
COPY --from=builder /app/node_modules node_modules
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["node", "server.js"]
GitHub Actions Workflow
A minimal but complete workflow: lint → test → build Docker image → push to registry → deploy. Each step is a gate — failure stops the pipeline.
Environment Variables in CI
Store secrets in GitHub Actions secrets, never in the repository. Reference them in the workflow with ${{ secrets.MY_SECRET }}. For staging vs production, use environment-specific secret sets.
Zero-Downtime Deploys
Use rolling updates or blue-green deployments. With Kubernetes, a rolling update replaces pods one at a time. Health checks ensure new pods are healthy before old ones are terminated.
Observability
Every pipeline should emit structured logs, metrics, and traces. Instrument your Node.js app with OpenTelemetry from day one — retrofitting observability is painful.