Distroless & Alpine Trade-offs
Choose a container base image based on libc compatibility, image size, and debuggability - not habit.
Search across all documentation pages
Choose a container base image based on libc compatibility, image size, and debuggability - not habit.
Quick-reference recipe card - copy-paste ready.
# Default: Debian slim (glibc) - best npm compatibility
FROM node:24-bookworm-slim AS prod
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY dist ./dist
USER node
CMD ["node", "dist/main.js"]When to reach for this: Greenfield Node APIs with no native modules that require musl-specific builds. This is the team default.
Three final-stage options for the same Express API:
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json src ./
RUN npm run build
# Option A: bookworm-slim (recommended default)
FROM node:24-bookworm-slim AS slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
# Option B: Alpine (smaller, musl)
FROM node:24-alpine AS alpine
WORKDIR /app
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
# Option C: Distroless (minimal, no shell)
FROM gcr.io/distroless/nodejs24-debian12 AS distroless
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=slim /app/node_modules ./node_modules
USER nonroot
CMD ["dist/main.js"]docker build --target slim -t api:slim .
docker build --target alpine -t api:alpine .
docker build --target distroless -t api:distroless .What this demonstrates:
dist/ across all three runtimeslibc6-compat for some prebuilt binariesnode_modules from a glibc build stage (never compile inside distroless)| Base | libc | Typical size | npm native modules |
|---|---|---|---|
bookworm-slim | glibc | ~180 MB | Best compatibility |
alpine | musl | ~120 MB | Frequent rebuild issues (sharp, bcrypt, prisma) |
distroless/nodejs24 | glibc | ~130 MB | Good if built on glibc stage |
Node itself ships prebuilt binaries for glibc Linux. Alpine often forces npm rebuild or source compiles.
npm ci on Alpine in CI# bookworm-slim: shell available
docker run -it --entrypoint bash api:slim
# distroless: no shell - use debug pod or copy core dumps off-box
kubectl debug pod/api-xyz -it --image=busyboxsharp on Alpine - common failure. Fix: use bookworm-slim or official sharp Alpine install docs with pinned versions.docker build on Linux CI target.prisma generate in wrong stage - wrong OpenSSL bindings. Fix: generate in glibc build stage; copy artifacts.USER nonroot - runs as root by default on some tags. Fix: explicit USER nonroot.apk in production Dockerfile - bloats layers. Fix: multi-stage; runtime has no package manager.| Alternative | Use When | Don't Use When |
|---|---|---|
| bookworm-slim | Default for Node APIs | You need smallest possible image and have tested Alpine |
| Alpine | Size-sensitive edge deploys, pure JS stack | Heavy native module usage |
| Distroless | Hardened prod, K8s with debug tooling elsewhere | Team relies on docker exec bash |
| Chainguard Node images | Supply-chain hardened bases | You cannot adopt new base image cadence |
node:24-bookworm-slim unless a measured need (size, security scanner mandate) justifies Alpine or distroless.
Add a matrix job: docker build --target alpine and run smoke tests. Fail the PR if native modules break.
Yes. Build on bookworm-slim, copy dist/ and node_modules into distroless. No code changes required.
No. All installs happen in an earlier stage. The runtime only runs node.
Often 40-80 MB saved, depending on node_modules. Measure with docker images after production npm ci.
Pin minor Alpine versions in regulated environments. Track Node Docker release notes for base image updates.
USER node / nonrootStack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 18, 2026