EngineeringSolution ArchitectPlatform and DevOps

Session 05 - Containerization & Docker

Multi-stage Dockerfile cho .NET

Multi-stage build giữ image production nhỏ gọn bằng cách tách build environment ra khỏi runtime.

dockerfile
# ===== Stage 1: Build =====
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy csproj trước để tận dụng Docker layer cache
COPY ["src/MyApp.Api/MyApp.Api.csproj", "MyApp.Api/"]
COPY ["src/MyApp.Core/MyApp.Core.csproj", "MyApp.Core/"]
COPY ["src/MyApp.Infrastructure/MyApp.Infrastructure.csproj", "MyApp.Infrastructure/"]
RUN dotnet restore "MyApp.Api/MyApp.Api.csproj"

# Copy toàn bộ source code
COPY src/ .

# Build và publish
RUN dotnet publish "MyApp.Api/MyApp.Api.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore \
    /p:UseAppHost=false

# ===== Stage 2: Runtime =====
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app

# Security: chạy non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Copy published output
COPY --from=build /app/publish .

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD wget -qO- http://localhost:8080/health || exit 1

EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "MyApp.Api.dll"]

Image size comparison

Base imageSizeUse case
aspnet:8.0~220 MBDefault, đầy đủ
aspnet:8.0-alpine~110 MBProduction (nhỏ hơn 50%)
aspnet:8.0-jammy-chiseled~110 MBUbuntu chiseled, non-root mặc định
runtime-deps:8.0-alpine + self-contained~85 MBNhỏ nhất, cần publish self-contained

Trade-off: Alpine image nhỏ hơn nhưng dùng musl libc thay vì glibc - hiếm khi gây vấn đề với .NET nhưng cần test kỹ. Chiseled images là lựa chọn cân bằng tốt.

.dockerignore cho .NET

# .dockerignore
**/.vs
**/.vscode
**/bin
**/obj
**/node_modules
**/.git
**/.gitignore
**/docker-compose*.yml
**/Dockerfile*
**/*.md
**/*.sln.DotSettings
**/appsettings.Development.json
**/.env
**/TestResults

Docker Compose cho Local Development

yaml
# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: src/MyApp.Api/Dockerfile
    ports:
      - "5000:8080"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__Default=Server=sqlserver;Database=MyAppDb;User=sa;Password=Dev@Pass123;TrustServerCertificate=true
      - Redis__ConnectionString=redis:6379
    depends_on:
      sqlserver:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./src:/src:ro  # Hot reload với dotnet watch
    networks:
      - myapp-network

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=Dev@Pass123
    ports:
      - "1433:1433"
    volumes:
      - sqlserver-data:/var/opt/mssql
    healthcheck:
      test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Dev@Pass123" -C -Q "SELECT 1" || exit 1
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - myapp-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - myapp-network

  seq:
    image: datalust/seq:latest
    environment:
      - ACCEPT_EULA=Y
    ports:
      - "5341:80"
    networks:
      - myapp-network

volumes:
  sqlserver-data:
  redis-data:

networks:
  myapp-network:
    driver: bridge
bash
# Khởi động toàn bộ stack
docker compose up -d

# Xem logs
docker compose logs -f api

# Chỉ rebuild api service
docker compose up -d --build api

# Dọn dẹp
docker compose down -v

Azure Container Registry (ACR) - Setup Playbook

bash
# 1. Tạo ACR
az acr create \
  --name myappcr \
  --resource-group rg-myapp \
  --sku Basic \
  --admin-enabled false

# 2. Login vào ACR
az acr login --name myappcr

# 3. Build và push image
docker build -t myappcr.azurecr.io/myapp-api:1.0.0 -f src/MyApp.Api/Dockerfile .
docker push myappcr.azurecr.io/myapp-api:1.0.0

# 4. Hoặc dùng ACR Tasks (build trên cloud - không cần Docker local)
az acr build \
  --registry myappcr \
  --image myapp-api:1.0.0 \
  --file src/MyApp.Api/Dockerfile \
  .

