Docker � �9 min read

Reducing Image Size in .NET Containers

Docker makes deploying .NET applications easier, but container images can become unnecessarily large if they include development tools, unused files and dependencies. In this article, we’ll explore practical ways to reduce the size of .NET container images using multi-stage builds, smaller runtime images, efficient publishing and Docker build optimisation, helping you create leaner, faster and more maintainable deployments.

Admin
Admin
.NET & IoT Developer
Reducing Image Size in .NET Containers

Why Container Image Size Matters

Docker containers provide a convenient way to package and deploy .NET applications. However, a container image can contain far more than an application needs to run in production.

A typical .NET project requires the .NET SDK to restore NuGet packages, compile the source code and publish the application. The SDK includes development tools and components that are useful during the build process but unnecessary when the application is running.

Including everything in the final image increases its size without necessarily providing any benefit.

Smaller images can reduce the time required to push images to a container registry, pull them onto a server and deploy updated versions. They can also reduce storage requirements and, when unnecessary components are removed, help reduce the potential attack surface.

The objective is not simply to make an image as small as possible. It is to produce an image that contains everything the application needs, without carrying unnecessary components into production.

Use Multi-Stage Docker Builds

One of the most effective ways to reduce the size of a .NET container image is to use a multi-stage Dockerfile.

A multi-stage build separates the environment used to compile an application from the environment used to run it. The first stage uses the .NET SDK, while the final stage uses the appropriate runtime image and receives only the published application files.

Consider the following example for an ASP.NET Core application.

# Build the application
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src

COPY *.csproj ./
RUN dotnet restore

COPY . ./

RUN dotnet publish -c Release \
    -o /app/publish \
    --no-restore

# Run the application
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

WORKDIR /app

COPY --from=build /app/publish ./

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

Replace MyApplication.dll with the name of your application's published assembly.

The first stage restores the NuGet dependencies and compiles the application. The publish command produces the files required for deployment.

The second stage starts with the ASP.NET Core runtime image and copies the published output from the build stage. It does not inherit the SDK, compiler or other build-stage files.

This separation can significantly reduce the final image size compared with using the SDK image for the entire application.

For an application that does not use ASP.NET Core, such as a console application or background worker, consider using the corresponding mcr.microsoft.com/dotnet/runtime image instead of the ASP.NET Core runtime image.

Choose the Right Base Image

The base image you select has a direct impact on the size of your finished container.

Microsoft provides several official .NET images for different purposes. The SDK images are intended for building applications, while runtime images are designed to execute them.

For a typical ASP.NET Core application, the following images serve different purposes.

mcr.microsoft.com/dotnet/sdk:10.0 provides the tools needed to build a .NET application.

mcr.microsoft.com/dotnet/aspnet:10.0 provides the ASP.NET Core runtime needed to host a web application.

mcr.microsoft.com/dotnet/runtime:10.0 provides the runtime for applications that do not require ASP.NET Core.

Using the appropriate runtime image avoids including the complete SDK in the final container.

You can also investigate smaller image variants, including Alpine Linux and Ubuntu Chiseled images. These can reduce image size further, although they have different compatibility characteristics and available system components.

For example, some minimal images omit ICU and time-zone data. Applications that depend on culture-specific formatting, internationalisation or particular time-zone operations may require additional components or configuration.

Before changing your base image, check the official .NET container image documentation and test the application thoroughly.

Publish Only What You Need

The dotnet publish command prepares an application for deployment. Its output is designed to contain the application files and dependencies required by the selected publishing configuration.

Using Release configuration is a sensible starting point for production containers.

dotnet publish -c Release -o ./publish

You can then copy the published files into your runtime image rather than copying the entire source project.

For framework-dependent deployments, the target container must provide the appropriate .NET runtime. Self-contained deployments include the .NET runtime with the application, which changes the publishing requirements and may increase the amount of application content that must be packaged.

Single-file publishing and assembly trimming are other options worth investigating. However, trimming can remove code that is accessed through reflection or other patterns that the trimmer cannot fully analyse.

For that reason, trimming should be enabled only after checking compatibility and testing the published application. It should not be treated as a guaranteed size reduction for every .NET project.

Optimise Your Docker Build Context

Reducing the final image size is only part of the optimisation process. The files sent to Docker during a build can also affect build speed and efficiency.

A .dockerignore file tells Docker which files and directories should be excluded from the build context.

