1) Scaffold the Project

To start deploying on Docker we need a sample project. I will use the default ASP.NET Core API project. You can create the project by the dotnet new webapi -o MyWebApp command in the terminal.

The command will create the new ASP.NET Core Web API project

2) Publish the project as a framework- dependent app

You can publish the application as a self-contained application or framework-dependent. At this part of the article, we will publish the project as framework-dependent.

To publish, enter the command below in the terminal:

dotnet publish -c Release -o ./publish

It’s important to make sure you are publishing your app with the release profile. The -c Release the option will tell the .NET publisher which profile it should use.

2.1) Create a docker image with ASP.NET Core Runtime

Create a Dockerfile with contents below:

Build the image:

docker build -t web1 -f Dockerfile .

Docker will build the image and will produce output as below:

Sending build context to Docker daemon 1.807MB

Step 1/6 : FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime

3.1: Pulling from dotnet/core/aspnet

8ec398bc0356: Pull complete 9584d2ef7ebe: Pull complete 62b61706cd9b: Pull complete 8f13df7c0cb1: Pull complete 9c72d70b702b: Pull complete Digest: sha256:9f0aebb2e83a9f455e4ac123db10bc263e729e1faaf733709db04d0d6df2b77c

Status: Downloaded newer image for mcr.microsoft.com/dotnet/core/aspnet:3.1

---> a843e0fbe833

Step 2/6 : EXPOSE 80

---> Running in 870640b52084

Removing intermediate container 870640b52084

---> f9a31931a780

Step 3/6 : EXPOSE 443

---> Running in 7e477e68bd5a

Removing intermediate container 7e477e68bd5a

---> 7522ce7349fa

Step 4/6 : WORKDIR /app

---> Running in 7c4d3e1490a3

Removing intermediate container 7c4d3e1490a3

---> 784d7ff4e4bd

Step 5/6 : COPY ./publish ./

---> 5a7b9b9163c0

Step 6/6 : ENTRYPOINT ["dotnet", "MyWebApp.dll"]

---> Running in ce7cdd5dd2c4

Removing intermediate container ce7cdd5dd2c4

---> bc92fbe6e1b2

Successfully built bc92fbe6e1b2

Successfully tagged web1:latest

📔 To make sure the web app is working fine, try to run the image by the command docker run -p 8080:80 web1 . Then navigate to http://localhost:8080/Weather to see a list of JSON.

Sample output

It seems the image is running fine, let’s measure the size of the image. With docker images command, you can get the list of the images.

207 MB for ASP.NET Core App Base Image and 1 MB for the Web App. For a simple web application 208 MB is big, isn’t it? 😲

Let’s inspect the image, to see what made our image size 208 MB:

docker history web1:latest # result CREATED BY SIZE

/bin/sh -c #(nop) ENTRYPOINT ["dotnet" "MyW… 0B

/bin/sh -c #(nop) COPY dir:36b502377fe8f29be… 289kB

/bin/sh -c #(nop) WORKDIR /app 0B

/bin/sh -c #(nop) EXPOSE 443 0B

/bin/sh -c #(nop) EXPOSE 80 0B

/bin/sh -c aspnetcore_version=3.1.0 && c… 17.8MB

/bin/sh -c dotnet_version=3.1.0 && curl … 76.7MB

/bin/sh -c apt-get update && apt-get ins… 2.28MB

/bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B

/bin/sh -c apt-get update && apt-get ins… 41.3MB

/bin/sh -c #(nop) CMD ["bash"] 0B

/bin/sh -c #(nop) ADD file:04caaf303199c81ff… 69.2MB

94 MB for .NET and 112MB for the base image and libs.

3) Publish the project as self-contained

In section 2, we published the project as framework-dependent and we saw the image size is about 208 MB.

With the self-contained deployment, you deploy your app and any required third-party dependencies along with the version of .NET Core that you used to build the app. To publish as self-contained, run the command below:

dotnet publish --runtime alpine-x64 -c Release --self-contained true -o ./publish

The app is going to run on top of Linux docker, so I picked linux-x64 runtime for deployment. To see other runtimes supported by the publish command read this document.

324 Files and 93.2 MB

