Docker ha transformado fundamentalmente cómo construimos, enviamos y ejecutamos aplicaciones.
Lo que comenzó como una herramienta de contenedorización simple ha evolucionado hasta convertirse en un ecosistema completo para el desarrollo y el despliegue de aplicaciones modernas.
ImageSharp con Docker
.NET Aspire
: El enfoque .NET moderno para la orquestación de contenedores
Es divertido y llena un hueco que no he visto en ningún otro lugar.
Ya sea que esté implementando una sencilla aplicación web o orquestando una compleja arquitectura de microservicios con modelos de aprendizaje automático acelerados por GPU, esta guía le lleva desde lo básico de Docker a aplicaciones en contenedores listas para la producción, con ejemplos reales de ejecutar mayoritariamentelucid.com.
# The classic developer problem
"It works on my machine!"
# The container solution
"Ship your machine!"
Máquinas virtuales
# 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
: Iniciar contenedores en segundos, no minutosEficiencia en el uso de los recursos
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
: Ejecutar docenas de contenedores en un solo anfitrión
: Las capas inalteradas se reutilizan, acelerando las construccionesCompartir
: Múltiples imágenes pueden compartir capas base
Your Machine (Windows/Mac/Linux)
↓ (reads Dockerfile)
Build Image (usually Linux)
↓ (executes RUN commands here)
Output Image (contains results)
Eficiencia
# 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
: Sólo es necesario descargar/cargar capas cambiadas
COPYLas órdenes en un Dockerfile no se ejecutan en su máquina - se ejecutan dentro del sistema operativo del contenedor de construcción.ADDEsto es lo que realmente sucede:RUNSistema de archivos local: Tuapt-getlos comandos se ejecutan en el sistema operativo del contenedor (no en su máquina)
Imagen de salida
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
: La imagen final contiene todas las capas creadas durante la construcción
: Puede estar en Windows, construyendo una imagen de Linux, usando comandos 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"]
comandos en un Dockerfile en Windows - no se están ejecutando en 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
Están corriendo dentro del contenedor de construcción basado en Linux.
.csprojComprender el flujo de construcciónnpm run buildImagen SDK descartadaappsettings.jsonCaché de capaswwwroot/: CopiandoArchivos de configuración
# 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
: Ejecutar como usuario no root
: Los orquestadores de contenedores pueden monitorear la salud de las aplicaciones
En lugar de administrar contenedores individualmente, describe toda la pila de aplicaciones en un archivo YAML.docker run¿Por qué Docker Compose?
ASP.NET Aplicación web básica**Base de datos PostgreSQLdocker-compose.yml**Redis caché
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 para el registro
app_networklas órdenes se vuelven difíciles de manejar./mnt/*Aquí está el.env: Almacenamiento administrado por Docker para datos que no necesita acceso directo aservices:
web:
depends_on:
db:
condition: service_healthy # Wait for health check
redis:
condition: service_started # Just wait for start
Límites de recursoscondition: service_healthy: Las limitaciones de CPU en el servicio de traducción previenen el hambre de recursos
# .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 en 5266 en lugar de 5432 para evitar conflictos con otras instancias
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
: Secretos en:
Los:
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
web:
networks:
- frontend
- backend
db:
networks:
- backend # Not exposed to frontend
Volúmenes designados
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Persista los datos a través de los reinicios del contenedor
# 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
Archivos de configuración, registros, cargas
Establecimiento de redesLas redes proporcionan aislamiento.
services:
web:
build: .
environment:
- ASPNETCORE_ENVIRONMENT=Development
**Aquí, la base de datos sólo es accesible a los servicios de backend, no directamente expuestos.**Controles de salud
services:
web:
volumes:
- .:/app # Live code reloading
ports:
- "5000:8080"
**Los controles médicos permiten a Docker:**Determinar si un contenedor está realmente listo (no acaba de empezar)
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
Proporcionar estatus a los orquestadores (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(desarrollo - auto-fusionado):docker-compose.prod.yml
(producción):
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 servicio de traducción automática neuronal que construí que permite la auto-traducción en este 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:
El proyecto demuestra:
scottgal/mostlylucid-nmt:gpuVariantes de GPU y CPUscottgal/mostlylucid-nmt:cpu- Misma base de código, diferentes imágenes de basescottgal/mostlylucid-nmt:gpu-minMulti-arquitectura construyescottgal/mostlylucid-nmt:cpu-min- Soporta AMD64 y ARM64Imágenes Docker optimizadas
/health- Construcción mínima de GPU, sin modelos precargados (~4GB)/ready- Creación mínima de CPU (~1,5 GB): 10-15x traducción más rápida con CUDAAuto-Descarga del modelo: Descargas de modelos de traducción bajo demanda
: Tries Opus-MT → mBART50 → M2M100 para la cobertura máxima del lenguaje
# 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 .
Puntos finales en materia de salud
# 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
para orquestadores
# 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 \
.
Ver la
proyecto completo en 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
para ejemplos de Dockerfile, scripts de compilación y documentación de 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
Las aplicaciones modernas necesitan ejecutarse en múltiples arquitecturas: x86_64 (AMD64) para servidores, ARM64 para Raspberry Pi y Apple Silicon Macs, a veces incluso ARM32 para dispositivos integrados.
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
Por qué es importante la multiarquitectura
Multi-arquitectura con Docker Compose
Soluciones:
# 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
Opción 2: Construir 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.
Ejemplo del mundo real: oleoducto CI/CD
dotnet run --project MyDistributedApp.AppHost
El flujo de trabajo de GitHub Actions para multi-arquitectura construye:
Utiliza almacenamiento en caché de registro para construcciones más rápidas
Configuración de las propiedades del contenedor
Todos los servicios con la configuración adecuadaRedis y PostgreSQL en contenedores
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
Docker Compose:
# Docker Compose
docker-compose -f deploy/docker-compose.yml up -d
# Kubernetes
kubectl apply -f deploy/k8s/
Centrada en la infraestructura
// 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);
Trae tu propia observabilidad.
.NET-specificCentrada en el desarrolloDescubrimiento automático del servicio
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
Telemetría incorporada
Componentes AspireLas integraciones preconstruidas hacen que añadir servicios sea trivial:Auto-acogida en recursos limitados: Optimización Práctica
Si te alojas en un VPS con 4 GB de RAM o un viejo portátil, aquí tienes estrategias prácticas para reducir el consumo de recursos mientras mantienes la funcionalidad.
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:
Dependencias de desarrollo únicamente
docker logsPor qué esto funciona para el desarrollo:Guía de dependencias de desarrollo completo
para las instrucciones de instalación.
# Just app + database + reverse proxy
docker-compose up -d mostlylucid db caddy
Configuración de la producción con conocimientos de recursos
# Add lightweight monitoring
docker-compose up -d mostlylucid db caddy seq
# Use Seq free 10GB/month
Para un presupuesto VPS (2-4GB RAM), priorizar los servicios esenciales:
# Add everything
docker-compose up -d
services:
myapp:
image: postgres:16-alpine # 50% smaller than postgres:16
# vs
image: postgres:16 # Full Debian base
**: Utilizar monitoreo externo (UptimeRobot, BetterStack grabe libre)**No Seq
, o nivel libre de Seq Cloud
db:
image: postgres:16-alpine
# One instance, multiple databases
# Umami, Mostlylucid, etc. all share this PostgreSQL
No hay Watchtower: Actualizaciones manuales con las notificaciones de GitHub Actions
easynmt:
deploy:
resources:
limits:
cpus: "2.0" # Don't let translation consume all CPU
reservations:
cpus: "0.5" # Guarantee minimum
: Ejecute bajo demanda en un contenedor separado que inicie/detenga manualmente
: Impedir que cualquier servicio único consuma toda la memoriaEstrategia de mejora progresivaComience mínimo, agregue servicios según sea necesario:
mostlylucid:
volumes:
- /mnt/imagecache:/app/wwwroot/cache # ImageSharp cache persists across restarts
**Etapa 1: Base (512MB-1GB VPS)**Etapa 2: Añadir Observabilidad (2GB VPS)
Técnicas de optimización de recursos |---------|-----------------|---------------------| 1. Usar imágenes alpinas Ahorros
: Las imágenes alpinas son 50-70% más pequeñas2.
En lugar de una base de datos por servicio, utilice una instancia de PostgreSQL con múltiples bases de datos:
# 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
Ahorros: 400 MB de RAM por base de datos adicional que consolida
Limite la CPU para los servicios de fondo
# 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
Esto evita que los trabajos de fondo de hambre su aplicación web.
ImageSharp con Docker
docker logs: Sin esto, cada contenedor reinicia regenera todas las miniaturas/imágenes procesadas.Prometheus + Grafana ~600MB Nube de Grafana (nivel libre)
Umami ~200MB Plausible (pagado) o auto-anfitrión en otra parte
# 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
Estrategia
# 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
: Descargue la observabilidad a niveles libres, mantenga la aplicación de núcleo en su VPS.
# Quick resource check
docker stats --no-stream
# Pretty output
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
Servicios en régimen de depósito
Ahorros
# Add Aspire to your solution
dotnet new aspire-apphost -n Mostlylucid.AppHost
cd Mostlylucid.AppHost
: 1-2GB RAM cuando el servicio de traducción no se está ejecutando
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();
Esto es lo que funciona con un VPS Hetzner $6/mes (2 vCPU, 4GB RAM):**Utilización total de los recursos:**RAM: ~800MB (deja libre 3.2GB)
// 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();
Lo que es diferente:
dotnet run --project Mostlylucid.AppHostNo Seq (utilización.envAhora vamos a reimaginar toda la pila usando .NET Aspire.Esto le da todos los beneficios de orquestación con una mejor integración .NET y una experiencia de desarrollador increíble.
Configuración de Aspire para la mayoría de los lúcidos
# 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
**En primer lugar, crear el Aspire App Host:**Principalmente lúcido.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
Crear
|---------|---------------|-------------|
| Mayormentelucid.ServiceDefaultsproyecto:
| Actualizar la mayoría de lucid/Program.csBeneficios del enfoque Aspire
| **Experiencia en materia de desarrollo:**Comando único
| empieza todo. | docker-compose up | dotnet runTablero
| **: Hermosa interfaz de usuario en http://localhost:15888 mostrando:**Todos los servicios en vivo
| Registros de todos los servicios en un solo lugar | docker logsRastros distribuidos entre los servicios
| Métricas y controles sanitariosDescubrimiento de servicio
| : Los servicios se encuentran automáticamente a través de nombresConfiguración
| : Centralizado en AppHost, no másmalabares
Generar manifiestos de despliegue de Aspire:
Manual YAML C# código con IntelliSense
+ Tablero
Rastreo
Es necesario distribuir el rastreo fuera de la caja
latest).dockerignore: Sólo la aplicación, Cloudflared, y Watchtower.envHoydepends_onComience simple, agregue complejidad sólo cuando sea necesario.dockerignoreUsar volúmenes nombrados para los datos# 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
Ejecutar como usuario no root
Escanea imágenes para detectar vulnerabilidades
Uso
Solución de problemas comunes
Esta guía le ha llevado de los fundamentos a los despliegues listos para la producción, con ejemplos del mundo real de ejecutar mayoritariamentelucid.com.
Uso
Para autoaficionados en VPS de presupuesto:
Inicio mínimo: App + Base de datos + Proxy Inverso (~800MB RAM) |-------|----------|-----------|------------| | Usar imágenes alpinas y límites de recursosDescarga la observabilidad a niveles libres (Seq Cloud, Grafana Cloud, UptimeRobot) | Ejecutar servicios caros (traducción, ML) sólo a peticiónUn VPS de $6/mes puede ejecutar un blog de producción con espacio de sobra | **Para despliegues de producción:**Los chequeos de salud permiten actualizaciones de tiempo cero con Watchtower | **Las redes separadas proporcionan seguridad (aislamiento frontal/retroceso)**Montajes encuadernados para datos que necesita para hacer una copia de seguridad/acceso | Volúmenes designados para el almacenamiento administrado por DockerLos límites CPU/memoria previenen la inanición de recursos
El túnel Cloudflare elimina la necesidad de direcciones IP públicasPara los desarrolladores de .NET:
Las imágenes de Ubuntu cinceladas proporcionan una superficie de ataque mínima
Etapa 2
(Hoy) + Aspire opción Variable Moderna
Self-Hosting
: Echa un vistazo a la
Happy containerizing! 🐳
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.