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 Image | Best For |
|---|
| SDK | Build environments only |
| ASP.NET Runtime | Web APIs and MVC applications |
| Runtime | Console applications |
| Chiseled/Distroless | Production 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:
| Feature | Benefit | Drawback |
|---|
| ReadyToRun | Faster startup | Larger binaries |
| No ReadyToRun | Smaller images | Slightly 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:
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:
| Metric | Why It Matters |
|---|
| Final Image Size | Storage efficiency |
| Build Duration | CI/CD performance |
| Image Pull Time | Deployment speed |
| Container Startup Time | Runtime responsiveness |
| Registry Storage | Infrastructure cost |
| Pod Startup Time | Kubernetes performance |
Collect measurements before and after each optimization to identify which changes provide the greatest benefit.
Common Production Mistakes
| Problem | Root Cause |
|---|
| 1 GB runtime image | SDK shipped to production |
| Slow Docker builds | Large build context |
| Large intermediate layers | Multiple unnecessary RUN commands |
| Frequent cache invalidation | COPY executed too early |
| Slow Kubernetes deployments | Oversized container images |
| Security vulnerabilities | Unnecessary 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.