It will generate 324 files (93.2 MB) containing assemblies, etc. For running it on docker, we need a Linux base image. There are different base images such as Ubuntu, CentOS, OpenSuse, Alpine, etc.

Size, Security, Efficiency, Community are the important values to decide which one you need.

As you can see in the image above, Alpine Linux is the lightest docker base image. It provides security, simplicity and it is resource-efficient!

Alpine Linux is an independent, non-commercial, general purpose Linux distribution designed for power users who appreciate security, simplicity and resource efficiency.

https://alpinelinux.org/about/

3.1) Create a docker image with Alpine base image

Create a Docker file with contents below:

I used alpine:3.9.4 as the base image for docker, installed some library packages in the Docker file. Now if you build the image:

docker build -t web2 -f Dockerfile .

Docker will build the image and will produce output below:



Step 1/8 : FROM alpine:3.9.4

3.9.4: Pulling from library/alpine

e7c96db7181b: Pull complete Digest: sha256:7746df395af22f04212cd25a92c1d6dbc5a06a0ca9579a229ef43008d4d1302a

Status: Downloaded newer image for alpine:3.9.4

---> 055936d39205

Step 2/8 : RUN apk add --no-cache openssh libunwind nghttp2-libs libidn krb5-libs libuuid lttng-ust zlib libstdc++ libintl icu

---> Running in 9471cd447603

fetch

fetch

(1/26) Installing libgcc (8.3.0-r0)

(2/26) Installing libstdc++ (8.3.0-r0)

(3/26) Installing icu-libs (62.1-r0)

(4/26) Installing icu (62.1-r0)

(5/26) Installing krb5-conf (1.0-r1)

(6/26) Installing libcom_err (1.44.5-r1)

(7/26) Installing keyutils-libs (1.6-r0)

(8/26) Installing libverto (0.3.0-r1)

(9/26) Installing krb5-libs (1.15.5-r0)

(10/26) Installing libidn (1.35-r0)

(11/26) Installing libintl (0.19.8.1-r4)

(12/26) Installing libunwind (1.2.1-r3)

(13/26) Installing libuuid (2.33-r0)

(14/26) Installing userspace-rcu (0.10.1-r0)

(15/26) Installing lttng-ust (2.10.1-r0)

(16/26) Installing nghttp2-libs (1.35.1-r1)

(17/26) Installing openssh-keygen (7.9_p1-r6)

(18/26) Installing ncurses-terminfo-base (6.1_p20190105-r0)

(19/26) Installing ncurses-terminfo (6.1_p20190105-r0)

(20/26) Installing ncurses-libs (6.1_p20190105-r0)

(21/26) Installing libedit (20181209.3.1-r0)

(22/26) Installing openssh-client (7.9_p1-r6)

(23/26) Installing openssh-sftp-server (7.9_p1-r6)

(24/26) Installing openssh-server-common (7.9_p1-r6)

(25/26) Installing openssh-server (7.9_p1-r6)

(26/26) Installing openssh (7.9_p1-r6)

Executing busybox-1.29.3-r10.trigger

OK: 53 MiB in 40 packages

Removing intermediate container 9471cd447603

---> 13d705c0f17d

Step 3/8 : EXPOSE 80

---> Running in 1e9629765f45

Removing intermediate container 1e9629765f45

---> 3e9b13c5f712

Step 4/8 : EXPOSE 443

---> Running in d203c44684f4

Removing intermediate container d203c44684f4

---> d28ff936ef66

Step 5/8 : WORKDIR /app

---> Running in c0e330d7f71e

Removing intermediate container c0e330d7f71e

---> 914a79994c12

Step 6/8 : COPY ./publish ./

---> f264c40e2341

Step 7/8 : RUN ["chmod", "+x", "MyWebApp"]

---> Running in a66b965fda5b

Removing intermediate container a66b965fda5b

---> 94e004ed94a7

