ASP.NET Core Dockerfile Best Practices: Smaller, Safer Production Images
A Dockerfile that merely runs is not necessarily a Dockerfile you want to ship. For an ASP.NET Core API, a production image should contain only runtime artifacts, rebuild quickly when source changes, and avoid giving the application unnecessary privileges. This guide turns those goals into a practical baseline you can adapt to a real solution.
This is deliberately a container-build article, not an application architecture tutorial. If you are still shaping the API itself, start with the .NET and ASP.NET Core learning track. For deployment context after the image is ready, see our practical guide to deploying an ASP.NET Core Web API with Docker, Kubernetes, and AWS.
What a production Dockerfile should optimize
There are three different concerns here. A multi-stage build keeps the SDK, source tree, and intermediate output out of the final runtime image. Copying project files before application source lets Docker reuse the restore layer until dependencies change. Finally, a non-root runtime limits the damage if the process is compromised; it does not replace ordinary application security.
Docker evaluates instructions in order and invalidates later cache layers when an earlier layer changes. That is why copying the whole repository before dotnet restore makes ordinary code edits needlessly expensive. Docker’s current build best-practices documentation recommends multi-stage builds, and its cache guidance explains the ordering trade-off.
A dependable multi-stage Dockerfile
The example assumes a solution with an API project at src/Orders.Api. Adjust the paths, solution name, and listening port to match your repository. The final stage starts from the smaller ASP.NET Core runtime image; the SDK is used only to restore, build, and publish.
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy dependency declarations first so restore can be cached.
COPY ["src/Orders.Api/Orders.Api.csproj", "src/Orders.Api/"]
RUN dotnet restore "src/Orders.Api/Orders.Api.csproj"
# Copy source only after restore.
COPY . .
RUN dotnet publish "src/Orders.Api/Orders.Api.csproj" \
--configuration Release \
--no-restore \
--output /app/publish \
/p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
# The .NET container images provide APP_UID for their non-root app user.
USER $APP_UID
COPY --from=build /app/publish .
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "Orders.Api.dll"]
Publishing with /p:UseAppHost=false avoids an operating-system-specific executable wrapper when the container starts the application through dotnet. More importantly, copy only /app/publish into the final stage. Do not copy the entire SDK stage, the source tree, or a local bin directory forward.
Keep irrelevant files out of the build context
A .dockerignore file is both a speed and hygiene control. It prevents local build output, Git metadata, editor files, and environment files from being sent as part of the build context. It is not a secret-management mechanism: do not place production secrets in a build context in the first place.
**/bin/
**/obj/
.git/
.github/
.vs/
.vscode/
.env
.env.*
Dockerfile*
README.md
*.user
*.suo
Review these rules against your CI checkout. If your build genuinely needs a generated file, make that dependency explicit rather than broadly re-including a directory. For secrets needed during a build, Docker supports build secrets; never use ARG or ENV as a way to smuggle credentials into an image layer.
Make the container easy to operate
Containers need an honest health endpoint and a predictable port. If you add a health endpoint, keep its readiness semantics aligned with your orchestrator; our ASP.NET Core health-checks guide shows how to separate liveness from dependency readiness. At deployment time, inject connection strings and secrets through the platform’s secret store or runtime configuration, not the image.
Build the image locally, then run it with a mapped port. The API should work as the non-root user before you hand it to CI or Kubernetes.
docker build --tag orders-api:local .
docker run --rm --publish 8080:8080 orders-api:local
curl http://localhost:8080/health
Production checklist
- Use a multi-stage build and copy only published output into the runtime stage.
- Copy project files before source files so dependency restore can be cached.
- Add and maintain a narrow
.dockerignore. - Run the final image as the image’s non-root application user.
- Keep secrets out of Dockerfile instructions, image layers, and source-controlled environment files.
- Rebuild regularly from supported base images; test the image and its health endpoint in CI.
- Pin image versions deliberately and track base-image security updates in your normal maintenance flow.
Conclusion
A good ASP.NET Core container image is intentionally boring: it restores predictably, publishes once, contains only what it runs, and starts without root privileges. Establish this baseline early, then let deployment, observability, and security checks evolve with the service.
1 Comments