Skip to main content

.NET Docker Containerization

Introduction

Docker containerization has revolutionized how we build, ship, and run applications. For .NET developers, Docker provides a consistent, lightweight, and efficient way to package applications and their dependencies. This guide will introduce you to containerizing .NET applications using Docker, making your deployment process more reliable and scalable.

Containers are standalone, executable packages that include everything needed to run an application: code, runtime, system tools, system libraries, and settings. This ensures that your application works the same way regardless of where it's deployed.

Why Containerize .NET Applications?

Before diving into the how, let's understand why containerization is beneficial:

  • Consistency: Eliminates "it works on my machine" problems
  • Isolation: Applications run independently without interfering with each other
  • Portability: Run anywhere Docker is installed (development, testing, production)
  • Scalability: Easily scale containers up or down based on demand
  • Efficiency: Uses resources more efficiently than traditional VMs

Prerequisites

To follow along with this guide, you'll need:

  • .NET SDK installed
  • Docker Desktop installed
  • Basic familiarity with .NET development
  • A text editor or IDE (like Visual Studio or VS Code)

Understanding Docker Concepts

Let's quickly review some key Docker concepts:

  • Docker Image: A read-only template with instructions for creating a Docker container
  • Dockerfile: A text file with commands to assemble an image
  • Container: A runnable instance of an image
  • Docker Hub: A registry service for storing and sharing Docker images

Creating a Simple .NET Application

Let's start by creating a simple .NET web API application:

bash
dotnet new webapi -n DockerDemo
cd DockerDemo

This creates a basic web API project with a weather forecast endpoint.

Creating a Dockerfile

In the root of your project, create a file named Dockerfile (no extension) with the following content:

dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /source

# Copy csproj and restore dependencies
COPY *.csproj .
RUN dotnet restore

# Copy all files and build
COPY . .
RUN dotnet publish -c Release -o /app

# Final stage
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app .
EXPOSE 80
ENTRYPOINT ["dotnet", "DockerDemo.dll"]

Let's break down this Dockerfile:

  1. We use a multi-stage build for efficiency:

    • The first stage uses the .NET SDK image to build the application
    • The second stage uses the smaller ASP.NET runtime image for the final container
  2. We first copy just the project file and restore dependencies separately to take advantage of Docker's layer caching

  3. Then we copy the rest of the code and publish the application

  4. Finally, we set up the runtime container, exposing port 80 and defining how to start the application

Building the Docker Image

Now, let's build a Docker image from our Dockerfile:

bash
docker build -t dockerdemo .

This command creates an image named dockerdemo from the Dockerfile in the current directory (.).

After building, you can see your new image by running:

bash
docker images

Output will look something like:

REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
dockerdemo latest a1b2c3d4e5f6 10 seconds ago 209MB

Running the Container

Now let's run our containerized application:

bash
docker run -p 8080:80 dockerdemo

This command:

  • Starts a container from the dockerdemo image
  • Maps port 8080 on your host to port 80 in the container

You can now access your API at http://localhost:8080/weatherforecast.

Docker Compose for Multi-Container Applications

For more complex applications with multiple services (like an API and a database), Docker Compose helps manage multiple containers.

Create a file named docker-compose.yml in your project root:

yaml
version: '3.8'
services:
api:
build: .
ports:
- "8080:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
depends_on:
- db

db:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourStrongPassword123!
ports:
- "1433:1433"
volumes:
- sqldata:/var/opt/mssql

volumes:
sqldata:

This compose file defines two services:

  1. Our .NET API (built from our Dockerfile)
  2. A SQL Server database (using the official Microsoft SQL Server image)

To run this multi-container application:

bash
docker-compose up

Using .dockerignore

Similar to .gitignore, a .dockerignore file specifies which files and directories Docker should ignore when building an image. Create a .dockerignore file in your project root:

bin/
obj/
**/bin/
**/obj/
.vs/
.vscode/
*.user
*.suo

This improves build performance and reduces image size by excluding unnecessary files.

