Docker a fondamentalement transformé notre façon de construire, d'expédier et d'exécuter des applications.
Ce qui a commencé comme un simple outil de conteneurisation a évolué en un écosystème complet pour le développement et le déploiement d'applications modernes.
ImageSharp avec Docker
.NET Aspire
: L'approche moderne du .NET à l'orchestration des conteneurs
C'est amusant et il comble un vide que je n'ai pas vu comblé nulle part ailleurs.
Que vous déployiez une simple application web ou orchestrez une architecture complexe de microservices avec des modèles d'apprentissage automatique accélérés par GPU, ce guide vous emmène des bases de Docker aux applications containerizzato prêtes à la production - avec de vrais exemples de courir principalementlucid.com.
# The classic developer problem
"It works on my machine!"
# The container solution
"Ship your machine!"
Machines virtuelles
# An image is a template (like a class in OOP)
docker pull mcr.microsoft.com/dotnet/aspnet:9.0
# A container is a running instance (like an object)
docker run -d -p 8080:8080 myapp:latest
: Démarrer les conteneurs en quelques secondes, pas quelques minutesEfficacité des ressources
FROM mcr.microsoft.com/dotnet/aspnet:9.0 # Layer 1: Base OS + .NET runtime
WORKDIR /app # Layer 2: Directory structure
COPY *.dll ./ # Layer 3: Application files
ENTRYPOINT ["dotnet", "MyApp.dll"] # Layer 4: Startup command
: Exécuter des dizaines de conteneurs sur un seul hôte
: Les couches non modifiées sont réutilisées, accélérant les constructionsPartage
: Plusieurs images peuvent partager des couches de base
Your Machine (Windows/Mac/Linux)
↓ (reads Dockerfile)
Build Image (usually Linux)
↓ (executes RUN commands here)
Output Image (contains results)
Efficacité
# You're on Windows, writing this Dockerfile
FROM ubuntu:24.04
# This RUN command executes in Ubuntu, NOT on your Windows machine!
RUN apt-get update && apt-get install -y curl
# This copies FROM your Windows filesystem
COPY myapp.exe /app/
# This executes IN the Ubuntu container
RUN chmod +x /app/myapp.exe
: Seuls les calques modifiés doivent être téléchargés/uploadés
COPYLes commandes dans un fichier Docker ne fonctionnent pas sur votre machine - elles fonctionnent à l'intérieur du système d'exploitation du conteneur de construction.ADDVoici ce qui se passe :RUNSystème de fichiers local: Votreapt-getcommandes exécutées dans le système d'exploitation du conteneur (pas votre machine)
Image de sortie
FROM mcr.microsoft.com/dotnet/sdk:9.0 # This is Linux-based
# You might think: "But I'm on Windows, how can I use these Linux commands?"
RUN apt-get update # ← Executes in the Linux build container, not your Windows machine
RUN dotnet restore # ← Executes in the Linux build container
: L'image finale contient tous les calques créés pendant la construction
: Vous pouvez être sous Windows, construire une image Linux, en utilisant des commandes Linux
# Multi-stage build: separates build environment from runtime
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy only csproj files first (better layer caching)
COPY ["MyApp/MyApp.csproj", "MyApp/"]
COPY ["MyApp.Core/MyApp.Core.csproj", "MyApp.Core/"]
# Restore dependencies (cached unless csproj changes)
RUN dotnet restore "MyApp/MyApp.csproj"
# Copy everything else
COPY . .
# Build the application
WORKDIR "/src/MyApp"
RUN dotnet build "MyApp.csproj" -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 3: Final runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
WORKDIR /app
# Create non-root user for security
RUN addgroup --gid 1001 appuser && \
adduser --uid 1001 --gid 1001 --disabled-password --gecos "" appuser
# Copy published output from publish stage
COPY --from=publish /app/publish .
# Switch to non-root user
USER appuser
# Expose port (documentation only, doesn't actually publish)
EXPOSE 8080
# Set environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.dll"]
commandes dans un fichier Docker sur Windows - ils ne sont pas en cours d'exécution sur Windows!
graph TB
subgraph "Your Machine"
A[Source Code<br/>*.cs, *.csproj]
B[Frontend Assets<br/>*.js, *.css]
C[Configuration<br/>appsettings.json]
D[Static Files<br/>wwwroot/]
end
subgraph "Stage 1: Build Container (SDK Image)"
E[dotnet/sdk:9.0<br/>~1.5GB]
F[Copy .csproj files]
G[dotnet restore<br/>Download NuGet packages]
H[Copy source code]
I[dotnet build<br/>Compile to DLLs]
J[Build artifacts<br/>/app/build/]
end
subgraph "Stage 2: Publish Container"
K[dotnet publish<br/>Optimize & trim]
L[Published output<br/>/app/publish/]
end
subgraph "Stage 3: Final Runtime Container (ASPNET Image)"
M[dotnet/aspnet:9.0<br/>~220MB]
N[Create app user<br/>Security]
O[Copy published files<br/>ONLY production artifacts]
P[Final image<br/>~250MB total]
end
subgraph "Frontend Build Pipeline (Parallel)"
Q[npm install<br/>node_modules/]
R[Webpack bundling<br/>*.js → dist/]
S[TailwindCSS + PostCSS<br/>*.css → dist/]
T[Optimized assets<br/>wwwroot/js/dist/<br/>wwwroot/css/dist/]
end
A --> F
A --> H
F --> G
G --> H
H --> I
I --> J
J --> K
K --> L
B --> Q
Q --> R
Q --> S
R --> T
S --> T
L --> O
T --> O
C --> O
D --> O
M --> N
N --> O
O --> P
Ils fonctionnent à l'intérieur du conteneur de construction basé sur Linux.
.csprojComprendre le flux de constructionnpm run buildImage SDK suppriméeappsettings.jsonCalquewwwroot/: CopierFichiers de configuration
# Build the image
docker build -t myapp:1.0.0 -t myapp:latest .
# Run with common options
docker run -d \
--name myapp \
-p 8080:8080 \
-e ConnectionStrings__DefaultConnection="Server=db;Database=myapp" \
-v /data/logs:/app/logs \
--restart unless-stopped \
myapp:latest
# View logs
docker logs -f myapp
# Execute commands inside running container
docker exec -it myapp /bin/bash
# Stop and remove
docker stop myapp
docker rm myapp
: Exécuter en tant qu'utilisateur non root
: Les orchestrateurs de conteneurs peuvent surveiller la santé des applications
Au lieu de gérer les conteneurs individuellement, vous décrivez toute votre pile d'applications dans un fichier YAML.docker runPourquoi Docker Compose ?
L'application web ASP.NET Core**Base de données PostgreSQLdocker-compose.yml**Redis le cache
services:
# Main ASP.NET Core application
mostlylucid:
image: scottgal/mostlylucid:latest
restart: always
healthcheck:
test: [ "CMD", "curl", "-f -K", "https://mostlylucid:7240/healthy" ]
interval: 30s
timeout: 10s
retries: 5
labels:
- "com.centurylinklabs.watchtower.enable=true"
env_file:
- .env
environment:
- Auth__GoogleClientId=${AUTH_GOOGLECLIENTID}
- Auth__GoogleClientSecret=${AUTH_GOOGLECLIENTSECRET}
- Auth__AdminUserGoogleId=${AUTH_ADMINUSERGOOGLEID}
- SmtpSettings__UserName=${SMTPSETTINGS_USERNAME}
- SmtpSettings__Password=${SMTPSETTINGS_PASSWORD}
- Analytics__UmamiPath=${ANALYTICS_UMAMIPATH}
- Analytics__WebsiteId=${ANALYTICS_WEBSITEID}
- ConnectionStrings__DefaultConnection=${POSTGRES_CONNECTIONSTRING}
- TranslateService__ServiceIPs=${EASYNMT_IPS}
- Serilog__WriteTo__0__Args__apiKey=${SEQ_API_KEY}
- Markdown__MarkdownPath=${MARKDOWN_MARKDOWNPATH}
volumes:
- /mnt/imagecache:/app/wwwroot/cache
- /mnt/logs:/app/logs
- /mnt/markdown:/app/markdown
- ./mostlylucid.pfx:/app/mostlylucid.pfx
- /mnt/articleimages:/app/wwwroot/articleimages
- /mnt/mostlylucid/uploads:/app/wwwroot/uploads
networks:
- app_network
depends_on:
- db
# PostgreSQL database
db:
image: postgres:16-alpine
ports:
- 5266:5432 # Custom external port to avoid conflicts
env_file:
- .env
networks:
- app_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- /mnt/umami/postgres:/var/lib/postgresql/data
restart: always
# Cloudflare tunnel for secure external access
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --token ${CLOUDFLARED_TOKEN}
env_file:
- .env
restart: always
networks:
- app_network
# Umami analytics
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
env_file: .env
environment:
DATABASE_URL: ${DATABASE_URL}
DATABASE_TYPE: ${DATABASE_TYPE}
HASH_SALT: ${HASH_SALT}
APP_SECRET: ${APP_SECRET}
TRACKER_SCRIPT_NAME: getinfo
API_COLLECT_ENDPOINT: all
depends_on:
- db
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- app_network
restart: always
# Translation service (CPU-limited for resource management)
easynmt:
image: easynmt/api:2.0.2-cpu
volumes:
- /mnt/easynmt:/cache/
deploy:
resources:
limits:
cpus: "4.0" # Prevent translation service from consuming all CPU
networks:
- app_network
# Caddy reverse proxy with automatic HTTPS
caddy:
image: caddy:latest
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- app_network
restart: always
# Seq centralized logging
seq:
image: datalust/seq
container_name: seq
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
SEQ_FIRSTRUN_ADMINPASSWORDHASH: ${SEQ_DEFAULT_HASH}
volumes:
- /mnt/seq:/data
networks:
- app_network
# Prometheus metrics collection
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- prometheus-data:/prometheus
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- app_network
# Grafana visualization
grafana:
image: grafana/grafana:latest
container_name: grafana
labels:
- "com.centurylinklabs.watchtower.enable=true"
volumes:
- grafana-data:/var/lib/grafana
networks:
- app_network
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
# Host metrics exporter
node_exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node_exporter
command:
- '--path.rootfs=/host'
networks:
- app_network
restart: unless-stopped
volumes:
- '/:/host:ro,rslave'
# Automatic container updates
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_LABEL_ENABLE=true
command: --interval 300 # Check every 5 minutes
volumes:
grafana-data:
caddy_data:
caddy_config:
prometheus-data:
networks:
app_network:
driver: bridge
Seq pour l'exploitation forestière
app_networkles commandes deviennent incompréhensibles./mnt/*Voici le.env: Stockage Docker-géré pour les données vous n'avez pas besoin d'accès direct àservices:
web:
depends_on:
db:
condition: service_healthy # Wait for health check
redis:
condition: service_started # Just wait for start
Limites de ressourcescondition: service_healthy: Les contraintes du CPU sur le service de traduction empêchent la famine des ressources
# .env file (never commit to git!)
DB_PASSWORD=super_secret_password
SMTP_PASSWORD=another_secret
services:
web:
environment:
- DB_PASSWORD=${DB_PASSWORD} # From .env file
- STATIC_VALUE=production # Hardcoded
env_file:
- .env # Load entire file
: PostgreSQL sur 5266 au lieu de 5432 pour éviter les conflits avec d'autres instances
services:
db:
volumes:
# Named volume (managed by Docker)
- postgres_data:/var/lib/postgresql/data
web:
volumes:
# Bind mount (maps host directory to container)
- ./data/markdown:/app/Markdown
- ./logs:/app/logs
: Les secrets en:
Les:
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
web:
networks:
- frontend
- backend
db:
networks:
- backend # Not exposed to frontend
Volumes désignés
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Données persistantes sur les redémarrages de conteneurs
# Start all services (detached)
docker-compose up -d
# Start specific services
docker-compose up -d web db
# View logs (all services)
docker-compose logs -f
# View logs (specific service)
docker-compose logs -f web
# Stop services (containers remain)
docker-compose stop
# Stop and remove containers
docker-compose down
# Stop, remove containers, and remove volumes
docker-compose down -v
# Rebuild and restart
docker-compose up -d --build
# Scale a service
docker-compose up -d --scale worker=3
# Execute command in running service
docker-compose exec web /bin/bash
# Run one-off command
docker-compose run --rm web dotnet ef database update
Fichiers de configuration, journaux, téléchargements
Mise en réseauLes réseaux assurent l'isolement.
services:
web:
build: .
environment:
- ASPNETCORE_ENVIRONMENT=Development
**Ici, la base de données n'est accessible qu'aux services de backend, non directement exposés.**Contrôles de santé
services:
web:
volumes:
- .:/app # Live code reloading
ports:
- "5000:8080"
**Les contrôles de santé permettent à Docker:**Déterminer si un conteneur est prêt (pas seulement commencé)
services:
web:
image: registry.example.com/myapp:${VERSION}
restart: always
deploy:
replicas: 3
resources:
limits:
cpus: '2'
memory: 2G
# Development (base + override)
docker-compose up -d
# Production
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Fournir un statut aux orchestres (Kubernetes, Docker Swarm)
# Install NVIDIA Container Toolkit (Ubuntu/Debian)
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
# Configure Docker daemon
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Test GPU access
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
# Use NVIDIA CUDA base image
FROM nvidia/cuda:12.6.0-cudnn-runtime-ubuntu24.04
# Install Python
RUN apt-get update && apt-get install -y \
python3.12 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install PyTorch with CUDA support
COPY requirements.txt .
RUN pip3 install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
# Copy application
COPY . .
# Test GPU on container start
RUN python3 -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}'); print(f'GPU: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"None\"}')"
ENTRYPOINT ["python3", "train.py"]
# Run with all GPUs
docker run --gpus all myapp:gpu
# Run with specific GPUs
docker run --gpus '"device=0,2"' myapp:gpu
# Run with GPU memory limits
docker run --gpus all --memory=16g myapp:gpu
services:
ml-trainer:
build:
context: .
dockerfile: Dockerfile.gpu
image: myapp:gpu
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all # or specific count: 1, 2, etc.
capabilities: [gpu]
volumes:
- ./models:/app/models
- ./data:/app/data
environment:
- NVIDIA_VISIBLE_DEVICES=all
- CUDA_VISIBLE_DEVICES=0,1 # Use GPUs 0 and 1
docker-compose.override.yml(développement - fusion automatique):docker-compose.prod.yml
(production):
services:
translation:
image: scottgal/mostlylucid-nmt:gpu
container_name: translation-gpu
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
environment:
- MODEL_FAMILY=opus-mt
- FALLBACK_MODELS=mbart50,m2m100
- CUDA_VISIBLE_DEVICES=0
- LOG_LEVEL=info
volumes:
- model_cache:/app/cache # Persistent model storage
ports:
- "8888:8888"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8888/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
model_cache:
, un service de traduction automatique neuronale que j'ai construit qui alimente la traduction automatique sur ce blog.
services:
translation:
image: scottgal/mostlylucid-nmt:cpu
container_name: translation-cpu
environment:
- MODEL_FAMILY=opus-mt
- FALLBACK_MODELS=mbart50,m2m100
volumes:
- model_cache:/app/cache
ports:
- "8888:8888"
restart: unless-stopped
volumes:
model_cache:
Le projet démontre :
scottgal/mostlylucid-nmt:gpuVariantes GPU et CPUscottgal/mostlylucid-nmt:cpu- Même base de code, différentes images de basescottgal/mostlylucid-nmt:gpu-minConstructions multi-architecturesscottgal/mostlylucid-nmt:cpu-min- Supporte AMD64 et ARM64Images Docker optimisées
/health- Minimal GPU build, pas de modèles préchargés (~4 Go)/ready- Construction d'un processeur minimal (~1.5 Go): 10-15x traduction plus rapide avec CUDAModèle de téléchargement automatique: Téléchargements modèles de traduction à la demande
: Tries Opus-MT → mBART50 → M2M100 pour une couverture linguistique maximale
# Problem: Image built on M1 Mac won't run on Linux server
docker build -t myapp:latest . # Builds for ARM64
docker push myapp:latest
# Server tries to run it... error: "exec format error"
# Solution: Build for multiple platforms
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
Points finals pour la santé
# Verify buildx is available
docker buildx version
# Create a new builder instance
docker buildx create --name multiarch --driver docker-container --use
# Inspect and bootstrap the builder
docker buildx inspect --bootstrap
# List available platforms
docker buildx inspect | grep Platforms
pour orchestres
# Use official multi-arch base images
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
# For platform-specific operations, use build arguments
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
# Install architecture-specific dependencies
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
apt-get update && apt-get install -y some-arm64-package; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
apt-get update && apt-get install -y some-amd64-package; \
fi
# Build and push for AMD64 and ARM64
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry/myapp:latest \
-t myregistry/myapp:1.0.0 \
--push \
.
# Build without pushing (loads into local Docker)
# Note: Can only load one platform at a time
docker buildx build \
--platform linux/amd64 \
-t myapp:latest \
--load \
.
# Build and export to tar files
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
-o type=tar,dest=./myapp.tar \
.
Voir
Projet complet sur GitHub
# Build multi-arch images first
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
# Then use in compose
services:
web:
image: myapp:latest # Already built for multiple architectures
pour les exemples de Dockerfile, construire des scripts et la documentation de l'API.
#!/bin/bash
# build-multiarch.sh
docker buildx build --platform linux/amd64,linux/arm64 \
-t myregistry/web:latest \
-f web/Dockerfile \
--push \
web/
docker buildx build --platform linux/amd64,linux/arm64 \
-t myregistry/worker:latest \
-f worker/Dockerfile \
--push \
worker/
docker-compose pull # Pull the multi-arch images
docker-compose up -d
Les applications modernes doivent fonctionner sur plusieurs architectures : x86_64 (AMD64) pour les serveurs, ARM64 pour Raspberry Pi et Apple Silicon Macs, parfois même ARM32 pour les appareils embarqués.
name: Build and Push Multi-Arch Images
on:
push:
branches: [ main ]
tags: [ 'v*' ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: myregistry/myapp
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=myregistry/myapp:buildcache
cache-to: type=registry,ref=myregistry/myapp:buildcache,mode=max
Pourquoi l'Architecture Multi-Architecture compte-t-elle?
Multi-Architecture avec Docker Compose
Rapprochement des travaux :
# Publish as a container image (no Dockerfile needed!)
dotnet publish --os linux --arch x64 -p:PublishProfile=DefaultContainer
# Specify image name and tag
dotnet publish \
--os linux \
--arch x64 \
-p:PublishProfile=DefaultContainer \
-p:ContainerImageName=myapp \
-p:ContainerImageTag=1.0.0
# Multi-architecture
dotnet publish --os linux --arch arm64 -p:PublishProfile=DefaultContainer
Option 2: Créer un script.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<!-- Container Configuration -->
<ContainerImageName>myapp</ContainerImageName>
<ContainerImageTag>$(Version)</ContainerImageTag>
<ContainerRegistry>myregistry.azurecr.io</ContainerRegistry>
<!-- Base image (defaults to mcr.microsoft.com/dotnet/aspnet:9.0) -->
<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:9.0-alpine</ContainerBaseImage>
<!-- Container runtime configuration -->
<ContainerWorkingDirectory>/app</ContainerWorkingDirectory>
<ContainerPort>8080</ContainerPort>
<ContainerEnvironmentVariable Include="ASPNETCORE_ENVIRONMENT">Production</ContainerEnvironmentVariable>
<!-- User (security best practice) -->
<ContainerUser>app</ContainerUser>
<!-- Labels -->
<ContainerLabel Include="org.opencontainers.image.description">My awesome app</ContainerLabel>
<ContainerLabel Include="org.opencontainers.image. automatically configures these based on AppHost
builder.AddServiceDefaults();
builder.AddRedisClient("cache");
builder.AddNpgsqlDbContext<MyDbContext>("mydb");
var app = builder.Build();
app.MapDefaultEndpoints(); // Health, metrics, etc.
Exemple du monde réel : pipeline CI/CD
dotnet run --project MyDistributedApp.AppHost
Le workflow GitHub Actions pour les constructions multi-architectures :
Utilise le cache de registre pour les constructions plus rapides
Configuration des propriétés des conteneurs
Tous les services avec une configuration appropriéeRedis et PostgreSQL dans des conteneurs
Aspire vs Docker Compose
# Generate Docker Compose
dotnet run --project MyDistributedApp.AppHost -- \
--publisher compose \
--output-path ../deploy
# Generate Kubernetes manifests
dotnet run --project MyDistributedApp.AppHost -- \
--publisher manifest \
--output-path ../deploy/k8s
Composez Docker :
# Docker Compose
docker-compose -f deploy/docker-compose.yml up -d
# Kubernetes
kubectl apply -f deploy/k8s/
Orientation vers l'infrastructure
// Add various backing services
var redis = builder.AddRedis("cache");
var postgres = builder.AddPostgres("db").AddDatabase("mydb");
var rabbitmq = builder.AddRabbitMQ("messaging");
var mongodb = builder.AddMongoDB("mongo").AddDatabase("docs");
var sql = builder.AddSqlServer("sql").AddDatabase("business");
// Add Azure services
var storage = builder.AddAzureStorage("storage");
var cosmos = builder.AddAzureCosmosDB("cosmos");
var servicebus = builder.AddAzureServiceBus("messaging");
// Use in services
builder.AddProject<Projects.MyService>("service")
.WithReference(redis)
.WithReference(postgres)
.WithReference(rabbitmq);
Apportez votre propre observabilité
Spécifique au .NETOrientation vers le développementDécouverte automatique du service
services:
smtp4dev:
image: rnwood/smtp4dev
ports:
- "3002:80"
- "2525:25"
volumes:
- e:/smtp4dev-data:/smtp4dev
restart: always
postgres:
image: postgres:16-alpine
container_name: postgres
ports:
- "5432:5432"
env_file:
- .env
volumes:
- e:/data:/var/lib/postgresql/data
restart: always
Télémétrie intégrée
Composants d'aspirationLes intégrations préconçues rendent l'ajout de services trivial :Self-Hosting sur les ressources limitées: Optimisation pratique
Si vous vous auto-hébergez sur un VPS avec 4 Go de RAM ou un ancien ordinateur portable, voici des stratégies pratiques pour réduire la consommation de ressources tout en maintenant la fonctionnalité.
services:
# Core application
mostlylucid:
image: scottgal/mostlylucid:latest
restart: always
env_file: .env
volumes:
- ./markdown:/app/markdown
- ./logs:/app/logs
networks:
- app_network
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
reservations:
memory: 256M
# Database only
db:
image: postgres:16-alpine
env_file: .env
volumes:
- db_data:/var/lib/postgresql/data
networks:
- app_network
deploy:
resources:
limits:
memory: 512M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 30s
timeout: 5s
retries: 3
# Caddy for HTTPS
caddy:
image: caddy:latest
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
networks:
- app_network
deploy:
resources:
limits:
memory: 128M
volumes:
db_data:
caddy_data:
networks:
app_network:
Dépendances de développement seulement
docker logsPourquoi cela fonctionne pour le développement:Guide des dépendances en matière de développement
pour les instructions d'installation.
# Just app + database + reverse proxy
docker-compose up -d mostlylucid db caddy
Mise en place de la production axée sur les ressources
# Add lightweight monitoring
docker-compose up -d mostlylucid db caddy seq
# Use Seq free 10GB/month
Pour un VPS budgétaire (2-4 Go RAM), prioriser les services essentiels :
# Add everything
docker-compose up -d
services:
myapp:
image: postgres:16-alpine # 50% smaller than postgres:16
# vs
image: postgres:16 # Full Debian base
**: Utiliser la surveillance externe (UptimeRobot, BetterStack free tier)**Pas de seq
, ou niveau sans nuage Seq
db:
image: postgres:16-alpine
# One instance, multiple databases
# Umami, Mostlylucid, etc. all share this PostgreSQL
Pas de Tour de Garde: Mises à jour manuelles avec les notifications GitHub Actions
easynmt:
deploy:
resources:
limits:
cpus: "2.0" # Don't let translation consume all CPU
reservations:
cpus: "0.5" # Guarantee minimum
: Exécutez à la demande dans un conteneur séparé que vous démarrez/arrêtez manuellement
: Empêcher tout service unique de consommer toute la mémoireStratégie d'amélioration progressiveCommencer au minimum, ajouter des services au besoin :
mostlylucid:
volumes:
- /mnt/imagecache:/app/wwwroot/cache # ImageSharp cache persists across restarts
**Étape 1 : Noyau (512MB-1GB VPS)**Étape 2: Ajouter l'observabilité (2 Go VPS)
Techniques d'optimisation des ressources |---------|-----------------|---------------------|
: Les images alpines sont 50-70% plus petites2. Le Président. — L'ordre du jour appelle le rapport (doc.
Au lieu d'une base de données par service, utilisez une instance PostgreSQLTM avec plusieurs bases de données :
# Create a separate compose file
# translation-compose.yml
services:
easynmt:
image: easynmt/api:2.0.2-cpu
ports:
- "8888:8888"
volumes:
- /mnt/easynmt:/cache/
# Only run when needed
docker-compose -f translation-compose.yml up -d
# Translate your content
# ...
# Shut down when done
docker-compose -f translation-compose.yml down
Économies: 400 Mo de RAM par base de données supplémentaire que vous consolidez
Limiter le processeur pour les services d'arrière-plan
# Minimal production compose
services:
mostlylucid:
image: scottgal/mostlylucid:latest
restart: always
env_file: .env
volumes:
- /mnt/markdown:/app/markdown
- /mnt/logs:/app/logs
- /mnt/imagecache:/app/wwwroot/cache
depends_on:
- db
db:
image: postgres:16-alpine
env_file: .env
volumes:
- /mnt/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 30s
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel run --token ${CLOUDFLARED_TOKEN}
restart: always
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_LABEL_ENABLE: "true"
command: --interval 3600 # Check once per hour, not every 5 minutes
Cela empêche les tâches de fond de mourir de faim votre application web.
ImageSharp avec Docker
docker logs: Sans cela, chaque redémarrage de conteneur régénère toutes les vignettes/images traitées.Prométhée + Grafana
Umami, ~200MB, Plausible (payé) ou auto-accueil ailleurs
# Simple health check script
#!/bin/bash
while true; do
curl -f http://localhost/healthz || echo "Health check failed!" | mail -s "Alert" [email protected]
sleep 300
done
Stratégie
# Watch for errors
docker-compose logs -f --tail=100 | grep -i error
# Email on critical errors
docker-compose logs -f | grep -i "critical" | while read line; do
echo "$line" | mail -s "Critical Error" [email protected]
done
: Déchargez l'observabilité aux niveaux libres, gardez l'application centrale sur votre VPS.
# Quick resource check
docker stats --no-stream
# Pretty output
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
Services sur demande
Économies
# Add Aspire to your solution
dotnet new aspire-apphost -n Mostlylucid.AppHost
cd Mostlylucid.AppHost
: 1-2 Go de RAM lorsque le service de traduction n'est pas en cours d'exécution
var builder = DistributedApplication.CreateBuilder(args);
// PostgreSQL with persistent data
var postgres = builder.AddPostgres("postgres")
.WithDataVolume() // Persistent storage
.WithPgAdmin(); // Optional: PgAdmin for database management
var mostlylucidDb = postgres.AddDatabase("mostlylucid");
var umamiDb = postgres.AddDatabase("umami");
// Seq for centralized logging
var seq = builder.AddSeq("seq")
.WithDataVolume();
// Redis for caching (if needed)
var redis = builder.AddRedis("cache")
.WithDataVolume()
.WithRedisCommander(); // Optional: Redis Commander UI
// Main blog application
var mostlylucid = builder.AddProject<Projects.Mostlylucid>("web")
.WithReference(mostlylucidDb)
.WithReference(seq)
.WithReference(redis)
.WithEnvironment("TranslateService__Enabled", "false") // Disable for dev
.WithHttpsEndpoint(port: 7240, name: "https");
// Umami analytics
var umami = builder.AddContainer("umami", "ghcr.io/umami-software/umami", "postgresql-latest")
.WithReference(umamiDb)
.WithEnvironment("DATABASE_TYPE", "postgresql")
.WithEnvironment("TRACKER_SCRIPT_NAME", "getinfo")
.WithEnvironment("API_COLLECT_ENDPOINT", "all")
.WithHttpEndpoint(port: 3000, name: "http");
// Translation service (CPU version, with resource limits)
var translation = builder.AddContainer("easynmt", "easynmt/api", "2.0.2-cpu")
.WithDataVolume("/cache")
.WithHttpEndpoint(port: 8888, name: "http")
.WithEnvironment("MODEL_FAMILY", "opus-mt");
// Scheduler service (Hangfire background jobs)
var scheduler = builder.AddProject<Projects.Mostlylucid_SchedulerService>("scheduler")
.WithReference(mostlylucidDb)
.WithReference(seq);
// Prometheus for metrics
var prometheus = builder.AddContainer("prometheus", "prom/prometheus", "latest")
.WithDataVolume()
.WithBindMount("./prometheus.yml", "/etc/prometheus/prometheus.yml")
.WithHttpEndpoint(port: 9090);
// Grafana for visualization
var grafana = builder.AddContainer("grafana", "grafana/grafana", "latest")
.WithDataVolume()
.WithHttpEndpoint(port: 3001)
.WithEnvironment("GF_SECURITY_ADMIN_PASSWORD", builder.Configuration["Grafana:AdminPassword"] ?? "admin");
builder.Build().Run();
Voici ce qui fonctionne sur un Hetzner VPS de 6 $/mois (2 vCPU, 4 Go RAM):**Utilisation totale des ressources :**RAM: ~800 Mo (libère 3,2 Go)
// Extensions.cs
public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
if (builder.Environment.IsDevelopment())
{
tracing.SetSampler(new AlwaysOnSampler());
}
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation();
});
// Health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" });
return builder;
}
public static IApplicationBuilder MapDefaultEndpoints(this WebApplication app)
{
app.MapHealthChecks("/healthz");
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
return app;
}
}
var builder = WebApplication.CreateBuilder(args);
// Add Aspire service defaults (telemetry, health checks)
builder.AddServiceDefaults();
// Add services
builder.AddNpgsqlDbContext<MostlylucidDbContext>("mostlylucid");
builder.AddRedisClient("cache");
// Existing service registrations...
// builder.Services.AddControllersWithViews();
// etc...
var app = builder.Build();
// Map Aspire default endpoints
app.MapDefaultEndpoints();
// Existing middleware...
app.Run();
Qu'est-ce qui est différent :
dotnet run --project Mostlylucid.AppHostPas de Seq (utilisation.envRepensons l'ensemble de la pile en utilisant .NET Aspire.Cela vous donne tous les avantages de l'orchestration avec une meilleure intégration .NET et une expérience de développeur incroyable.
Mise en place d'Aspire pour Mostlylucide
# Generate Docker Compose
dotnet run --project Mostlylucid.AppHost -- \
--publisher compose \
--output-path ./deploy
# This creates a production-ready docker-compose.yml
cd deploy
docker-compose up -d
**Tout d'abord, créez l'hôte d'Aspire App :**Pour la plupart, l'AppHost/Program.cs :
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
mostlylucid-db:
image: postgres:16
# Database initialization
seq:
image: datalust/seq:latest
environment:
ACCEPT_EULA: Y
volumes:
- seq-data:/data
web:
image: scottgal/mostlylucid:latest
environment:
ConnectionStrings__mostlylucid: Host=postgres;Database=mostlylucid;Username=postgres;Password=${POSTGRES_PASSWORD}
ConnectionStrings__cache: cache:6379
depends_on:
- postgres
- cache
- seq
cache:
image: redis:7-alpine
volumes:
- redis-data:/data
# ... other services
Créer
|---------|---------------|-------------|
| La plupart du temps, c'est le serviceDéfautsprojet:
| Mettre à jour Mostlylucide/Program.csAvantages de l'approche d'aspiration
| **Expérience en matière de développement :**Commande unique
| commence tout | docker-compose up | dotnet runTableau de bord
| **: Belle UI à http://localhost:15888 montrant :**Tous les services en direct
| Registres de tous les services en un seul endroit | docker logsTraces réparties entre les services
| Statistiques et bilans de santéDécouverte des services
| : Les services se retrouvent automatiquement via des nomsConfiguration
| : Centralisé dans AppHost, pas plusJonglage
Générer des manifestes de déploiement d'Aspire:
Code YAML (model) C# avec IntelliSense
+ Tableau de bord
Recherche
Vous avez besoin d'un tracage distribué hors-de-la-boîte
latest).dockerignore: Juste l'application, Cloudflared, et La Tour de Garde.envAujourd'huidepends_onCommencer simple, ajouter la complexité seulement lorsque nécessaire.dockerignoreUtiliser des volumes nommés pour les données# Check logs
docker logs container-name
# Common issues:
# 1. Port already in use
docker ps | grep 8080 # Find conflicting container
docker stop conflicting-container
# 2. Missing environment variables
docker inspect container-name | grep Env
# 3. Failed health check
docker inspect container-name | grep Health -A 20
# Enable BuildKit for faster builds
export DOCKER_BUILDKIT=1
# Use build cache
docker build --cache-from myapp:latest -t myapp:latest .
# Check what's taking time
docker build --progress=plain -t myapp:latest .
# Containers can't communicate
# Solution: Ensure they're on the same network
docker network ls
docker network inspect network-name
# DNS not working
# Container names are DNS names within Docker networks
docker exec web ping db # Should work if both on same network
# Permission denied on volume
# Solution: Match user IDs
FROM ubuntu
RUN useradd -u 1000 appuser # Match host user ID
USER appuser
Exécuter en tant qu'utilisateur non root
Analyser les images pour détecter les vulnérabilités
Utilisation
Dépannage de problèmes communs
Ce guide vous a amené des fondamentaux aux déploiements prêts à la production, avec des exemples du monde réel de courir principalementlucid.com.
Utilisation
Pour les auto-hébergements dans le budget VPS:
Début minimal: App + base de données + proxy inverse (~800 Mo RAM) |-------|----------|-----------|------------| | Utiliser les images alpines et les limites des ressourcesDécharge l'observabilité aux niveaux libres (Seq Cloud, Grafana Cloud, UptimeRobot) | Exécuter des services coûteux (traduction, ML) à la demande seulementUn VPS de 6 $/mois peut gérer un blog de production avec de la place à épargner | **Pour les déploiements de production :**Les contrôles de santé permettent des mises à jour à temps zéro avec La Tour de Garde | **Des réseaux séparés assurent la sécurité (isolement frontal/arrière-fin)**Bind montures pour les données dont vous avez besoin pour sauvegarder / accéder | Volumes désignés pour le stockage géré par DockerLes limites du CPU/mémoire empêchent la famine des ressources
Cloudflare Tunnel élimine le besoin d'adresses IP publiquesPour les développeurs .NET :
Les images ciselées Ubuntu fournissent une surface d'attaque minimale
Étape 2
(Aujourd'hui)
Accueillant soi-même
: Regardez la
Happy containerizing! 🐳
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.