Docker  

Docker Image Size Benchmark: 8 Techniques That Actually Work

Large Docker images slow down CI/CD pipelines, increase registry storage costs, delay deployments, and extend Kubernetes pod startup times. While Docker makes containerizing applications straightforward, many production images contain unnecessary SDKs, package caches, debugging tools, and unused dependencies that significantly increase image size.

Rather than blindly applying optimization tips, this article explains eight techniques that consistently reduce Docker image size, why they work, and how to measure their impact in your own environment.

Note: Image size improvements vary depending on your application, .NET version, Linux distribution, and installed dependencies. Always benchmark before and after each optimization.

Why Smaller Images Matter

A 1 GB image isn't just a storage problem.

Large images result in:

  • Slower CI/CD pipelines

  • Longer deployments

  • Increased bandwidth usage

  • Slower Kubernetes pod startup

  • Higher registry storage costs

  • Longer rollback times

In production environments with dozens of deployments per day, even a modest reduction in image size can improve operational efficiency.

Measuring Before Optimizing

Never optimize based on assumptions.

Useful commands include:

docker images

docker history your-image

docker image inspect your-image

These commands help identify which layers consume the most space.

Technique #1 – Use Multi-Stage Builds

One of the largest mistakes is shipping the .NET SDK with your application.

Poor Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:9.0

WORKDIR /app

COPY . .

RUN dotnet publish -c Release

ENTRYPOINT ["dotnet","MyApi.dll"]

The SDK includes compilers, build tools, NuGet packages, and other components that aren't required at runtime.

A multi-stage build separates compilation from execution.

FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

WORKDIR /src

COPY . .

RUN dotnet publish -c Release -o /publish

FROM mcr.microsoft.com/dotnet/aspnet:9.0

WORKDIR /app

COPY --from=build /publish .

ENTRYPOINT ["dotnet","MyApi.dll"]

This is often the single most effective optimization for .NET applications.

Technique #2 – Choose the Right Base Image

Not every application requires the same runtime image.

Typical options include:

Base ImageBest For
SDKBuild environments only
ASP.NET RuntimeWeb APIs and MVC applications
RuntimeConsole applications
Chiseled/DistrolessProduction containers with minimal OS

Smaller runtime images reduce attack surface while decreasing download time.

Always choose the smallest image that supports your application requirements.

Technique #3 – Reduce Build Context

Many repositories send unnecessary files into the Docker build context.

Typical offenders include:

  • bin/

  • obj/

  • .git/

  • node_modules/

  • Test results

  • IDE settings

Create a .dockerignore file.

bin/
obj/
.git/
.vscode/
node_modules/

Smaller build contexts reduce build time and prevent unnecessary cache invalidation.

Technique #4 – Combine RUN Instructions

Every RUN instruction creates a new image layer.

Instead of:

RUN apt update
RUN apt install curl
RUN rm -rf /var/lib/apt/lists/*

Use:

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

Combining related operations minimizes image layers and prevents temporary files from remaining in intermediate layers.

Technique #5 – Remove Package Cache

Package managers leave behind downloaded metadata.

Example:

RUN apt update && \
    apt install -y tzdata && \
    rm -rf /var/lib/apt/lists/*

Without cleanup, package indexes remain inside the image and increase its size unnecessarily.

The same principle applies to npm, pip, apk, and other package managers.

Technique #6 – Publish with Trimming

Modern .NET supports assembly trimming.

dotnet publish \
-c Release \
-p:PublishTrimmed=true

Trimming removes unused framework assemblies.

Benefits include:

  • Smaller deployment size

  • Faster image downloads

  • Reduced disk usage

However, applications using heavy reflection or dynamic loading should be tested carefully because trimming can remove assemblies required at runtime.

Technique #7 – Enable ReadyToRun Only When Appropriate

ReadyToRun (R2R) precompiles assemblies.

dotnet publish \
-p:PublishReadyToRun=true

Trade-offs:

FeatureBenefitDrawback
ReadyToRunFaster startupLarger binaries
No ReadyToRunSmaller imagesSlightly slower startup

For serverless applications or frequently restarted containers, startup improvements may outweigh the increased image size.

Benchmark before enabling it.

Technique #8 – Avoid Installing Unnecessary Tools

Production images often contain:

  • Git

  • Curl

  • Nano

  • Vim

  • SSH

  • Build utilities

These tools are convenient during debugging but unnecessary in production.

Instead of troubleshooting inside running containers, rely on:

  • Structured logging

  • Health checks

  • Metrics

  • OpenTelemetry

  • Kubernetes exec (only when necessary)

Minimal images improve both security and maintainability.

Image Layer Visualization

flowchart TD

A[Base Runtime Image]
B[Application Files]
C[Dependencies]
D[Configuration]
E[Final Image]

A --> B
B --> C
C --> D
D --> E

Every additional layer increases storage and download size. Keep layers purposeful and reusable.

Benchmarking Your Optimizations

Rather than publishing arbitrary numbers, measure these metrics:

MetricWhy It Matters
Final Image SizeStorage efficiency
Build DurationCI/CD performance
Image Pull TimeDeployment speed
Container Startup TimeRuntime responsiveness
Registry StorageInfrastructure cost
Pod Startup TimeKubernetes performance

Collect measurements before and after each optimization to identify which changes provide the greatest benefit.

Common Production Mistakes

ProblemRoot Cause
1 GB runtime imageSDK shipped to production
Slow Docker buildsLarge build context
Large intermediate layersMultiple unnecessary RUN commands
Frequent cache invalidationCOPY executed too early
Slow Kubernetes deploymentsOversized container images
Security vulnerabilitiesUnnecessary packages installed

Best Practices

  • Use multi-stage builds for every production image.

  • Select the smallest supported runtime image.

  • Keep .dockerignore up to date.

  • Remove package caches after installation.

  • Copy project files strategically to maximize Docker layer caching.

  • Review image layers using docker history.

  • Benchmark image size after every optimization.

  • Scan images regularly for vulnerabilities.

Common Anti-Patterns

Avoid these common mistakes:

  • Deploying SDK images to production.

  • Installing debugging tools permanently.

  • Copying the entire repository before restoring dependencies.

  • Ignoring unused files in the build context.

  • Optimizing solely for image size without considering startup performance.

  • Assuming smaller images are always faster—runtime characteristics also matter.

FAQ

Should every .NET application use multi-stage builds?

Yes. Separating build and runtime environments is considered a production best practice and typically results in smaller, more secure images.

Are Alpine images always the best choice?

Not necessarily. Alpine images are smaller, but some native libraries and third-party dependencies may not be fully compatible. Validate your workload before adopting them.

Does trimming always reduce image size?

Usually, but applications that rely heavily on reflection, plugins, or dynamic assembly loading require careful testing to avoid runtime issues.

Should I optimize image size before measuring performance?

No. Measure first. Some optimizations reduce image size while increasing startup time or complexity. Use evidence to guide your decisions.

Conclusion

Optimizing Docker image size is about more than reducing megabytes. Smaller images improve deployment speed, reduce infrastructure costs, shorten CI/CD pipelines, and decrease the attack surface of production containers.

The most effective optimizations—such as multi-stage builds, selecting the appropriate runtime image, reducing build context, cleaning package caches, and trimming published applications—are straightforward to implement and easy to validate with benchmarks. Rather than chasing arbitrary image-size targets, focus on measurable improvements that enhance both operational efficiency and application reliability in production.