.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:
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:
# 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:
-
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
-
We first copy just the project file and restore dependencies separately to take advantage of Docker's layer caching
-
Then we copy the rest of the code and publish the application
-
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:
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:
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:
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:
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:
- Our .NET API (built from our Dockerfile)
- A SQL Server database (using the official Microsoft SQL Server image)
To run this multi-container application:
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
-
Use multi-stage builds to keep final images small
-
Optimize for layers:
- Put files that change less often earlier in the Dockerfile
- Group related commands to reduce layers
-
Set proper environment variables:
dockerfileENV ASPNETCORE_URLS=http://+:80
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false -
Handle application shutdown properly: Configure your app to handle SIGTERM signals for graceful shutdowns
-
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 -
Use health checks to verify application status:
dockerfileHEALTHCHECK --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:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
Create a Product
model:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Create a database context:
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:
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
:
[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:
{
"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:
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
# 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
# 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:
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:
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
- Official .NET Docker Guide
- Docker Documentation
- Microsoft's .NET Docker Images on Docker Hub
- Containerize an ASP.NET Core application
Exercises
- Create a Dockerfile for an existing .NET Core MVC application
- Add a Redis cache container to the docker-compose.yml file and configure your application to use it
- Implement a healthcheck in your Dockerfile that verifies your API is running correctly
- Create a CI/CD pipeline that builds a Docker image and deploys it to a registry
- 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! :)