Skip to content

Small Base Images & .dockerignore

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.

Create .dockerignore in the same directory as your Dockerfile:

# Dependencies — reinstalled inside the image
node_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.ts

After 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.

The base image is often the largest single contributor to image size. Here are the common options, from largest to smallest:

BaseMeasured sizeNotes
ubuntu:24.04~101 MBFull apt ecosystem; large
debian:bookworm~139 MBDebian full; common
node:24~1.14 GBIncludes full Debian + devtools
node:24-slim~249 MBDebian slim; no devtools
node:24-alpine~166 MBAlpine Linux OS layer is tiny, but the Node.js runtime itself is now the dominant cost
gcr.io/distroless/nodejs24-debian13~161 MBDistroless; minimal, no shell
scratch0 MBTruly 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 is based on musl libc and BusyBox. It is the most popular small base for containers because:

  • The base image is under 5 MB.
  • apk package manager covers most common tools.
  • Official images for Node.js, Python, Go, Ruby, and others all publish -alpine variants.
FROM node:24-alpine

One 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.

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-debian13

Because there is no shell, CMD and ENTRYPOINT must use exec form (JSON array):

CMD ["dist/server.js"]

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:alpine
OptionBenefitCost
Write a .dockerignore covering every unneeded pathSmall build context that sends fast, lowers the risk of secrets leaking into the imageHave 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 MBSome native addons compiled against glibc may need rebuilding for musl
  • Having no .dockerignore at all, so .git/ and node_modules/ get sent to the build context every time, slowing builds down badly on projects with a long history.
  • Forgetting to exclude .env in .dockerignore, letting secrets from a local file leak into the image unintentionally.
  • Reaching for a large base image like node:24 instead of node:24-alpine when 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 .dockerignore often sends a build context of hundreds of MB, because node_modules/ gets included every time even though dependencies get reinstalled inside the image anyway (docs.docker.com/build/building/context).

What is the primary purpose of a .dockerignore file?
Why must rm -rf /var/lib/apt/lists/* be in the SAME RUN instruction as apt-get install?
Which of the following base images is the smallest and most commonly used for Node.js production images?
What is a key limitation of distroless images?