At the end of lesson 2.9.6, there was an uncomfortable number: your image was more than five times as big as the one from
dotnet publish. In it were the whole SDK, all NuGet packages, the source code and tools like git, curl and PowerShell. The running
Kladde needs none of that. In this lesson, you separate building and running. The result is an image that is smaller than the one from
dotnet publish, contains no shell and can still handle cultures like en-US and the office's time zone.
Two stages in one Dockerfile
A Dockerfile may contain several FROM instructions. Each starts a new stage with a base image of its own. A later stage can fetch
files from an earlier one with COPY --from=. Only the last stage goes into the finished image; all the others are workbenches
that are cleared away after the build.
For the Kladde, there are two stages. In the branch feature/dockerfile-multistage, replace the Dockerfile from lesson 2.9.6:
# Stage 1: build with the full SDK.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY global.json Directory.Build.props ./
COPY src/Kladde.Core/Kladde.Core.csproj src/Kladde.Core/
COPY src/Kladde.Data/Kladde.Data.csproj src/Kladde.Data/
COPY src/Kladde.Ted/Kladde.Ted.csproj src/Kladde.Ted/
COPY src/Kladde.ServiceDefaults/Kladde.ServiceDefaults.csproj src/Kladde.ServiceDefaults/
COPY src/Kladde.Web/Kladde.Web.csproj src/Kladde.Web/
RUN dotnet restore src/Kladde.Web/Kladde.Web.csproj
COPY src/ src/
RUN dotnet publish src/Kladde.Web/Kladde.Web.csproj -c Release --no-restore -o /app
# Stage 2: run on the chiseled ASP.NET Core image with ICU and time zones.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra AS final
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "Kladde.Web.dll"]
AS buildgives the first stage a name. Up to thepublish, it's the same as in lesson 2.9.6, with the same order for the cache.- The second
FROMstarts from scratch, on an image that contains only the ASP.NET Core runtime and no SDK. COPY --from=build /app .fetches exactly one folder from the first stage: the published Kladde. Packages, source code and compiler stay behind.USER $APP_UID: the .NET images set the environment variableAPP_UIDto 1654, the number of the userapp. A number instead of a name has an advantage: platforms like Kubernetes can use it to verify that the container doesn't run asroot, without looking into the image. The chiseled images run asappanyway; the line makes it visible and also holds if someone changes the base image.EXPOSE 8080is the note from lesson 2.9.6 for people and tools. The port itself is set by the base image withASPNETCORE_HTTP_PORTS=8080. Port 8080 instead of 80 has a reason: on Linux, onlyrootmay open ports below 1024.
Build as usual from the Kladde's main folder:
docker build -f src/Kladde.Web/Dockerfile -t kladde-web:multistage --progress=plain .
docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" kladde-web
REPOSITORY TAG SIZE
kladde-web multistage 245MB
kladde-web dockerfile 1.72GB
kladde-web 0.9.0 314MB
From 1.72 gigabytes down to 245 megabytes, and also smaller than the image from dotnet publish, which builds on the full Ubuntu image
aspnet:10.0. The numbers come from an example Kladde; yours differ.
If you want to build only the first stage, for example to look for an error in it, say so with --target build. Exercise 2 makes use of
that.