ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs

ASP.NET Core + Docker: Mastering Multi‑Stage Builds for Web APIs

“Visual Studio gave me this Dockerfile… but what is it actually doing?”

If you’ve ever right‑clicked “Add > Docker Support” in an ASP.NET project, you’ve probably seen …


This content originally appeared on DEV Community and was authored by Cristian Sifuentes

ASP.NET Core + Docker: Mastering Multi‑Stage Builds for Web APIs

ASP.NET Core + Docker: Mastering Multi‑Stage Builds for Web APIs

“Visual Studio gave me this Dockerfile… but what is it actually doing?”

If you’ve ever right‑clicked “Add > Docker Support” in an ASP.NET project, you’ve probably seen a fairly complex multi‑stage Dockerfile appear in your repo.

It looks smart. It builds. It even runs in Debug.

But:

  • What does each stage really do?
  • What do you actually need installed to make it work?
  • Why is there a mysterious USER $APP_UID line?
  • How do you safely build and run the final image in your own environment?

This guide takes a real Dockerfile for an ASP.NET Core Web API and turns it into a clear mental model you can reuse in any .NET + Docker project.

TL;DR — What You’ll Learn

✅ How a multi‑stage Dockerfile for ASP.NET Core is structured (base → build → publish → final)

✅ What the USER $APP_UID line does and how to avoid permission problems

✅ What you actually need installed to build and run this image

✅ How to build and run the container step by step

✅ Why aligning aspnet:9.0 and sdk:10.0 versions matters

✅ A checklist to verify “yes, I can run this Dockerfile in my environment”

Copy‑paste friendly commands included. Let’s dissect this thing. 🪓

1. The Dockerfile We’re Analyzing (Big Picture)

Here’s the multi‑stage Dockerfile, slightly formatted:

# Base runtime stage (used for running the app)
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

# Build stage (used to compile the project)
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Packages.props", "."]
COPY ["Directory.Build.props", "."]
COPY ["Web.Api/Web.Api.csproj", "Web.Api/"]
RUN dotnet restore "./Web.Api/Web.Api.csproj"
COPY . .
WORKDIR "/src/Web.Api"
RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

# Publish stage (produces the final published output)
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

# Final runtime stage (what actually runs in prod)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.Api.dll"]

At a high level, this is a classic multi‑stage Dockerfile:

  1. base → ASP.NET runtime, non‑root user, ports exposed
  2. build → full .NET SDK, restores & compiles your Web API
  3. publish → takes the build output and publishes a trimmed app
  4. final → runtime image + published app + clean entrypoint

Let’s go stage by stage.

2. Stage‑by‑Stage Breakdown

2.1 Base Stage — Runtime Image

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

What this does:

  • Uses ASP.NET Core 9.0 runtime (Linux container).
  • Switches to a non‑root user via USER $APP_UID.
  • Sets /app as the working directory.
  • Exposes ports 8080 and 8081 (for HTTP/HTTPS or multiple endpoints).

Mental model:

This is the “slim, production‑ready base” where your app will run. No SDK, just runtime + your files.

2.2 Build Stage — SDK Image for Compiling

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Packages.props", "."]
COPY ["Directory.Build.props", "."]
COPY ["Web.Api/Web.Api.csproj", "Web.Api/"]
RUN dotnet restore "./Web.Api/Web.Api.csproj"
COPY . .
WORKDIR "/src/Web.Api"
RUN dotnet build "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build

Key points:

  • Uses .NET SDK 10.0 (preview/future version) to restore and build the project.
  • Assumes these files exist in the build context (folder where you call docker build):
  Directory.Packages.props
  Directory.Build.props
  Web.Api/Web.Api.csproj
  Web.Api/...
  • Flow:
    1. Copy minimal files (.props + .csproj) for faster restore caching.
    2. dotnet restore downloads all NuGet packages.
    3. Copy the rest of the source (COPY . .).
    4. Build the Web API project into /app/build.

Mental model:

This stage is your “build server inside a container”. It contains the full SDK and compiles your code, but it won’t be shipped as‑is to production.

2.3 Publish Stage — Final Output

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Web.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

What happens here:

  • Reuses the build stage (SDK + source + dependencies).
  • Runs dotnet publish to generate an optimized output into /app/publish.
  • Uses /p:UseAppHost=false to avoid bundling a platform‑specific executable; you’ll run with dotnet Web.Api.dll.

