Learn how to use Docker with .NET 9 to containerize and deploy your ASP.NET Core app. Step-by-step tutorial with Dockerfile examples. Start building today!
If you're building modern web APIs or web apps, learning how to use Docker with .NET 9 is one of the highest-leverage skills you can pick up in 2026. Containers let you package your ASP.NET Core application — code, runtime, and dependencies — into a single portable image that runs identically on your laptop, your teammate's machine, and production in the cloud. In this tutorial, you'll learn how to containerize an ASP.NET Core app from scratch, write an optimized Dockerfile, use Docker Compose for multi-container setups, and deploy with confidence.
By the end, you'll understand not just how to dockerize ASP.NET Core, but why each step matters — including the performance and security trade-offs that separate a beginner image from a production-ready one.
Why Use Docker with .NET 9?
The phrase "it works on my machine" has cost development teams millions of hours. Docker solves this by bundling your app with everything it needs to run. When you containerize an ASP.NET Core application, you get several concrete benefits:
- Consistency: The same image runs in dev, staging, and production — no surprise version mismatches.
- Fast onboarding: A new developer runs one command instead of installing the .NET SDK, databases, and config by hand.
- Scalability: Containers are the foundation of Kubernetes, Azure Container Apps, AWS ECS, and Google Cloud Run.
- Isolation: Each container has its own filesystem and process space, so dependencies never collide.
.NET 9 also ships with first-class container support built directly into the SDK, meaning you can produce images without even writing a Dockerfile if you want. We'll cover both approaches.
Prerequisites
Before you containerize your .NET 9 app, make sure you have:
- Docker Desktop (Windows, macOS) or Docker Engine (Linux) installed and running.
- The .NET 9 SDK installed locally.
- A basic ASP.NET Core project, or you can create one with the command below.
// Create a new minimal Web API project
dotnet new webapi -n DockerDemo
cd DockerDemo
// Run it locally to confirm it works
dotnet run
Writing Your First .NET 9 Dockerfile
A Dockerfile is a plain-text recipe that tells Docker how to build your image. The single most important best practice for a .NET 9 Dockerfile is the multi-stage build. This pattern uses one large image to compile your app and a separate, tiny image to run it — keeping your final image small and secure.
Create a file named Dockerfile (no extension) in your project root:
# ---- Stage 1: Build ----
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy csproj and restore first (better layer caching)
COPY ["DockerDemo.csproj", "./"]
RUN dotnet restore "DockerDemo.csproj"
# Copy the rest of the source and publish
COPY . .
RUN dotnet publish "DockerDemo.csproj" -c Release -o /app/publish --no-restore
# ---- Stage 2: Runtime ----
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
# Expose the port and define the entry point
EXPOSE 8080
ENTRYPOINT ["dotnet", "DockerDemo.dll"]
Notice the port 8080. This is important: starting with .NET 8 and continuing in .NET 9, the official container images default to port 8080 (not 80) because the containers now run as a non-root user that can't bind to privileged ports below 1024. This is a security improvement you get for free.
Why Copy the .csproj First?
Docker builds images in layers and caches each one. By copying only the .csproj and running dotnet restore before copying your source code, Docker can reuse the restored NuGet packages layer whenever you change a .cs file but not your dependencies. This single trick can cut your rebuild times from minutes to seconds.
Add a .dockerignore File
Just like .gitignore, a .dockerignore file prevents unnecessary files from being copied into the build context. This speeds up builds and avoids leaking secrets or bloating your image.
# .dockerignore
bin/
obj/
.vs/
.git/
.gitignore
**/appsettings.Development.json
Dockerfile
docker-compose.yml
Building and Running Your Container
Now build the image and run it. The -t flag tags your image with a friendly name:
# Build the image
docker build -t dockerdemo:latest .
# Run the container, mapping host port 5000 to container port 8080
docker run -d -p 5000:8080 --name myapp dockerdemo:latest
# Check it's running
docker ps
# View logs
docker logs myapp
Open http://localhost:5000/weatherforecast in your browser and you'll see your containerized ASP.NET Core API responding. Congratulations — you just dockerized a .NET 9 app!
The Easier Way: SDK Container Publishing
.NET 9 can build a container image without a Dockerfile at all. This is perfect for simple apps and CI pipelines. Just run:
# Publish directly to a container image
dotnet publish -c Release -t:PublishContainer
# Customize the image name and tag in your .csproj
# <ContainerRepository>dockerdemo</ContainerRepository>
# <ContainerImageTag>1.0.0</ContainerImageTag>
Under the hood, this uses the same Microsoft base images and applies sensible defaults. For most microservices, this is all you need. Reach for a hand-written Dockerfile when you need custom system packages, multiple build steps, or fine-grained control.
Using Docker Compose for Multi-Container Apps
Real-world apps rarely run alone — they need a database, a cache, or a message queue. Docker Compose lets you define and run multiple containers together with a single YAML file. Here's a typical setup pairing your ASP.NET Core app with a PostgreSQL database:
# docker-compose.yml
services:
webapi:
build: .
ports:
- "5000:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ConnectionStrings__Default=Host=db;Database=appdb;Username=postgres;Password=secret
depends_on:
- db
db:
image: postgres:17
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=appdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Start the entire stack with one command:
docker compose up -d --build
Notice how the connection string uses Host=db — that's the service name from the Compose file. Docker Compose creates an internal network where containers reach each other by service name, so you never hardcode IP addresses.
Best Practices for Production-Ready .NET 9 Containers
Getting a container to run is easy. Making it production-grade requires a few more steps. Here are the best practices every senior developer applies when deploying ASP.NET Core with Docker:
1. Use the Chiseled or Alpine Images for Smaller Size
Microsoft offers ultra-small "chiseled" Ubuntu images that strip out shells and package managers, drastically reducing attack surface and image size:
FROM mcr.microsoft.com/dotnet/aspnet:9.0-noble-chiseled AS final
A chiseled image can be under 110 MB compared to 220 MB+ for the standard image. Smaller images pull faster, cost less to store, and have fewer vulnerabilities.
2. Run as a Non-Root User
The official .NET 9 images already default to a non-root user. If you build a custom base, explicitly set USER app so a compromised process can't escalate privileges.
3. Add a Health Check
Orchestrators like Kubernetes need to know if your app is healthy. Add a health check endpoint and configure it:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapControllers();
app.Run();
# In your Dockerfile
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
4. Never Bake Secrets Into Images
Connection strings, API keys, and passwords should come from environment variables, Docker secrets, or a secrets manager — never hardcoded in the Dockerfile or committed to source control. Anyone who pulls your image can inspect every layer.
Common Pitfalls to Avoid
- Forgetting the port: Mapping to port 80 when .NET 9 listens on 8080 is the #1 reason "the container runs but I can't reach it."
- Single-stage builds: Shipping the full SDK image to production bloats your image 4x and includes compilers attackers can abuse.
- Ignoring layer caching: Copying everything before restoring NuGet packages forces a full rebuild on every code change.
- No .dockerignore: Copying
bin/andobj/folders can cause confusing build errors and leak local artifacts. - Latest tag in production: Pin specific versions (e.g.
9.0) instead oflatestfor reproducible deployments.
Deploying Your Container to the Cloud
Once your image is built, push it to a registry and deploy. Push to Docker Hub, Azure Container Registry, or Amazon ECR:
# Tag and push to a registry
docker tag dockerdemo:latest myregistry.azurecr.io/dockerdemo:1.0.0
docker push myregistry.azurecr.io/dockerdemo:1.0.0
From there, services like Azure Container Apps, AWS ECS/Fargate, Google Cloud Run, and Kubernetes can pull and run your image with auto-scaling, load balancing, and rolling deployments. Because your app is containerized, moving between these platforms requires almost no code changes — that's the portability payoff.
Key Takeaways
You've learned how to use Docker with .NET 9 to containerize and deploy a real ASP.NET Core application from start to finish. Here's what to remember:
- Use multi-stage builds to keep production images small and secure.
- Remember that .NET 9 containers listen on port 8080 by default and run as a non-root user.
- Copy your
.csprojand restore before copying source to take advantage of layer caching. - Add a
.dockerignorefile to speed up builds and protect secrets. - Use Docker Compose to run your app alongside databases and other services locally.
- For the simplest path, try SDK container publishing with
dotnet publish -t:PublishContainer— no Dockerfile required. - Choose chiseled images, add health checks, and keep secrets out of your images for production.
Containerizing your ASP.NET Core app with Docker and .NET 9 is the gateway to modern cloud-native deployment. Start with the multi-stage Dockerfile above, experiment with Docker Compose, and you'll be deploying portable, scalable .NET apps in no time. Happy containerizing!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment