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:

  1. Build stage: Uses the .NET SDK to restore dependencies and compile the application.

  2. Publish stage: Produces the application files required for deployment.

  3. 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:

Code
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.

SQL
# 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:

Code
docker build -t myapp:production .

Inspect the image size:

Code
docker image ls myapp
docker image inspect myapp:production

Run the container and map a host port to the application's listening port:

Code
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:

Code
.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:

Code
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

  1. Use multi-stage builds. Keep the SDK and compilation tools in earlier stages.

  2. Pin compatible image versions. Update base images deliberately and test changes.

  3. Optimize the build context. Exclude source-control data, build output, and unnecessary local files.

  4. Copy only published artifacts. Avoid copying the complete repository into the final stage.

  5. Run as a non-root user. Apply the least-privilege principle wherever practical.

  6. Scan and update images. Smaller images still require security updates and vulnerability assessment.

  7. Test the final image. Verify startup, health checks, configuration, and application behavior.

  8. Measure image size and build performance. Use actual build output to assess whether an optimization is useful.

  9. Keep secrets outside the image. Supply sensitive configuration through a secure runtime mechanism.

  10. 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.