Lesson 42 of 45 intermediate

Docker for .NET and React

Package the app and its runtime once, so it runs the same wherever the image runs

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

What is the difference between an image and a container?

Open interactive version (quiz + challenge)

Real-world analogy

A food truck owner fits the kitchen into a truck once, and it is the same at every festival. Fitting it needs welding gear, but the finished truck does not carry it, so it stays small. The fitting plan is the Dockerfile, the finished truck is the image, and a truck serving food is a container. The welding gear left behind is the SDK stage.

What is it?

Docker packages an application together with its runtime and libraries into an image. A container is a running copy of that image. For .NET you normally write a multi-stage Dockerfile. One stage uses the SDK image to restore, build and publish. The final stage starts from the smaller ASP.NET runtime image and copies in only the published output. Settings and secrets come in from outside as environment variables. For React you build static files and serve them with nginx. Docker Compose ties several containers together for local work.

Real-world relevance

A team's API ran for two years on one hand-built Windows server. When the server died, nobody remembered the exact settings, and the rebuild took three days. With a Dockerfile in the repo, a new environment is one docker run away. The CI pipeline also builds the same image that runs in production. The usual traps are a slow build that restores every package on each change, and a baked-in connection string.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Why does a multi-stage Dockerfile use the SDK image only for the build stage?

Why copy the .csproj and run dotnet restore before copying the rest of the source?

Code example

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy only the project file first, so the restore layer is cached
COPY Shop.Api/Shop.Api.csproj Shop.Api/
RUN dotnet restore Shop.Api/Shop.Api.csproj

# Now copy the source and publish
COPY Shop.Api/ Shop.Api/
RUN dotnet publish Shop.Api/Shop.Api.csproj -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app .

# Settings such as the connection string are passed at run time
EXPOSE 8080
USER $APP_UID
ENTRYPOINT ["dotnet", "Shop.Api.dll"]

Line-by-line walkthrough

  1. 1. This Dockerfile starts a build stage from the .NET SDK image, which has the compiler and tools. WORKDIR sets the folder inside the image.
  2. 2. COPY brings in only the csproj file, and RUN dotnet restore downloads the packages. This layer is cached until the csproj changes.
  3. 3. The second COPY brings in the source code. dotnet publish builds it in Release mode into the /app folder, without restoring again.
  4. 4. The second FROM starts a new stage from the small ASP.NET runtime image. Nothing from the SDK stage comes along except what we copy.
  5. 5. COPY --from=build takes the published output from the first stage into the runtime stage.
  6. 6. EXPOSE documents port 8080. USER switches to the non-root user provided by the image. ENTRYPOINT starts the app.

Spot the bug

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app --no-restore
Need a hint?
Think about which layers Docker must rebuild after you change one line of C#.
Show answer
COPY . . comes before dotnet restore, so any source change invalidates the copy layer. Every layer after it rebuilds too, so every package downloads again on each build. Fix: copy only the csproj files, run dotnet restore, and then copy the rest of the source.

Explain like I'm 5

Imagine you pack a lunch box with your sandwich, your fork and your napkin. Wherever you open it, the lunch is the same. You use big tools to make the lunch, but you do not put them in the box.

Fun fact

Since .NET 8 the official ASP.NET images listen on port 8080 by default, not port 80. The change was made so the container can run as a non-root user, because low port numbers need extra privileges.

Hands-on challenge

Say it out loud in 30 seconds: "How would you containerize an ASP.NET Core API?" Then check: did you mention a multi-stage Dockerfile with the SDK to build and the runtime image to run, copying the csproj and restoring first for layer caching, configuration through environment variables, and running as non-root?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook