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.
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!
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
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).
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.
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!
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 😊 .
PON. - PT. 10:00 - 18:00
office@knsdata.com