Skip to content
Munish Thakur

Docker Image Optimization: 35% Smaller Images and Better Build Efficiency

4 min read

Docker image optimization often starts as a size problem and becomes a reliability problem. A smaller image helps only when the application still behaves correctly, the build remains understandable, and the team can reproduce it.

In production work at Solytics Partners, I improved Docker build efficiency by 75% and reduced image size by 35%. The useful lesson was not a single Dockerfile trick. The result came from measuring layers, separating build and runtime requirements, and changing one dependency boundary at a time.

Start with evidence

Before editing the Dockerfile, inspect the current image and build path:

1
2
docker image history application:current
docker build --progress=plain -t application:candidate .

docker image history shows which instructions create the largest layers. Plain build output shows where dependency resolution, compilation, or asset generation consumes time.

For deeper layer inspection, a tool such as Dive can show files added or removed in each layer:

1
dive application:current

This baseline prevents speculative changes. It also gives the team a measurable definition of improvement.

Separate build and runtime dependencies

Compiled Python packages, JavaScript assets, and native libraries may require compilers and development headers during the build. The running application usually does not need those tools.

A multi-stage build keeps the toolchain in a builder stage and copies only the required runtime output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
FROM python:3.11-slim AS builder

WORKDIR /build
RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.11-slim AS runtime

WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .

CMD ["python", "app.py"]

The exact copy boundary depends on the application. Native libraries, browser dependencies, Java runtimes, and data-processing packages need explicit runtime testing. Copying an incomplete environment can produce a small image that fails only after deployment.

Protect the dependency cache

Docker reuses a layer when the instruction and its inputs have not changed. Copy dependency manifests before application source so code changes do not invalidate package installation:

1
2
3
4
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

The same pattern applies to package-lock.json, go.mod, or other dependency manifests. Stable dependency layers reduced unnecessary work in repeated CI builds and contributed to the build-efficiency improvement.

Reduce package-manager waste

Package managers often leave indexes and caches that the running container does not need:

1
2
3
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

For Python, --no-cache-dir avoids retaining downloaded wheels. Similar cleanup applies to npm, Maven, and operating-system package managers, but cleanup must happen in the same layer that created the cache.

Keep the build context small

A .dockerignore prevents local caches, Git history, test output, and development artifacts from entering the build context:

1
2
3
4
5
6
7
.git
.github
.venv
node_modules
coverage
dist
*.log

A smaller context improves transfer time and reduces accidental cache invalidation. It also lowers the risk of copying credentials or local configuration into an image.

Test the candidate as a production artifact

Size and build duration are only two checks. A candidate image should also pass:

  • Application startup and health probes.
  • Database and external-service connectivity.
  • Native-library imports.
  • File permissions under the production user.
  • Browser or Java runtime requirements.
  • Readiness and liveness behavior.
  • Vulnerability and configuration scans.

The safest workflow compares the current and candidate images in the same staging conditions. If a package removal creates an unclear runtime dependency, restore it and investigate before pursuing another reduction.

Add regression checks

Optimization lasts only when CI detects regressions. A simple size gate can compare the candidate image against an agreed threshold:

1
2
3
4
5
6
7
bytes=$(docker image inspect application:candidate --format '{{ .Size }}')
max_bytes=1500000000

if [ "$bytes" -gt "$max_bytes" ]; then
  echo "Image exceeds the approved size threshold"
  exit 1
fi

BuildKit cache exports can also improve repeated CI builds when the runner environment supports them. The cache configuration should remain visible and reproducible rather than depending on an undocumented runner state.

Result

The production optimization reduced image size by 35% and improved Docker build efficiency by 75%. More important, the final build retained the dependencies and runtime behavior required by the application.

The sequence is repeatable:

  1. Measure layers and build time.
  2. Separate build and runtime dependencies.
  3. Preserve dependency caching.
  4. Remove package-manager waste.
  5. Limit the build context.
  6. Test the candidate under production-like conditions.
  7. Add CI checks that prevent regression.

Docker optimization works best as controlled engineering work. A maintainable 35% reduction is more valuable than an aggressive image that the team cannot trust.

View Resume