For a typical .NET project, a starting point might look like this:

**/bin/
**/obj/
.git/
.vs/
.vscode/
TestResults/
*.user
*.suo

These exclusions prevent local build output, development settings and version-control data from being unnecessarily sent to the build process.

Be careful not to exclude files that your Dockerfile actually needs. For example, if the build restores dependencies from local NuGet configuration files, make sure those files remain available where required.

A clean build context can reduce the amount of data Docker needs to process and can help make builds more predictable.

Improve Docker Layer Caching

Docker builds images in layers, and the order of instructions affects how effectively those layers can be reused.

In a .NET application, dependency restoration generally does not need to run again every time a source file changes. By copying the project file before copying the rest of the source code, Docker can reuse the restore layer when the project dependencies remain unchanged.

For example:

WORKDIR /src

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release \
    -o /app/publish \
    --no-restore

This arrangement separates dependency restoration from changes to application source files.

When you update a controller or service without changing the project dependencies, Docker can often reuse the existing restore layer. This helps reduce build times during development and continuous integration.

For larger solutions, the Dockerfile may need to copy several project files and solution-level configuration files before restoring dependencies. The exact structure depends on the layout of your repository.

Layer caching primarily improves build efficiency rather than directly reducing the final image size, but it is an important part of maintaining an efficient container workflow.

Consider Distroless and Chiseled Images

For applications that need an even smaller production image, minimal runtime images are worth considering.

Ubuntu Chiseled images contain a reduced set of operating system components. They can offer a smaller image and a reduced attack surface compared with more general-purpose images.

Distroless images take a similar approach by excluding many tools that are unnecessary for running an application.

These images can be particularly useful for production services where the runtime requirements are well understood.

However, minimal images have trade-offs. They may not include a shell, package manager or familiar diagnostic utilities. Troubleshooting a running container can therefore require different techniques.

You should also check whether the selected image supports your application's native dependencies, certificate requirements, internationalisation needs and other operating system requirements.

A minimal image is a good choice when it meets your application's needs, rather than simply because it has the smallest published size.

Measure the Results

It is important to measure image sizes before and after making changes. Otherwise, it is difficult to know which optimisations have provided the greatest benefit.

Build your Docker image using a command such as:

docker build -t myapplication:optimised .

You can then inspect the resulting image size with:

docker images myapplication

To understand where the image size comes from, inspect the image's layer history:

docker history myapplication:optimised

Compare the original and optimised images, ensuring that you are comparing the same application version and equivalent runtime requirements.

Remember that the displayed image size is not necessarily identical to the amount of storage used by a registry or the amount of data transferred when pulling an image. Shared layers, compression and caching can affect those figures.

The important measurement is whether your changes have reduced the actual deployment overhead for your particular environment.

Avoid Unnecessary Optimisation

Although reducing image size is beneficial, it is possible to spend too much time pursuing marginal improvements.

For most .NET applications, start with a multi-stage Dockerfile, an appropriate runtime image and a sensible .dockerignore file. These changes are usually straightforward and provide a strong foundation for further optimisation.

Only investigate more advanced techniques, such as trimming, native AOT compilation or highly specialised minimal images, when there is a clear benefit for your application.

Each additional optimisation should be tested for compatibility, reliability and maintainability. A slightly larger image that is easy to diagnose and operate may be a better choice than a smaller image that introduces deployment problems.

Conclusion

Reducing image size in .NET containers starts with separating the build environment from the production runtime. Multi-stage Docker builds, appropriate base images, efficient publishing and a clean build context can all help eliminate unnecessary content.

Once these fundamentals are in place, you can investigate smaller runtime variants, publishing options and improved caching to achieve further gains.

The best approach is to measure your existing image, make changes incrementally and test the resulting container. This allows you to build leaner .NET deployments without sacrificing application functionality or maintainability.

Share:

Become a member

Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!

Read next

Deploying a .NET App with Docker Compose

Docker makes it easy to package a .NET application and its dependencies into a consistent, portable environment. Docker Compose takes this a step further by allowing you to define and run your application and supporting services from a single configuration file. In this article, we’ll look at how to deploy a .NET application using Docker Compose.

Admin 06-Oct-2026

The Bedroom Coder — retro computers, modern .NET, and late-night experiments.

Navigation

Contact

Want to talk retro tech or modern coding? I'd love to hear your thoughts.

© 2026 The Bedroom Coder. All rights reserved.