Docker for .NET Developers: From First Container to Production Image
A practical walkthrough of containerising an ASP.NET Core app: multi-stage builds, small secure images, configuration, health checks and common mistakes.

Containers solve a familiar problem: "it works on my machine". With Docker, your ASP.NET Core application runs with the same runtime, dependencies and settings on a laptop, a test server and production. This guide walks through containerising a .NET web API properly, not just getting it to start.
The basic multi-stage Dockerfile
A multi-stage build compiles the app in an SDK image and copies only the published output into a smaller runtime image.
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["Shop.Api/Shop.Api.csproj", "Shop.Api/"]
RUN dotnet restore "Shop.Api/Shop.Api.csproj"
COPY . .
RUN dotnet publish "Shop.Api/Shop.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER app
EXPOSE 8080
ENTRYPOINT ["dotnet", "Shop.Api.dll"]
Two details matter here:
- Copying the
.csprojand restoring before copying the rest of the source lets Docker cache the restore step. Builds become much faster when only code changes. - The final image contains the ASP.NET runtime, not the SDK, so it is much smaller and has fewer tools an attacker could use.
Run as a non-root user
Recent official .NET images include a non-root user named app, and listen on port 8080 by default. The USER app line ensures your process does not run as root inside the container. This is a simple but important security improvement.
Add a .dockerignore
Without it, Docker sends your bin, obj, .git and local secrets to the build context.
**/bin
**/obj
**/.vs
**/.git
**/*.user
**/appsettings.Development.json
Configuration and secrets
Containers should be configured from the environment, not by editing files inside the image.
- ASP.NET Core reads environment variables automatically.
ConnectionStrings__Defaultmaps toConnectionStrings:Default. - Never bake passwords or API keys into the image. Pass them at runtime through your orchestrator's secret store (Docker secrets, Kubernetes secrets, a cloud key vault).
- Use the same image for every environment and change only the configuration.
docker run -p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__Default="Server=db;Database=shop;..." \
shop-api:1.4.0
Health checks
Orchestrators need to know whether your app is alive and ready.
builder.Services.AddHealthChecks()
.AddDbContextCheck<ShopDb>();
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
app.MapHealthChecks("/health/ready");
The liveness endpoint only proves the process responds; the readiness endpoint also checks dependencies such as the database. Kubernetes and most cloud container services can call these endpoints.
Local development with Docker Compose
Compose lets you start the API and its database together:
services:
api:
build: .
ports: ["8080:8080"]
environment:
ConnectionStrings__Default: "Server=db;Database=shop;User Id=sa;Password=${SA_PASSWORD};TrustServerCertificate=True"
depends_on: [db]
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: "${SA_PASSWORD}"
Keep the password in a local .env file that is not committed to Git.
Making images smaller and safer
- Use the runtime or aspnet image, not the SDK, for the final stage.
- Consider the chiseled Ubuntu-based .NET images, which remove the shell and package manager for a smaller attack surface.
- Rebuild images regularly to pick up security patches in the base image.
- Scan images for known vulnerabilities in your CI pipeline.
- Tag images with a version or commit hash, never deploy
latestto production.
Common mistakes
| Mistake | Better approach |
|---|---|
| Copying the whole source before restore | Copy project files, restore, then copy source |
| Running as root | Use the app user |
| Secrets in Dockerfile or appsettings | Environment variables and secret stores |
| One image per environment | One image, different configuration |
| Writing files inside the container | Use volumes or object storage |
| No health endpoints | Separate liveness and readiness checks |
Frequently asked questions
Do we need Docker on Windows servers? Not necessarily. ASP.NET Core runs well on IIS or as a Windows service. Docker becomes valuable when you deploy to Linux, Kubernetes or cloud container services, or want identical environments across machines.
Should the database also run in a container? For local development and testing, yes, it is very convenient. For production, most teams prefer a managed database or a dedicated server with proper backups and storage.
How do we see logs from a container? Write logs to standard output (the console). Docker and container platforms collect them, and you can forward them to a central logging system.
Key takeaways
- Use multi-stage builds with cached restore and a small runtime image.
- Run as non-root, add a
.dockerignoreand keep secrets out of images. - Configure through environment variables and use one image for all environments.
- Add health checks and rebuild images regularly for security patches.
At KHR Digital Solutions we containerise .NET APIs and Next.js frontends for clients who need repeatable deployments. Once the Dockerfile is right, moving between servers and cloud providers becomes much simpler.
Related articles

Entity Framework Core Performance: 10 Practical Tips for Faster .NET Apps
Slow pages are often slow queries. Ten EF Core techniques, from AsNoTracking and projections to split queries and compiled models, with code examples.

CI/CD for .NET with GitHub Actions: Build, Test and Deploy Automatically
A practical GitHub Actions pipeline for ASP.NET Core: restore, build, test, publish, and deploy safely with secrets and environment approvals.

ASP.NET Core Minimal APIs vs Controllers: An Architect's Guide for 2026
Beyond hello-world: how minimal APIs and controllers compare on structure, validation, filters, OpenAPI, versioning, testing and performance in large production codebases.