How to Add Docker to an Existing ASP.NET Core Project
An existing .NET app rarely needs code changes to run in a container. You add a Dockerfile and a .dockerignore, check how the app gets its configuration, and decide where TLS is terminated. The examples use .NET 10. For another version, change the image tags to match the project's TargetFramework (net8.0 means 8.0).
What changed in .NET 8 and later
Many older guides are out of date on two points:
- Port 8080, not 80. The official ASP.NET Core images set
ASPNETCORE_HTTP_PORTS=8080, so the app listens on 8080 inside the container.EXPOSE 80and-p 5000:80come from the .NET 7 era and earlier. - A non-root user. The images include an
appuser, and its UID is in theAPP_UIDvariable. Switch to it withUSER $APP_UID. The default port moved to 8080 so the app can run as this user without extra privileges.
Option 1: a multi-stage Dockerfile
Put the Dockerfile at the solution root so the build can see every project the app references. For a layout like src/MyApp/MyApp.csproj:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Restore first, so this layer is cached until a project file changes
COPY ["src/MyApp/MyApp.csproj", "src/MyApp/"]
RUN dotnet restore "src/MyApp/MyApp.csproj"
COPY . .
RUN dotnet publish "src/MyApp/MyApp.csproj" -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
The SDK image (several hundred MB) only exists in the build stage. The final image contains just the ASP.NET Core runtime and your published output. If MyApp references other projects, copy their .csproj files before the restore as well, along with Directory.Build.props, Directory.Packages.props and NuGet.config if the repository has them. Otherwise restore fails or ignores your package sources.
For a console app or worker, use mcr.microsoft.com/dotnet/runtime:10.0 as the final image instead of aspnet.
Add a .dockerignore next to the Dockerfile:
**/bin/
**/obj/
**/.vs/
**/.vscode/
**/*.user
**/node_modules/
.git/
Excluding bin and obj is not just about size. Restore output copied in from your machine (for example from Windows) can break the restore inside the Linux container.
Build and run:
docker build -t my-app:1.0 .
docker run --rm -p 8080:8080 -e ASPNETCORE_ENVIRONMENT=Development my-app:1.0
Open http://localhost:8080.
Option 2: no Dockerfile, using dotnet publish
The .NET SDK (8 and later) can build a container image itself, without a Dockerfile and without a NuGet package:
dotnet publish src/MyApp/MyApp.csproj --os linux --arch x64 /t:PublishContainer \
-p ContainerRepository=my-app -p ContainerImageTag=1.0
By default the image goes to the local Docker or Podman daemon. Add -p ContainerRegistry=ghcr.io (with ContainerRepository=example/my-app) to push straight to a registry, or -p ContainerArchiveOutputPath=./my-app.tar.gz to write a tarball with no daemon at all. The base image, user and port follow the same conventions as above. This works well for plain web apps and APIs. Use a Dockerfile once you need OS packages or extra build steps.
Visual Studio
In Visual Studio, right-click the project and choose Add > Container Support (called "Docker Support" in older versions). It generates a Dockerfile with base, build, publish and final stages and sets up debugging inside the container. The generated file expects the solution directory as the build context, so run docker build -f src/MyApp/Dockerfile . from the solution root when building outside Visual Studio.
Configuration and secrets
ASP.NET Core reads environment variables on top of appsettings.json. Nested keys use a double underscore:
docker run -d -p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Production \
-e ConnectionStrings__Default="Host=db;Database=app;Username=app;Password=change-me" \
my-app:1.0
Keep secrets out of the image and out of appsettings.json. Pass them at runtime from your orchestrator's secret store.
Three more things usually come up when an existing app moves into a container:
- HTTPS. Usually a reverse proxy or load balancer terminates TLS and the container serves plain HTTP on 8080. Enable the forwarded headers middleware so the app sees the original scheme and client IP. Otherwise
UseHttpsRedirection()logs "Failed to determine the https port for redirect" or redirects in a loop. - Data Protection keys. By default they are stored inside the container and lost on restart, which logs users out and breaks antiforgery tokens. Once you run more than one instance, keep the keys in a volume or a shared store.
- Health checks. Add
builder.Services.AddHealthChecks();andapp.MapHealthChecks("/healthz");and point your load balancer or Kubernetes probes at it. The runtime images do not includecurl, so a DockerfileHEALTHCHECKbased on curl will fail.
Docker Compose for the app and its database
services:
web:
build: .
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings__Default: "Host=db;Database=app;Username=app;Password=example"
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 10
volumes:
db-data:
docker compose up --build starts both. Inside the Compose network, the app reaches the database by its service name db, not localhost.
Push the image
docker tag my-app:1.0 registry.example.com/my-app:1.0
docker push registry.example.com/my-app:1.0
From there the image deploys like any other container: Azure Container Apps, ECS, Cloud Run, Kubernetes or a plain server with Docker.
Quick reference
Base images mcr.microsoft.com/dotnet/sdk:10.0 (build), aspnet:10.0 (run)
Container port 8080 (ASPNETCORE_HTTP_PORTS), USER $APP_UID
Build docker build -t my-app:1.0 .
Run docker run --rm -p 8080:8080 my-app:1.0
No Dockerfile dotnet publish --os linux --arch x64 /t:PublishContainer
Config -e Section__Key=value
Ignore bin/ obj/ .vs/ .git/ in .dockerignore
