

How to build smaller and secure Docker Images for .NET
source link: https://thorsten-hans.com/how-to-build-smaller-and-secure-docker-images-for-net5
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

TL;DR
This post explains how to optimize a .NET 5 Docker image and address two main concerns. Docker image size and security vulnerabilities. The article shows how to reduce the size from ~210 to ~58 MB. Additionally, it shows how to remove all 59 vulnerabilities.
The default Docker image for .NET applications
Visual Studio (also Visual Studio for Mac) creates a relatively simple Dockerfile
for .NET projects. It is a good starting point for users new to Docker. However, users should apply several optimizations before using the Dockerfile
to create Docker images for production environments.
Create a .NET 5 Web API project
Different kinds of .NET applications can run in containers. As example for this post, we use a .NET 5 Web API. Use .NET CLI dotnet
or Visual Studio to create a new .NET 5 Web API project.
# create a new Web API project in .NET 5
dotnet new webapi -n ContainerSample -o ContainerSample
Optimize .NET Web API for container execution
The default .NET Web API template registers the UseHttpsRedirection
middleware by default. However, when running the application in a Docker container, things like proper HTTPS configuration are in the hosting environment’s responsibility (eg. Kubernetes, Azure Container Instances, or the App Services runtime).
That said, remove the registration of UseHttpsRedirection
from Startup.cs
. The ConfigureServices
method of your Startup.cs
should now look like this:
// Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Thns.ContainerSample v1"));
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Add Docker Support to the .NET Web API project
Both, Visual Studio and Visual Studio for Mac can also add Docker support to different projects. Right-click the project in the IDE and select Add → Docker Support. Consider reading the detailed instructions for Visual Studio.
Both IDEs will generate a Dockerfile
like this:
FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "ContainerSample.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ContainerSample.dll"]
Default Docker Image - Status Quo
As mentioned during the introduction, we will verify size, function, and vulnerabilities after every optimization. First, we have to build the Docker image using docker
CLI:
# navigate to the project directory
cd ContainerSample
# build the Docker image
docker build . -t container-sample:0.0.1
Docker CLI will transfer all required files and folders to the Docker daemon and start the image build process. In the end you find a new Docker image on your local machine.
Take a look at your local Docker images, which will also display the actual size of the Docker image:
# list all all container-sample docker images
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 1 minute ago 210MB
Test if the container starts successfully:
docker run --rm container-sample:0.0.1
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
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
You can terminate the application again using Ctrl + C
. Because we specified the --rm
flag when running the Docker image, stopping the container will remove it from the list of stopped containers.
Finally, let us take a look at the vulnerabilities of the image by using docker scan
. If you want to learn more about Docker image scanning, you should read my article on Docker image scanning.
# scan image for vulnerabilities
docker scan container-sample:0.0.1
## trimmed scan logs
Tested 94 dependencies for known vulnerabilities, found 59 vulnerabilities.
Bottom line: We end up with a working Docker image that is 210 MB large and has 59 vulnerabilities.
Step 1 - Switch to Alpine Linux for .NET apps
First, switch from Debian to Alpine Linux to decrease the size of the resulting Docker image. The .NET team provides base images for a wide variety of operating systems and architectures.
Change the Dockerfile
to match the following:
# Use Alpine Base Image
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# Use Alpine Base Image
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS build
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "ContainerSample.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ContainerSample.dll"]
.NET Web API on Alpine Linux - Status Quo
To build and verify the current state, use now the 0.0.2
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.2
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 2 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 1 minute ago 108MB
# verify that it works
docker run --rm container-sample:0.0.2
# scan for vulnerabilities
docker scan container-sample:0.0.2
## trimmed scan logs
Tested 23 dependencies for known issues, found 1 issue.
Switching to Alpine Linux reduced our image from 210 to 108 MB. Regarding security vulnerabilities, it is down from 59 to 1.
Step 2 - Optimize .NET Web API distributable
At this point, optimize how the application is published. Until now, it was published for the Release
configuration, using default settings. However, .NET allows several tweaks when publishing apps.
The following Dockerfile
passes several arguments to dotnet publish
to ensure our app is published as a self-contained executable, targeting the given runtime (alpine-x64
).
Bundle trimming (/p:PublishTrimmed=true
) removes unnecessary framework components from the application bundle to optimize the distributable size. Also, create a single-file distributable by adding /p:PublishSingleFile=true
.
Last but not least, change the ENTRYPOINT
of the Dockerfile
to use the new single-file distributable.
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS build
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "ContainerSample.csproj" -c Release -o /app/build
FROM build AS publish
# optimize dotnet publish
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# new ENTRYPOINT (no more .dll is generated)
ENTRYPOINT ["./ContainerSample"]
Optimized .NET distributable - Status Quo
To build and verify the current state, use now the 0.0.3
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.3
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 3 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 2 minutes ago 108MB
container-sample 0.0.3 f655d10572b0 1 minute ago 148MB
# verify that it works
docker run --rm container-sample:0.0.3
# scan for vulnerabilities
docker scan container-sample:0.0.3
## trimmed scan logs
Tested 23 dependencies for known issues, found 1 issue.
The size of the image went up to 148 MB after this optimization. The final image remains having 1 vulnerability.
Step 3 - Restore NuGet packages once
The previous Dockerfiles
interacted with dotnet
several times. Many dotnet
commands execute restore
automatically to ensure all dependencies for your applications are in place. Optimize the Dockerfile
to restore
dependencies only once, which leads to a faster Docker image build-time:
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# remove the build stage
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS publish
WORKDIR /src
COPY ContainerSample.csproj ./
# specify target runtime for destroy
RUN dotnet restore "./ContainerSample.csproj" --runtime alpine-x64
COPY . .
# add --no-restore flag to dotnet publish
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish \
--no-restore \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["./ContainerSample"]
Restore NuGet packages once - Status Quo
To build and verify the current state, use now the 0.0.4
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.4
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 4 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 3 minutes ago 108MB
container-sample 0.0.3 f655d10572b0 2 minutes ago 148MB
container-sample 0.0.4 6586a96f6808 1 minute ago 148MB
# verify that it works
docker run --rm container-sample:0.0.4
# scan for vulnerabilities
docker scan container-sample:0.0.4
## trimmed scan logs
Tested 23 dependencies for known issues, found 1 issue.
At this point, neither size nor number of vulnerabilities changed. This step was all about optimizing the image build-time. The image remains at 148 MB and 1 vulnerability.
Step 4 - Use .NET runtime dependencies Docker image
Using build a self-containing executable, we can switch to slightly different base images for the application. The .NET team offers a dedicated image, which comes with all required dependencies to run a .NET application. However, in contrast to the images used previously, neither the SDK nor the .NET runtime are part of the image. Again, this has a significant impact on the size of the final Docker image.
This is also a excellent time to clean-up the Dockerfile
and remove unnecessary EXPOSE
and WORKDIR
instructions:
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS publish
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj" --runtime alpine-x64
COPY . .
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish \
--no-restore \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true
# use different image
FROM mcr.microsoft.com/dotnet/runtime-deps:5.0-alpine AS final
WORKDIR /app
# just expose port 80
EXPOSE 80
COPY --from=publish /app/publish .
ENTRYPOINT ["./ContainerSample"]
.NET runtime dependencies Docker Image - Status Quo
To build and verify the current state, use now the 0.0.5
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.5
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 4 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 3 minutes ago 108MB
container-sample 0.0.3 f655d10572b0 2 minutes ago 148MB
container-sample 0.0.4 6586a96f6808 2 minutes ago 148MB
container-sample 0.0.5 5b176150ad0b 1 minute ago 54.9MB
# verify that it works
docker run --rm container-sample:0.0.5
# scan for vulnerabilities
docker scan container-sample:0.0.5
## trimmed scan logs
Tested 23 dependencies for known issues, found 1 issue.
Switching the base image for distribution leads to decreasing the size of the image by ~100 MB. The new Docker image is ~55 MB in size and has 1 known vulnerability.
Step 5 - Run .NET apps in Docker as non-root
I am sure you have noticed that the Docker image still runs with root
privileges. This is something, we should address.
On Linux, new users are created using the adduser
command. (The gecos
argument prevents the system from asking for additional details like full name, phone and so on…).
Additionally, use chown
to set the new user (dotnetuser
) as owner of the working directory (/app
).
Impersonate into the user context with USER dotnetuser
. Non-root users are not allowed to allocate network ports below 1024
. Use EXPOSE
with port 5000 and instruct Kestrel to run on that port by using the --urls
argument:
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS publish
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj" --runtime alpine-x64
COPY . .
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish \
--no-restore \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true
FROM mcr.microsoft.com/dotnet/runtime-deps:5.0-alpine AS final
# create a new user and change directory ownership
RUN adduser --disabled-password \
--home /app \
--gecos '' dotnetuser && chown -R dotnetuser /app
# impersonate into the new user
USER dotnetuser
WORKDIR /app
# use port 5000 because
EXPOSE 5000
COPY --from=publish /app/publish .
# instruct Kestrel to expose API on port 5000
ENTRYPOINT ["./ContainerSample", "--urls", "http://localhost:5000"]
.NET in Docker as non-root user - Status Quo
To build and verify the current state, use now the 0.0.6
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.6
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 6 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 5 minutes ago 108MB
container-sample 0.0.3 f655d10572b0 4 minutes ago 148MB
container-sample 0.0.4 6586a96f6808 3 minutes ago 148MB
container-sample 0.0.5 5b176150ad0b 2 minutes ago 54.9MB
container-sample 0.0.6 db9f0a9a0370 1 minute ago 54.9MB
# verify that it works
docker run --rm container-sample:0.0.6
# scan for vulnerabilities
docker scan container-sample:0.0.6
## trimmed scan logs
Tested 23 dependencies for known issues, found 1 issue.
Running with non-root privileges does not affect the size and number of vulnerabilities. The Docker image remains at ~55 MB and having 1 known vulnerability.
Step 6 - Fix Docker image Vulnerabilities
We made huge improvements and went down from 58 vulnerabilities to just 1. Fortunately, we get further information from docker scan
about how to deal with the vulnerabilities that are found in the image.
To fix the vulnerability from step 5, use apk
(the package manager of alpine
base image) and update the vulnerable musl
package:
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS publish
WORKDIR /src
COPY ContainerSample.csproj ./
RUN dotnet restore "./ContainerSample.csproj" --runtime alpine-x64
COPY . .
RUN dotnet publish "ContainerSample.csproj" -c Release -o /app/publish \
--no-restore \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true
FROM mcr.microsoft.com/dotnet/runtime-deps:5.0-alpine AS final
RUN adduser --disabled-password \
--home /app \
--gecos '' dotnetuser && chown -R dotnetuser /app
# upgrade musl to remove potential vulnerability
RUN apk upgrade musl
USER dotnetuser
WORKDIR /app
EXPOSE 5000
COPY --from=publish /app/publish .
ENTRYPOINT ["./ContainerSample", "--urls", "http://localhost:5000"]
Fix Docker image vulnerabilities - Status Quo
To build and verify the current state, use now the 0.0.7
tag:
# build the image
cd ContainerSample
docker build . -t container-sample:0.0.7
# check the size
docker image ls | grep container-sample
container-sample 0.0.1 6ba4eeed8e21 7 minutes ago 210MB
container-sample 0.0.2 5696cf7ba51b 6 minutes ago 108MB
container-sample 0.0.3 f655d10572b0 5 minutes ago 148MB
container-sample 0.0.4 6586a96f6808 4 minutes ago 148MB
container-sample 0.0.5 5b176150ad0b 3 minutes ago 54.9MB
container-sample 0.0.6 db9f0a9a0370 2 minutes ago 54.9MB
container-sample 0.0.7 ae9c20b7f786 1 minute ago 57.4MB
# verify that it works
docker run --rm container-sample:0.0.7
# scan for vulnerabilities
docker scan container-sample:0.0.7
## trimmed scan logs
✓ Tested 23 dependencies for known vulnerabilities, no vulnerable paths found.
Removing the last vulnerability from the Docker image results in the Docker image being slightly more prominent. The final Docker image is 57.4MB small and as docker scan
states, no vulnerabilities are left.
Conclusion
You nailed it! You completed seven steps that brought you here, and your Docker image is way smaller and has zero known vulnerabilities. Compare the final result with the first image build by using the standard Dockerfile
. The difference is immersive.
Creating smaller Docker images has several advantages:
- Environments can initially pull images faster
- Images allocate less storage of your container registry
- CI executions will be faster (push time decreases too)
Hopefully, removing all vulnerabilities from the Docker image does not require any further explanations ;-).
Recommend
-
79
In a previous blog post, I've described how we can use Docker multi-stages build feature to generate thin images. In this article, we're going to push the limit a bit further by combining multi-stage build and Google’s distroless base images! Th...
-
47
:construction_worker: Build images with images. About Tiler is a tool to create an image using all kin...
-
2
Smaller, Faster-starting Container Images With jlink and AppCDS Posted at Dec 13, 2020 A few months ago I
-
48
TAURI Tauri Apps footprint: minuscule performance: ludicrous flexibility: gymnastic security: hardened Current Releases Component Description Version Lin Win Mac
-
5
Is Docker Secure Enough? Advice for Configuring Secure Container Images and Runtimes Feb 23, 2022...
-
6
Technical | 2022-03-13 A COMPLETE guide on how to make Docker...
-
3
Not FoundYou just hit a route that doesn't exist... the sadness.LoginRadius empowers businesses to deliver a delightful customer experience and win customer trust. Using the LoginRadius Identity...
-
11
Depot: A Faster and Smarter Way To Build Docker ImagesLast Updated: April 30, 2022It's been a while since I have written anything new! But I'm excited to finally share why that has been the case and what I have been working on....
-
6
The Source-to-Image (S2I) framework makes it easy for developers to transfer their projects into ready-to-use and reproducible container images. S2I consists of a
-
10
Using JLink to create smaller Docker images for your Spring Boot Java applicationWritten by: Brian Vermeer
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK