Introduction
This post compares different strategies to dockerize a .NET 6 application and how to create a < 100mb docker image to host a .NET 6 asp.net web application. Using the docker multi stage builds feature and a self-contained .NET with some build options the final docker image can be reduced from 760mb to 83mb, almost 10x smaller!
All the images used were taken from the official Microsoft .NET docker repository.
1. .NET 6 SDK image
Let’s start with a simple Dockerfile based on the official .NET 6 SDK build image.
FROM mcr.microsoft.com/dotnet/sdk:6.0
WORKDIR /app
COPY . ./
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish
ENTRYPOINT ["dotnet", "/app/publish/WebApi.dll"]
$ docker build -t api-aspnet .
$ docker images api-aspnet
REPOSITORY TAG IMAGE ID CREATED SIZE
api-aspnet latest 700870327c08 1 weeks ago 761MB
We are using a docker single stage build using the base .net sdk image. After building the image we can see that the final image size is 761mb. Let’s try to check where the space is being used. For that we can use the docker history to see the layers created for this image.
$ docker history api-aspnet
IMAGE CREATED CREATED BY SIZE COMMENT
700870327c08 1 weeks ago ENTRYPOINT ["dotnet" "/app/publish/WebApi.dl… 0B buildkit.dockerfile.v0
<missing> 1 weeks ago RUN /bin/sh -c dotnet publish "WebApi.csproj… 4.18MB buildkit.dockerfile.v0
<missing> 1 weeks ago RUN /bin/sh -c dotnet build -c Release # bui… 12.2MB buildkit.dockerfile.v0
<missing> 1 weeks ago COPY . ./ # buildkit 721kB buildkit.dockerfile.v0
<missing> 1 weeks ago RUN /bin/sh -c dotnet restore # buildkit 28.2MB buildkit.dockerfile.v0
<missing> 1 weeks ago COPY WebApi.csproj ./ # buildkit 327B buildkit.dockerfile.v0
<missing> 1 weeks ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 2 months ago /bin/sh -c powershell_version=7.2.1 && c… 40.6MB
<missing> 2 months ago /bin/sh -c curl -fSL --output dotnet.tar.gz … 392MB
<missing> 2 months ago /bin/sh -c apt-get update && apt-get ins… 74.8MB
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS= DOTN… 0B
<missing> 2 months ago /bin/sh -c #(nop) COPY dir:a54b266469a09b122… 20.3MB
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNET_VERSION=6.0.1 … 0B
<missing> 2 months ago /bin/sh -c ln -s /usr/share/dotnet/dotnet /u… 24B
<missing> 2 months ago /bin/sh -c #(nop) COPY dir:6c537cc098876a5f6… 70.6MB
<missing> 2 months ago /bin/sh -c #(nop) ENV DOTNET_VERSION=6.0.1 0B
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 2 months ago /bin/sh -c apt-get update && apt-get ins… 37MB
<missing> 2 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 months ago /bin/sh -c #(nop) ADD file:09675d11695f65c55… 80.4MB
base image | 20% |
.NET SDK + runtime | 60% |
Linux libs | 15% |
Web App source + compiled code | 25% |
The .NET SDK + runtime takes most of the space, around 60%. So let’s try to shrink it.
2. .NET 6 SDK image + docker multi stage build
The previous section showed us that the .NET SDK + runtime takes most of the space of the final image. This is because we are using the same docker base image to build and to run the app so the final image has all the things needed to compile and run the app. Using a docker multi stage build we can cut the .NET SDK from the final image and use it only to build our app and include only the .NET runtime in the final image to run the application.
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /app
COPY . ./
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "WebApi.dll"]
Here we define 2 images: the build
image used to compile the app which includes all the things needed to build our .NET 6 application and the final one, the runtime
which only includes the .NET runtime to run our app. The first one to use for development (which contained everything needed to build your application), and a slimmed-down one to use for production, which only contained your compiled code and what is needed to run it. With multi-stage builds, you use multiple FROM
statements in your Dockerfile. Each FROM
instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image.
$ docker build -t api-aspnet-multistage .
$ docker images api-aspnet-multistage
REPOSITORY TAG IMAGE ID CREATED SIZE
api-aspnet-multistage latest 1021a94feca4 1 weeks ago 212MB
Bang, we reduced our image to 212mb, this is because the complete SDK was removed from the final image.
Let’s see again the layers and the space being used for each one:
USER@DESKTOP-BJUBV5T MINGW64 ~/Documents/sonae/identitymanager-credenciaiscontinente (fix/invalid-account-association-check)
$ docker history api-aspnet-multistage
IMAGE CREATED CREATED BY SIZE COMMENT
1021a94feca4 1 weeks ago ENTRYPOINT ["dotnet" "WebApi.dll"] 0B buildkit.dockerfile.v0
<missing> 1 weeks ago COPY /app/publish . # buildkit 4.18MB buildkit.dockerfile.v0
<missing> 1 weeks ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 2 months ago /bin/sh -c #(nop) COPY dir:a54b266469a09b122… 20.3MB
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNET_VERSION=6.0.1 … 0B
<missing> 2 months ago /bin/sh -c ln -s /usr/share/dotnet/dotnet /u… 24B
<missing> 2 months ago /bin/sh -c #(nop) COPY dir:6c537cc098876a5f6… 70.6MB
<missing> 2 months ago /bin/sh -c #(nop) ENV DOTNET_VERSION=6.0.1 0B
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 2 months ago /bin/sh -c apt-get update && apt-get ins… 37MB
<missing> 2 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 months ago /bin/sh -c #(nop) ADD file:09675d11695f65c55… 80.4MB
base image | 50% |
.NET runtime | 40% |
Web App source + compiled code | 10% |
Let´s try to reduce the bigger part, the base image.
3. Multi stage build with Alpine linux Microsoft official image
We can change our base image to use the Alpine Linux instead of the default one Debian bullsyeye which is the base image of the most of the official Microsoft .NET docker images.
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /app
COPY . ./
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "WebApi.dll"]
docker build -t api-multi-alpine .
docker images api-multi-alpine
REPOSITORY TAG IMAGE ID CREATED SIZE
api-multi-alpine latest 0c7f1c3b0bfa 1 weeks ago 104MB
$ docker history api-multi-alpine
IMAGE CREATED CREATED BY SIZE COMMENT
0c7f1c3b0bfa 1 weeks ago ENTRYPOINT ["dotnet" "WebApi.dll"] 0B buildkit.dockerfile.v0
<missing> 1 weeks ago COPY /app/publish . # buildkit 4.12MB buildkit.dockerfile.v0
<missing> 1 weeks ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 2 months ago /bin/sh -c wget -O aspnetcore.tar.gz https:/… 20.3MB
<missing> 2 months ago /bin/sh -c #(nop) ENV ASPNET_VERSION=6.0.1 … 0B
<missing> 2 months ago /bin/sh -c wget -O dotnet.tar.gz https://dot… 69.8MB
<missing> 2 months ago /bin/sh -c #(nop) ENV DOTNET_VERSION=6.0.1 0B
<missing> 3 months ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 3 months ago /bin/sh -c apk add --no-cache ca-cer… 4.35MB
<missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:762c899ec0505d1a3… 5.61MB
base image | 10% |
.NET runtime | 80% |
Web App source + compiled code | 10% |
Using the alpine instead of the debian base image we were able to reduce our base image size to 10% of our final image.
Now the .NET runtime is the part that is using most of the space of our image so let’s try to reduce this part.
4. Raw alpine with a trim-self-contained and trimmed .NET build
Instead of using the official Microsoft Alpine image which comes with the full .NET runtime we can use the vanilla Alpine base image and build our App using the .NET self-contained build PublishSingleFile
option.
Publishing our app as self-contained produces an application that includes the .NET runtime and libraries in a single binary, so we don’t have to use the .NET runtime installed on our base system.
We can also use the PublishTrimmed
option while building and publishing our app. With this options the final binary includes only a subset of the .NET framework assemblies, the assemblies that are referenced by our app. The other ones are removed from the final procuded binary file. However, there is a risk that the build-time analysis of the application can cause failures at run time, due to not being able to reliably analyze various problematic code patterns (largely centered on reflection use). To mitigate these problems, warnings are produced whenever the trimmer cannot fully analyze a code pattern. For information on what the trim warnings mean and how to resolve them, see Introduction to trim warnings.
Also, with a self-contained build we also need to specify the target platform which the final binary will be compiled. In this case we are using the alpine-x64 because our base image is the Alpine Linux.
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build
WORKDIR /app
COPY . ./
RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:TrimMode=Link \
/p:PublishSingleFile=true
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./WebApi"]
docker build -t api-multi-alpine-raw .
docker images api-multi-alpine-raw
$ docker history api-multi-alpine-raw
IMAGE CREATED CREATED BY SIZE COMMENT
ac78719fc09a 1 weeks ago ENTRYPOINT ["./WebApi"] 0B buildkit.dockerfile.v0
<missing> 1 weeks ago COPY /app/publish . # buildkit 42.2MB buildkit.dockerfile.v0
<missing> 1 weeks ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 1 weeks ago RUN /bin/sh -c apk add --no-cache libstdc++ … 35.5MB buildkit.dockerfile.v0
<missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:8f5bc5ce64ef781ad… 5.59MB
The final image was reduced to 83mb because it only includes the Alpine base image and a single binary that includes our WebApp compiled code and the assemblies that are referenced by our app!
References
https://docs.docker.com/develop/develop-images/multistage-build/
https://docs.microsoft.com/en-us/dotnet/core/deploying/
https://docs.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained