They do if you build and run in the same stage. A multi-stage Dockerfile fixes that. You build with the bundler, compiler and test tools in one stage, then copy only the built output into a clean final stage, which leaves the build tooling and its dependencies out of the image you ship.
What Docker documents
Docker's multi-stage builds guide says you use multiple FROM statements in a Dockerfile, each starting a new stage with its own base. You can selectively copy artifacts from one stage to another, leaving behind everything you do not want in the final image. The example in the docs builds a binary in one stage and copies it into a second stage.
For a front-end app the same idea applies. The first stage has Node.js, your full dependency tree, the bundler and the plugins. The final stage needs only static files and a web server, or the small runtime for a server rendered app.
Why this is a supply chain matter
Every package in an image is something that can carry a vulnerability, a license, or a malicious update. Build tools bring many of them. If they stay in the production image:
- A scanner reports advisories for packages that never run in production, which adds noise.
- Attackers who get into a running container find compilers, package managers and credentials for registries.
- Your SBOM for the image overstates what the application is, and a reader cannot tell which packages matter.
| Image contents | Build stage | Final stage |
|---|---|---|
| Node.js and package manager | Yes | Only if the app runs on Node |
| Bundler, transpiler, linter, test runner | Yes | No |
| devDependencies | Yes | No |
| Built static assets | Created here | Copied in |
| Registry tokens or .npmrc | Avoid, or use build secrets | Never |
A simple pattern
FROM node:lts AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:stable
COPY --from=build /app/dist /usr/share/nginx/html
The final image contains the web server and the built files. The Node.js stage, its tools and its node_modules are not part of it. Pin the base images to a version you have reviewed, and see a starter Dockerfile with secure defaults for more line by line detail.
Steps to check your own image
- List the stages in your Dockerfile. If there is only one FROM, you probably ship your build tools.
- Inspect the final image with a container scan or an SBOM tool and look for the bundler and test packages.
- If the app runs on Node.js, install only production dependencies in the final stage, using the production install option of your package manager.
- Keep credentials out of layers. A token passed in an environment variable or file during build can persist in image history, so use build secrets or pass them in a way Docker does not store.
- Re-scan after every base image or dependency change.
What this does not fix
A clean final stage does not make the build safe. The build stage still runs the bundler and all its plugins with your source code. Protect it by limiting secrets and reviewing toolchain changes, as in is your bundler plugin trusted code.
Safeguard's container image scanning and SBOM generation let you see what is in each layer, and its free scan is at gold.safeguard.sh/scan.
Try it yourself
Click through it at your own pace, or open it full screen. Look for packages that belong to the build and not to the running app.
Quick check
Does a multi-stage build remove vulnerabilities from my app?
It removes build tool packages from the final image. Vulnerabilities in what you ship remain.
Can secrets used in the build stage leak into the final image?
Only if they are copied or baked into a layer you keep. Use build secrets and copy only the output.
Sources
Last reviewed: 19 October 2026.