Safeguard
Containers

Node.js in Docker: A Practical Setup Guide

A practical setup guide for running node.js docker containers in production, choosing between docker node slim and full images, and locking down what actually matters for security.

Safeguard Team
Product
5 min read

Running node.js docker containers well in production comes down to three decisions: which base image variant to use, whether to build in multiple stages to strip dev dependencies out of the final image, and whether the process runs as root — and getting all three wrong is the default outcome if you just copy the first Dockerfile example you find. The official node docker images publish several variants for a reason; picking the wrong one by default is how a lot of production images end up several times larger and meaningfully less secure than they need to be.

Which docker hub nodejs image variant should you actually use?

Docker Hub nodejs images ship in roughly four flavors: the default (Debian-based, full), -slim (Debian-based, minimal packages), -alpine (musl-based, smallest), and a plain version tag with no suffix, which is the largest and includes build tools you almost never need at runtime. For a production runtime image, docker node slim is usually the right default — it strips out compilers, documentation, and other build-time tooling that the full image includes, while staying on glibc (Debian) rather than musl (Alpine), which avoids a category of subtle compatibility bugs some native Node addons hit under Alpine's different C standard library. Alpine images are smaller still and fine for many workloads, but if your dependency tree includes native modules (anything using node-gyp), test explicitly on Alpine before committing to it in production — the failure mode is usually a cryptic runtime error, not a build-time one.

How does a multi-stage build change what ships in the final image?

A multi-stage build separates "everything needed to build and test the application" from "everything needed to run it," and only the second set ends up in the image you actually deploy. The pattern: a first stage installs full devDependencies, runs the build (TypeScript compilation, bundling, whatever your project needs), and a second stage starts fresh from a slim base image, copies over only the compiled output and production dependencies (npm ci --omit=dev), and never carries forward the build toolchain, test files, or dev-only packages. This matters for size — a typical Node project's final image can shrink dramatically once devDependencies and build artifacts are excluded — but it matters more for security: every package in the final image is something that has to be scanned, patched, and accounted for, and dev-only dependencies (test frameworks, linters, bundlers) are pure attack surface with zero runtime benefit if they end up shipped anyway.

Should nodejs docker images run as root by default?

No — and this is the single most common security gap in Node Dockerfiles pulled from tutorials, most of which never set a non-root user at all. The official Node images since a few major versions back include a pre-created node user specifically for this purpose, and switching to it is one line: USER node after your COPY and dependency-install steps (ordered so file ownership and permissions are set correctly before the switch). Running as root inside a container doesn't grant root on the host by default under normal Docker configurations, but it does remove a layer of defense — a container escape or a misconfigured volume mount is meaningfully worse if the process inside was running as root. Pair this with a read-only root filesystem where your application doesn't need to write to disk, and drop Linux capabilities the process doesn't need; this is the same reasoning behind Kubernetes's securityContext settings for pods running these images once they're deployed to a cluster.

What should be scanned before a node.js docker image ships?

Both layers need scanning, not just one: the application dependencies (your package.json tree, via SCA) and the base image's OS packages (via container image scanning), since a vulnerability can live in either layer independently. A clean npm audit result says nothing about whether the underlying Debian or Alpine base image has an unpatched OpenSSL CVE, and vice versa. The SCA product handles the dependency-tree side of this, ideally wired into the same CI pipeline that builds the image so a new base-image CVE or a newly disclosed npm vulnerability blocks a merge rather than getting discovered after deployment. The academy has a walkthrough of structuring a production Node Dockerfile end to end, including the multi-stage pattern and non-root user setup described above.

Safeguard scans both the application dependency layer and the container base image layer of a Node.js Docker build in the same pipeline pass, so a finding in an npm package and a finding in the underlying OS image show up together instead of requiring two disconnected scanners.

FAQ

Is docker node slim always better than the full image for production?

For most applications, yes — it strips build tooling you don't need at runtime without the compatibility edge cases Alpine can introduce for native modules. Confirm your dependencies don't need something the slim image excludes before switching an existing production image.

Do node docker images need a .dockerignore file?

Yes — without one, COPY . . pulls in node_modules, .git, and local .env files into the build context, bloating the image and occasionally leaking local secrets or stale dependencies into the final build. A .dockerignore mirroring your .gitignore plus node_modules is the minimum.

How much smaller is a multi-stage node.js docker build in practice?

It varies by project, but excluding devDependencies, build tooling, and source maps commonly cuts a final image significantly compared to a single-stage build that carries the entire build environment forward — the exact reduction depends on how dependency-heavy your build toolchain is.

Does running as a non-root user break anything in a typical Node app?

Usually not, as long as file permissions are set correctly before switching users in the Dockerfile and the app doesn't need to bind to a privileged port (below 1024) or write to a directory owned by root without explicit permission changes.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.