Good practices for creating .NET Core 5.0 Docker images

Creating docker images may seem to be easy, at first glance. We can also use many examples found on the Internet. But we can not be aware of a couple of issues, however. These images may not be optimized or even secure.

In this article, we try to show you a couple of tricks that could be useful for daily-basis images creation.

Base image for .NET Core app

First, let's create a simple .net console application:

PS C:\vs> dotnet new console -n MyApp The template "Console Application" was created successfully. Processing post-creation actions... Running 'dotnet restore' on MyApp\MyApp.csproj... Determining projects to restore... Restored C:\vs\MyApp\MyApp.csproj (in 58 ms). Restore succeeded.

Let's have a look at a Dockerfile example found on the Internet:

FROM mcr.microsoft.com/dotnet/sdk:5.0 # set /app as our context directory WORKDIR /app # copy all the files from the current host directory (like source code) COPY . ./ # restore packages RUN dotnet restore # build the app and place binaries to the out dir (/app/out) RUN dotnet publish -c Release -o out # set the working dir WORKDIR /app/out ENTRYPOINT ["dotnet", "MyApp.dll"]

Let's save the contents posted above to file Dockerfile just next to the Program.cs.

This image is 100% valid, so we can run it without any issues:

PS C:\vs\MyApp> docker build -t my-app . PS C:\vs\MyApp> docker run my-app Hello World!

Improvement No. 1: Cache

In order to save the resources, Docker puts the result of some operations into the cache. Once we build the image for the first time, the consecutive build attempts will end much quicker as some of the layers will be read from the cache.

For instance, RUN and COPY operations results are supposed to be cached.

As you can imagine, if Docker detects that a specific, cached operations results may be different (due to source code changes as example) the cache is invalidated and the layer is built from the scratch.

We need to think what are the artefacts the may change rarelier than other and takes a lot of time to process them in our .NET world.

The natural candidate are... nuget packages and their restoration. Nuget packages management (adding / updating) is rarelier than source code modifictions.

Let's add a packaged to our project and check that:

PS C:\vs\MyApp> dotnet add package Newtonsoft.Json Determining projects to restore... Writing C:\Users\Patryk\AppData\Local\Temp\tmpE547.tmp info : Adding PackageReference for package 'Newtonsoft.Json' into project 'C:\vs\MyApp\MyApp.csproj'. info : GET https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json info : OK https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/index.json 142ms info : Restoring packages for C:\vs\MyApp\MyApp.csproj... info : Package 'Newtonsoft.Json' is compatible with all the specified frameworks in project 'C:\vs\MyApp\MyApp.cs proj'. info : PackageReference for package 'Newtonsoft.Json' version '12.0.3' added to file 'C:\vs\MyApp\MyApp.csproj'. info : Committing restore... info : Writing assets file to disk. Path: C:\vs\MyApp\obj\project.assets.json log : Restored C:\vs\MyApp\MyApp.csproj (in 78 ms).

We can verify whtere package was added properly:

PS C:\vs\MyApp> cat .\MyApp.csproj <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> </ItemGroup> </Project>

As we know, we need nothing expect csproj in order to restore nuget packaged smiley

Let's improve our Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:5.0 WORKDIR /app # MOD1: copy csproj only COPY MyApp.csproj ./ # MOD2: restore package (and cache the layer by the way) RUN dotnet restore COPY . ./ RUN dotnet publish -c Release -o out WORKDIR /app/out ENTRYPOINT ["dotnet", "MyApp.dll"]

From now on, if we change anything in the source code except MyApp.csproj file then Docker will read layers (MOD1 and MOD2) from the cache. So, source code modifictions will not trigger package downloading and restoration every time image is built.

Let's measure the image building process speed.

For the first time:

PS C:\vs\MyApp> docker build -t my-app . [+] Building 6.4s (12/12) FINISHED [...] Let's modify the source code a little bit: ```Powershell PS C:\vs\MyApp> ((Get-Content -path Program.cs -Raw) -replace 'Hello','Hi, Hello') | Set-Content -Path Program.cs

The second image building attempt:

PS C:\vs\MyApp> docker build -t my-app . [+] Building 2.8s (12/12) FINISHED [...] => CACHED [2/7] WORKDIR /app => CACHED [3/7] COPY MyApp.csproj . => CACHED [4/7] RUN dotnet restore [...]

As you can see, the second attempt took much less time than the first one. Docker didn't had to download packages and layer was restored from the cache (the console output also points out that cache was used).

Improvement No. 2: Security

Our image is built fast now. But still - it should not be uploaded to a public repository or delivered to the customer. The issue is one of the layers:

COPY . ./

The COPY . ./ command copies the whole content of host's directory including source code. Our directory tree inside after compilation looks like below:

=> app => MyApp.csproj => Program.cs => out => MyApp.dll ... other files built by compiler

What's more, we are using image tagged as SDK - so the image contains not only runtime libraries but also all necessary tools for a compilation that are not needed to run the app. Without them, the final image could be much smaller.

Multi-stage building

To get rid of the source from the final image we use the so-called multi-stage building. Docker allows us to create temporary images (we use them as a building environment) from which we copy built binaries to the final image:

# image No. 1 (temporary) FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build-env WORKDIR /app COPY MyApp.csproj ./ RUN dotnet restore COPY . ./ RUN dotnet publish -c Release -o out # image No. 2 (final) FROM mcr.microsoft.com/dotnet/runtime:5.0 WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "MyApp.dll"]

For the first temporary image, we are using .NET SDK base image. We compile the app do the image directory /app/out. The next image (final) copies the whole contents from /app/out from the temporary image to the /app and the runs app. The final image is based on .NET Runtime base image, which is lighter in terms of size.

We are ready to test our Dockerfile:

PS C:\vs\MyApp> docker build -t my-app . PS C:\vs\MyApp> docker run my-app Hi, Hello World!

Summary

These two simple improvements allow us to create simple, light, and secure images. But keep in mind that we built a simple console app. To handle complex solutions like web apps we need to introduce other improvements, but it is material for another article 😊 .

Author:

Patryk Wąsiewicz


PON. - PT. 10:00 - 18:00

office@knsdata.com

KNS Data Sp. z o. o.
ul. Hoża 86 lok. 410
00-682 Warszawa
NIP 7010903351
REGON 382381463
KRS 0000767896