Large Docker images are slow to build, slow to push, slow to pull on every deploy and full of software your application never uses. Every extra package is also extra attack surface for vulnerability scanners to flag.

Most images can shrink dramatically with a handful of techniques.

1. Start from a smaller base image

  • node:22 includes a full Debian system with compilers.
  • node:22-slim removes most of that.
  • Distroless or Alpine images are smaller still.

Alpine uses musl instead of glibc, which occasionally breaks native modules. Slim and distroless images are safer defaults for most apps.

2. Use multi-stage builds

Build in one stage with all the tools, then copy only the output into a clean runtime stage.

dockerfile
# Build stage
FROM node:22-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod

# Runtime stage
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]

Compilers, dev dependencies and source files never reach the final image.

3. Add a .dockerignore

Without it, COPY . . sends everything to the build, including node_modules, .git and local env files.

text
node_modules
.git
dist
*.log
.env*

4. Order layers for caching

Copy dependency manifests and install before copying source code. Source changes then reuse the cached dependency layer, making rebuilds much faster.

5. Clean up in the same layer

Files deleted in a later layer still exist in the earlier one. Install and clean up in a single RUN:

dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends curl \
 && rm -rf /var/lib/apt/lists/*

6. Only install production dependencies

Prune dev dependencies (pnpm prune --prod, npm ci --omit=dev) and remove build tools from the runtime image.

7. Compiled languages: go static

Go and Rust can produce a single static binary. The runtime image can then be distroless/static or even scratch, often only a few megabytes.

Measure it

bash
docker images myapp
docker history myapp:latest   # see which layers are large

Tools like dive show exactly which files each layer adds.

Security bonus

Smaller images have fewer packages with known vulnerabilities. Combine that with running as a non-root user (USER node) and scanning images in CI.

Key takeaways

  • Choose slim or distroless base images.
  • Multi-stage builds keep build tools out of production.
  • A .dockerignore and good layer order speed up every build.
  • Smaller images deploy faster and have fewer vulnerabilities.