Docker Deployment
Framework: Excalibur.Dispatch Deployment Target: Docker containers Last Updated: 2026-01-01
Before You Start
- .NET 8.0+ (or .NET 9/10 for latest features)
- Docker Desktop or Docker Engine installed
- Familiarity with ASP.NET Core deployment and dependency injection
Overview
This guide covers Docker containerization for Excalibur applications, including multi-stage builds, production optimizations, and container orchestration patterns.
Use Docker when:
- Deploying to cloud platforms (Azure Container Instances, AWS ECS, Google Cloud Run)
- Running in Kubernetes clusters
- Ensuring consistent environments across dev, staging, and production
- Simplifying dependency management
Quick Start
Minimal Dockerfile
# Minimal ASP.NET Core application with Excalibur.Dispatch
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime
WORKDIR /app
COPY publish/ ./
ENTRYPOINT ["dotnet", "YourApp.dll"]
Build and Run
# Publish application
dotnet publish -c Release -o publish
# Build Docker image
docker build -t your-app:latest .
# Run container
docker run -p 8080:8080 your-app:latest
Production-Ready Dockerfile
Multi-Stage Build
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /src
# Copy project files
COPY ["src/YourApp/YourApp.csproj", "src/YourApp/"]
COPY ["src/YourApp.Domain/YourApp.Domain.csproj", "src/YourApp.Domain/"]
# Restore dependencies
RUN dotnet restore "src/YourApp/YourApp.csproj"
# Copy source code
COPY . .
# Build application
WORKDIR "/src/src/YourApp"
RUN dotnet build "YourApp.csproj" -c Release -o /app/build
# Publish stage
FROM build AS publish
RUN dotnet publish "YourApp.csproj" \
-c Release \
-o /app/publish \
--no-restore \
/p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime
WORKDIR /app
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -u 1000 -G appuser -s /bin/sh -D appuser
# Copy published files
COPY --from=publish /app/publish .
# Set ownership
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Expose port
EXPOSE 8080
# Entry point
ENTRYPOINT ["dotnet", "YourApp.dll"]
Benefits:
- ✅ Multi-stage build minimizes final image size (~100MB vs ~1GB)
- ✅ Alpine Linux base reduces attack surface
- ✅ Non-root user improves security
- ✅ Health check enables container orchestration
- ✅ Layer caching optimizes build time
Environment-Specific Configurations
Development (docker-compose.yml)
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:8080
- ConnectionStrings__Default=Server=db;Database=AppDb;User=sa;Password=YourStrong!Passw0rd
depends_on:
db:
condition: service_healthy
networks:
- app-network
db:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=YourStrong!Passw0rd
ports:
- "1433:1433"
healthcheck:
test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong!Passw0rd -Q 'SELECT 1' -C || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
networks:
- app-network
volumes:
- sqldata:/var/opt/mssql
networks:
app-network:
driver: bridge
volumes:
sqldata:
Usage:
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f app
# Stop all services
docker-compose down
# Clean up volumes
docker-compose down -v
Production (Azure Container Registry)
# Production Dockerfile with optimizations
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /src
# Copy and restore (cached layer)
COPY ["*.sln", "./"]
COPY ["src/**/*.csproj", "src/"]
RUN for file in $(find src -name "*.csproj"); do \
mkdir -p $(dirname $file) && \
mv $(basename $file) $file; \
done
RUN dotnet restore
# Copy source and build
COPY . .
RUN dotnet publish "src/YourApp/YourApp.csproj" \
-c Release \
-o /app/publish \
--no-restore \
-p:PublishTrimmed=false \
-p:PublishSingleFile=false
# Runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
WORKDIR /app
# Install curl for health checks
RUN apk add --no-cache curl
# Non-root user
RUN addgroup -g 1000 appuser && \
adduser -u 1000 -G appuser -s /bin/sh -D appuser
# Copy application
COPY --from=build /app/publish .
RUN chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourApp.dll"]
Deployment:
# Build image
docker build -t yourregistry.azurecr.io/your-app:v1.0.0 .
# Push to registry
docker push yourregistry.azurecr.io/your-app:v1.0.0
# Deploy to Azure Container Instances
az container create \
--resource-group your-rg \
--name your-app \
--image yourregistry.azurecr.io/your-app:v1.0.0 \
--dns-name-label your-app \
--ports 8080
Excalibur Configuration
Outbox Pattern with Background Service
# Enable background services in container
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
WORKDIR /app
# ... (previous steps)
# Required for background services
ENV DOTNET_RUNNING_IN_CONTAINER=true
# Configure outbox processor interval
ENV Dispatch__Outbox__ProcessorInterval=00:00:30
ENV Dispatch__Outbox__BatchSize=100
ENTRYPOINT ["dotnet", "YourApp.dll"]
Program.cs configuration:
var builder = WebApplication.CreateBuilder(args);
// Configure outbox with SQL Server storage and background processing
builder.Services.AddExcaliburOutbox(outbox =>
{
outbox.UseSqlServer(builder.Configuration.GetConnectionString("Default")!)
.WithProcessing(p => p.BatchSize(100)
.PollingInterval(TimeSpan.FromSeconds(30)))
.EnableBackgroundProcessing();
});
var app = builder.Build();
app.Run();
Event Sourcing with Projections
# Multi-container setup for event sourcing
# docker-compose.yml
services:
write-api:
build:
context: .
dockerfile: Dockerfile.WriteApi
environment:
- EventStore__ConnectionString=Server=eventstore;Database=Events;...
depends_on:
- eventstore
networks:
- app-network
projections:
build:
context: .
dockerfile: Dockerfile.Projections
environment:
- EventStore__ConnectionString=Server=eventstore;Database=Events;...
- ReadStore__ConnectionString=Server=readstore;Database=Projections;...
depends_on:
- eventstore
- readstore
networks:
- app-network
read-api:
build:
context: .
dockerfile: Dockerfile.ReadApi
environment:
- ReadStore__ConnectionString=Server=readstore;Database=Projections;...
depends_on:
- readstore
networks:
- app-network
eventstore:
image: mcr.microsoft.com/mssql/server:2022-latest
# ... (SQL Server configuration)
readstore:
image: mcr.microsoft.com/mssql/server:2022-latest
# ... (SQL Server configuration)
networks:
app-network:
Security Best Practices
1. Use Non-Root Users
# GOOD: Non-root user
RUN addgroup -g 1000 appuser && \
adduser -u 1000 -G appuser -s /bin/sh -D appuser
USER appuser
# BAD: Running as root
# (no USER directive = runs as root)
2. Scan for Vulnerabilities
# Using Docker Scout
docker scout cves your-app:latest
# Using Trivy
trivy image your-app:latest
3. Use Minimal Base Images
# GOOD: Alpine (smaller attack surface)
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
# ACCEPTABLE: Debian Slim
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
# AVOID: Full Debian (larger attack surface)
# FROM mcr.microsoft.com/dotnet/aspnet:9.0
4. Secrets Management
# GOOD: Use Docker secrets or environment variables
docker run -e ConnectionStrings__Default="$CONNECTION_STRING" your-app
# BAD: Hardcoding secrets in Dockerfile
# ENV ConnectionStrings__Default="Server=..."
Better: Use Azure Key Vault or AWS Secrets Manager
// Program.cs
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsProduction())
{
// Load secrets from Azure Key Vault
builder.Configuration.AddAzureKeyVault(
new Uri($"https://{vaultName}.vault.azure.net/"),
new DefaultAzureCredential());
}
Optimizations
Build Cache Optimization
# GOOD: Copy only project files first (cached layer)
COPY ["src/YourApp/YourApp.csproj", "src/YourApp/"]
RUN dotnet restore
# Then copy source (invalidates cache only when code changes)
COPY . .
RUN dotnet build
# BAD: Copy everything at once (cache invalidated on any change)
# COPY . .
# RUN dotnet restore && dotnet build
Multi-Platform Builds
# Build for multiple architectures
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t your-app:latest \
--push \
.
Image Size Reduction
# Use trimming (reduces size by ~30%)
RUN dotnet publish \
-c Release \
-o /app/publish \
-p:PublishTrimmed=true \
-p:TrimMode=partial
Before: 120MB After: 85MB
Health Checks
ASP.NET Core Health Checks
// Program.cs
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database")
.AddCheck<OutboxHealthCheck>("outbox");
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
Dockerfile Health Check
# HTTP health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# For images without curl
HEALTHCHECK --interval=30s CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
Troubleshooting
Container Won't Start
# Check logs
docker logs <container-id>
# Interactive shell
docker run -it --entrypoint /bin/sh your-app:latest
# Check environment variables
docker inspect <container-id> | grep Env -A 20
High Memory Usage
# Limit memory
docker run -m 512m your-app:latest
# Monitor resource usage
docker stats <container-id>
Connection Issues
# Check network
docker network inspect bridge
# Test connectivity
docker run --rm --network container:<container-id> alpine ping -c 3 db
CI/CD Integration
GitHub Actions
name: Build and Push Docker Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: yourregistry.azurecr.io
username: ${{ secrets.ACR_USERNAME }}
password: ${{ secrets.ACR_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: yourregistry.azurecr.io/your-app:${{ github.sha }}
cache-from: type=registry,ref=yourregistry.azurecr.io/your-app:latest
cache-to: type=inline
Next Steps
- Kubernetes: Deploy to Kubernetes for production orchestration
- Azure: Azure Functions for serverless deployment
- Security: Security Best Practices for hardening
- Monitoring: Health Checks for observability
See Also
- Kubernetes Deployment - Orchestrate Docker containers in production with Kubernetes
- On-Premises Deployment - Deploy to on-premises servers using IIS, Windows Services, or systemd
- Health Checks - Configure health check endpoints for container orchestration
Last Updated: 2026-01-01 Framework: Excalibur 1.0.0 Target: Docker 20.10+, .NET 9.0