# 5. Cấu hình App Service dùng ACR image
az webapp config container set \
  --name myapp-production \
  --resource-group rg-myapp \
  --container-image-name myappcr.azurecr.io/myapp-api:1.0.0 \
  --container-registry-url https://myappcr.azurecr.io

# 6. Enable managed identity để App Service pull từ ACR
az webapp identity assign --name myapp-production --resource-group rg-myapp
az acr update --name myappcr --anonymous-pull-enabled false
# Gán AcrPull role
PRINCIPAL_ID=$(az webapp identity show --name myapp-production --resource-group rg-myapp --query principalId -o tsv)
ACR_ID=$(az acr show --name myappcr --query id -o tsv)
az role assignment create --assignee $PRINCIPAL_ID --role AcrPull --scope $ACR_ID

CI/CD Pipeline - Build và Push Image

yaml
# GitHub Actions
build-image:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Login to ACR
      uses: azure/docker-login@v2
      with:
        login-server: myappcr.azurecr.io
        username: ${{ secrets.ACR_USERNAME }}
        password: ${{ secrets.ACR_PASSWORD }}

    - name: Build and Push
      run: |
        IMAGE_TAG=${{ github.sha }}
        docker build \
          -t myappcr.azurecr.io/myapp-api:${IMAGE_TAG} \
          -t myappcr.azurecr.io/myapp-api:latest \
          -f src/MyApp.Api/Dockerfile .
        docker push myappcr.azurecr.io/myapp-api --all-tags

Image Scanning & Vulnerability Management

bash
# Scan image với Trivy (CI pipeline)
trivy image --severity HIGH,CRITICAL --exit-code 1 \
  myappcr.azurecr.io/myapp-api:latest

# Enable Microsoft Defender for ACR (continuous scanning)
az security pricing create \
  --name ContainerRegistry \
  --tier Standard
yaml
# GitHub Actions - scan trước khi push
- name: Scan Image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myappcr.azurecr.io/myapp-api:${{ github.sha }}
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

- name: Upload Scan Results
  uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: 'trivy-results.sarif'

Container Best Practices

Checklist

PracticeTại saoCách làm
Non-root userGiảm attack surfaceUSER appuser trong Dockerfile
Small base imageGiảm vulnerabilities, pull nhanhDùng alpine hoặc chiseled
Health checkOrchestrator biết container healthyHEALTHCHECK instruction
No secrets in imageTránh leak credentialsDùng env vars hoặc mounted secrets
Pin image tagsReproducible buildsaspnet:8.0.4-alpine thay vì latest
Multi-stage buildImage nhỏ, không chứa SDKTách build vs runtime stage
Read-only filesystemHạn chế write access--read-only flag khi run
One process per containerDễ scale, monitorKhông chạy nhiều service trong 1 container

.NET-specific Tips

csharp
// Program.cs - graceful shutdown trong container
var builder = WebApplication.CreateBuilder(args);

// Respond to SIGTERM từ Docker/Kubernetes
builder.Host.ConfigureHostOptions(opts =>
{
    opts.ShutdownTimeout = TimeSpan.FromSeconds(30);
});

var app = builder.Build();

app.Lifetime.ApplicationStopping.Register(() =>
{
    // Drain connections, flush logs
    Log.CloseAndFlush();
});

Dev Containers

Chuẩn hoá development environment cho cả team:

json
// .devcontainer/devcontainer.json
{
  "name": "MyApp .NET Dev",
  "dockerComposeFile": ["../docker-compose.yml", "docker-compose.devcontainer.yml"],
  "service": "api",
  "workspaceFolder": "/workspace",
  "features": {
    "ghcr.io/devcontainers/features/dotnet:2": {
      "version": "8.0"
    },
    "ghcr.io/devcontainers/features/azure-cli:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-dotnettools.csdevkit",
        "ms-azuretools.vscode-docker",
        "humao.rest-client"
      ]
    }
  },
  "postCreateCommand": "dotnet restore",
  "remoteUser": "vscode"
}

Lợi ích Dev Containers:

  • Mọi developer có cùng toolchain version
  • Onboard developer mới trong vài phút thay vì hàng giờ
  • Không "works on my machine" - environment identical
  • Tích hợp tốt với GitHub Codespaces cho remote development