
Every microservices project I've worked on runs on Docker. It solves the "works on my machine" problem, makes deployments reproducible, and lets you scale individual services independently. Here's my practical approach to containerizing .NET microservices.
When you have multiple services — an order service, a payment service, a notification service — each with its own dependencies and runtime requirements, Docker gives you:
docker compose up and everything is runningA well-structured Dockerfile for .NET uses multi-stage builds to keep the final image small:
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy csproj files first for layer caching
COPY ["src/OrderService.API/OrderService.API.csproj", "OrderService.API/"]
COPY ["src/OrderService.Application/OrderService.Application.csproj", "OrderService.Application/"]
COPY ["src/OrderService.Domain/OrderService.Domain.csproj", "OrderService.Domain/"]
COPY ["src/OrderService.Infrastructure/OrderService.Infrastructure.csproj", "OrderService.Infrastructure/"]
RUN dotnet restore "OrderService.API/OrderService.API.csproj"
# Copy everything and build
COPY src/ .
RUN dotnet publish "OrderService.API/OrderService.API.csproj" \
-c Release -o /app/publish --no-restore
# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "OrderService.API.dll"]Key optimizations:
.csproj files first — Docker caches the restore step, so unchanged dependencies don't re-downloadWith multiple services, Docker Compose orchestrates everything:
services:
order-service:
build: ./src/OrderService
ports:
- "5001:8080"
environment:
- ConnectionStrings__Database=Server=db;Database=Orders;User=sa;Password=Strong@Pass1;TrustServerCertificate=true
- RabbitMQ__Host=rabbitmq
depends_on:
db:
condition: service_healthy
rabbitmq:
condition: service_healthy
payment-service:
build: ./src/PaymentService
ports:
- "5002:8080"
environment:
- ConnectionStrings__Database=Server=db;Database=Payments;User=sa;Password=Strong@Pass1;TrustServerCertificate=true
- RabbitMQ__Host=rabbitmq
depends_on:
db:
condition: service_healthy
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- SA_PASSWORD=Strong@Pass1
ports:
- "1433:1433"
healthcheck:
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Strong@Pass1" -C -Q "SELECT 1"
interval: 10s
retries: 5
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672"
- "15672:15672"
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 10s
retries: 5One command — docker compose up — and you have a full microservices environment with SQL Server and RabbitMQ.
Notice the depends_on with condition: service_healthy. This prevents services from starting before their dependencies are ready. Without this, your API would crash on startup trying to connect to a database that's still initializing.
In your .NET service, add a health check endpoint:
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString)
.AddRabbitMQ(rabbitConnectionString);
app.MapHealthChecks("/health");Docker makes it easy to swap configurations per environment. In .NET, environment variables override appsettings.json:
// appsettings.json — defaults for development
{
"RabbitMQ": {
"Host": "localhost",
"Port": 5672
}
}
// In Docker, override with environment variables:
// RabbitMQ__Host=rabbitmq
// RabbitMQ__Port=5672The double underscore (__) maps to the JSON nesting. No code changes needed between environments.
In our Azure DevOps pipeline, every push triggers:
- task: Docker@2
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryConnection)
tags: |
$(Build.BuildId)
latestAfter running Docker in production across multiple projects, here's what I've learned:
latest. Pin your base images to avoid surprise breaking changes..dockerignore to exclude bin/, obj/, .git/, and other unnecessary files from the build context.Docker isn't just a deployment tool — it's a development tool that makes microservices manageable from day one.