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 image | Size | Use case |
|---|---|---|
aspnet:8.0 | ~220 MB | Default, đầy đủ |
aspnet:8.0-alpine | ~110 MB | Production (nhỏ hơn 50%) |
aspnet:8.0-jammy-chiseled | ~110 MB | Ubuntu chiseled, non-root mặc định |
runtime-deps:8.0-alpine + self-contained | ~85 MB | Nhỏ 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
| Practice | Tại sao | Cách làm |
|---|---|---|
| Non-root user | Giảm attack surface | USER appuser trong Dockerfile |
| Small base image | Giảm vulnerabilities, pull nhanh | Dùng alpine hoặc chiseled |
| Health check | Orchestrator biết container healthy | HEALTHCHECK instruction |
| No secrets in image | Tránh leak credentials | Dùng env vars hoặc mounted secrets |
| Pin image tags | Reproducible builds | aspnet:8.0.4-alpine thay vì latest |
| Multi-stage build | Image nhỏ, không chứa SDK | Tách build vs runtime stage |
| Read-only filesystem | Hạn chế write access | --read-only flag khi run |
| One process per container | Dễ scale, monitor | Khô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