Small Base Images & .dockerignore
The build context
Section titled “The build context”When you run docker build ., Docker packages everything in the current directory into a build context and sends it to the build daemon. Large contexts slow down every build, even if most files are never used by a COPY instruction.
#5 transferring context: 823.4MB ← node_modules included!A .dockerignore file tells Docker which paths to exclude from the build context — exactly like .gitignore does for Git.
Writing a .dockerignore file
Section titled “Writing a .dockerignore file”Create .dockerignore in the same directory as your Dockerfile:
# Dependencies — reinstalled inside the imagenode_modules/vendor/
# Version control.git/.gitignore
# Editor and OS noise.DS_Store*.log.env.env.*
# Test and CI files not needed at runtime__tests__/coverage/.github/*.test.ts*.spec.tsAfter adding this file the build context drops from hundreds of MB to a few KB for a typical Node.js project. As an added bonus, excluding .env files prevents secrets from accidentally leaking into the image.
Choosing a small base image
Section titled “Choosing a small base image”The base image is often the largest single contributor to image size. Here are the common options, from largest to smallest:
| Base | Measured size | Notes |
|---|---|---|
ubuntu:24.04 | ~101 MB | Full apt ecosystem; large |
debian:bookworm | ~139 MB | Debian full; common |
node:24 | ~1.14 GB | Includes full Debian + devtools |
node:24-slim | ~249 MB | Debian slim; no devtools |
node:24-alpine | ~166 MB | Alpine Linux OS layer is tiny, but the Node.js runtime itself is now the dominant cost |
gcr.io/distroless/nodejs24-debian13 | ~161 MB | Distroless; minimal, no shell |
scratch | 0 MB | Truly empty; for static binaries only, not for Node.js |
Sizes above were measured directly with docker images after docker pull on this machine (arm64, September 2026) — expect them to vary somewhat by architecture, but the relative ordering and the fact that no current Node.js base image gets anywhere near the ~50 MB figures older tutorials quote still holds: the Node.js runtime itself, not the OS layer, is what dominates image size today.
Alpine Linux
Section titled “Alpine Linux”Alpine is based on musl libc and BusyBox. It is the most popular small base for containers because:
- The base image is under 5 MB.
apkpackage manager covers most common tools.- Official images for Node.js, Python, Go, Ruby, and others all publish
-alpinevariants.
FROM node:24-alpineOne caveat: Alpine uses musl instead of glibc. Some packages compiled against glibc (e.g., certain native Node addons) need extra work. For pure-JS applications it is nearly always drop-in.
Distroless images
Section titled “Distroless images”Google’s distroless images contain only the language runtime — no shell, no package manager, no ls or cat. They present a much smaller attack surface than a full distro because there is no tooling for an attacker to abuse, even though the total byte size is now similar to -alpine (both are dominated by the Node.js runtime itself).
FROM gcr.io/distroless/nodejs24-debian13Because there is no shell, CMD and ENTRYPOINT must use exec form (JSON array):
CMD ["dist/server.js"]Remove apt caches after installing
Section titled “Remove apt caches after installing”When you must install packages with apt-get, always clean up the package lists in the same RUN layer:
RUN apt-get update \ && apt-get install -y --no-install-recommends curl \ && rm -rf /var/lib/apt/lists/*--no-install-recommends skips optional recommended packages. rm -rf /var/lib/apt/lists/* removes the downloaded package index from the layer — if you put it in a separate RUN instruction the size is already committed and the cleanup has no effect.
Hands-on: compare build contexts and base images
Section titled “Hands-on: compare build contexts and base images”The snippet below demonstrates the .dockerignore pattern and an Alpine base image.
# syntax=docker/dockerfile:1
# .dockerignore (create alongside this Dockerfile):
# node_modules/
# .git/
# .env
# coverage/
FROM node:24-alpine
WORKDIR /app
# Only manifests first for cache efficiency
RUN echo '{}' > package.json && echo '{}' > package-lock.json
# Application source (node_modules excluded by .dockerignore)
RUN mkdir -p src && echo "console.log('Running on Alpine, image is tiny!');" > src/index.js
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node","src/index.js"]
# --- Build and check the size ---
# docker build -t myapp:alpine .
# docker images myapp:alpineTrade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Write a .dockerignore covering every unneeded path | Small build context that sends fast, lowers the risk of secrets leaking into the image | Have to keep the list in sync as the project’s structure changes over time |
| Pick a smaller base image (alpine instead of a full distro) | Cuts the final image by hundreds of MB | Some native addons compiled against glibc may need rebuilding for musl |
Common mistakes
Section titled “Common mistakes”- Having no
.dockerignoreat all, so.git/andnode_modules/get sent to the build context every time, slowing builds down badly on projects with a long history. - Forgetting to exclude
.envin.dockerignore, letting secrets from a local file leak into the image unintentionally. - Reaching for a large base image like
node:24instead ofnode:24-alpinewhen the application does not need any extra tooling at runtime.
💡 Real-world example
Docker’s official best-practices docs repeatedly warn that a Node.js project with no
.dockerignoreoften sends a build context of hundreds of MB, becausenode_modules/gets included every time even though dependencies get reinstalled inside the image anyway (docs.docker.com/build/building/context).