Production should install only what it needs to run. When dev and CI install test frameworks, linters, and build tools, and production images accidentally include them too, you carry extra packages and extra advisories. Install runtime dependencies only in production builds, and compare the lists to confirm.
Where this kind of drift comes from
Environments legitimately differ in dependency sets: developers need test runners, production does not. The drift is the unintended part. A Dockerfile that runs a full install in the final stage, or a deploy script that copies a whole working directory, brings dev packages along.
The npm documentation shows how the tool treats this. For npm ci, the omit setting defaults to dev when the NODE_ENV environment variable is set to production, otherwise it is empty. It also notes that omitted types are still resolved and recorded in the lockfile, they are just not physically installed. So the lockfile alone cannot tell you what is on disk, and an environment variable can change what gets installed.
Why it matters for security
Dev packages are still third party code. A malicious or vulnerable build tool inside a production image is a risk with no benefit. Scanners and customers asking for your SBOM will also see it, leading to findings you then have to explain.
| Approach | Result |
|---|---|
| One image for all environments, dev tools included | Largest surface, simplest build |
| Multi-stage build, runtime only in the final stage | Smaller surface, slightly more setup |
| Separate dev and prod dependency groups | Clear intent, needs discipline |
Steps to fix it
- List what is installed in the production image today, and mark the packages that exist only for tests or builds.
- Move build steps into an earlier stage of a multi-stage build. Copy only the output into the final stage.
- Make the final stage install runtime dependencies only.
- Do not rely on an environment variable alone. Make the production install command explicit, and check the result.
- Add a check that fails if known dev packages appear in the production SBOM.
- Compare dev, CI, staging, and production package lists after the change. Differences should now be explainable.
Do not hide the dev side
Dev dependencies still deserve scanning. A compromised test tool can run on developer laptops and CI runners, where secrets live. Scan them, but triage them separately from what ships. For SBOMs, mark component scope so readers can tell the groups apart; our post on CycloneDX component scope covers that.
Try it yourself
Click through it at your own pace, or open it full screen.
Look at the full inventory and imagine splitting it into runtime and dev groups. That split is the target.
What Safeguard does here
Safeguard scans dependency manifests and images, produces SBOMs, and monitors them continuously, so unexpected packages in a production artifact stand out. Try the free scan at gold.safeguard.sh/scan.
Quick check
Does omitting dev packages remove them from the lockfile?
No. The npm docs say omitted packages are still resolved and recorded. They are only not installed.
Is a smaller image always safer?
Usually it has fewer things to go wrong, but verify by scanning. Size is a proxy, not a guarantee.
Sources
Last reviewed: 19 October 2026.