Skip to main content

Docker with .NET 9: Containerize ASP.NET Core (2026)

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/ and obj/ folders can cause confusing build errors and leak local artifacts.
  • Latest tag in production: Pin specific versions (e.g. 9.0) instead of latest for 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 .csproj and restore before copying source to take advantage of layer caching.
  • Add a .dockerignore file 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!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...