Step 8/8 : ENTRYPOINT ["./MyWebApp", "--urls", "

---> Running in ded6415d0e8b

Removing intermediate container ded6415d0e8b

---> 2686bfb52f0f

Successfully built 2686bfb52f0f

Successfully tagged web2:latest Sending build context to Docker daemon 296.2MBStep 1/8 : FROM alpine:3.9.43.9.4: Pulling from library/alpinee7c96db7181b: Pull complete Digest: sha256:7746df395af22f04212cd25a92c1d6dbc5a06a0ca9579a229ef43008d4d1302aStatus: Downloaded newer image for alpine:3.9.4---> 055936d39205Step 2/8 : RUN apk add --no-cache openssh libunwind nghttp2-libs libidn krb5-libs libuuid lttng-ust zlib libstdc++ libintl icu---> Running in 9471cd447603fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz (1/26) Installing libgcc (8.3.0-r0)(2/26) Installing libstdc++ (8.3.0-r0)(3/26) Installing icu-libs (62.1-r0)(4/26) Installing icu (62.1-r0)(5/26) Installing krb5-conf (1.0-r1)(6/26) Installing libcom_err (1.44.5-r1)(7/26) Installing keyutils-libs (1.6-r0)(8/26) Installing libverto (0.3.0-r1)(9/26) Installing krb5-libs (1.15.5-r0)(10/26) Installing libidn (1.35-r0)(11/26) Installing libintl (0.19.8.1-r4)(12/26) Installing libunwind (1.2.1-r3)(13/26) Installing libuuid (2.33-r0)(14/26) Installing userspace-rcu (0.10.1-r0)(15/26) Installing lttng-ust (2.10.1-r0)(16/26) Installing nghttp2-libs (1.35.1-r1)(17/26) Installing openssh-keygen (7.9_p1-r6)(18/26) Installing ncurses-terminfo-base (6.1_p20190105-r0)(19/26) Installing ncurses-terminfo (6.1_p20190105-r0)(20/26) Installing ncurses-libs (6.1_p20190105-r0)(21/26) Installing libedit (20181209.3.1-r0)(22/26) Installing openssh-client (7.9_p1-r6)(23/26) Installing openssh-sftp-server (7.9_p1-r6)(24/26) Installing openssh-server-common (7.9_p1-r6)(25/26) Installing openssh-server (7.9_p1-r6)(26/26) Installing openssh (7.9_p1-r6)Executing busybox-1.29.3-r10.triggerOK: 53 MiB in 40 packagesRemoving intermediate container 9471cd447603---> 13d705c0f17dStep 3/8 : EXPOSE 80---> Running in 1e9629765f45Removing intermediate container 1e9629765f45---> 3e9b13c5f712Step 4/8 : EXPOSE 443---> Running in d203c44684f4Removing intermediate container d203c44684f4---> d28ff936ef66Step 5/8 : WORKDIR /app---> Running in c0e330d7f71eRemoving intermediate container c0e330d7f71e---> 914a79994c12Step 6/8 : COPY ./publish ./---> f264c40e2341Step 7/8 : RUN ["chmod", "+x", "MyWebApp"]---> Running in a66b965fda5bRemoving intermediate container a66b965fda5b---> 94e004ed94a7Step 8/8 : ENTRYPOINT ["./MyWebApp", "--urls", " http://0.0.0.0:80 "]---> Running in ded6415d0e8bRemoving intermediate container ded6415d0e8b---> 2686bfb52f0fSuccessfully built 2686bfb52f0fSuccessfully tagged web2:latest

The log is a bit longer than the previous one; because it’s going to install some libraries for Alpine that are needed by .NET Core. Let’s measure the image size:

docker images

29% reduction in the size

The image size is 147 MB, 61 MB less than the previous one. Let’s inspect image layers to see what 147 MB is concluded of:

docker history web2:latest

The summarized result would be something like this:

CREATED BY SIZE

/bin/sh -c #(nop) ENTRYPOINT ["./MyWebApp" … 0B

/bin/sh -c #(nop) COPY dir:eec964e257409b648… 97.8MB

/bin/sh -c #(nop) WORKDIR /app 0B

/bin/sh -c #(nop) EXPOSE 443 0B

/bin/sh -c #(nop) EXPOSE 80 0B

/bin/sh -c apk add --no-cache openssh li… 43.8MB

/bin/sh -c #(nop) CMD ["/bin/sh"] 0B

/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB

49.33 MB for Base Image and Libs, 97.8 MB for App

We managed to shrink the image size by using a light base Linux image compare to the previous one. The .NET runtime is added to the self-contained app, can we make it a bit smaller?

4) Combine self-contained and IL-Linker

