Table of Contents
In this tutorial, we will use multi-stage Docker builds to illustrate how we can build an application in the first stage and in next stage, use the output from first stage to create an application image. A multi-stage build is done by creating different sections of a Dockerfile, each referencing a different base image. This allows a multi-stage build to fulfill a function previously filled by using multiple docker files, copying files between containers, or running different pipelines.
Refer to section Create new console application in Create your first .Net Core console application on Ubuntu to understand basics about how to create .Net Core console application. We will re-use the application created here.
Multi-stage docker file
In the application build stage, we will use mcr.microsoft.com/dotnet/core/sdk:2.1
as base image. We will copy source code inside the container and publish a self-contained application. We will use linux-musl-x64
as runtime configuration since we are using Apline as base image as mentioned below. Output for this stage will be available in /source/bin/Release/netcoreapp2.1/linux-musl-x64/publish/
.
Next stage we will create application image which will consume the output from build stage. We will use amd64/alpine:3.12
as base image this time. It is a very small image with size 5.57 MB. However, to be able to sucessfully run .Net Core applications, we will need to install few packages. The list of packages is mentioned in https://github.com/dotnet/dotnet-docker/blob/master/src/runtime-deps/2.1/alpine3.12/amd64/Dockerfile.
We will also add two environment variables DOTNET_RUNNING_IN_CONTAINER
and DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
and set them to true
as suggested by Microsoft.
DOTNET_RUNNING_IN_CONTAINER
variable is used to detect whether the .Net Core application is running inside the docker.
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
is used to enable Globalization Invariant Mode. In earlier tutorial we had used by adding following line under <PropertyGroup>
tag in hellodocker.csproj:
<InvariantGlobalization>true</InvariantGlobalization>
Hence, we can remove that line from hellodocker.csproj this time.
Final Dockerfile contents:
Dockerfile
# Create application buildFROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-envWORKDIR /sourceCOPY *.csproj ./RUN dotnet restoreCOPY . ./RUN dotnet publish --configuration Release --self-contained true --runtime linux-musl-x64# Create application imageFROM amd64/alpine:3.12RUN apk add --no-cache \ca-certificates \# .NET Core dependencieskrb5-libs libgcc libintl libssl1.1 zlib \libstdc++ lttng-ust tzdata userspace-rcu# Enable detection of running in a containerENV DOTNET_RUNNING_IN_CONTAINER=true# Set the invariant mode since icu_libs isn't included (see https://github.com/dotnet/announcements/issues/20)ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=trueWORKDIR /appCOPY --from=build-env /source/bin/Release/netcoreapp2.1/linux-musl-x64/publish/ .ENTRYPOINT ["./hellodocker"]
To increase the build's performance, exclude files and directories, create a .dockerignore
file in the same directory as Dockerfile
with following contents:
.dockerignore
Dockerfile[b|B]in/[O|o]bj/
Now that our Dockerfile
is ready along with .dockerignore
file, we can run docker build
command like below:
docker build -t hellodocker:3.0 .
We have tagged the image with 3.0
, so that we can differntiate it with earlier docker images created earlier for hellodocker application. We will use the earlier images to do a size comparison later.
Sample output from docker build
command:
Sending build context to Docker daemon 5.632kBStep 1/13 : FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS build-env---> 156e5cc5d7a3Step 2/13 : WORKDIR /source---> Using cache---> c3afbc8b54b3Step 3/13 : COPY *.csproj ./---> Using cache---> 7066245f5486Step 4/13 : RUN dotnet restore---> Using cache---> 1bd61a20491aStep 5/13 : COPY . ./---> Using cache---> f7be8f770396Step 6/13 : RUN dotnet publish --configuration Release --self-contained true --runtime linux-musl-x64---> Using cache---> ed14de370beeStep 7/13 : FROM amd64/alpine:3.12---> a24bb4013296Step 8/13 : RUN apk add --no-cache ca-certificates krb5-libs libgcc libintl libssl1.1 zlib libstdc++ lttng-ust tzdata userspace-rcu---> Running in 34c216b91c78fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gzfetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz(1/12) Installing ca-certificates (20191127-r4)(2/12) Installing krb5-conf (1.0-r2)(3/12) Installing libcom_err (1.45.6-r0)(4/12) Installing keyutils-libs (1.6.1-r1)(5/12) Installing libverto (0.3.1-r1)(6/12) Installing krb5-libs (1.18.2-r0)(7/12) Installing libgcc (9.3.0-r2)(8/12) Installing libintl (0.20.2-r0)(9/12) Installing libstdc++ (9.3.0-r2)(10/12) Installing userspace-rcu (0.12.1-r0)(11/12) Installing lttng-ust (2.12.0-r1)(12/12) Installing tzdata (2020a-r0)Executing busybox-1.31.1-r16.triggerExecuting ca-certificates-20191127-r4.triggerOK: 14 MiB in 26 packagesRemoving intermediate container 34c216b91c78---> 343db9e44ba9Step 9/13 : ENV DOTNET_RUNNING_IN_CONTAINER=true---> Running in 76d1e455c34fRemoving intermediate container 76d1e455c34f---> 98de9a9abf35Step 10/13 : ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true---> Running in 93927d2a8f4dRemoving intermediate container 93927d2a8f4d---> d0682cb9935aStep 11/13 : WORKDIR /app---> Running in 85d1cb292ed3Removing intermediate container 85d1cb292ed3---> 012ed99836ffStep 12/13 : COPY --from=build-env /source/bin/Release/netcoreapp2.1/linux-musl-x64/publish/ .---> 0f55de06ca79Step 13/13 : ENTRYPOINT ["./hellodocker"]---> Running in 0d16235fb56dRemoving intermediate container 0d16235fb56d---> bb6dd22b46f3Successfully built bb6dd22b46f3Successfully tagged hellodocker:3.0
We can verify that the hellodocker
application still runs without any issue with below command:
$ docker run --name hellodocker3 hellodocker:3.0
Sample output:
Hello Docker!
Docker size comparison
Now let us try to do a size comparison for docker images created earlier with the latest image:
$ docker images
Sample output:
REPOSITORY TAG IMAGE ID CREATED SIZEhellodocker 3.0 67bdd4ac2207 About a minute ago 86.4MBhellodocker 2.0 9ff4eeed6631 3 days ago 138MBhellodocker 1.0 0a19597e8d5e 4 days ago 180MB
hellodocker:1.0
was created with mcr.microsoft.com/dotnet/core/runtime:2.1
as base image. This base image itself has size of around 180 MB. As our application was not published as self-contained, it required .Net runtime installed to run. Appllication size was only 24 KB.
hellodocker:2.0
was created with ubuntu:18.04
as base image. This base image still has size of around 64 MB. Our application was published as self-contained, it did not require .Net runtime installed to run. However, application size had grown to 71.2 MB. Hence, the resultant size of image was 138 MB.
hellodocker:3.0
we have created just now uses alpine:3.12
as base image. This base image is just 5.57 MB in size. But in order to run .Net Core applications, we have to install few packages as mentioned in the Dockerfile
. Here the size of docker image is just 86.4 MB. That is a significant reduction in size in comparison to 180 MB.
$ docker history hellodocker:3.0
Sample output:
IMAGE CREATED CREATED BY SIZE COMMENT67bdd4ac2207 3 minutes ago /bin/sh -c #(nop) ENTRYPOINT ["./hellodocke... 0B2bcdb10bc1b0 3 minutes ago /bin/sh -c #(nop) COPY dir:ab880fd822eea1843... 74.2MB437dce78118a 3 minutes ago /bin/sh -c #(nop) WORKDIR /app 0B4338bc2a6eec 3 minutes ago /bin/sh -c #(nop) ENV DOTNET_SYSTEM_GLOBALI... 0B0cd34d5a50d8 3 minutes ago /bin/sh -c #(nop) ENV DOTNET_RUNNING_IN_CON... 0Ba55da04ad332 3 minutes ago /bin/sh -c apk add --no-cache ca-certifi... 6.66MBa24bb4013296 3 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B<missing> 3 weeks ago /bin/sh -c #(nop) ADD file:c92c248239f8c7b9b... 5.57MB
In .Net Core 3.0 onward, there is an additional option p:PublishTrimmed
to further reduce the size of self-contained executable. We will cover that option as well in another tutorial. You can refer to link given in References section below to take a look at it.
References
- https://github.com/dotnet/core/blob/master/Documentation/linux-prereqs.md
- https://github.com/dotnet/dotnet-docker/blob/master/src/runtime-deps/2.1/alpine3.12/amd64/Dockerfile