HomeNode.js Docker images: full vs slim vs alpine vs distroless

Node.js Docker images: full vs slim vs alpine vs distroless

By · Node.js & JavaScript developer
Published July 5, 2026

Every Node.js Dockerfile starts with a base-image decision, and it's a bigger one than it looks: the same tiny app ranges from ~1.8 GB to ~210 MB depending on the base, and the smaller images also change how you debug and how much attack surface you ship. Here's the comparison with real numbers — every size below came from docker images after building the same app four ways.

The app

A trivial HTTP server, so the numbers reflect the base image, not the code:

// server.js
const http = require('node:http');
http.createServer((req, res) => res.end('ok\n')).listen(3000);

The results

Same app, four base images, final image size:

tiny:full         1.77GB    FROM node:26
tiny:slim          371MB    FROM node:26-slim
tiny:alpine        247MB    FROM node:26-alpine
tiny:distroless    212MB    FROM gcr.io/distroless/nodejs22-debian12

The jump from the full image to slim is the single biggest win — nearly 1.4 GB for free — and most projects never need what the full image adds.

node:26 — the full image (1.77 GB)

The default tag bundles a complete Debian userland plus build toolchains (git, Python, a C/C++ compiler) so that native npm modules compile out of the box. That convenience is why it's huge. It's fine as a build stage; it's rarely what you want as the final image you deploy.

node:26-slim — the sensible default (371 MB)

Debian with the build toolchains stripped out. You keep glibc, a real shell, and apt for debugging, and native modules that ship prebuilt binaries still work. For most apps this is the right default.

FROM node:26-slim
WORKDIR /app
COPY package.json server.js ./
# RUN npm ci --omit=dev   # in a real app
CMD ["node", "server.js"]

node:26-alpine — smallest official (247 MB)

Built on Alpine Linux, which is tiny — but it uses musl libc instead of glibc. That's the catch: native modules with prebuilt binaries usually target glibc, so on Alpine they may fall back to compiling from source (slower builds, extra toolchain) or misbehave. It does keep a minimal busybox shell.

$ docker run --rm tiny:alpine sh -c 'ls /lib | grep musl'
ld-musl-aarch64.so.1
libc.musl-aarch64.so.1

distroless — smallest and most locked-down (212 MB)

Google's distroless images contain only the Node runtime and its libraries — no shell, no package manager, no apt, no coreutils. That's the smallest footprint and the smallest attack surface, but you can't docker exec into a shell to poke around, so you build it with a multi-stage Dockerfile and debug elsewhere. Note the entrypoint is already node, so CMD is just your script.

FROM node:26-slim AS build
WORKDIR /app
COPY package.json server.js ./
# RUN npm ci --omit=dev

FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=build /app /app
CMD ["server.js"]

There really is no shell — but Node runs fine, because it's the image's entrypoint:

$ docker run --rm --entrypoint sh tiny:distroless -c 'echo hi'
docker: Error ... exec: "sh": executable file not found in $PATH

$ docker run --rm tiny:distroless -e "console.log('node runs, no shell needed')"
node runs, no shell needed

One real limitation: distroless publishes specific Node versions, and at the time of writing the newest is Node 22 — so choosing distroless can mean trailing the latest runtime.

Which should you pick?

  • node:26-slim — the default. Small enough, glibc, a shell for debugging, and painless native modules. Start here.
  • distroless — for hardened production. Smallest attack surface and size; use a multi-stage build, and accept that debugging happens outside the container and you may trail the newest Node.
  • alpine — when every megabyte counts and you've verified your native dependencies work against musl.
  • node:26 (full) — build stage only. Great for compiling; don't ship it as your runtime image.

The common winning pattern is a multi-stage build: install and build in node:26 or node:26-slim, then copy just the app and node_modules into slim or distroless for the final image — full-image convenience at build time, small-image size at deploy time.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.