Docker images often contain more than an application needs to run. A typical development image may include the .NET SDK, compilers, package managers, source files, debugging utilities, and other tools used only during the build process.
Shipping all these components to production increases image size, expands the number of installed packages, and creates additional maintenance work. It can also introduce unnecessary security exposure when tools that are useful during development are available inside a running application container.
Docker multi-stage builds provide a practical solution. They allow developers to compile and publish an application in one stage, then copy only the required runtime artifacts into a separate final image.
This article explains how to create smaller Docker images for ASP.NET Core applications, how to choose appropriate base images, how to verify what is included in the final image, and which mistakes to avoid when optimizing production containers.
Why Build Tools Should Stay Out of Production Images
The .NET SDK contains the tools required to compile applications, restore packages, and publish assemblies. A production ASP.NET Core application generally needs the .NET runtime and its published application files rather than the full SDK.
Keeping build tools in the production image creates several disadvantages:
The image includes components that are not required to serve requests.
Larger images can take longer to transfer to registries and deployment environments.
More installed software can increase the maintenance and vulnerability-management burden.
Development files may be accidentally included in the final image.
The production container has access to tools that are unnecessary for its intended role.
A smaller image does not automatically make an application faster or secure. Application performance still depends on code, runtime configuration, resource limits, and infrastructure. However, excluding unnecessary build components is a useful part of production container design.
Understand Docker Multi-Stage Builds
A multi-stage Dockerfile uses multiple FROM instructions. Each stage can use a different base image and perform a different part of the build process.
For a .NET application, the common stages are:
Build stage: Uses the .NET SDK to restore dependencies and compile the application.
Publish stage: Produces the application files required for deployment.
Runtime stage: Uses a smaller runtime image and copies the published output into it.
The final stage determines what is included in the resulting image. Files from earlier stages are not automatically included unless they are copied or otherwise added to the final stage.
This separation lets the application use a complete development toolchain during compilation without carrying that toolchain into production.
Create a Multi-Stage Dockerfile for ASP.NET Core
Assume a solution contains the following files:
MyApp/
├── MyApp.sln
├── MyApp/
│ ├── MyApp.csproj
│ ├── Program.cs
│ └── appsettings.json
└── Dockerfile
The following Dockerfile demonstrates a multi-stage build for a framework-dependent ASP.NET Core application.
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY MyApp/MyApp.csproj MyApp/
RUN dotnet restore MyApp/MyApp.csproj
COPY MyApp/ MyApp/
WORKDIR /src/MyApp
RUN dotnet publish MyApp.csproj \
-c Release \
-o /app/publish \
--no-restore
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
The example uses the .NET 10 SDK and ASP.NET Core runtime image tags. Choose tags that match the target framework and supported deployment environment.
Why the build stage uses the SDK
The SDK image provides the compiler, NuGet tooling, and other components needed to build the application.
The first COPY instruction copies the project file separately before restoring dependencies. This can improve build-cache reuse because source-code changes do not necessarily invalidate the dependency-restore layer.
The second COPY instruction adds the source files. The dotnet publish command compiles the application and produces deployment artifacts in /app/publish.
Why the final stage uses the runtime image
The final stage uses mcr.microsoft.com/dotnet/aspnet, which contains the ASP.NET Core runtime rather than the full SDK.
The command COPY --from=build transfers the published application files from the build stage into the final image. It does not copy the entire build environment.
The final image therefore contains the application output and the runtime components needed to execute it, rather than the complete SDK and source tree.
The ASPNETCORE_HTTP_PORTS setting configures the application's HTTP listening port for supported ASP.NET Core versions. EXPOSE documents the intended container port but does not publish it to the host automatically.
Build and Run the Image
Build the image from the directory containing the Dockerfile:
docker build -t myapp:production .
Inspect the image size:
docker image ls myapp
docker image inspect myapp:production
Run the container and map a host port to the application's listening port:
docker run --rm -p 8080:8080 myapp:production
The application should be reachable through the mapped host port if its endpoint configuration and network environment are correct.
To compare the final image with an SDK-based image, inspect both images rather than relying on an assumed size reduction. Actual image sizes depend on the operating system variant, architecture, framework version, application dependencies, and image layers.
Add a .dockerignore File
A multi-stage build does not help as much as it should if the Docker build context contains unnecessary files.
Create a .dockerignore file beside the Dockerfile:
.git
.gitignore
.vs
.vscode
**/bin
**/obj
TestResults
*.user
*.suo
.env
Docker excludes matching files from the build context, reducing the amount of data sent to the builder and preventing common development artifacts from being copied accidentally.
Be careful when excluding configuration files. If the application genuinely needs a particular file during the build, do not exclude it without adjusting the build process.
Likewise, never copy secrets into an image. Build arguments and image environment variables are not appropriate substitutes for a proper secret-management mechanism when credentials must remain confidential.
Choose the Right Runtime Image
The aspnet image is appropriate for most framework-dependent ASP.NET Core applications. Other applications may use a different runtime image depending on their requirements.
Image type | Typical purpose | Main consideration |
|---|
.NET SDK | Restoring, building, and publishing | Includes development tools that are usually unnecessary at runtime |
ASP.NET Core runtime | Running ASP.NET Core applications | Includes the framework runtime needed by web applications |
.NET runtime | Running applications that do not require the ASP.NET Core shared framework | Not sufficient for applications that require ASP.NET Core runtime components |
Chiseled or minimal runtime image | Reducing the operating-system footprint | Requires attention to globalization, native dependencies, and diagnostic requirements |
Distroless-style runtime image | Running an application with a minimal user-space environment | Interactive debugging and package installation may be more limited |
Choose the smallest supported image that satisfies the application's actual requirements. A minimal image is not a good choice if the application depends on native libraries or operating-system features that the image does not provide.
Framework-Dependent vs. Self-Contained Publishing
A framework-dependent deployment uses a compatible .NET runtime installed in the final image.
A self-contained deployment includes the .NET runtime components needed by the published application. This can change the required base image and increase the amount of application output copied into the final stage.
For example, a self-contained Linux deployment can be published with a command such as:
dotnet publish MyApp.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o /app/publish
The target runtime identifier must match the deployment environment. The final image must still provide the required operating-system libraries and native dependencies.
Do not combine a self-contained publishing strategy with an incompatible runtime image without verifying the deployment requirements.
Common Mistakes
Copying the entire source directory into the final image
Copying the repository into the final stage can reintroduce source files, development artifacts, and potentially sensitive configuration.
Copy only the published output and other explicitly required runtime files.
Using mismatched SDK and runtime versions
The SDK, target framework, runtime image, and deployment architecture must be compatible. A successful build does not guarantee that the final image contains everything required to start the application.
Test the final image, not just the build stage.
Choosing an image that lacks required libraries
Minimal images may omit native dependencies, diagnostic tools, or globalization components that an application expects.
Investigate startup errors and missing-library messages before deciding that a smaller image is suitable.
Running as root unnecessarily
A small image can still have excessive privileges. Configure an appropriate non-root user where the image and application support it, and ensure the application can access required files and directories.
Ignoring secrets in the build context
A .dockerignore file helps prevent accidental inclusion of files such as local environment settings. It should complement, not replace, secure secret handling.
Best Practices for Production Images
Use multi-stage builds. Keep the SDK and compilation tools in earlier stages.
Pin compatible image versions. Update base images deliberately and test changes.
Optimize the build context. Exclude source-control data, build output, and unnecessary local files.
Copy only published artifacts. Avoid copying the complete repository into the final stage.
Run as a non-root user. Apply the least-privilege principle wherever practical.
Scan and update images. Smaller images still require security updates and vulnerability assessment.
Test the final image. Verify startup, health checks, configuration, and application behavior.
Measure image size and build performance. Use actual build output to assess whether an optimization is useful.
Keep secrets outside the image. Supply sensitive configuration through a secure runtime mechanism.
Use reproducible builds. Keep dependency and base-image choices controlled so that rebuilds are easier to investigate.
Advantages and Disadvantages
Advantages | Disadvantages |
|---|
Excludes the SDK and build tools from the final image. | Requires a correctly structured multi-stage build. |
Can reduce image size and transfer overhead. | Actual size savings depend on the application and base image. |
Reduces unnecessary software in the production environment. | Minimal images can make interactive debugging more difficult. |
Separates compilation from runtime execution. | Runtime dependencies must be identified explicitly. |
Supports cleaner CI/CD pipelines. | Incorrect stage configuration can produce an image that fails at startup. |
Summary
Building smaller Docker images is primarily about separating build-time requirements from runtime requirements. For ASP.NET Core applications, a multi-stage Dockerfile allows the SDK to restore, compile, and publish the application while the final image contains only the required runtime and published artifacts.
Use a .dockerignore file, choose a compatible runtime image, avoid copying unnecessary files, and keep secrets outside the image. Validate the final container under realistic conditions rather than assuming that a successful build proves the image is ready for production.
These practices reduce unnecessary components in the deployment artifact and make container images easier to maintain, distribute, and operate.
Join the conversation! Your thoughts help the community grow.