docker-containers

v2026.09.25

Multi-container .NET applications with Docker Compose, container orchestration, and development environments

GitHub
Install command
npx skhub add thelobbi/docker-containers
Markdown
SKILL.md

Multi-Container .NET Applications

Multi-Stage Dockerfile (.NET 10)

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy solution and project files for layer caching
COPY *.sln .
COPY Directory.Build.props Directory.Packages.props ./
COPY src/MyApp.Web/MyApp.Web.csproj src/MyApp.Web/
COPY src/MyApp.Api/MyApp.Api.csproj src/MyApp.Api/
COPY src/MyApp.Domain/MyApp.Domain.csproj src/MyApp.Domain/
COPY src/MyApp.Infrastructure/MyApp.Infrastructure.csproj src/MyApp.Infrastructure/
RUN dotnet restore

# Copy source and publish
COPY . .
RUN dotnet publish src/MyApp.Web/MyApp.Web.csproj -c Release -o /app/publish --no-restore

# Runtime stage (minimal image)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV DOTNET_RUNNING_IN_CONTAINER=true

# Non-root user for security
USER $APP_UID

COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.Web.dll"]

Docker Compose (Development)

services:
  # Blazor Web App
  webapp:
    build:
      context: .
      dockerfile: src/MyApp.Web/Dockerfile
    ports:
      - "5000:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Host=postgres;Database=myapp;Username=postgres;Password=devpass
      - ConnectionStrings__Redis=redis:6379
      - RabbitMQ__Host=rabbitmq
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ~/.aspnet/https:/https:ro  # Dev HTTPS certs
    networks:
      - backend

  # API Service
  api:
    build:
      context: .
      dockerfile: src/MyApp.Api/Dockerfile
    ports:
      - "5001:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Host=postgres;Database=myapp;Username=postgres;Password=devpass
      - ConnectionStrings__Redis=redis:6379
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - backend

  # Worker Service
  worker:
    build:
      context: .
      dockerfile: src/MyApp.Worker/Dockerfile
    environment:
      - ConnectionStrings__Default=Host=postgres;Database=myapp;Username=postgres;Password=devpass
      - RabbitMQ__Host=rabbitmq
    depends_on:
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - backend

  # PostgreSQL
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: devpass
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # Redis
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - backend

  # RabbitMQ
  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"   # AMQP
      - "15672:15672" # Management UI
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # SQL Server (alternative to PostgreSQL)
  # sqlserver:
  #   image: mcr.microsoft.com/mssql/server:2022-latest
  #   environment:
  #     ACCEPT_EULA: "Y"
  #     MSSQL_SA_PASSWORD: "YourStrong!Password"
  #   ports:
  #     - "1433:1433"

volumes:
  postgres_data:
  redis_data:

networks:
  backend:
    driver: bridge

Integration Event Bus (RabbitMQ)

// Integration event (crosses service boundaries)
public sealed record OrderPaymentSucceededIntegrationEvent(
    int OrderId, DateTime PaymentDate) : IntegrationEvent;

// Event bus interface
public interface IEventBus
{
    Task PublishAsync<T>(T @event, CancellationToken ct = default) where T : IntegrationEvent;
    void Subscribe<T, THandler>() where T : IntegrationEvent where THandler : IIntegrationEventHandler<T>;
}

// Integration event handler
public sealed class OrderPaymentSucceededHandler(
    IOrderRepository orderRepo,
    ILogger<OrderPaymentSucceededHandler> logger)
    : IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>
{
    public async Task Handle(OrderPaymentSucceededIntegrationEvent @event, CancellationToken ct)
    {
        logger.LogInformation("Payment succeeded for order {OrderId}", @event.OrderId);
        var order = await orderRepo.GetAsync(@event.OrderId, ct);
        order?.SetPaidStatus();
        await orderRepo.UnitOfWork.SaveEntitiesAsync(ct);
    }
}

API Gateway Pattern

// Using YARP (Yet Another Reverse Proxy) - Microsoft's recommended gateway
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();
app.MapReverseProxy();
app.Run();
// appsettings.json for YARP
{
  "ReverseProxy": {
    "Routes": {
      "catalog-route": {
        "ClusterId": "catalog-cluster",
        "Match": { "Path": "/api/catalog/{**catch-all}" }
      },
      "ordering-route": {
        "ClusterId": "ordering-cluster",
        "Match": { "Path": "/api/orders/{**catch-all}" }
      }
    },
    "Clusters": {
      "catalog-cluster": {
        "Destinations": {
          "destination1": { "Address": "http://catalog-api:8080/" }
        }
      },
      "ordering-cluster": {
        "Destinations": {
          "destination1": { "Address": "http://ordering-api:8080/" }
        }
      }
    }
  }
}

.NET Docker Images

ImageUse forSize
mcr.microsoft.com/dotnet/sdk:10.0Build stage only~800MB
mcr.microsoft.com/dotnet/aspnet:10.0Web apps, APIs~220MB
mcr.microsoft.com/dotnet/runtime:10.0Console/worker apps~190MB
mcr.microsoft.com/dotnet/runtime-deps:10.0Self-contained AOT~110MB

Best Practices

  • Use multi-stage builds (build with SDK, run with aspnet/runtime)
  • Copy .csproj files first for Docker layer caching on restore
  • Use health checks in docker-compose for dependency ordering
  • Run as non-root user in production containers
  • Use .dockerignore to exclude bin/, obj/, .git/
  • Pin image versions (never use :latest alone in production)
  • Use named volumes for persistent data

Reference

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.25

Published

Sep 25, 2026

Category

Uncategorized

License

MIT

Source path

plugins/dotnet-blazor/skills/docker-containers

Default branch

main

Latest commit

2f1269c

Tree SHA

629e050