The final stage needs the runtime, your built artifact, its runtime libraries and a non-root user, and nothing else. Compilers, package managers, test tools and source code stay in the build stage. What counts as the artifact differs: a jar for Java, a binary for Go, a virtual environment for Python, node_modules plus built output for Node.js.
The side by side table
| Node.js | Python | Java | Go | |
|---|---|---|---|---|
| Build stage has | Full Node image, npm, compiler for native add-ons | Compilers and headers if wheels are missing | JDK and build tool | Go toolchain |
| Artifact to copy | node_modules and built output | Virtual environment and app code | Jar, or extracted layers | One static binary |
| Runtime in final stage | Node binary | Python interpreter | JRE or custom runtime | None if static |
| Package manager in final stage | Avoid | Avoid | Not needed | Not needed |
| Common base choices | slim, alpine, distroless | slim, distroless | JRE image, distroless | scratch, distroless static |
| Non-root user | node user, uid 1000 in official images | Create one, numeric USER | Create one, numeric USER | nonroot tag or numeric USER |
| Entry point | node, exec form | python, exec form | java, exec form | the binary |
Each column is covered in detail elsewhere in this series. See the posts on Node.js images without npm, pip install failures on slim images, JRE or JDK final images and static Go images.
Rules that hold in every language
- Use multi-stage builds. Docker's documentation describes multi-stage builds as a way to keep build tools out of the final image.
- Pin base images. Tags are mutable, and Docker's guidance describes pinning to a digest for a guaranteed image, then updating tags and digests on a schedule.
- Rebuild often. The same guidance recommends rebuilding images to pick up fresh base layers.
- Use a .dockerignore file so local files, caches and credentials do not enter the build context.
- Run as a non-root user and verify it with the id command.
- Use exec form for CMD and ENTRYPOINT so signals reach the app.
Review prompts for a pull request
Use these in code review when a Dockerfile changes:
- Does the final stage start from a different image than the builder?
- Is the base pinned, and is there a job that refreshes it?
- Does the final image contain a compiler or package manager? Why?
- Which user does it run as?
- Does anything copy the whole build context into the final image?
- Are secrets passed through build arguments or environment variables?
What a table cannot tell you
The right base image depends on your dependencies. A Python service with a compiled data library may need glibc, which moves you off alpine. A Java service that loads classes at run time may need more than a jlink runtime includes. Test the final image with your real integration tests, not only a hello world.
Try it yourself
Open the container image scan demo and look at which findings come from the base image and which come from application packages. The table above helps you decide which of the two you can shrink by changing the final stage.
Click through it at your own pace, or open it full screen.
Where Safeguard fits
Safeguard scans dependencies in all four ecosystems, builds SBOMs, uses reachability analysis to rank findings and lets you set policy gates for pull requests. The free scan is a first step.
Quick check
Is a multi-stage build enough to make an image secure?
No. It keeps build tools out of the final image, but dependencies, user settings and the base image still need attention.
Should every language use distroless?
Not necessarily. It suits many services, but if your team needs a shell for operations or your dependencies need libraries outside it, a slim image with a clear patch routine is reasonable.
Sources
- Docker build guidance
- Docker multi-stage builds
- Node.js Docker guidance, nodejs/docker-node
- Distroless container images, GoogleContainerTools
Last reviewed: 19 October 2026.