Mental model:

This stage transforms your compiled app into the final published bundle that will be copied into the runtime image.

2.4 Final Stage — What Actually Runs in Production

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Web.Api.dll"]

This is the stage that creates the image you actually run:

  • Starts from the base runtime image (ASP.NET 9.0, non‑root user, ports exposed).
  • Copies the published output from the publish stage.
  • Sets the entrypoint to:
  dotnet Web.Api.dll

Mental model:

Final image = runtime + published app. Small, clean, production‑oriented.

3. What You Must Have Installed to Build This Image

The good news: you don’t need .NET SDK installed on your host to build this image. The Dockerfile uses SDK images inside the container.

Mandatory

  1. Docker Engine / Docker Desktop

    • Windows, macOS, or Linux, with Linux containers enabled.
  2. Correct project layout matching the Dockerfile

    In the directory where you run docker build, you should see something like:

   Directory.Packages.props
   Directory.Build.props
   Web.Api/
       Web.Api.csproj
       Program.cs
       appsettings.json
       ...
   Dockerfile
  1. Internet access (at least for the first build) Docker must be able to pull:
  • mcr.microsoft.com/dotnet/aspnet:9.0
  • mcr.microsoft.com/dotnet/sdk:10.0 (or 9.0 if you align versions)
  • All NuGet packages during dotnet restore.

Optional (Nice to Have)

  • .NET SDK on your host Only needed if you also want to run:
  dotnet run
  dotnet test

directly on your machine. The Docker build itself doesn’t require it.

4. The USER $APP_UID Trap (And How to Fix It)

This line lives in the base stage:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER $APP_UID

It tries to ensure your app does not run as root inside the container. Great for security.

But there’s a catch:

For this to work correctly:

  • The environment variable APP_UID must be set at build or runtime, and
  • That UID must map to a valid user inside the container.

If not, you can get:

  • Permission errors
  • “No such user” problems
  • Confusing runtime failures

✅ Option 1 — Easiest for Local Dev: Comment it Out

For local testing only, you can temporarily remove or comment the line:

# USER $APP_UID

Your app will run as root inside the container, which is fine for dev, but not ideal for production security.

✅ Option 2 — Define and Create a Non‑Root User

Hardened, production‑friendly version:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base

ARG APP_UID=1000
ARG APP_GID=1000

RUN groupadd -g $APP_GID appgroup     && useradd -u $APP_UID -g $APP_GID -m appuser

USER appuser
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

Now you can optionally override the UID/GID at build time:

docker build -t my-webapi --build-arg APP_UID=1001 --build-arg APP_GID=1001 .

✅ Option 3 — Use a Predefined Non‑Root User (If Image Provides It)

Some images define built‑in users like dotnet or app. In that case, you might see:

USER app

But that depends on the specific tag and image; check the official docs for the image you’re using.

Recommended strategy:

  • Dev: comment USER $APP_UID if it’s blocking you.
  • Prod: properly define and create the non‑root user as shown above.

5. How to Build the Image (Step by Step)

In the folder where your Dockerfile and Web.Api project live, run:

# Basic build (uses default Release configuration)
docker build -t my-webapi .

Want to be explicit about configuration?

docker build -t my-webapi --build-arg BUILD_CONFIGURATION=Release .

What Docker will do:

  1. Pull mcr.microsoft.com/dotnet/sdk:10.0 (or from cache)
  2. Restore NuGet packages for Web.Api.csproj
  3. Build the project to /app/build
  4. Publish the project to /app/publish
  5. Create a final runtime image from aspnet:9.0 with /app/publish copied in

If the build fails, check:

  • Are Directory.Packages.props and Directory.Build.props really in the context?
  • Is the project folder exactly Web.Api and the file exactly Web.Api.csproj?
  • Do you need to fix or remove USER $APP_UID?

6. How to Run the Container

Once the image builds successfully:

docker run --rm -p 8080:8080 --name my-webapi my-webapi

What this means:

  • --rm → removes the container when it stops
  • -p 8080:8080host port 8080 → container port 8080
  • --name my-webapi → gives the container a readable name
  • my-webapi → the image you built

Now browse to:

  • http://localhost:8080
  • Maybe http://localhost:8080/swagger depending on your API setup.