What’s IL-Linker?

The IL Linker is a tool one can use to only to ship the minimal possible IL code and metadata that a set of programs might require to run as opposed to the full libraries. It is used by the various Xamarin products to extract only the bits of code that are needed to run an application on Android, iOS and other platforms. — https://github.com/mono/linker

IL-Linker has already included in .NET SDK, and as it says it can reduce the size of .NET Core apps to 50%.

To use il-linker, just pass the /p:PublishTrimmed=true flag to the publish command.

dotnet publish -c Release -r alpine-x64 --self-contained true /p:PublishTrimmed=true -o ./publish

It takes a bit longer, because of the IL Linker process. The result is extraordinary!

226 Files and 53.3 MB, it’s 45% reduction ✌

4–1) Dockerize it with the previous DockerFile

docker build -t web3 -f Dockerfile .

Measure the image size:

docker history web3:latest # result /bin/sh -c #(nop) ENTRYPOINT ["./MyWebApp" … 0B

/bin/sh -c #(nop) COPY dir:80b91cb154520a076… 55.9MB

/bin/sh -c #(nop) WORKDIR /app 0B

/bin/sh -c #(nop) EXPOSE 443 0B

/bin/sh -c #(nop) EXPOSE 80 0B

/bin/sh -c apk add --no-cache openssh li… 43.8MB

/bin/sh -c #(nop) CMD ["/bin/sh"] 0B

/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB

55.9MB for the self-contained app, 49.33 for Alpine and libs. 49.52% smaller than the default image.

5) Remove libs and packages of Alpine

We installed libraries for Alpine that are needed by .NET Core. This documentation by Microsoft is saying these documents are only needed for RHEL6. Since we are using Alpine, let’s try to remove them and see what would happen?

# RUN apk add --no-cache \

# openssh libunwind \

# nghttp2-libs libidn krb5-libs libuuid lttng-ust zlib \

# libstdc++ libintl \

# icu

The first try:

Error loading shared library libstdc++.so.6: No such file or directory (needed by ./MyWebApp)

Error loading shared library libgcc_s.so.1: No such file or directory (needed by ./MyWebApp)

✔ libstdc++ package is needed.

2nd try with libstdc++ lib installed:

Failed to load �a)�V, error: Error loading shared library libintl.so.8: No such file or directory (needed by /app/libcoreclr.so)

Failed to bind to CoreCLR at '/app/'

Failed to create CoreCLR, HRESULT: 0x80008088

✔ libintl package is needed too.

3nd try with libstdc++ and libintl installed:

Process terminated. Couldn't find a valid ICU package installed on the system. Set the configuration flag ystem.Globalization.Invariant to true if you want to run with no globalization support.

It seems icu package is needed. You can do two things:

Install the icu package alongside libstdc++ and libintl. Config your application to be invariant to cultures. (of course, if globalization is not an option for your app)

With icu package alongside those two:

Image size with the required libraries: 94.8MB. 10.2 MB smaller than the previous one.

How about config the app to run the invariant culture mode?

The image size will be 63MB (the smallest possible size😉). Now you need to pass an environment variable to .NET for CultureInvarient option.

Environment variable: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1

docker run -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -p 8080:80 web5 #result > docker run -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -p 8080:80 web5

Now listening on:

info: Microsoft.Hosting.Lifetime[0]

Application started. Press Ctrl+C to shut down.

info: Microsoft.Hosting.Lifetime[0]

Hosting environment: Production

info: Microsoft.Hosting.Lifetime[0]

Content root path: /app info: Microsoft.Hosting.Lifetime[0]Now listening on: http://0.0.0.0:80 info: Microsoft.Hosting.Lifetime[0]Application started. Press Ctrl+C to shut down.info: Microsoft.Hosting.Lifetime[0]Hosting environment: Productioninfo: Microsoft.Hosting.Lifetime[0]Content root path: /app

Working fine and ~70% reduction in size ✌!