42. Docker & Containerization
About this chapter
Containerize your .NET 10 API using Docker for consistent, portable deployments across development, staging, and production environments.
- Docker benefits: Consistency, isolation, easy deployment, and scalability
- Images and containers: Understanding the blueprint and running instance
- Dockerfile creation: Multi-stage builds for optimized images
- Docker Compose: Running multi-container applications locally
- Registry management: Storing and retrieving container images
- Best practices: Layer caching, minimal images, and security
Learning outcomes:
- Understand Docker concepts and benefits
- Write Dockerfiles with multi-stage builds
- Build and run Docker containers
- Use Docker Compose for local development
- Push images to container registries
- Optimize Docker images for size and build time
42.1 Why Docker?
Without Docker:
Developer: "It works on my machine!"
DevOps: "But it doesn't on production"
(Different OS, .NET version, libraries)
With Docker:
Developer: "It works on my machine!"
DevOps: "It works everywhere—same container"
(Consistent environment, dependencies bundled)
Benefits of containerization:
- Consistency: Same environment dev → staging → production
- Isolation: API doesn’t interfere with other services
- Easy deployment: Container image → cloud → running service
- Scaling: Spawn multiple container instances
- Version control: Tag images with versions
- Resource efficiency: Lightweight vs virtual machines
42.2 Docker Fundamentals
Key concepts:
- Image: Blueprint for a container (like a class)
- Container: Running instance of an image (like an object)
- Dockerfile: Instructions to build an image
- Registry: Storage for images (Docker Hub, Azure Container Registry, etc.)
- Layer: Each instruction in Dockerfile creates a layer (cached for efficiency)
Analogy:
Dockerfile → Image → Container
Recipe → Baked → Cake
Good to eat
42.3 Multi-Stage Dockerfile for .NET 10
Single-stage build (large image—not recommended):
FROM mcr.microsoft.com/dotnet/sdk:10.0
WORKDIR /app
COPY . .
RUN dotnet build
RUN dotnet publish -o out
ENTRYPOINT ["dotnet", "out/CommandAPI.dll"]
Problems:
- Image contains the entire .NET SDK (1+ GB)
- Unnecessary for running (only need runtime)
- Security risk (build tools in production)
Multi-stage build (smaller, secure image):
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS builder
WORKDIR /app
# Copy project files
COPY CommandAPI/CommandAPI.csproj ./CommandAPI/
COPY CommandAPI.Tests/CommandAPI.Tests.csproj ./CommandAPI.Tests/
# Restore dependencies (cached layer)
RUN dotnet restore CommandAPI/CommandAPI.csproj
# Copy source code
COPY CommandAPI/ ./CommandAPI/
# Build the project
RUN dotnet build -c Release --no-restore
# Publish the application
RUN dotnet publish CommandAPI/CommandAPI.csproj \
-c Release \
-o /app/published \
--no-build
# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
# Copy only the published output (not the SDK)
COPY --from=builder /app/published .
# Expose port
EXPOSE 8080
# Set environment variables
ENV ASPNETCORE_URLS=http://+:8080
# Health check
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Run the application
ENTRYPOINT ["dotnet", "CommandAPI.dll"]
Why multi-stage is better:
Single-stage: 1.2 GB (entire SDK included)
Multi-stage: 200 MB (only runtime needed)
Reduction: 80% smaller!
Layer caching optimization:
# Copy .csproj first (rarely changes)
# This creates a cached layer
COPY CommandAPI/CommandAPI.csproj ./CommandAPI/
RUN dotnet restore CommandAPI/CommandAPI.csproj
# Copy source code (frequently changes)
# This layer rebuilds when code changes, but restore is cached
COPY CommandAPI/ ./CommandAPI/
RUN dotnet build -c Release --no-restore
If code changes, only the build step rebuilds. Restore is cached from previous builds.
42.4 Building Docker Images
Build the image:
# Build image with tag
docker build -t commandapi:1.0 .
# Build with multiple tags
docker build -t commandapi:1.0 -t commandapi:latest .
# Build specific Dockerfile
docker build -t commandapi:1.0 -f Dockerfile.prod .
# Build without cache (force rebuild)
docker build --no-cache -t commandapi:1.0 .
# View build progress
docker build -t commandapi:1.0 --progress=plain .
Verify the image:
# List images
docker images
# Output:
# REPOSITORY TAG IMAGE ID SIZE
# commandapi 1.0 a1b2c3d4 198MB
# commandapi latest a1b2c3d4 198MB
42.5 Running Containers Locally
Run a container:
# Basic run
docker run -p 8080:8080 commandapi:1.0
# Run in background (detached mode)
docker run -d -p 8080:8080 --name myapi commandapi:1.0
# Run with environment variables
docker run -d \
-p 8080:8080 \
-e ConnectionStrings__PostgreSqlConnection="Host=postgres;Database=commandapi;Username=user;Password=pass" \
-e ASPNETCORE_ENVIRONMENT=Development \
commandapi:1.0
# Run with volume mount (for development)
docker run -d \
-p 8080:8080 \
-v /home/dev/commandapi/appsettings.Development.json:/app/appsettings.Development.json \
commandapi:1.0
# Run with resource limits
docker run -d \
-p 8080:8080 \
--memory 512m \
--cpus 1 \
commandapi:1.0
Test the running container:
# Make request to API
curl http://localhost:8080/api/commands
# View container logs
docker logs myapi
# Execute command inside container
docker exec myapi dotnet --version
# Stop container
docker stop myapi
# Remove container
docker rm myapi
42.6 Docker Compose for Multi-Service Setup
docker-compose.yml for the API:
version: "3.8"
services:
api:
build:
context: .
dockerfile: Dockerfile
container_name: commandapi
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Development
ConnectionStrings__PostgreSqlConnection: "Host=postgres;Database=commandapi;Username=postgres;Password=${DB_PASSWORD}"
ASPNETCORE_URLS: http://+:8080
depends_on:
postgres:
condition: service_healthy
networks:
- app-network
volumes:
- ./logs:/app/logs
postgres:
image: postgres:16
container_name: commandapi-postgres
environment:
POSTGRES_DB: commandapi
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7
container_name: commandapi-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- app-network
adminer:
image: adminer
container_name: commandapi-adminer
ports:
- "8081:8080"
depends_on:
- postgres
networks:
- app-network
volumes:
postgres-data:
redis-data:
networks:
app-network:
driver: bridge
Use environment variables securely:
# Create .env file (add to .gitignore)
echo "DB_PASSWORD=SuperSecretPassword123!" > .env
# Run with .env
docker-compose --env-file .env up
# Or set environment variable
export DB_PASSWORD="SuperSecretPassword123!"
docker-compose up
Docker Compose commands:
# Start all services
docker-compose up
# Start in background
docker-compose up -d
# View logs
docker-compose logs
# View logs for specific service
docker-compose logs api
# Stop all services
docker-compose down
# Stop and remove volumes (careful—deletes data!)
docker-compose down -v
# Rebuild images
docker-compose build
# Rebuild and start
docker-compose up --build
# Scale a service (run multiple instances)
docker-compose up -d --scale api=3
42.7 Docker Networking
Networks allow containers to communicate:
networks:
app-network:
driver: bridge
Service discovery:
Inside a container, use service name as hostname:
// Inside api container, connect to postgres
var connectionString = "Host=postgres;Database=commandapi;...";
// "postgres" resolves to the postgres container's IP
Port mapping:
ports:
- "8080:8080" # host:container
Inside the container, the app listens on port 8080 (internal). From the host, access via port 8080.
Network modes:
# Bridge network (default, recommended)
networks:
- app-network
# Host network (share host's network stack)
network_mode: host
# No network
network_mode: none
42.8 Volume Management
Volumes persist data after container stops:
volumes:
postgres-data:
redis-data:
services:
postgres:
volumes:
- postgres-data:/var/lib/postgresql/data
Types of mounts:
# Named volume (Docker manages storage)
volumes:
- postgres-data:/var/lib/postgresql/data
# Bind mount (host directory)
volumes:
- ./logs:/app/logs
# Readonly mount
volumes:
- ./config.json:/app/config.json:ro
Volume commands:
# List volumes
docker volume ls
# Inspect volume
docker volume inspect commandapi_postgres-data
# Remove unused volumes
docker volume prune
# Remove specific volume
docker volume rm commandapi_postgres-data
42.9 Dockerfile Best Practices
1. Use specific base image tags:
# ❌ WRONG - latest can change unexpectedly
FROM mcr.microsoft.com/dotnet/aspnet:latest
# ✓ CORRECT - pinned version
FROM mcr.microsoft.com/dotnet/aspnet:10.0
2. Run as non-root user:
# Create non-root user
RUN useradd -m -u 1000 appuser
USER appuser
# Set permission for working directory
RUN chown -R appuser:appuser /app
3. Minimize layer count:
# ❌ WRONG - 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# ✓ CORRECT - 1 layer
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
4. Use .dockerignore:
# .dockerignore
*.md
.git
.gitignore
.vs
.vscode
bin
obj
test
*.trx
5. Include health checks:
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
6. Don’t run as root:
# ❌ WRONG
USER root
# ✓ CORRECT
RUN useradd -m appuser
USER appuser
42.10 Image Registry and Pushing
Docker Hub (public):
# Login to Docker Hub
docker login
# Tag image
docker tag commandapi:1.0 yourusername/commandapi:1.0
# Push to Docker Hub
docker push yourusername/commandapi:1.0
# Pull from Docker Hub
docker pull yourusername/commandapi:1.0
Azure Container Registry (private):
# Login to ACR
az acr login --name myregistry
# Tag image
docker tag commandapi:1.0 myregistry.azurecr.io/commandapi:1.0
# Push to ACR
docker push myregistry.azurecr.io/commandapi:1.0
# Pull from ACR
docker pull myregistry.azurecr.io/commandapi:1.0
GitHub Container Registry:
# Login with GitHub token
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin
# Tag image
docker tag commandapi:1.0 ghcr.io/yourusername/commandapi:1.0
# Push
docker push ghcr.io/yourusername/commandapi:1.0
42.11 Debugging Docker Containers
View running containers:
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View container details
docker inspect myapi
View logs:
# View logs
docker logs myapi
# Follow logs (tail -f)
docker logs -f myapi
# View last 100 lines
docker logs --tail 100 myapi
# View logs since specific time
docker logs --since 10m myapi
Execute commands inside container:
# Interactive bash shell
docker exec -it myapi bash
# Run dotnet CLI
docker exec myapi dotnet --version
# Check environment variables
docker exec myapi env | grep ASPNETCORE
Common issues and solutions:
# Port already in use
# Solution: Use different port or stop conflicting container
docker run -p 9080:8080 commandapi:1.0
# Container exits immediately
# Solution: Check logs
docker logs myapi
# Container out of memory
# Solution: Increase memory limit
docker run --memory 1g commandapi:1.0
# Slow startup
# Solution: Check health checks
curl http://localhost:8080/health
42.12 Image Size Optimization
Analyze image layers:
# View image history
docker history commandapi:1.0
# Output shows each layer and size
Reduce image size:
- Use alpine images (if available):
# Standard: 352 MB
FROM mcr.microsoft.com/dotnet/aspnet:10.0
# Alpine-based: 200 MB (smaller, but has some limitations)
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
- Remove unnecessary files:
RUN apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
Use multi-stage builds (already covered)
-
Exclude unnecessary files (.dockerignore)
Typical image sizes:
.NET 10 SDK: 1.2 GB
.NET 10 Runtime: 200 MB
Multi-stage release build: 150-250 MB
Alpine-based: 100-150 MB
42.13 What’s Next
You now have:
- ✓ Understanding why Docker matters
- ✓ Multi-stage Dockerfiles for .NET 10
- ✓ Building and running containers
- ✓ Docker Compose for multi-service setup
- ✓ Secure environment variable handling
- ✓ Docker networking and communication
- ✓ Volume management for persistence
- ✓ Registry management (Docker Hub, Azure, GitHub)
- ✓ Debugging and troubleshooting
- ✓ Image size optimization
Next: Configuration Management—Managing application settings across environments safely.