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?
Real-world analogy
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
- Image and container — An image is a read-only package with your app and what it needs to run. A container is a running instance of an image, and you can start many from one image.
- Why containers help — The app carries its own runtime and libraries. "It works on my machine" gets rarer, because the machine, the server and CI all run the same image.
- Multi-stage builds — Use the large SDK image to build and publish. Then copy only the output into the small ASP.NET runtime image. The final image has no compiler or source code.
- Layer caching — Each instruction makes a layer, and Docker reuses it only if its inputs and every layer before it are unchanged. Copy the .csproj and restore first, so a code change does not repeat the restore.
- Use a .dockerignore — List bin, obj, .git and node_modules in .dockerignore. It keeps the build context small and stops local files from leaking into the image.
- Configuration from outside — Pass settings as environment variables when the container starts, such as ConnectionStrings__Shop. Never bake a password into the image, because anyone with the image can read it.
- Run as non-root — A process running as root inside a container has more power if an attacker breaks in. The .NET images provide a non-root user, which you switch to with USER.
- Compose for local development — docker compose reads one YAML file and starts the API, the database and the frontend together. A new developer can run the whole system with one command.
- React becomes static files — Build the React app with npm run build. Then serve the resulting static files from a small nginx image, which needs no Node.js at runtime.
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. 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. COPY brings in only the csproj file, and RUN dotnet restore downloads the packages. This layer is cached until the csproj changes.
- 3. The second COPY brings in the source code. dotnet publish builds it in Release mode into the /app folder, without restoring again.
- 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. COPY --from=build takes the published output from the first stage into the runtime stage.
- 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-restoreNeed a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Tutorial: containerize a .NET app (Microsoft Learn)
- Multi-stage builds (Docker Docs)
- Docker Compose overview (Docker Docs)
Lock it in