Best Practices for Containerizing .NET Applications

  1. Use multi-stage builds to keep final images small

  2. Optimize for layers:

    • Put files that change less often earlier in the Dockerfile
    • Group related commands to reduce layers
  3. Set proper environment variables:

    dockerfile
    ENV ASPNETCORE_URLS=http://+:80
    ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
  4. Handle application shutdown properly: Configure your app to handle SIGTERM signals for graceful shutdowns

  5. Don't run as root (for security):

    dockerfile
    # Add near the end of your Dockerfile
    RUN useradd -M -s /bin/bash -u 1000 appuser
    USER appuser
  6. Use health checks to verify application status:

    dockerfile
    HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost/health || exit 1

Real-World Example: Containerizing a .NET Microservice

Let's extend our example to a more realistic microservice scenario. We'll add Entity Framework Core for database access and implement a simple API.

First, add the required packages to your project:

bash
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design

Create a Product model:

csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

Create a database context:

csharp
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}

public DbSet<Product> Products { get; set; }
}

Update Program.cs to include Entity Framework configuration:

csharp
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Add a ProductsController:

csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;

public ProductsController(AppDbContext context)
{
_context = context;
}

[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
return await _context.Products.ToListAsync();
}

[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProducts), new { id = product.Id }, product);
}
}

Finally, update your appsettings.json to include the connection string:

json
{
"ConnectionStrings": {
"DefaultConnection": "Server=db;Database=ProductsDb;User=sa;Password=YourStrongPassword123!;TrustServerCertificate=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

Our updated docker-compose.yml would handle running the database migrations:

yaml
version: '3.8'
services:
api:
build: .
ports:
- "8080:80"
environment:
- ASPNETCORE_ENVIRONMENT=Development
depends_on:
- db
# Add a command to run migrations at startup
command: >
bash -c "dotnet ef database update &&
dotnet DockerDemo.dll"

db:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=YourStrongPassword123!
ports:
- "1433:1433"
volumes:
- sqldata:/var/opt/mssql

volumes:
sqldata:

Deploying to Container Registries

Once your containerized application is ready, you can push it to a container registry:

Docker Hub

bash
# Login to Docker Hub
docker login

# Tag your image
docker tag dockerdemo yourusername/dockerdemo:latest

# Push the image
docker push yourusername/dockerdemo:latest

Azure Container Registry

bash
# Login to Azure
az login

# Login to ACR
az acr login --name yourregistryname

# Tag your image
docker tag dockerdemo yourregistryname.azurecr.io/dockerdemo:latest

# Push the image
docker push yourregistryname.azurecr.io/dockerdemo:latest

Orchestration with Kubernetes

For production environments, you might want to use Kubernetes to orchestrate your containers. Here's a simple deployment.yaml for Kubernetes:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dockerdemo
spec:
replicas: 3
selector:
matchLabels:
app: dockerdemo
template:
metadata:
labels:
app: dockerdemo
spec:
containers:
- name: dockerdemo
image: yourusername/dockerdemo:latest
ports:
- containerPort: 80
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
---
apiVersion: v1
kind: Service
metadata:
name: dockerdemo
spec:
selector:
app: dockerdemo
ports:
- port: 80
targetPort: 80
type: LoadBalancer

Deploy to Kubernetes with:

bash
kubectl apply -f deployment.yaml

Summary

In this guide, we've covered:

  • The basics of Docker containerization for .NET applications
  • Creating a Dockerfile for a .NET application
  • Building and running Docker containers
  • Using Docker Compose for multi-container applications
  • Best practices for containerizing .NET applications
  • A real-world example with a database and Entity Framework
  • Deploying containers to registries and Kubernetes

Docker containerization provides a powerful way to package, distribute, and run .NET applications consistently across different environments. By containerizing your .NET applications, you'll improve deployment reliability, development consistency, and application scalability.

Additional Resources

Exercises

  1. Create a Dockerfile for an existing .NET Core MVC application
  2. Add a Redis cache container to the docker-compose.yml file and configure your application to use it
  3. Implement a healthcheck in your Dockerfile that verifies your API is running correctly
  4. Create a CI/CD pipeline that builds a Docker image and deploys it to a registry
  5. Deploy your containerized application to a cloud service like Azure App Service or AWS Elastic Container Service


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)