← maurobernal.com.ar

Etiqueta: arquitectura

  • Arquitectura de Web APIs en .NET 10 — Parte 3: Observabilidad, Resiliencia y Tiempo Real

    Llegamos a la tercera y última parte de la serie Arquitectura Esencial de Web APIs en .NET 10. En la Parte 1 vimos los fundamentos, en la Parte 2 los componentes de performance y escalabilidad. Ahora cerramos con los cuatro que elevan una API de «funcional» a «profesional»: observabilidad, resiliencia, despliegue progresivo y comunicación en tiempo real.

    8. OpenTelemetry: ver lo que pasa adentro

    Tuve años en los que mi «monitoreo» consistía en revisar logs manualmente y rezar para encontrar el error antes de que me llamara el cliente. Hoy, con OpenTelemetry integrado nativamente en .NET 10, no hay excusa para no tener observabilidad completa desde el día uno.

    OpenTelemetry unifica tres pilares de observabilidad:

    • Trazas distribuidas: seguir el flujo de una request a través de múltiples servicios
    • Métricas: CPU, memoria, requests por segundo, latencia
    • Logs: correlacionados automáticamente con las trazas
    builder.Services.AddOpenTelemetry()
        .WithMetrics(metrics => metrics
            .AddAspNetCoreInstrumentation()   // Métricas de HTTP
            .AddRuntimeInstrumentation()      // GC, threadpool, memoria
            .AddPrometheusExporter())         // Expone /metrics para Prometheus
        .WithTracing(tracing => tracing
            .AddAspNetCoreInstrumentation()   // Trazas de requests HTTP
            .AddHttpClientInstrumentation()   // Trazas de llamadas salientes
            .AddEntityFrameworkCoreInstrumentation() // Trazas de queries SQL
            .AddOtlpExporter(otlp =>          // Exporta a Jaeger, Grafana Tempo, etc.
            {
                otlp.Endpoint = new Uri("http://localhost:4317");
            }));
    
    // Opcional: exponer el endpoint de métricas de Prometheus
    app.MapPrometheusScrapingEndpoint("/metrics");

    Una vez que tenés OpenTelemetry configurado, herramientas como Grafana + Prometheus + Tempo te dan dashboards completos, alertas automáticas y trazas distribuidas entre microservicios. Todo lo que necesitás para diagnosticar cualquier problema en producción en minutos en lugar de horas.

    9. Resiliencia con Polly: sobrevivir a las caídas externas

    En arquitecturas de microservicios hay una certeza: el servicio que llamás va a fallar en algún momento. No es una posibilidad, es una garantía estadística. La pregunta es si tu API se cae con él o lo maneja con elegancia.

    Polly —integrado de forma estándar en .NET desde la versión 8— implementa patrones de resiliencia: reintentos con backoff exponencial, timeouts y circuit breakers.

    // Opción 1: Pipeline estándar preconfigurado (la manera más rápida)
    // Incluye: reintentos exponenciales + circuit breaker + timeout + hedging
    builder.Services.AddHttpClient("ExternalAPI", client =>
        client.BaseAddress = new Uri("https://api.externa.com"))
        .AddStandardResilienceHandler();
    
    // Opción 2: Pipeline personalizado para control fino
    builder.Services.AddHttpClient("CriticalService", client =>
        client.BaseAddress = new Uri("https://servicio-critico.com"))
        .AddResilienceHandler("custom", pipeline =>
        {
            // Timeout total de la operación
            pipeline.AddTimeout(TimeSpan.FromSeconds(10));
    
            // Reintentos: 3 intentos con backoff exponencial
            pipeline.AddRetry(new HttpRetryStrategyOptions
            {
                MaxRetryAttempts = 3,
                Delay = TimeSpan.FromMilliseconds(500),
                BackoffType = DelayBackoffType.Exponential,
                UseJitter = true, // Evita thundering herd
                ShouldHandle = args => args.Outcome switch
                {
                    { Exception: HttpRequestException } => PredicateResult.True(),
                    { Result.StatusCode: HttpStatusCode.ServiceUnavailable } => PredicateResult.True(),
                    _ => PredicateResult.False()
                }
            });
    
            // Circuit Breaker: abre después de 50% de fallos en 30 segundos
            pipeline.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
            {
                FailureRatio = 0.5,
                SamplingDuration = TimeSpan.FromSeconds(30),
                MinimumThroughput = 10,
                BreakDuration = TimeSpan.FromSeconds(15)
            });
        });

    El Circuit Breaker es especialmente importante: cuando un servicio externo falla repetidamente, el circuit breaker «abre» y deja de intentar llamarlo durante un período, evitando que los timeouts se acumulen y degraden toda tu API.

    10. Feature Flags: deployar sin activar

    Los feature flags cambiaron mi forma de deployar. Antes, cada feature nueva era un riesgo: si algo fallaba en producción, había que hacer rollback, lo cual es lento y estresante. Hoy puedo subir código a producción con la funcionalidad apagada, activarla para el 5% de los usuarios, monitorear, y si todo va bien, activarla para todos. Sin rollback, sin estrés.

    dotnet add package Microsoft.FeatureManagement.AspNetCore
    builder.Services.AddFeatureManagement();
    
    app.MapGet("/api/beta-feature", async (IFeatureManager featureManager) =>
    {
        if (await featureManager.IsEnabledAsync("BetaFeatureX"))
            return Results.Ok("¡Nueva funcionalidad activada!");
    
        return Results.StatusCode(StatusCodes.Status404NotFound);
    });
    
    // También podés proteger endpoints completos con filtros de acción
    app.MapGet("/api/new-algorithm", [FeatureGate("NewAlgorithm")] () =>
        Results.Ok(RunNewAlgorithm()));

    Los flags se configuran en appsettings.json (para desarrollo) o en Azure App Configuration (para producción con control en tiempo real sin redeploy):

    {
      "FeatureManagement": {
        "BetaFeatureX": false,
        "NewAlgorithm": {
          "EnabledFor": [
            {
              "Name": "Percentage",
              "Parameters": { "Value": 10 }
            }
          ]
        }
      }
    }

    Podés activar features para un porcentaje de usuarios, para usuarios específicos, por fecha de activación, o con cualquier lógica custom. Ideal para Canary Releases y pruebas A/B.

    11. Server-Sent Events (SSE): tiempo real sin la complejidad de WebSockets

    WebSockets es la primera opción que se te ocurre para «tiempo real», pero tiene overhead de setup y complejidad de manejo de conexiones bidireccionales. Si solo necesitás enviar datos del servidor al cliente —notificaciones, progreso de tareas, feeds de datos— SSE es mucho más simple y funciona sobre HTTP estándar.

    En .NET 10, implementarlo con IAsyncEnumerable es elegante y eficiente:

    // Ejemplo 1: Stream simple de notificaciones
    app.MapGet("/api/notifications", async IAsyncEnumerable<string> (CancellationToken ct) =>
    {
        for (int i = 0; i < 5 && !ct.IsCancellationRequested; i++)
        {
            await Task.Delay(1000, ct);
            yield return $"data: Notificación {i + 1}\n\n";
        }
    });
    
    // Ejemplo 2: Progreso de una tarea larga
    app.MapPost("/api/export", async IAsyncEnumerable<ProgressUpdate> (ExportRequest request, CancellationToken ct) =>
    {
        var items = await GetItemsToExport(request);
        int total = items.Count;
    
        for (int i = 0; i < total && !ct.IsCancellationRequested; i++)
        {
            await ProcessItem(items[i]);
            yield return new ProgressUpdate
            {
                Current = i + 1,
                Total = total,
                Percentage = (int)((i + 1.0) / total * 100)
            };
        }
    });
    
    record ProgressUpdate(int Current, int Total, int Percentage);

    El cliente recibe un flujo continuo de datos sin necesidad de polling. Perfecto para dashboards en tiempo real, barras de progreso de procesos largos, o feeds de eventos de sistema.

    El checklist completo de producción

    Antes de llevar cualquier Web API a producción, repaso esta lista de los 11 componentes que cubrimos en la serie:

    • Health Checks — Liveness y Readiness configurados
    • Exception Handler global — ProblemDetails para todos los errores
    • Validación — Endpoint Filters antes de la lógica de negocio
    • OpenAPI nativo — Documentación sin Swashbuckle
    • Rate Limiting — Protección en endpoints públicos
    • Output Cache — En endpoints de lectura costosos
    • API Versioning — Desde el primer endpoint
    • OpenTelemetry — Métricas, trazas y logs exportando
    • Polly — Resiliencia en todas las llamadas externas
    • Feature Flags — Para funcionalidades en desarrollo activo
    • SSE — En lugar de polling para actualizaciones en tiempo real

    No hace falta implementarlos todos desde el día uno, pero sí conviene tenerlos en el radar desde que empezás. Cuanto más temprano los incorporés, menos dolores de cabeza cuando la API escale.

    ¿Hay algún componente que uses habitualmente y no esté en la lista? Dejamelo en los comentarios.

    Parte 2: Rate Limiting, Output Cache y API Versioning

  • Arquitectura de Web APIs en .NET 10 — Parte 2: Performance y Escalabilidad

    Esta es la segunda parte de la serie Arquitectura Esencial de Web APIs en .NET 10. En la Parte 1 cubrimos los fundamentos: Health Checks, Exception Handling, Validación y OpenAPI nativo. Ahora le toca el turno a los componentes que marcan la diferencia cuando tu API empieza a recibir carga real.

    Estos tres los ignoré durante demasiado tiempo, hasta que los problemas aparecieron solos en producción.

    5. Rate Limiting: protegé tu API del abuso

    Una API pública sin rate limiting es un blanco fácil. Me tocó ver picos de miles de requests por minuto desde una sola IP dejando el servidor de rodillas. Desde .NET 7 el middleware de rate limiting viene integrado en el framework, y en .NET 10 está completamente maduro.

    .NET ofrece cuatro estrategias distintas según el caso de uso:

    • Fixed Window: X peticiones por ventana de tiempo fija
    • Sliding Window: igual pero la ventana se mueve (más preciso)
    • Token Bucket: permite bursts controlados
    • Concurrency: limita las peticiones simultáneas, no el rate
    builder.Services.AddRateLimiter(options =>
    {
        // Política general: 100 requests por minuto por IP
        options.AddFixedWindowLimiter("General", opt =>
        {
            opt.Window = TimeSpan.FromMinutes(1);
            opt.PermitLimit = 100;
            opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
            opt.QueueLimit = 10; // Cuántas requests pueden esperar en cola
        });
    
        // Política estricta para endpoints sensibles
        options.AddSlidingWindowLimiter("Strict", opt =>
        {
            opt.Window = TimeSpan.FromMinutes(1);
            opt.SegmentsPerWindow = 6; // Segmentos de 10 segundos
            opt.PermitLimit = 20;
        });
    
        // Respuesta personalizada cuando se supera el límite
        options.OnRejected = async (context, cancellationToken) =>
        {
            context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.HttpContext.Response.WriteAsJsonAsync(
                new ProblemDetails { Title = "Too Many Requests", Status = 429 },
                cancellationToken);
        };
    });
    
    var app = builder.Build();
    app.UseRateLimiter();
    
    // Aplicar a endpoints específicos
    app.MapGet("/api/data", () => "Datos")
       .RequireRateLimiting("General");
    
    app.MapPost("/api/auth/login", (LoginDto dto) => Results.Ok())
       .RequireRateLimiting("Strict");

    Para APIs que manejan usuarios autenticados, podés segmentar el rate limiting por user ID en lugar de IP, así los usuarios legítimos no se ven afectados por el comportamiento de otros.

    6. Output Cache: no calcules lo mismo dos veces

    En un proyecto de reportes que tenía consultas de 2 segundos contra SQL Server, implementar Output Caching llevó el tiempo de respuesta promedio a menos de 10ms. Sin tocar una sola línea de lógica de negocio.

    Para endpoints que devuelven datos que no cambian constantemente, el Output Cache guarda la respuesta completa en memoria (o Redis) y la sirve directamente sin ejecutar nada de la lógica de negocio.

    builder.Services.AddOutputCache(options =>
    {
        // Política base: cache de 60 segundos
        options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromSeconds(60)));
    
        // Política nombrada para datos de referencia
        options.AddPolicy("ReferenceData", builder =>
            builder.Expire(TimeSpan.FromMinutes(10))
                   .Tag("reference")); // Tag para invalidación selectiva
    });
    
    var app = builder.Build();
    app.UseOutputCache();
    
    // Cache por 10 segundos (override de la política base)
    app.MapGet("/api/stats", () => GetExpensiveStats())
       .CacheOutput(c => c.Expire(TimeSpan.FromSeconds(10)));
    
    // Usando política nombrada
    app.MapGet("/api/products", () => GetProducts())
       .CacheOutput("ReferenceData");
    
    // Invalidar el cache cuando cambian los datos
    app.MapPost("/api/products", async (ProductDto product, IOutputCacheStore cache) =>
    {
        await SaveProduct(product);
        await cache.EvictByTagAsync("reference", CancellationToken.None); // Invalida el cache
        return Results.Created($"/api/products/{product.Id}", product);
    });

    La invalidación por tags es la clave para usar Output Cache sin miedo a servir datos stale. Cuando mutás datos, invalidás el tag correspondiente y la próxima request regenera el cache.

    Para ambientes distribuidos (múltiples instancias), podés configurar Redis como backing store con AddStackExchangeRedisOutputCache().

    7. API Versioning: cambiar sin romper a tus clientes

    Esta es una de las lecciones más caras que aprendí: si tu API tiene clientes externos y no versiona desde el principio, el día que necesités hacer un cambio incompatible vas a tener un problema enorme. Agregarle versionado a una API ya desplegada es doloroso. Hacerlo desde el inicio es trivial.

    En Minimal APIs el versionado se maneja mediante Version Sets. Primero instalá el paquete Asp.Versioning.Http:

    dotnet add package Asp.Versioning.Http
    // Program.cs
    builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true; // Devuelve las versiones disponibles en headers
    });
    
    var app = builder.Build();
    
    var apiVersionSet = app.NewApiVersionSet()
        .HasApiVersion(new ApiVersion(1, 0))
        .HasApiVersion(new ApiVersion(2, 0))
        .ReportApiVersions()
        .Build();
    
    // V1: respuesta simple
    app.MapGet("/api/v{version:apiVersion}/users", () =>
        new[] { new { Id = 1, Name = "Juan" } })
       .WithApiVersionSet(apiVersionSet)
       .MapToApiVersion(1, 0);
    
    // V2: respuesta enriquecida con datos extra
    app.MapGet("/api/v{version:apiVersion}/users", () =>
        new[] { new { Id = 1, Name = "Juan", Email = "juan@ejemplo.com", Role = "Admin" } })
       .WithApiVersionSet(apiVersionSet)
       .MapToApiVersion(2, 0);

    Los clientes existentes siguen usando /api/v1/users sin ningún cambio. Los nuevos pueden adoptar /api/v2/users con el formato enriquecido. Convivencia perfecta.

    Además del versionado por URL, podés usar query string (?api-version=2.0) o header (X-API-Version: 2.0) según las necesidades de tus clientes.

    Resumen de la Parte 2

    Tres componentes que hacen la diferencia cuando la API escala:

    • Rate Limiting → protección contra abuso, con estrategias flexibles por caso de uso
    • Output Cache → performance brutal en endpoints de lectura, con invalidación inteligente
    • API Versioning → libertad para evolucionar sin romper contratos existentes

    En la Parte 3 cerramos la serie con los componentes más avanzados: OpenTelemetry, Polly, Feature Flags y Server-Sent Events.

  • Arquitectura de Web APIs en .NET 10 — Parte 1: Los fundamentos que no pueden faltar

    Cuando empecé a trabajar con .NET, mi idea de una «API lista para producción» era que compilara y los endpoints respondieran correctamente. Con el tiempo aprendí que hay una diferencia enorme entre una API que funciona en tu máquina y una que sobrevive el mundo real.

    Esta es la primera parte de una serie de tres artículos donde te cuento los 11 componentes que hoy considero esenciales en cualquier Web API con .NET 10. Empezamos por los fundamentos: los cuatro que deberías tener desde el primer día, sin importar el tamaño del proyecto.

    1. Health Checks: que el orquestador sepa si tu API está viva

    La primera vez que deployé en Kubernetes sin health checks fue un desastre silencioso. El pod figuraba como «Running», pero la base de datos no conectaba y las requests simplemente fallaban sin que el orquestador se enterara.

    Los Health Checks exponen endpoints que le dicen a Kubernetes (o a cualquier balanceador de carga) dos cosas fundamentales:

    • Liveness: ¿el proceso sigue vivo?
    • Readiness: ¿está listo para recibir tráfico? (sus dependencias —base de datos, Redis, etc.— están funcionando)
    // Program.cs
    builder.Services.AddHealthChecks()
        .AddNpgSql(builder.Configuration.GetConnectionString("Default")); // Verifica PostgreSQL
    
    var app = builder.Build();
    
    app.MapHealthChecks("/health");          // Endpoint general
    app.MapHealthChecks("/health/ready",    // Solo readiness
        new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });
    app.MapHealthChecks("/health/live",     // Solo liveness
        new HealthCheckOptions { Predicate = _ => false });

    Con el paquete AspNetCore.HealthChecks.* podés agregar chequeos para SQL Server, Redis, servicios HTTP externos y más. Una línea por dependencia crítica.

    2. Exception Handling Global con IExceptionHandler

    Nada peor que una API que le devuelve un stack trace de C# al cliente, o un error 500 genérico sin información útil. Durante mucho tiempo usé try/catch en cada endpoint, hasta que descubrí IExceptionHandler.

    Con esta interfaz centralizás el manejo de errores en un solo lugar y respondés siempre con el formato estándar ProblemDetails (RFC 7807), que es lo que los clientes modernos esperan.

    public class GlobalExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<GlobalExceptionHandler> _logger;
    
        public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        {
            _logger = logger;
        }
    
        public async ValueTask<bool> TryHandleAsync(
            HttpContext context, Exception exception, CancellationToken cancellationToken)
        {
            _logger.LogError(exception, "Error no controlado: {Message}", exception.Message);
    
            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Ocurrió un error inesperado",
                Detail = exception.Message
            };
    
            context.Response.StatusCode = problemDetails.Status.Value;
            await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
            return true; // Excepción manejada, detiene la cadena
        }
    }
    
    // Program.cs
    builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
    builder.Services.AddProblemDetails();
    app.UseExceptionHandler();

    Podés crear múltiples handlers y registrarlos en orden: el primero que devuelva true corta la cadena. Útil para manejar ValidationException, NotFoundException, etc., con respuestas específicas.

    3. Validación Nativa con Endpoint Filters

    Validar el payload antes de que llegue a la lógica de negocio es crítico. En .NET 10, las Minimal APIs tienen Endpoint Filters que actúan como un pipeline de validación declarativo, sin necesidad de librerías externas para los casos más comunes.

    app.MapPost("/api/users", (UserDto user) =>
    {
        return Results.Created($"/api/users/{user.Id}", user);
    })
    .AddEndpointFilter(async (context, next) =>
    {
        var user = context.GetArgument<UserDto>(0);
    
        var errors = new Dictionary<string, string[]>();
    
        if (string.IsNullOrEmpty(user.Name))
            errors["Name"] = new[] { "El nombre es obligatorio" };
    
        if (user.Age < 0 || user.Age > 120)
            errors["Age"] = new[] { "La edad debe estar entre 0 y 120" };
    
        if (errors.Any())
            return Results.ValidationProblem(errors);
    
        return await next(context);
    });

    Para proyectos más grandes donde necesitás validaciones complejas, FluentValidation sigue siendo una excelente opción que se integra perfectamente con este patrón.

    4. OpenAPI Integrado: adiós Swashbuckle

    Durante años, generar documentación Swagger significaba instalar Swashbuckle y rezar para que no conflictuara con otras dependencias. En .NET 10, la generación de especificación OpenAPI es nativa, sin dependencias externas, con mejor performance de arranque y mantenimiento garantizado por Microsoft.

    builder.Services.AddOpenApi(); // Generación nativa
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.MapOpenApi(); // Expone la spec en /openapi/v1.json
    
        // Opción A: Scalar (UI moderna, recomendada)
        app.MapScalarApiReference();
    
        // Opción B: Swagger UI clásico
        // app.UseSwaggerUI(c => c.SwaggerEndpoint("/openapi/v1.json", "API v1"));
    }

    Para la UI de documentación, te recomiendo probar Scalar: tiene mucho mejor UX que Swagger UI clásico y es trivial de integrar.

    Resumen de la Parte 1

    Estos cuatro componentes son los que agrego en cualquier proyecto nuevo desde el día uno:

    • Health Checks → para que Kubernetes y el balanceador sepan el estado real de tu API
    • Exception Handler global → respuestas de error consistentes y sin leakear internos
    • Validación con Endpoint Filters → datos limpios antes de tocar la lógica de negocio
    • OpenAPI nativo → documentación siempre actualizada sin overhead

    En la Parte 2 vemos los tres componentes que marcan la diferencia cuando la API empieza a escalar: Rate Limiting, Output Cache y API Versioning.

  • Por dentro del motor: entendiendo la arquitectura de Docker

    La primera vez que ejecuté docker run funcionó. Pero cuando algo falló, no tenía idea de dónde buscar. No entendía quién hacía qué, cómo se comunicaban las piezas ni por qué a veces el daemon parecía tener vida propia. Este artículo es lo que me hubiera gustado leer antes de ese momento.

    Docker no es un solo programa: es un sistema de piezas

    Uno de los errores conceptuales más comunes cuando arrancás con Docker es pensarlo como «el comando que corre contenedores». En realidad, Docker es una arquitectura cliente-servidor compuesta por varios componentes que trabajan juntos. Entenderlos hace que todo lo demás tenga sentido.

    El flujo completo en un diagrama

    ┌─────────────────────────────────────────────────────────────┐
    │                        TU TERMINAL                          │
    │                                                             │
    │   $ docker run nginx        ← Docker Client (CLI)          │
    └──────────────────┬──────────────────────────────────────────┘
                       │  REST API (Unix socket o TCP)
                       ▼
    ┌─────────────────────────────────────────────────────────────┐
    │                    DOCKER DAEMON (dockerd)                   │
    │                                                             │
    │   • Escucha comandos del cliente                            │
    │   • Administra imágenes, contenedores, redes, volúmenes     │
    │   • Delega la ejecución a containerd                        │
    └──────────┬─────────────────────────┬────────────────────────┘
               │                         │
               ▼                         ▼
    ┌──────────────────┐      ┌──────────────────────────────────┐
    │   containerd     │      │         Docker Registry           │
    │                  │      │   (Docker Hub / privado)          │
    │  • Gestiona      │      │                                   │
    │    ciclo de vida │      │  • Almacena imágenes              │
    │    del contenedor│      │  • docker pull baja de acá        │
    │  • Usa runc para │      │  • docker push sube acá           │
    │    crear procesos│      └──────────────────────────────────┘
    └──────────────────┘

    Docker Engine: el corazón del sistema

    El Docker Engine es el conjunto completo: client + daemon + la API REST que los conecta. Cuando instalás Docker en un servidor, lo que instalás es el Engine. En mis nodos SUSE Linux, el daemon corre como servicio systemd y arranca automáticamente con el sistema.

    # Ver estado del daemon
    sudo systemctl status docker
    
    # Ver logs del daemon en tiempo real
    sudo journalctl -u docker -f
    
    # Información completa del sistema Docker
    docker info

    Docker Client: lo que escribís en la terminal

    El cliente es simplemente la CLI: el binario docker que usás en la terminal. Su único trabajo es traducir tus comandos a llamadas a la API REST del daemon. Lo que importa saber: el cliente y el daemon pueden estar en máquinas diferentes. Puedo controlar el daemon de un servidor remoto desde mi notebook sin ningún problema.

    # Conectar el cliente a un daemon remoto
    export DOCKER_HOST=tcp://192.168.1.100:2376
    docker ps  # Lista contenedores del servidor remoto
    
    # O con contextos (la forma moderna)
    docker context create servidor-prod --docker "host=ssh://mbernal@192.168.1.100"
    docker context use servidor-prod
    docker ps  # Ahora habla con el servidor remoto

    Docker Daemon (dockerd): quien realmente hace el trabajo

    El daemon es el proceso que corre en background y gestiona todo: imágenes, contenedores, redes y volúmenes. Cuando ejecutás docker run nginx, es el daemon quien:

    1. Recibe el comando del cliente
    2. Verifica si la imagen nginx existe localmente
    3. Si no existe, la descarga del registry
    4. Crea el contenedor usando containerd y runc
    5. Configura la red y el sistema de archivos
    6. Arranca el proceso principal del contenedor

    Imágenes Docker: plantillas inmutables

    Una imagen es una plantilla de solo lectura que define el sistema de archivos y la configuración inicial de un contenedor. Está compuesta por capas (layers), donde cada instrucción del Dockerfile agrega una capa nueva. Esta arquitectura por capas es brillante: si dos imágenes comparten las mismas capas base, se almacenan una sola vez en disco.

    # Ver imágenes locales
    docker images
    
    # Ver las capas de una imagen
    docker history nginx:latest
    
    # Inspeccionar metadatos completos
    docker inspect nginx:latest

    Contenedores: instancias en ejecución de una imagen

    Un contenedor es una imagen en ejecución. La diferencia clave: la imagen es inmutable (solo lectura), mientras que el contenedor agrega una capa de escritura encima donde los procesos pueden crear y modificar archivos. Cuando el contenedor se destruye, esa capa desaparece. Por eso los datos importantes van en volúmenes — pero eso lo vemos en otro artículo.

    # Relación imagen → contenedor
    docker images ls          # ver imágenes (plantillas)
    docker ps -a              # ver contenedores (instancias)
    
    # Crear contenedor sin arrancarlo
    docker create --name mi-nginx nginx
    
    # Arrancarlo
    docker start mi-nginx
    
    # O directamente: crear + arrancar
    docker run -d --name mi-nginx -p 80:80 nginx

    Docker Registry: el repositorio de imágenes

    El registry es donde viven las imágenes. Docker Hub es el registry público por defecto, pero en producción muchas empresas usan registries privados. En mi entorno on-premise uso un registry privado para no depender de internet en los deploys.

    # Levantar un registry privado local
    docker run -d -p 5000:5000 --name registry-privado   -v /data/registry:/var/lib/registry   registry:2
    
    # Tagear imagen para el registry privado
    docker tag mi-api:latest localhost:5000/mi-api:latest
    
    # Subir al registry privado
    docker push localhost:5000/mi-api:latest
    
    # Bajar desde el registry privado
    docker pull localhost:5000/mi-api:latest

    Docker Compose: orquestación local

    Docker Compose es la herramienta para definir y ejecutar aplicaciones multi-contenedor. En lugar de ejecutar múltiples docker run, definís todos los servicios en un archivo YAML y los gestionás con un solo comando. Lo veremos en profundidad más adelante — te adelanto que cambia completamente la forma de trabajar.

    El flujo completo: qué pasa cuando ejecutás docker pull nginx

    # Esto es lo que pasa internamente:
    $ docker pull nginx
    
    # 1. Docker Client envía petición al daemon via /var/run/docker.sock
    # 2. Daemon consulta: ¿tengo nginx:latest localmente?
    # 3. Si no → contacta Docker Hub (registry.hub.docker.com)
    # 4. Autentica (si la imagen es privada)
    # 5. Descarga cada capa (layer) que no tenga en cache
    # 6. Verifica integridad con el digest SHA256
    # 7. Almacena las capas en /var/lib/docker/overlay2/
    
    Using default tag: latest
    latest: Pulling from library/nginx
    a803e7c4b030: Pull complete   ← cada línea es una capa
    8b625c47d697: Pull complete
    4d3239651a63: Pull complete
    Digest: sha256:bc5eac5eafc581aeda3008b4b1f07ebba230de2f27d47767129a6a905c84f470
    Status: Downloaded newer image for nginx:latest

    Por qué me importa entender esto

    El día que tuve un contenedor que no arrancaba y no sabía por dónde empezar a debuggear, entender la arquitectura me salvó. Saber que el daemon escribe en /var/lib/docker/, que los logs del daemon están en journalctl, que el socket Unix es /var/run/docker.sock — esos detalles marcan la diferencia entre resolver el problema en 5 minutos o perder una hora.

    # Cuando algo falla, estos son mis primeros comandos:
    sudo journalctl -u docker --since "1 hour ago"
    docker info
    docker system df          # ver uso de disco
    docker system events      # stream de eventos del daemon

    Artículo anterior: Cómo Docker cambió la forma en que trabajo | Serie Docker Completo | Próximo: Mi guía para escribir Dockerfiles →


    Artículo anterior: Cómo Docker cambió la forma en que trabajo (y por qué tardé en entenderlo) | Serie Docker Completo | Próximo: Mi guía para escribir Dockerfiles que no me den vergüenza →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)