Kennisbank › Docker Containers › Optimizing Docker images for production
docker containers gevorderd 25 August 2026

Optimizing Docker images for production

Optimizing Docker images for production

Optimizing Docker images for production

This article explains how to optimize Docker images for production environments according to the official Docker best practices, for developers and DevOps engineers working with Docker.

Prerequisites

- Docker Engine 24.0 or higher - Basic knowledge of Dockerfiles and container images - Access to a container registry (for example Docker Hub or a private registry) - An application you want to run in a container

Steps

1. Use an official base image

Start with a current official image as the basis for every image. Docker maintains these images with regular security updates and clear documentation. Official images reduce the risk of vulnerabilities in the underlying software stack.

Choose a specific version instead of `latest`:

FROM node:20-alpine

2. Choose the smallest possible base image

Use the smallest possible base image for the runtime stage. Smaller images deploy faster and contain fewer vulnerable components. Alpine variants are a good starting point for many workloads.

Compare:

Large: node:20 (about 1 GB)
Small: node:20-alpine (about 180 MB)
FROM node:20-alpine

3. Build with multi-stage builds

Use multi-stage builds to separate the build environment from the runtime environment. This results in a smaller and more secure production image.

Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Stage 2: runtime FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/package*.json ./ RUN npm ci --omit=dev EXPOSE 3000 CMD ["node", "dist/index.js"]

4. Order Dockerfile instructions for optimal caching

Place instructions from least frequently changing to most frequently changing. This keeps the cache valid for earlier layers when you only change the source code.

FROM node:20-alpine
WORKDIR /app

Copy package files first - these rarely change COPY package*.json ./ RUN npm ci

Copy source code - this changes often COPY . .

CMD ["node", "src/index.js"]

5. Avoid `COPY .` and use `.dockerignore`

Only copy the files the application needs. This reduces the image size and improves caching.

Create a `.dockerignore` file in the project root:

node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
.env
coverage
dist

Use targeted COPY instructions:

COPY package*.json ./
COPY src/ ./src/
COPY public/ ./public/

6. Remove unnecessary dependencies and tools

Leave out debugging tools and development dependencies from the production image. Use `npm ci --omit=dev` or the equivalent for your package manager.

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

7. Rebuild images regularly with updated dependencies

Older images contain vulnerabilities that have been fixed in newer versions of the base image or installed packages. Plan rebuilds at a fixed time, for example weekly or with every security patch.

docker build --pull -t myapp:$(date +%Y%m%d) .

The `--pull` flag ensures you always fetch the latest version of the base image.

8. Fetch remote files during the build

Download external files during the build instead of copying them from the context. This prevents unnecessarily enlarging the Docker build context.

FROM alpine:3.19
RUN wget -O /usr/local/bin/tool https://example.com/tool.tar.gz \
    && tar -xzf /usr/local/bin/tool.tar.gz -C /usr/local/bin/ \
    && rm /usr/local/bin/tool.tar.gz

Verification

Check whether your image is correctly optimized:

Show image size
docker images

Scan for vulnerabilities (Docker Scout or Trivy) docker scout cves myapp:latest

Show layers and their size docker history myapp:latest

A production image for a Node.js application should typically be smaller than 200 MB. An image with unnecessary build tools quickly exceeds this.

Common problems

1. Image remains large despite multi-stage build

Check whether you are copying unnecessary files in the runtime stage. Use `docker history ` to see the size per layer. Remove, for example, `node_modules` from the build context with `.dockerignore` and install only production dependencies.

2. Cache is not used on every build

Make sure the order of instructions is correct. Place `COPY package*.json ./` and `RUN npm ci` before `COPY . .`. If your `package.json` changes, the cache becomes invalid for all subsequent layers — that is correct behavior.

3. Alpine base image causes compatibility issues

Alpine uses musl libc instead of glibc. Some native modules do not work with this. Use an alternative small image such as `node:20-slim` (Debian-based) or build with a multi-stage build in which you compile the native module in the build stage.

Regular rebuilding is not a one-time activity but an ongoing process — the security landscape changes constantly, and your images need to move with it.

← Back to knowledge base
← Terug naar kennisbank