
Dockerizing Node.js: Best Practices & Multi-Stage Build Guide
Abhay Vachhani
Developer
Production-Ready Docker Checklist
- Multi-Stage Builds
- Non-root user (USER node)
- .dockerignore configured
- Tini for signal handling
Docker is the standard unit of deployment, but a naive Dockerfile can result in bloated, insecure images. A 1GB image slows down your CI pipeline and increases your attack surface. To build production-grade containers, you need to go on a diet. Dockerizing Node.js correctly means balancing security, performance, and image size.
1. Distroless Images: Less is More
Most Docker images are based on Debian or Alpine. They contain a full OS shell, package manager (apk/apt), and utilities. **Distroless** images (by Google) contain only your application and its runtime dependencies. No shell. No package manager.
FROM gcr.io/distroless/nodejs20-debian12 COPY --from=build /app /app WORKDIR /app CMD ["server.js"]
If a hacker breaks into your container, they can't even run ls or curl to download malware.
2. Handling Signals: Tini and Graceful Shutdowns
Node.js is not designed to run as PID 1 (the first process in a container). It doesn't handle kernel signals like SIGTERM correctly, meaning your app might not shut down gracefully when Kubernetes kills a pod.
Solution: Use **Tini**, a tiny init process that spawns Node.js as a child and forwards signals correctly.
ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "app.js"]
3. Security Scanning in CI
You scan your code, but do you scan your OS? Vulnerabilities in system libraries (like glibc or openssl) account for 90% of container exploits. Use **Docker Scout** or **Trivy** in your CI pipeline to block any build that contains High/Critical CVEs.
docker scout cves my-image:latest --exit-code --only-severity critical
Conclusion
A good Dockerfile is an investment in your deployment speed and security. By using Multi-Stage builds to toss out build tools, switching to Distroless to remove the OS, and using Tini for process management, you create containers that are lightweight, secure, and production-ready.
FAQs
What are the best practices for dockerizing Node.js?
Use multi-stage builds, use a non-root user (like 'node'), use '.dockerignore' to exclude 'node_modules', and use 'tini' to handle signals properly.
Why run as a non-root user?
If an attacker compromises your application inside the container, running as root gives them root access to the container and potentially the host. Running as `node` limits their blast radius.
Should I use Docker Scout for image scanning?
Yes, Docker Scout or Trivy are essential for identifying vulnerabilities in system libraries before deploying your images to production.
What is Tini, and why do I need it?
Node.js isn't designed to run as PID 1 (the first process). It doesn't handle termination signals well. Tini is a tiny init process that wraps Node, ensuring your container stops gracefully when told to.