← maurobernal.com.ar

Etiqueta: virtualización

  • Docker Compose de cero a producción — Parte 3: Proyectos reales y estrategias Dev vs Prod

    Llegamos al cierre de la serie Docker Compose de cero a producción. En la Parte 1 vimos el archivo compose.yaml, en la Parte 2 los comandos y las dependencias robustas. Ahora lo llevamos a la práctica: dos proyectos reales completos y la estrategia que uso para manejar configuraciones distintas entre desarrollo y producción.

    Proyecto 1: WordPress + MySQL

    El ejemplo clásico de un stack de dos niveles: una aplicación web y su base de datos. Este proyecto completo vive en un solo archivo.

    Estructura del proyecto

    wordpress_project/
    └── compose.yaml

    compose.yaml

    services:
      db:
        image: mysql:8.0
        container_name: wordpress_db
        restart: unless-stopped
        environment:
          MYSQL_DATABASE: wordpress
          MYSQL_USER: wp_user
          MYSQL_PASSWORD: wp_password
          MYSQL_ROOT_PASSWORD: root_secret_password
        volumes:
          - db_data:/var/lib/mysql
        networks:
          - app_network
        healthcheck:
          test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
          interval: 10s
          timeout: 5s
          retries: 5
          start_period: 30s
    
      wordpress:
        image: wordpress:latest
        container_name: wordpress_app
        restart: unless-stopped
        depends_on:
          db:
            condition: service_healthy   # Espera que MySQL esté listo de verdad
        ports:
          - "8000:80"
        environment:
          WORDPRESS_DB_HOST: db:3306     # 'db' es el nombre del servicio = hostname
          WORDPRESS_DB_USER: wp_user
          WORDPRESS_DB_PASSWORD: wp_password
          WORDPRESS_DB_NAME: wordpress
        networks:
          - app_network
    
    networks:
      app_network:
        driver: bridge
    
    volumes:
      db_data:
        driver: local

    Puesta en marcha

    cd wordpress_project
    docker compose up -d
    
    # Esperar unos segundos y visitar http://localhost:8000
    # El instalador de WordPress estará listo

    Lo que me parece elegante de este ejemplo: WORDPRESS_DB_HOST: db:3306. No hay IP, no hay configuración de DNS manual. El nombre del servicio db funciona directamente como hostname gracias a la red que Compose crea automáticamente. Esto es lo que hace que los entornos sean tan portables.

    Proyecto 2: API REST con Node.js y PostgreSQL

    Este ejemplo es más representativo del trabajo diario: construimos nuestra propia imagen desde un Dockerfile y configuramos un entorno de desarrollo con hot-reload.

    Estructura del proyecto

    api_project/
    ├── compose.yaml
    ├── Dockerfile
    ├── index.js
    └── package.json

    Dockerfile

    FROM node:18-alpine
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    EXPOSE 3000
    CMD ["npm", "start"]

    index.js

    const express = require('express');
    const { Pool } = require('pg');
    const app = express();
    
    const pool = new Pool({
      user: process.env.DB_USER,
      host: process.env.DB_HOST,      // Viene del ambiente: 'db'
      database: process.env.DB_NAME,
      password: process.env.DB_PASSWORD,
      port: 5432,
    });
    
    app.get('/', async (req, res) => {
      try {
        const result = await pool.query('SELECT NOW()');
        res.send(`Hora en la base de datos: ${result.rows[0].now}`);
      } catch (err) {
        res.status(500).send('Error conectando a la base de datos');
      }
    });
    
    app.listen(3000, () => console.log('API corriendo en http://localhost:3000'));

    compose.yaml

    services:
      db:
        image: postgres:15-alpine
        environment:
          POSTGRES_DB: apidb
          POSTGRES_USER: apiuser
          POSTGRES_PASSWORD: apipassword
        volumes:
          - pg_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U apiuser -d apidb"]
          interval: 10s
          timeout: 5s
          retries: 5
    
      api:
        build: .
        ports:
          - "3000:3000"
        volumes:
          - .:/usr/src/app       # Bind mount: cambios en el código se reflejan al instante
        environment:
          DB_HOST: db
          DB_USER: apiuser
          DB_PASSWORD: apipassword
          DB_NAME: apidb
        depends_on:
          db:
            condition: service_healthy
    
    volumes:
      pg_data: {}
    # La primera vez, --build para construir la imagen
    docker compose up --build -d
    
    # Verificar que funciona
    curl http://localhost:3000
    # → Hora en la base de datos: 2025-05-01 14:23:45.123456+00

    El bind mount (- .:/usr/src/app) es el truco de desarrollo: cualquier cambio en el código se refleja dentro del contenedor sin reconstruir la imagen. Para que los cambios se apliquen automáticamente en Node.js, agregá nodemon como dependencia de desarrollo y usalo en el CMD.

    Gestión de entornos: desarrollo vs producción

    Esta fue una de las lecciones más importantes que aprendí: nunca uses el mismo archivo compose.yaml sin modificaciones para desarrollo y producción. Las necesidades son opuestas: en desarrollo querés agilidad y debuggear fácil; en producción, estabilidad, seguridad y recursos bien definidos.

    Estrategia con compose.override.yaml

    Compose tiene un mecanismo automático: si existe un compose.override.yaml en el mismo directorio, lo fusiona automáticamente con el compose.yaml base al ejecutar docker compose up.

    # compose.yaml — Base (versionado en Git)
    services:
      api:
        build: .
        restart: unless-stopped
        environment:
          - APP_ENV=production
    # compose.override.yaml — Desarrollo (puede NO versionarse si tiene configs locales)
    services:
      api:
        ports:
          - "8000:8000"
          - "9229:9229"        # Puerto de debugging de Node.js
        volumes:
          - .:/app             # Bind mount para hot-reload
        environment:
          - APP_ENV=development   # Sobreescribe el valor del base
    
      # Servicio adicional solo para desarrollo
      pgadmin:
        image: dpage/pgadmin4
        ports:
          - "5050:80"
        environment:
          PGADMIN_DEFAULT_EMAIL: dev@local.com
          PGADMIN_DEFAULT_PASSWORD: dev

    Estrategia con múltiples archivos explícitos

    # compose.prod.yaml — Producción
    services:
      api:
        restart: always
        deploy:
          resources:
            limits:
              cpus: '1.0'
              memory: 512M
        logging:
          driver: "json-file"
          options:
            max-size: "10m"
            max-file: "3"
    # Desarrollo (usa override automático)
    docker compose up -d
    
    # Producción (combina base + prod explícitamente)
    docker compose -f compose.yaml -f compose.prod.yaml up -d

    Archivos .env para variables de entorno

    Para evitar hardcodear valores en los archivos YAML (especialmente secretos), uso archivos .env. Compose los carga automáticamente si existen en el mismo directorio.

    # .env (NO versionar si tiene contraseñas reales — agregarlo al .gitignore)
    TAG=15-alpine
    POSTGRES_USER=miusuario
    POSTGRES_PASSWORD=supersecreto
    POSTGRES_DB=miapp
    # compose.yaml usando las variables del .env
    services:
      db:
        image: postgres:${TAG}
        environment:
          POSTGRES_USER: ${POSTGRES_USER}
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: ${POSTGRES_DB}

    Cada desarrollador tiene su propio .env local con sus credenciales. El archivo compose.yaml se versiona sin datos sensibles. Para producción, las variables se inyectan desde el CI/CD o desde un gestor de secretos.

    Builds multi-stage para producción

    Las imágenes de desarrollo son gordas (incluyen herramientas de build, dependencias de dev, etc.). Las de producción deben ser mínimas. Los builds multi-stage resuelven esto con un solo Dockerfile:

    # Dockerfile
    # ---- Etapa de Build ----
    FROM node:20-alpine AS builder
    WORKDIR /app
    COPY package.json .
    RUN npm install
    COPY . .
    RUN npm run build
    
    # ---- Etapa de Producción ----
    FROM node:20-slim AS production
    WORKDIR /app
    COPY --from=builder /app/dist ./dist
    COPY --from=builder /app/node_modules ./node_modules
    EXPOSE 3000
    CMD ["node", "dist/index.js"]
    # compose.yaml
    services:
      api:
        build:
          context: .
          target: production   # Solo construye hasta la etapa 'production'

    La imagen de producción solo contiene lo necesario para correr — sin el compilador, sin herramientas de build, sin dependencias de desarrollo. Más pequeña, más segura, más rápida de desplegar.

    El checklist final para ir a producción con Compose

    • restart: unless-stopped o always en todos los servicios críticos
    • ✅ Healthchecks definidos en las bases de datos y servicios de los que dependen otros
    • depends_on con condition: service_healthy donde corresponde
    • ✅ Volúmenes nombrados para cualquier dato que deba persistir
    • ✅ Secretos y contraseñas en .env o variables de CI/CD, nunca hardcodeados
    • ✅ Imágenes multi-stage para producción
    • deploy.resources.limits configurados para evitar que un servicio consuma todo el host
    • ✅ Logging configurado (json-file con max-size para evitar llenar el disco)

    ¿Y después de Compose?

    Docker Compose es ideal para un único host. Cuando necesitás escalar a múltiples máquinas, alta disponibilidad real o gestión de carga compleja, los próximos pasos naturales son:

    • Docker Swarm: orquestación multi-host con conceptos muy similares a Compose. Curva de aprendizaje baja si ya dominás Compose.
    • Kubernetes: el estándar de la industria para producción a gran escala. La herramienta kompose convert traduce tus archivos Compose a manifiestos de Kubernetes para facilitar la migración.

    Lo bueno: los conceptos que aprendiste en esta serie (servicios, redes, volúmenes, variables de entorno, healthchecks) se trasladan directamente a esos entornos.

    Parte 2: CLI esencial, depends_on, healthchecks y perfiles

  • Docker Compose de cero a producción — Parte 2: CLI, dependencias robustas y perfiles

    Esta es la segunda parte de la serie Docker Compose de cero a producción. En la Parte 1 vimos qué es Compose y cómo estructurar el archivo compose.yaml. Ahora le toca al día a día: los comandos que vas a escribir decenas de veces, cómo hacer que las dependencias entre servicios funcionen bien de verdad, y cómo manejar servicios opcionales con perfiles.

    Los comandos que más vas a usar

    Ciclo de vida

    # Levantar todo en segundo plano
    docker compose up -d
    
    # Levantar y forzar reconstrucción de imágenes
    docker compose up -d --build
    
    # Levantar solo servicios específicos
    docker compose up -d api db
    
    # Detener y eliminar contenedores + redes (conserva volúmenes)
    docker compose down
    
    # Detener y eliminar TODO incluyendo volúmenes (¡cuidado: borra datos!)
    docker compose down -v
    
    # Solo detener sin eliminar (los contenedores quedan, solo se apagan)
    docker compose stop
    
    # Iniciar contenedores ya existentes (sin recrear)
    docker compose start
    
    # Reiniciar servicios
    docker compose restart api

    La diferencia entre stop/start y down/up importa: stop preserva los contenedores existentes (más rápido para reinicios), mientras que down los elimina completamente (útil para empezar limpio).

    Inspección y debugging

    # Ver estado de todos los contenedores del proyecto
    docker compose ps
    
    # Ver logs de todos los servicios en tiempo real
    docker compose logs -f
    
    # Ver logs solo de un servicio, últimas 50 líneas
    docker compose logs -f --tail=50 api
    
    # Entrar a un contenedor con una shell interactiva
    docker compose exec api bash
    docker compose exec db psql -U myuser -d mydb
    
    # Ejecutar un comando puntual en un contenedor nuevo (y eliminarlo al terminar)
    docker compose run --rm api npm run migrate
    
    # Ver procesos dentro de cada servicio
    docker compose top
    
    # Validar y ver la configuración final (muy útil para depurar overrides y variables)
    docker compose config

    docker compose config es uno de los comandos más útiles que tardé en descubrir: muestra la configuración final después de fusionar todos los archivos y reemplazar variables de entorno. Ideal para verificar que los overrides están funcionando como esperás.

    Gestión de imágenes

    # Construir imágenes sin levantar servicios
    docker compose build
    
    # Construir solo un servicio específico
    docker compose build api
    
    # Descargar las últimas versiones de las imágenes
    docker compose pull
    
    # Ver qué imágenes usa el proyecto
    docker compose images

    depends_on y healthchecks: dependencias que funcionan de verdad

    Este es uno de los puntos donde más errores cometí al principio. depends_on: db solo garantiza que el contenedor de la base de datos inicie antes — no que la base de datos esté lista para aceptar conexiones. El resultado: la API arranca, intenta conectarse a Postgres que todavía está inicializándose, falla, y el contenedor muere.

    La solución correcta: combinar depends_on con healthcheck.

    services:
      api:
        build: .
        depends_on:
          db:
            condition: service_healthy   # Espera hasta que db esté HEALTHY, no solo running
          cache:
            condition: service_started   # Para este, alcanza con que esté corriendo
    
      db:
        image: postgres:15-alpine
        environment:
          POSTGRES_USER: myuser
          POSTGRES_DB: mydb
          POSTGRES_PASSWORD: mypassword
        healthcheck:
          # Comando que se ejecuta dentro del contenedor para verificar salud
          test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
          interval: 10s      # Verificar cada 10 segundos
          timeout: 5s        # Esperar máximo 5 segundos por respuesta
          retries: 5         # Marcar como unhealthy después de 5 fallos
          start_period: 30s  # Período de gracia al inicio (no cuenta como fallo)
    
      cache:
        image: redis:alpine
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 5s
          timeout: 3s
          retries: 3

    Las tres condiciones disponibles para depends_on:

    • service_started: el contenedor arrancó (comportamiento por defecto, no espera que la app esté lista)
    • service_healthy: el healthcheck del servicio devuelve éxito — el que más usás
    • service_completed_successfully: para tareas de inicialización que deben completarse antes de continuar (migraciones, seeds)

    Ejemplo práctico de service_completed_successfully para migraciones:

    services:
      migrate:
        build: .
        command: ["npm", "run", "migrate"]   # Se ejecuta una vez y termina
        depends_on:
          db:
            condition: service_healthy
    
      api:
        build: .
        depends_on:
          migrate:
            condition: service_completed_successfully  # La API arranca después de migrar
          db:
            condition: service_healthy

    Escalado: múltiples instancias de un servicio

    Compose permite correr múltiples instancias del mismo servicio con el flag --scale. Útil para simular carga durante desarrollo o para escalar workers en background.

    # Levantar 3 instancias del servicio 'worker'
    docker compose up -d --scale worker=3
    
    # Escalar mientras el proyecto ya está corriendo
    docker compose up -d --scale api=2 --scale worker=5

    Dos cosas importantes para que el escalado funcione:

    • No uses container_name en servicios que vas a escalar (los nombres deben ser únicos)
    • No mapees puertos fijos al host — usá puertos efímeros ("3000" en lugar de "3000:3000") o rangos ("3000-3005:3000")
    services:
      api:
        build: .
        ports:
          - "3000"    # Puerto efímero: Docker asigna un puerto libre del host por instancia
        # NO poner container_name si vas a escalar

    Perfiles: servicios que no siempre querés levantar

    Siempre hay servicios auxiliares que solo necesitás en ciertas situaciones: un gestor de base de datos visual, un servidor de correo falso para desarrollo, un servicio de métricas. Con perfiles podés tenerlos definidos en el mismo archivo pero inactivos por defecto.

    services:
      api:
        build: .
        # Sin perfil = siempre activo
    
      db:
        image: postgres:15
        # Sin perfil = siempre activo
    
      # Solo activo con el perfil 'tools'
      pgadmin:
        image: dpage/pgadmin4
        profiles: ["tools"]
        ports:
          - "5050:80"
        environment:
          PGADMIN_DEFAULT_EMAIL: admin@local.com
          PGADMIN_DEFAULT_PASSWORD: admin
    
      # Solo activo con el perfil 'tools'
      mailhog:
        image: mailhog/mailhog
        profiles: ["tools"]
        ports:
          - "8025:8025"   # UI web para ver emails enviados en desarrollo
    
      # Solo activo con el perfil 'monitoring'
      prometheus:
        image: prom/prometheus
        profiles: ["monitoring"]
    # Solo servicios base (api + db)
    docker compose up -d
    
    # Servicios base + herramientas de administración
    docker compose --profile tools up -d
    
    # Todo a la vez
    docker compose --profile tools --profile monitoring up -d

    Uso esto constantemente para pgAdmin, Adminer, MailHog o Kibana. Están definidos, documentados y listos para usar cuando los necesito, pero no arrancan por defecto y no consumen recursos innecesariamente.

    Resumen de la Parte 2

    • CLI: up/down/logs/exec/config son los comandos del día a día
    • depends_on + healthcheck: la combinación correcta para dependencias reales entre servicios
    • Escalado: --scale para múltiples instancias, sin container_name ni puertos fijos
    • Perfiles: servicios auxiliares disponibles cuando los necesitás, sin que estorben el resto

    En la Parte 3 cerramos la serie con dos proyectos reales completos y las estrategias para manejar entornos de desarrollo y producción desde el mismo base de archivos.

  • Docker Compose de cero a producción — Parte 1: Qué es y cómo funciona

    Antes de Docker Compose, levantar un entorno de desarrollo con varios servicios era un ritual de paciencia. Un docker run para la base de datos, otro para el backend, otro para Redis, acordarse de los flags de red, los volúmenes, las variables de entorno… y rezar para que el próximo dev del equipo pudiera repetir exactamente los mismos pasos.

    Docker Compose resolvió todo eso con un solo archivo YAML y un comando. En esta primera parte de la serie te cuento qué es, de dónde viene y cómo se estructura ese archivo que se vuelve la única fuente de verdad de tu entorno.

    ¿Qué es Docker Compose y por qué importa?

    Docker Compose es una herramienta diseñada para definir y ejecutar aplicaciones que constan de múltiples contenedores. Su propósito es simple pero poderoso: en lugar de gestionar cada contenedor con comandos individuales, describís el estado deseado de todo el sistema en un archivo compose.yaml, y Compose se encarga de alcanzar ese estado.

    Los beneficios que más uso en el día a día:

    • Entornos reproducibles: el clásico «funciona en mi máquina» desaparece. Si el entorno está en código, todos ejecutan lo mismo.
    • Onboarding en minutos: un nuevo dev solo necesita Docker instalado. Un docker compose up y tiene todo funcionando.
    • IaC a nivel desarrollador: el compose.yaml vive junto al código en Git. El entorno es versionable, revisable y auditable.
    • Ciclos rápidos: Compose reutiliza contenedores que no cambiaron. Los reinicios son rápidos.

    De Fig a Compose V2: una historia corta pero importante

    Docker Compose no nació en Docker. Empezó como Fig, un proyecto de la empresa Orchardup que ya ofrecía exactamente esta idea: definir y levantar entornos multi-contenedor con un archivo YAML. Docker Inc. vio el valor, adquirió Orchardup en 2013 y relanzó Fig como Docker Compose.

    Hoy existen dos versiones del CLI que vale la pena distinguir:

    CaracterísticaV1 (legado)V2 (actual)
    Comandodocker-compose (con guion)docker compose (sin guion)
    LenguajePythonGo
    Campo version:Requerido (2.0 a 3.8)Ignorado (usa Compose Spec)
    IntegraciónBinario separadoPlugin del Docker CLI

    Si encontrás documentación con docker-compose (con guion) y version: '3.8' en la primera línea, es sintaxis de V1. Todo el contenido de esta serie usa V2, que es el estándar actual.

    El archivo compose.yaml: la única fuente de verdad

    El corazón de Docker Compose es su archivo de configuración. Puede llamarse compose.yaml (preferido), compose.yml, docker-compose.yaml o docker-compose.yml — todos son reconocidos por compatibilidad.

    Las directivas de nivel superior que vas a usar en prácticamente todo proyecto:

    # compose.yaml
    services:     # Los contenedores de tu aplicación (obligatorio)
    networks:     # Redes personalizadas entre servicios (opcional)
    volumes:      # Volúmenes nombrados para persistencia (opcional)
    secrets:      # Datos sensibles (contraseñas, claves API) (opcional)
    configs:      # Configuración no sensible externa a la imagen (opcional)

    Definiendo servicios

    Cada servicio representa un componente de tu aplicación corriendo en uno o más contenedores. Los atributos que más uso:

    services:
      web:
        image: nginx:alpine            # Imagen de Docker Hub
        container_name: mi_nginx       # Nombre personalizado (cuidado si escalás)
        ports:
          - "8080:80"                  # host:contenedor
        environment:
          APP_ENV: production
        env_file:
          - .env                       # Variables desde archivo (no versionarlo con secretos)
        restart: unless-stopped        # Reinicio automático excepto si lo detenés manualmente
    
      api:
        build:                         # Construir desde Dockerfile en lugar de usar imagen
          context: .
          dockerfile: Dockerfile
          args:
            - NODE_VERSION=20          # ARGs del Dockerfile
          target: production           # Para builds multi-stage
        command: ["npm", "start"]      # Sobrescribe CMD del Dockerfile
        depends_on:
          - db                         # Arranca después de 'db'
        
      db:
        image: postgres:15-alpine
        volumes:
          - pg_data:/var/lib/postgresql/data   # Volumen nombrado para persistencia

    La flexibilidad de elegir entre image y build es clave: usás imagen oficial para componentes estándar (Postgres, Redis, Nginx) y build para tu código propio. Pueden convivir sin problema en el mismo archivo.

    Redes: cómo se comunican los servicios

    Cuando levantás un proyecto con docker compose up, Compose crea automáticamente una red bridge y conecta todos los servicios a ella. La magia: los servicios se encuentran entre sí usando su nombre como hostname DNS.

    services:
      api:
        image: mi-api
        environment:
          # 'db' es el nombre del servicio, funciona como hostname
          DB_HOST: db
          REDIS_HOST: cache
    
      db:
        image: postgres:15
    
      cache:
        image: redis:alpine

    Para mayor control, podés definir redes personalizadas y segmentar qué servicios pueden verse entre sí:

    services:
      frontend:
        networks: [public]             # Solo en la red pública
    
      api:
        networks: [public, internal]   # En ambas redes
    
      db:
        networks: [internal]           # Solo en la red interna, no expuesta al exterior
    
    networks:
      public:
        driver: bridge
      internal:
        driver: bridge
        internal: true                 # Sin acceso al exterior

    Volúmenes: persistencia de datos

    Por defecto, los datos dentro de un contenedor desaparecen cuando el contenedor se elimina. Los volúmenes resuelven esto. Hay dos tipos que uso constantemente:

    services:
      db:
        image: postgres:15
        volumes:
          # Tipo 1: Volumen nombrado — Docker gestiona el almacenamiento
          # Ideal para producción y datos de base de datos
          - pg_data:/var/lib/postgresql/data
    
      api:
        build: .
        volumes:
          # Tipo 2: Bind mount — mapea un directorio del host al contenedor
          # Ideal para desarrollo: cambios en el código se reflejan al instante
          - .:/usr/src/app
    
    # Los volúmenes nombrados se declaran en el nivel superior
    volumes:
      pg_data:
        driver: local

    La regla que sigo: volúmenes nombrados para datos que deben persistir (bases de datos, uploads, logs), bind mounts para el código fuente durante el desarrollo para tener hot-reload sin reconstruir la imagen.

    Resumen de la Parte 1

    • Docker Compose define entornos multi-contenedor en un solo archivo declarativo
    • Usá siempre V2 (docker compose sin guion); el campo version: ya no es necesario
    • Los servicios se comunican usando su nombre como hostname — no necesitás IPs ni configuración manual
    • Volúmenes nombrados para persistencia, bind mounts para desarrollo ágil

    En la Parte 2 vemos los comandos esenciales del CLI y las características que hacen que los entornos sean robustos: depends_on con healthchecks, escalado y perfiles.

  • Cómo Docker cambió la forma en que trabajo (y por qué tardé en entenderlo)

    Durante años trabajé con máquinas virtuales y pensé que era suficiente. Cada nuevo proyecto significaba clonar una VM, esperar que levantara, rezar para que las dependencias fueran las correctas y luego descubrir, inevitablemente, que algo fallaba en producción que no fallaba en mi máquina. Docker cambió todo eso. Pero no fue amor a primera vista.

    El problema que todos tenemos (y nadie quiere admitir)

    Me acuerdo exactamente del momento. Eran las 11 de la noche, estábamos por hacer un deploy de una API .NET y el servidor de producción se negaba a arrancar la aplicación. El error era críptico: una biblioteca de dependencias con versión diferente a la que teníamos en desarrollo. Tres horas después, con el equipo exhausto, logramos resolver el problema manualmente. Al día siguiente alguien preguntó: «¿y si usamos Docker?»

    En ese momento yo tenía mis reservas. «¿Para qué complicar las cosas si las VMs ya funcionan?», pensé. Tardé un tiempo en entender que no se trataba de complicar, sino exactamente de lo contrario.

    ¿Qué es Docker, realmente?

    Docker es una plataforma de código abierto que permite empaquetar una aplicación junto con todas sus dependencias —bibliotecas, runtime, herramientas del sistema, configuración— en una unidad estandarizada llamada contenedor. Ese contenedor se puede ejecutar de forma idéntica en cualquier entorno que tenga Docker instalado: tu notebook, el servidor de staging, producción en cloud o un cluster on-premise como el que manejo yo en SUSE Linux HA.

    La definición técnica es elegante: Docker comparte el kernel del sistema operativo anfitrión en lugar de virtualizar hardware completo. Eso lo hace liviano, rápido y eficiente. Pero lo que me convenció no fue la definición técnica sino lo que significa en la práctica: el contenedor que probé en mi máquina es exactamente el mismo que se ejecuta en producción. Sin sorpresas.

    # Así de simple es levantar un servidor nginx con Docker
    docker run -d -p 8080:80 --name mi-web nginx
    
    # En segundos tenés un servidor web corriendo
    # Sin instalar nginx, sin configurar nada en el sistema

    Un poco de historia: Docker no inventó los contenedores

    Algo que me resultó fascinante cuando profundicé en el tema es que la idea de contenedores no nació con Docker. La tecnología de aislamiento de procesos tiene décadas:

    • 1979 — chroot: El abuelo de todo. Permitía cambiar el directorio raíz de un proceso, aislándolo del sistema de archivos principal. Unix V7.
    • 2000 — FreeBSD Jails: Llevó el concepto más lejos: aislamiento de procesos, usuarios y red.
    • 2008 — Linux Containers (LXC): El kernel de Linux incorporó cgroups (para limitar uso de recursos) y namespaces (para aislar procesos, red, sistema de archivos). Esta es la base técnica sobre la que se construyó Docker.
    • 2013 — Docker: Solomon Hykes, trabajando en dotCloud (una empresa PaaS francesa), tomó toda esa tecnología del kernel y la envolvió en una herramienta con una CLI amigable y un ecosistema de distribución de imágenes. Presentado en PyCon 2013, liberado como open source ese mismo año.

    La genialidad de Docker no fue inventar algo nuevo sino hacer accesible lo que ya existía. Y eso, en tecnología, suele ser la innovación más poderosa.

    Por qué Docker importa: los 5 beneficios que cambiaron mi forma de trabajar

    1. Consistencia entre entornos

    Este es el grande. El que me hizo pasar de escéptico a evangelista. Con Docker, la frase «funciona en mi máquina» pierde sentido. Si el contenedor corre en dev, corre en prod. La imagen es la misma. Las dependencias son las mismas. El runtime es el mismo.

    # Si esto funciona en tu máquina...
    docker run --rm -v $(pwd):/app -w /app node:20 node app.js
    
    # ...esto funcionará igual en el servidor de producción
    docker run --rm -v /apps/miapp:/app -w /app node:20 node app.js

    2. Arranque en segundos (no en minutos)

    Venía de un mundo donde levantar una VM significaba esperar 2 o 3 minutos mínimo. Con Docker, un contenedor arranca en segundos o incluso milisegundos. Eso transforma completamente el flujo de trabajo: podés destruir y recrear entornos constantemente sin perder el hilo.

    # Levantar PostgreSQL 16 completo en segundos:
    docker run -d   --name postgres-dev   -e POSTGRES_PASSWORD=mipassword   -e POSTGRES_DB=miapp   -p 5432:5432   postgres:16-alpine
    
    # Listo. Base de datos funcionando sin instalar nada en el sistema.

    3. Eficiencia de recursos

    En mi cluster on-premise de dos nodos SUSE Linux, cada recurso cuenta. Antes, cada servicio necesitaba su propia VM con un SO completo. Hoy tengo decenas de contenedores corriendo en los mismos nodos que antes apenas soportaban cinco VMs. Los contenedores comparten el kernel del host: no replican el sistema operativo, solo empaquetan lo que la aplicación necesita.

    4. Portabilidad real

    Construís la imagen una vez y la ejecutás en cualquier lado: tu notebook con WSL2, un servidor Ubuntu on-premise, AWS EC2, Azure Container Instances, Google Cloud Run. La misma imagen. Sin recompilar, sin reconfigurar. Esta portabilidad es lo que después abre la puerta a Kubernetes, que orquesta esos contenedores a escala.

    5. Aislamiento y seguridad

    Cada contenedor vive en su propio mundo: sus procesos, su sistema de archivos, su red. Si un contenedor tiene un problema, los demás no se enteran. Puedo tener dos versiones de Python corriendo en paralelo sin conflictos. Puedo experimentar con una nueva versión de una librería sin tocar el sistema. Para un entorno de producción, eso es tranquilidad.

    Docker vs. Máquinas Virtuales: la comparación que aclara todo

    Cuando empecé con Docker, la pregunta que más me hacían era: «¿y entonces las VMs mueren?». La respuesta corta es no. La respuesta larga es más interesante.

    CaracterísticaDocker (Contenedor)Máquina Virtual
    AbstracciónNivel de SONivel de hardware
    KernelComparte el kernel del hostKernel propio por VM
    Tiempo de arranqueSegundos / milisegundosMinutos
    Tamaño de imagenMBsGBs
    Consumo de recursosMuy bajoAlto (OS completo por VM)
    Densidad en el hostAlta (decenas/cientos)Baja (pocas VMs)
    AislamientoA nivel de procesoAislamiento completo de SO
    PortabilidadMuy altaMedia (imágenes pesadas)

    ¿Cuándo uso Docker?

    • Microservicios y APIs independientes
    • Entornos de desarrollo locales (base de datos, cache, message broker)
    • Pipelines de CI/CD donde necesito reproducibilidad
    • Cuando necesito escalar rápido y aprovechar recursos
    • Como base para orquestar con Kubernetes

    ¿Cuándo sigo usando VMs?

    • Cuando necesito aislar completamente a nivel de SO (seguridad máxima)
    • Aplicaciones legacy que dependen de un SO específico
    • Cuando el equipo no está listo para el cambio (la convivencia es válida)
    • Workloads que requieren acceso directo a hardware específico

    En muchos de mis entornos actuales uso ambos: contenedores Docker corriendo dentro de VMs. Así combino el aislamiento fuerte de las VMs con la eficiencia y portabilidad de los contenedores.

    Un ejemplo real: de «funciona en mi máquina» a «funciona en todos lados»

    Hace poco tuve que desplegar una aplicación .NET 8 que en desarrollo corría perfecto y en producción fallaba por diferencias en las librerías del sistema. Con Docker, la solución fue crear una imagen que empaquetara exactamente el runtime y las dependencias necesarias:

    # Dockerfile simple para una API .NET 8
    FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
    WORKDIR /app
    EXPOSE 80
    
    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    WORKDIR /src
    COPY ["MiApi.csproj", "."]
    RUN dotnet restore
    COPY . .
    RUN dotnet build -c Release -o /app/build
    
    FROM build AS publish
    RUN dotnet publish -c Release -o /app/publish
    
    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "MiApi.dll"]

    Desde ese día, el deploy es siempre el mismo comando:

    docker build -t miapi:latest .
    docker run -d -p 80:80 --name miapi miapi:latest

    Sin sorpresas. Sin diferencias entre entornos. Sin las 11 de la noche arreglando dependencias.

    Conclusión: vale la curva de aprendizaje

    Si estás evaluando si meterte con Docker, mi respuesta es sí, sin dudar. La curva de aprendizaje existe, pero no es empinada. En pocos días podés tener tus primeros contenedores funcionando. Y una vez que lo incorporás a tu flujo de trabajo, no entendés cómo trabajabas sin él.

    En los próximos artículos de esta serie voy a ir a fondo con la arquitectura interna de Docker, los Dockerfiles, la gestión de redes, volúmenes y finalmente Docker Compose —el salto que te lleva de un contenedor a un entorno completo con múltiples servicios coordinados.

    Si tenés preguntas o querés compartir tu propia experiencia con Docker (o con las VMs que todavía no pudiste abandonar), dejá tu comentario abajo. 👇


    Este artículo forma parte de la serie Docker Completo, donde recorro desde los conceptos básicos hasta casos de uso avanzados en entornos on-premise y cloud.


    Serie Docker Completo | Próximo: Por dentro del motor: entendiendo la arquitectura de Docker →

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)