What if your app listens on port 80 inside the container?

Sometimes ASP.NET is configured to listen on http://+:80 inside the container. In that case, change the mapping:

docker run --rm -p 8080:80 my-webapi
  • Host 8080 → Container 80

Tip:

Always confirm your Kestrel configuration (ASPNETCORE_URLS, appsettings.json, or Program.cs) to map ports correctly.

7. Version Alignment: aspnet:9.0 vs sdk:10.0

Right now the Dockerfile uses:

  • Runtime: mcr.microsoft.com/dotnet/aspnet:9.0
  • SDK: mcr.microsoft.com/dotnet/sdk:10.0

This can work (SDK 10 building a .NET 9 app), but in most real‑world setups you want matching major versions, e.g.:

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build

Why align versions?

  • Less surprise when APIs change between SDK versions
  • Consistent behavior between build and runtime
  • Easier upgrades: bump both to 10.0 at once later

Rule of thumb:

Unless you explicitly know why you need a newer SDK, keep runtime and SDK on the same major version.

8. Quick Checklist: “Can I Run This Dockerfile?”

Use this as a quick validation list before you start debugging in circles:

Environment

  • [ ] Docker Desktop / Engine is installed and running
  • [ ] You are using Linux containers, not Windows containers

Project Layout

  • [ ] Dockerfile is at the root of the solution (where you intend to build)
  • [ ] Directory.Packages.props and Directory.Build.props exist in that directory
  • [ ] There is a Web.Api folder with Web.Api.csproj and the API source files

Security/User

  • [ ] Either:
    • [ ] USER $APP_UID is temporarily commented out for dev, or
    • [ ] A valid non‑root user is created and APP_UID/APP_GID are configured

Build

  • [ ] docker build -t my-webapi . completes successfully
  • [ ] No dotnet restore errors (NuGet sources reachable, correct TFMs, etc.)

Run

  • [ ] docker run -p 8080:8080 my-webapi starts the container
  • [ ] The app responds at http://localhost:8080 (or mapped port/route)
  • [ ] Logs show the app listening on the expected URL/port

If all items are checked, you’re in a solid place to start iterating and hardening.

Final Thoughts

Visual Studio’s generated Dockerfile isn’t magic — it’s a clean example of a multi‑stage build:

  • base → runtime foundation
  • build → SDK and compilation
  • publish → final app output
  • final → minimal runtime image

Once you fully understand a Dockerfile like this, you can:

  • Tweak it for different projects (other Web APIs, gRPC, background workers)
  • Enforce non‑root users correctly in production
  • Align SDK/runtime versions with intent
  • Plug the same image into Kubernetes, Azure Container Apps, ECS, Cloud Run, etc.

If you’d like a follow‑up article, here are some natural next steps:

  • Multi‑stage builds with Node + .NET (SPA + API in one image)
  • Using multi‑arch images for ARM (Apple Silicon, Raspberry Pi)
  • Dockerfile patterns for minimal images (Alpine, distroless)
  • Integrating this image into CI/CD pipelines (GitHub Actions, Azure DevOps, GitLab CI)

✍️ Written for engineers who don’t just want Docker to “work”, but want to understand what’s happening in every layer.


This content originally appeared on DEV Community and was authored by Cristian Sifuentes


Print Share Comment Cite Upload Translate Updates
APA

Cristian Sifuentes | Sciencx (2025-11-25T21:02:39+00:00) ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs. Retrieved from https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/

MLA
" » ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs." Cristian Sifuentes | Sciencx - Tuesday November 25, 2025, https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/
HARVARD
Cristian Sifuentes | Sciencx Tuesday November 25, 2025 » ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs., viewed ,<https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/>
VANCOUVER
Cristian Sifuentes | Sciencx - » ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/
CHICAGO
" » ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs." Cristian Sifuentes | Sciencx - Accessed . https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/
IEEE
" » ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs." Cristian Sifuentes | Sciencx [Online]. Available: https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/. [Accessed: ]
rf:citation
» ASP.NET Core + Docker: Mastering Multi-Stage Builds for Web APIs | Cristian Sifuentes | Sciencx | https://www.scien.cx/2025/11/25/asp-net-core-docker-mastering-multi-stage-builds-for-web-apis/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.