Tag: Docker

  • Desplegar agentes LangChain en producción sin perder el estado

    Desplegar agentes LangChain en producción sin perder el estado

    En local funcionaba perfecto.

    El agente respondía, llamaba a sus herramientas, escribía token a token en la terminal. Lo metí en un contenedor y lo subí. A los pocos días empecé a ver el mismo patrón en los logs: conversaciones cortadas a mitad, y usuarios que volvían y encontraban un agente sin memoria de nada.

    No había ningún error en el código del agente. El agente estaba bien. Lo que estaba mal era todo lo que hay entre el agente y el usuario.

    Y es que desplegar agentes LangChain en producción no se parece a desplegar una API REST. Una API REST responde en 200 milisegundos y no recuerda nada. Un agente tarda treinta segundos, mantiene la conexión abierta todo ese rato, guarda estado entre turnos y llama a servicios externos que fallan. Cuatro propiedades que rompen, una por una, las suposiciones sobre las que está construida tu infraestructura.

    Si todavía estás decidiendo la forma del agente —grafo de estados o bucle— eso lo desarrollé en LangGraph TypeScript: cuándo un grafo gana al while loop. Este post empieza donde acaba aquel: ya tienes el grafo, ahora hay que sacarlo del portátil.

    Todo el código está escrito contra langchain 1.5 y @langchain/langgraph 1.4, con @langchain/langgraph-checkpoint-postgres 1.0. Es importante que mires las versiones: la API de creación de agentes y la de streaming cambiaron en LangChain 1, y casi todos los tutoriales que vas a encontrar están escritos contra la anterior.

    Última revisión: 31 de agosto de 2026. Si LangGraph publica una 2.x, el PostgresSaver es lo primero que hay que volver a comprobar.


    Los 3 fallos al desplegar agentes LangChain en producción

    Los tres fallos que rompen un agente en producción son la conexión que corta el proxy, el estado que vive en RAM y la herramienta sin timeout. No son los que parecen, y son los que me han costado tiempo de verdad:

    # Fallo Por qué pasa
    1 La conexión se corta a mitad de respuesta El proxy cierra la conexión por inactividad: mientras el modelo "piensa" no viajan bytes
    2 El agente pierde la memoria El historial vivía en RAM y el contenedor se reinició o escaló a otra instancia
    3 Una herramienta se cuelga y arrastra al proceso Sin timeout ni cancelación, la petición queda colgada y la conexión SSE ocupando memoria

    Conviene desmontar un mito antes de seguir, porque lo he leído muchas veces: el bucle del agente no bloquea el event loop. El trabajo de un agente es esperar respuestas HTTP del modelo y de sus herramientas, así que es I/O, y Node o Bun siguen atendiendo peticiones mientras tanto. Lo que sí se te agota es otra cosa. La memoria que ocupa cada conexión abierta, el límite de concurrencia de tu plataforma, y los sockets que nadie cerró porque el cliente se fue sin avisar.


    El estado: sácalo de la RAM el primer día

    El estado de un agente LangGraph no puede vivir en una variable del proceso: en cuanto el contenedor se reinicia o escala, la conversación desaparece. Este es el arreglo con más retorno y el más barato de aplicar.

    Mientras el estado vive en memoria, tu agente recuerda hasta el próximo despliegue. Y como los reinicios no los decides tú —los decide el autoescalado, un health check o un deploy—, no es un riesgo teórico: pasa.

    La solución en LangGraph es un checkpointer, que guarda el estado del grafo después de cada paso en una base de datos externa:

    import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
    
    const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
    
    // Solo la primera vez: crea las tablas que necesita el checkpointer.
    await checkpointer.setup();
    

    Ese setup() va en el paso de migraciones de tu despliegue, no en el arranque de cada instancia. Si lo dejas en el boot y levantas diez réplicas, tienes diez procesos creando las mismas tablas a la vez.

    A partir de ahí, cada conversación se identifica con un thread_id. El agente no "recuerda" nada en memoria: al recibir un turno nuevo, lee el estado de ese hilo desde Postgres, avanza y vuelve a escribirlo.

    Eso cambia una propiedad importante de tu servicio: pasa a ser reemplazable. Puedes matar el contenedor, desplegar una versión nueva o levantar diez réplicas detrás de un balanceador, y cualquiera de ellas puede continuar cualquier conversación, porque el estado no está en ninguna de ellas.


    El servidor: streaming que sobrevive al proxy

    El segundo problema es la conexión. Un agente tarda decenas de segundos en completar una respuesta, y durante buena parte de ese tiempo no manda ni un byte, porque está esperando al modelo o ejecutando una herramienta.

    Para un proxy —Nginx, Cloudflare, el balanceador de tu PaaS— una conexión abierta que no transmite nada es una conexión muerta, y la cierra.

    Así que hay tres cosas que hacer, y las tres se olvidan:

    • Enviar las cabeceras SSE inmediatamente, para que el proxy sepa que esto es un stream y no espere a tener el cuerpo entero.
    • Mandar un latido cada pocos segundos aunque no haya nada que decir, para que la conexión nunca esté inactiva.
    • Abortar el trabajo si el cliente se va, o seguirás pagando tokens de una respuesta que ya no lee nadie.
    import express from "express";
    import { createAgent } from "langchain";
    
    // Necesita @langchain/anthropic instalado y ANTHROPIC_API_KEY en el entorno.
    const agent = createAgent({
      model: "anthropic:claude-sonnet-5",
      tools: [buscarPedido], // la definimos más abajo
      checkpointer,          // el PostgresSaver de arriba
    });
    
    const app = express();
    app.use(express.json());
    
    app.post("/api/agent/chat", async (req, res) => {
      // El thread_id se valida contra el usuario autenticado: si no,
      // cualquiera puede leer la conversación de cualquier otro.
      const { threadId, message } = req.body;
    
      res.setHeader("Content-Type", "text/event-stream");
      res.setHeader("Cache-Control", "no-cache, no-transform");
      res.setHeader("Connection", "keep-alive");
      res.setHeader("X-Accel-Buffering", "no"); // que Nginx no acumule el stream
      res.flushHeaders();                       // sin esto, el proxy espera
    
      // Latido: mantiene viva la conexión frente al idle timeout del proxy.
      const heartbeat = setInterval(() => {
        if (res.writableEnded || res.destroyed) return;
        res.write(": ping\n\n");
      }, 15_000);
    
      // Si el cliente cierra la pestaña, se cancela el trabajo del agente.
      const controller = new AbortController();
      res.on("close", () => {
        clearInterval(heartbeat);
        controller.abort();
      });
    
      try {
        const stream = await agent.streamEvents(
          { messages: [{ role: "user", content: message }] },
          {
            version: "v3",
            configurable: { thread_id: threadId },
            signal: controller.signal,
          },
        );
    
        await Promise.all([
          (async () => {
            for await (const m of stream.messages) {
              for await (const token of m.text) {
                res.write(`data: ${JSON.stringify({ type: "token", text: token })}\n\n`);
              }
            }
          })(),
          (async () => {
            for await (const call of stream.toolCalls) {
              res.write(`data: ${JSON.stringify({ type: "tool", name: call.name })}\n\n`);
            }
          })(),
        ]);
    
        res.write("data: [DONE]\n\n");
      } catch (err) {
        if (!controller.signal.aborted) {
          res.write(`data: ${JSON.stringify({ type: "error" })}\n\n`);
        }
      } finally {
        clearInterval(heartbeat);
        res.end();
      }
    });
    
    // Cloud Run y casi cualquier PaaS inyectan PORT: no lo fijes a mano.
    app.listen(process.env.PORT ?? 3000);
    

    Dos detalles que merecen su párrafo.

    El version: "v3". Es la API de streaming con proyecciones tipadas, y aparece en langchain a partir de la 1.4.0. En vez de recibir un chorro plano de eventos y filtrar por nombre, iteras stream.messages para los tokens y stream.toolCalls para las herramientas, cada uno por su lado. Si copias un tutorial que usa version: "v2" y compara event.event === "on_chat_model_stream", estás escribiendo contra la API anterior.

    Un aviso que no vas a encontrar en esos tutoriales: LangChain la marca como experimental en su propia definición de tipos —"This v3 stream is experimental and its API may change in future releases"—. La uso igualmente porque la alternativa envejece peor, pero fija la versión en tu package.json y no la des por estable.

    El signal. RunnableConfig acepta un AbortSignal, y es lo que convierte el res.on("close") en una cancelación real en lugar de un simple return. Sin él, el cliente se va pero tu servidor sigue generando tokens contra la API del modelo hasta el final.

    Si vienes del stack de Vercel, el mismo problema con otras piezas lo resolví en streaming de respuestas de IA con NestJS y el Vercel AI SDK.


    Las herramientas: donde se cuelga todo

    El fallo que más veces he tenido que diagnosticar en producción no está en el modelo ni en el grafo. Está en una herramienta que llama a una API de terceros que ese día tarda cuarenta segundos en responder.

    Sin timeout propio, esa herramienta se lleva por delante la petición entera. El usuario ve un cursor parpadeando, la conexión sigue abierta consumiendo memoria, y tú no sabes en qué paso se quedó.

    La regla es simple: toda herramienta que salga a la red lleva su propio timeout, más corto que el de la petición completa, y devuelve un texto en lugar de reventar. Ésta es la buscarPedido que usa el agente de arriba:

    import { tool } from "langchain";
    import * as z from "zod";
    
    const buscarPedido = tool(
      async ({ id }) => {
        try {
          const res = await fetch(`${API}/pedidos/${id}`, {
            signal: AbortSignal.timeout(8_000), // esta tool falla en 8s o no falla
          });
          return JSON.stringify(await res.json());
        } catch {
          // El agente lee esto y decide: reintentar o admitir que no puede.
          return "El servicio de pedidos no respondió en 8 segundos.";
        }
      },
      {
        name: "buscar_pedido",
        description: "Busca un pedido por su identificador",
        schema: z.object({ id: z.string() }),
      },
    );
    

    Y que falle está bien. Un error controlado vuelve al agente como resultado de la herramienta, el modelo lo lee y puede reintentar o decir que no ha podido. Una herramienta colgada, en cambio, no le da ninguna información con la que trabajar: el agente se queda esperando y el usuario también.

    Si además quieres que la herramienta muera cuando el cliente cierra la pestaña, combina su propio timeout con el signal que le llega en el config: el AbortSignal.timeout por sí solo no escucha esa cancelación.

    Ese diseño de herramientas —contrato claro, fallo rápido y un error que el modelo pueda leer— es el que trabajo paso a paso en el curso Construye con IA con Claude Code.

    Cómo evitar que ese reintento se convierta en un bucle sin fin lo desarrollé en Agentic Loop en TypeScript. Y cómo probar todo esto en CI antes de que llegue a producción, en test harness para agentes de IA.


    Qué pasa de verdad cuando el contenedor se reinicia

    Aquí es donde casi todas las guías te dicen una verdad a medias. "Con un checkpointer no pierdes el estado" es cierto, pero conviene saber exactamente qué se salva y qué no.

    Si el contenedor muere mientras un agente está a mitad de una tarea:

    • Se conserva todo lo que ya estaba confirmado en el último checkpoint: los turnos anteriores, los resultados de las herramientas que ya terminaron y el estado del grafo hasta ese punto.
    • Se pierde el paso en vuelo. Los tokens que se estaban generando en ese momento no están en ninguna parte, y la conexión SSE del cliente se cae con el proceso.
    • No se reanuda solo. No hay nadie que retome la tarea al arrancar el contenedor nuevo. Y ojo con lo que significa "volver a llamar". El checkpoint se escribe por paso del grafo. Si el proceso murió justo después de que el modelo pidiera una herramienta, el estado guardado termina en un mensaje del asistente con tool_calls y ninguna respuesta. Mandar ahí un mensaje nuevo del usuario produce un 400 del proveedor, porque todo tool_use exige su tool_result. Antes de aceptar el turno siguiente hay que cerrar el paso pendiente de ese hilo.

    Esto tiene una consecuencia de diseño que hay que asumir pronto: el thread_id tiene que sobrevivir al navegador y estar atado al usuario. Que lo genere el cliente está bien; que el servidor se lo crea sin comprobar contra quién ha iniciado sesión, no. Y si el identificador solo vive en la memoria del navegador, un refresco lo pierde y la conversación se queda huérfana en la base de datos: existe, pero nadie sabe pedirla.

    Y si la tarea es larga de verdad —un informe que tarda diez minutos, un procesamiento por lotes—, el patrón correcto no es este. Es aceptar la petición, devolver un identificador y ejecutar el trabajo en una cola aparte, con el cliente consultando el progreso. Un agente detrás de una petición HTTP tiene sentido para conversación, no para trabajo de fondo.


    Empaquetar y desplegar agentes LangChain en producción

    Empaquetar un agente es un Dockerfile normal con un detalle que rompe builds: desde Bun 1.2 el lockfile por defecto es bun.lock, no bun.lockb.

    FROM oven/bun:1-alpine
    WORKDIR /app
    
    # Desde Bun 1.2 el lockfile por defecto es bun.lock (texto), no bun.lockb.
    COPY package.json bun.lock ./
    RUN bun install --frozen-lockfile --production
    
    COPY . .
    
    ENV NODE_ENV=production
    USER bun
    CMD ["bun", "run", "src/server.ts"]
    

    Si copias un Dockerfile de hace un par de años vas a ver COPY package.json bun.lockb ./, y con un proyecto actual esa línea falla porque ese archivo ya no existe.

    Y un .dockerignore al lado, que es el otro detalle que rompe builds:

    node_modules
    .git
    .env*
    

    Sin él, el COPY . . te mete el node_modules de tu portátil encima del que acabas de instalar dentro del contenedor, con binarios compilados para otra plataforma.

    Sobre dónde desplegarlo, lo único que importa de verdad es cuánto tiempo te dejan tener una conexión abierta:

    Plataforma Timeout por defecto Máximo Qué tienes que tocar
    Cloud Run 300 s (5 min) 3.600 s (60 min) Subir el timeout y fijar una instancia mínima para no pagar arranque en frío por conversación
    Render · Railway · Fly Idle timeout propio, más corto No es ilimitado El latido SSE: sin él la conexión cuenta como inactiva y la cortan

    Los números de Cloud Run salen de su documentación de timeouts. Para un agente conversacional con streaming, el valor de fábrica se queda corto en cuanto una herramienta se ralentiza.

    Y aquí hay una distinción que cuesta un incidente aprender: el latido no te salva del timeout de Cloud Run. El latido derrota los timeouts de inactividad, que es lo que aplican los PaaS. El de Cloud Run es duración máxima de la petición, y corta igual aunque estés emitiendo tokens sin parar. En todos los que he probado, además, ninguno mantiene una conexión abierta indefinidamente.

    Un agente en producción además habla con servicios externos, y ahí el problema deja de ser el deploy y pasa a ser el transporte y la autenticación. Eso lo cubrí en MCP en producción: lo que se rompe cuando tu server sale del portátil.


    No despliegues a ciegas

    En un backend clásico te basta con los errores HTTP. En un agente necesitas ver el árbol de decisiones: qué prompt se envió, qué herramienta se ejecutó, cuánto tardó y qué costó. Sin eso, "va lento" y "responde mal" son incidencias que no puedes investigar.

    No lo desarrollo aquí porque ya tiene su sitio. El planteamiento está en cómo monitorear agentes de IA en producción, la implementación en Langfuse paso a paso, y la parte que te va a llegar en la factura, en medir el consumo de tokens.


    Checklist antes de pulsar deploy

    1. El estado, fuera del proceso. Checkpointer con setup() ejecutado y thread_id generado y persistido por el cliente.
    2. El stream, blindado. flushHeaders(), latido cada 15 segundos y AbortSignal conectado al cierre de la conexión.
    3. Las herramientas, con timeout propio. Más corto que el de la petición, y que fallen con un error que el agente pueda leer.
    4. El timeout de la plataforma, subido. El de fábrica está pensado para APIs que responden rápido, no para agentes.
    5. Trazas desde el primer despliegue. No desde el primer incidente.

    Las arquitecturas de agentes que tengo funcionando, con sus fallos y lo que costó arreglarlos, las comparto cada semana en Dominicode Labs.

    Que un agente funcione en tu portátil es un experimento. Que sobreviva a un reinicio es ingeniería.


    Preguntas frecuentes

    ¿Cómo se despliega un agente LangChain en producción?

    Desplegar agentes LangChain en producción son cuatro decisiones, no una. Primera: sacar el estado del proceso con un checkpointer persistente —PostgresSaver sobre Postgres— para que cualquier réplica pueda continuar cualquier conversación. Segunda: servir la respuesta por SSE con flushHeaders(), un latido cada 15 segundos y un AbortSignal atado al cierre del cliente, para que ningún proxy corte el stream. Tercera: poner timeout propio a cada herramienta que salga a la red, más corto que el de la petición. Y cuarta: subir el timeout de la plataforma, que de fábrica está pensado para APIs que responden en milisegundos. El contenedor en sí es lo de menos.

    ¿Postgres o Redis para el checkpointer?

    Postgres por defecto. El estado de una conversación es un dato que quieres conservar, consultar y auditar más tarde, y Postgres te lo da sin trabajo extra. Redis tiene sentido cuando la latencia de lectura del estado empieza a notarse de verdad o cuando el historial es efímero y no te importa perderlo. Empezar por Redis "porque es más rápido" suele salir caro el día que necesitas saber qué le contestó el agente a un cliente hace tres semanas.

    Si el contenedor se reinicia a mitad de una tarea, ¿se reanuda sola?

    No. Se conserva el estado hasta el último checkpoint confirmado, pero el paso que estaba en vuelo se pierde y nadie retoma la tarea por su cuenta. La reanudación la dispara el cliente cuando vuelve a llamar con el mismo thread_id, siempre que el paso pendiente se cierre antes de mandar un mensaje nuevo. Si el hilo se quedó con una petición de herramienta sin responder, el proveedor devuelve un 400. Y si necesitas que el trabajo termine sí o sí aunque nadie esté mirando, eso no va en una petición HTTP: va en una cola.

    ¿SSE o WebSocket para un agente?

    SSE en la mayoría de casos. La comunicación de un agente conversacional es casi toda en un sentido —el servidor manda tokens— y SSE va sobre HTTP normal, así que atraviesa proxies y balanceadores sin configuración especial. La reconexión automática te la da EventSource, pero solo habla GET: con el endpoint POST de arriba consumes el stream con fetch y ReadableStream, y la reconexión la escribes tú. WebSocket compensa cuando de verdad necesitas un canal bidireccional con mucho tráfico del cliente hacia el servidor, y a cambio te complica el despliegue.

    ¿Cuánto timeout pongo en Cloud Run?

    El valor de fábrica son 5 minutos y el máximo son 60. Para un agente conversacional, subirlo a 10-15 minutos suele ser suficiente: cubre las respuestas largas y las herramientas lentas sin dejar conexiones zombis eternas. Ponerlo al máximo no es gratis, porque una conexión colgada ocupa una instancia durante todo ese tiempo.

    ¿Esto vale con otro modelo que no sea Claude?

    Sí. La arquitectura —checkpointer externo, streaming con latido, cancelación y timeouts por herramienta— es independiente del proveedor. Lo único que cambia es el identificador del modelo que le pasas a createAgent y el paquete de integración correspondiente.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • ¿Por qué todo dev debe aprender Docker?

    ¿Por qué todo dev debe aprender Docker?

    Tiempo estimado de lectura: 4 min

    • Consistencia: Docker asegura que el mismo artefacto se ejecute en todas partes.
    • Automatización: Imágenes reproducibles facilitan CI/CD y testing realista.
    • Aislamiento: Dependencias separadas sin la sobrecarga de VMs.
    • Onboarding rápido: Un `docker compose up` pone en marcha el stack.

    Introducción

    ¿Puedes hacer un post sobre por qué todo dev debe aprender Docker? Sí. Y si no lo aprendes ahora, tu siguiente bug será por culpa del entorno, no de tu código.

    Docker no es un lujo ni una moda. Es la herramienta que convierte el “funciona en mi máquina” en reproducibilidad real. Aquí explico, con ejemplos y pragmatismo, por qué todo desarrollador debería dominar Docker —desde Frontend hasta Data Science— y cómo empezar hoy mismo.

    Resumen rápido (lectores con prisa)

    Docker es un sistema de contenedores que empaqueta runtime, librerías y configuración en imágenes reproducibles. Úsalo para asegurar consistencia entre desarrollo, staging y producción. Importante en pipelines CI/CD, testing de integración y para aislar dependencias sin VMs. Reutilizable en infra y orquestadores.

    Qué aporta Docker en la práctica

    Predictibilidad

    Una imagen Docker contiene todo lo necesario: runtime, librerías y configuración. Construyes una imagen en tu laptop y la misma imagen se ejecuta igual en AWS, GCP o en la máquina del QA. Documentación oficial

    Velocidad de onboarding

    En lugar de instalar manualmente Postgres, Redis y versiones de Node/Python, un nuevo desarrollador clona y ejecuta docker compose up. En 10–15 minutos tiene el stack completo corriendo.

    Testing realista

    En CI puedes levantar una base de datos real en contenedores para pruebas de integración y destruirla al terminar. Eso reduce falsos positivos y hace tus tests representativos del entorno productivo.

    Aislamiento sin el peso de una VM

    Los contenedores comparten kernel pero aíslan procesos, arrancan en milisegundos y son baratos en recursos. No es lo mismo que una VM; es práctico para desarrollos locales y pipelines.

    Casos relevantes para Dominicode (n8n, IA y automatización)

    n8n

    n8n: la forma más robusta de self-hosting y actualizar workflows es con Docker. Ver guía

    Bases de datos vectoriales (RAG)

    Qdrant, Chroma y similares se levantan con un solo comando docker run o via Compose. Qdrant

    LLMs locales y experimentación

    Herramientas como Ollama o contenedores con runtimes Python permiten probar modelos sin ensuciar tu entorno.

    Si trabajas con pipelines de IA o microservicios, Docker es la columna vertebral que hace posible orquestar y reproducir todo.

    Ejemplo práctico: Dockerfile mínimo para Node.js

    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --production
    COPY . .
    EXPOSE 3000
    CMD ["node", "server.js"]

    Build y ejecución:

    • docker build -t myapp:latest .
    • docker run -p 3000:3000 myapp:latest

    Ese Dockerfile te obliga a pensar en dependencias, artefactos y tamaño de imagen —prácticas que mejoran tu disciplina como dev.

    Consideraciones arquitectónicas y profesionales

    • CI/CD: integrar docker build y push a un registry (Docker Hub, GitHub Container Registry) convierte imágenes en artefactos versionados. Pipeline reproducible = despliegues confiables.
    • Escalado: saber Docker es prerrequisito para entender Kubernetes y orquestadores. No necesitas dominar K8s ahora, pero sin contenedores no tiene sentido.
    • Seguridad: aplica buenas prácticas (no correr procesos como root, usar imágenes ligeras, escanear vulnerabilidades). Herramientas como Docker Scout ayudan a auditar imágenes.

    Dominar Docker demuestra que no solo escribes código, sabes cómo se ejecuta y se despliega. Eso te coloca automáticamente en otra liga profesional.

    Cómo empezar en 30 minutos

    1. Instala Docker Desktop
    2. Crea un Dockerfile básico (el de arriba funciona).
    3. Levanta un stack simple con docker-compose.yml (app + Postgres).
    4. Ejecuta pruebas locales y rompe tu entorno deliberadamente para aprender límites.

    Practica containerizando un proyecto real: un microservicio, un worker de background o un pipeline de pruebas. Si trabajas con IA, levanta un Qdrant y conecta tu RAG local.

    Cierre: beneficio claro y qué hacer ahora

    Aprender Docker cambia tu forma de trabajar: reduce fricción, acelera onboarding y hace tus despliegues previsibles. No es una habilidad opcional; es la base para cualquier cosa que hoy llamamos “despliegue moderno”.

    Haz esto ahora: escribe un Dockerfile para tu proyecto más reciente, crea una imagen y súbela a un registry privado. Habrá una pequeña inversión de tiempo. El retorno será inmediato: menos bugs por entorno, menos llamadas desesperadas al equipo de infra y más tiempo para escribir buen software. Esto no acaba aquí: dominar Docker es el primer paso para orquestar sistemas reales y escalar con criterio.

    Si quieres experimentar con workflows y automatización en entornos reproducibles, echa un vistazo a Dominicode Labs para ejemplos prácticos y plantillas que puedes levantar con Docker. Es un complemento lógico para quien trabaja en automatización y pipelines de IA.

    FAQ

    ¿Por qué Docker y no solo instalar dependencias localmente?

    Docker garantiza que el entorno sea idéntico entre desarrolladores, CI y producción. Instalar manualmente depende de versiones, PATHs y configuraciones locales que difieren entre máquinas.

    Con una imagen reproducible, el problema “works on my machine” desaparece porque ejecutas el mismo artefacto en todas partes.

    ¿Docker reemplaza a las máquinas virtuales?

    No exactamente. Los contenedores comparten el kernel del host y aíslan procesos, lo que los hace más ligeros y rápidos que una VM. Para casos que necesitan un kernel distinto o aislamiento completo, una VM sigue siendo necesaria.

    ¿Necesito conocer Kubernetes para usar Docker?

    No para empezar. Docker es la base; entender contenedores es prerequisito para aprender orquestadores como Kubernetes. Puedes ser productivo con Docker sin dominar K8s.

    ¿Cómo puedo usar Docker en CI para pruebas?

    Levanta servicios reales (bases de datos, colas) como contenedores durante la ejecución de tests de integración y destrúyelos después. Así tus tests reflejan el entorno productivo y reducen falsos positivos.

    ¿Cuáles son errores comunes al empezar con Docker?

    Errores típicos: usar imágenes demasiado grandes, ejecutar procesos como root, no versionar imágenes en un registry y olvidar variables de entorno sensibles en código. Aplicar buenas prácticas desde el inicio evita técnicos de seguridad y despliegue.

  • Por qué todo desarrollador debe aprender Docker

    Por qué todo desarrollador debe aprender Docker

    ¿Por qué todo dev debe aprender Docker?

    Tiempo estimado de lectura: 4 min

    • Consistencia entre entornos: elimina el “funciona en mi máquina”.
    • Onboarding y DX más rápidos: arranca el stack con un comando.
    • Tests y CI reproducibles: bases de datos y servicios reales en contenedores.
    • Aislamiento ligero: contenedores vs VMs para microservicios y pipelines.
    • Base para orquestación: prerequisito para Kubernetes y cloud-native.

    Introducción

    Sí —y la respuesta es práctica, técnica y urgente: Docker deja de ser una opción para quien escribe software serio. Aquí explico por qué, con ejemplos concretos, riesgos a evitar y pasos para empezar hoy mismo.

    Resumen rápido (para IA y lectores con prisa)

    Qué es: Docker es una plataforma para empaquetar aplicaciones en imágenes y ejecutar contenedores aislados.

    Cuándo usarlo: siempre que necesites reproducibilidad entre entornos, pruebas realistas o despliegues cloud-native.

    Por qué importa: elimina discrepancias de entorno, acelera onboarding y forma la base para orquestadores como Kubernetes.

    Cómo funciona (breve): construyes una imagen (Dockerfile), la ejecutas como contenedor y puedes orquestar varios servicios con Docker Compose o un registry/CI.

    ¿Por qué todo dev debe aprender Docker? — motivos prácticos

    1. Consistencia absoluta entre entornos

    Docker empaqueta la aplicación con su runtime, librerías y configuraciones. Una imagen construida en tu máquina es la misma que llega a staging o producción; el famoso “funciona en mi máquina” desaparece. Documentación oficial: Documentación oficial

    2. Velocidad de onboarding y Developer Experience

    Un nuevo desarrollador no necesita pelear con versiones de DB o runtimes. docker compose up levanta todo (app, DB, cache) en minutos. Eso reduce a horas lo que antes costaba días.

    3. Testing realista y reproducible

    Levantar una base de datos real, colas o servicios externos en CI es trivial. Tests de integración que usan contenedores reflejan mejor el entorno productivo y reducen falsos positivos.

    4. Aislamiento ligero frente a VMs

    Los contenedores comparten kernel pero aíslan procesos; arrancan en milisegundos y consumen mucho menos recursos que una VM, ideal para microservicios y pipelines locales.

    5. Fundamento para orquestación y cloud-native

    Kubernetes, ECS o Cloud Run funcionan sobre contenedores. Aprender Docker es prerrequisito para entender despliegues escalables.

    Ejemplos concretos aplicables hoy

    Dockerfile minimalista (Node.js)

    FROM node:20-alpine AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    RUN npm run build
    
    FROM node:20-alpine
    WORKDIR /app
    COPY --from=builder /app/dist ./dist
    USER node
    CMD ["node", "dist/main.js"]

    Multi-stage builds reducen tamaño y separan dependencias de build/runtime — práctica obligatoria en producción.

    docker-compose para desarrollo (app + Postgres)

    version: '3.8'
    services:
      app:
        build: .
        ports: ["3000:3000"]
        volumes: ["./src:/app/src"]
        depends_on: ["db"]
      db:
        image: postgres:15
        environment:
          POSTGRES_DB: appdb

    Con esto tienes un entorno reproducible y editable localmente.

    Docker en automatización e IA (casos Dominicode)

    n8n

    n8n: el self-hosting recomendado se hace con Docker Compose. Levantar n8n + Postgres en staging evita sorpresas en workflows productivos. Guía: n8n – self-hosting

    RAG y vectores

    Qdrant o Chroma se levantan por Docker sin configurar dependencias nativas complejas. Más info: Qdrant

    LLMs locales

    Herramientas como Ollama usan contenedores o runtimes aislados para correr modelos sin ensuciar el host. Ollama

    Si trabajas en automatización o agentes, Docker te permite orquestar componentes heterogéneos (API, indexadores, workers) con control y repetibilidad.

    Riesgos y buenas prácticas que todo dev debe conocer

    • No uses latest para producción: versiona imágenes para rollback fiable.
    • Ejecuta procesos como non-root: USER node y minimiza la superficie (imágenes alpine o distroless).
    • Usa multi-stage builds: para reducir tamaño y exposición de secretos.
    • Limita recursos en containers: usa --memory, --cpus en entornos controlados.
    • Escanea imágenes por vulnerabilidades: Docker Scout, Trivy y firma artefactos (cosign).

    Estas prácticas evitan que Docker convierta problemas locales en problemas a escala.

    Integración CI/CD y artefactos

    En CI, convierte la imagen en artefacto: docker build, docker push a un registry (ECR/GCR/GHCR). Desplegar la imagen exacta que se probó en CI elimina discrepancias. GitHub Actions, GitLab CI y Jenkins tienen integraciones nativas para esto.

    Cómo empezar en 3 pasos (acción inmediata)

    1. Instala Docker Desktop: Docker Desktop
    2. Containeriza tu proyecto más reciente con un Dockerfile simple y prueba docker run.
    3. Define un docker-compose.yml con tu app y DB; sustitúyelo en tu README como el primer paso de onboarding.

    Aprende haciendo: containeriza un microservicio, añade pruebas de integración que levanten el stack y publica la imagen en un registry privado.

    Conclusión — beneficio claro y retorno rápido

    Aprender Docker es una inversión corta con retorno inmediato: menos tiempo perdido en debugging de entornos, onboarding más rápido y despliegues reproducibles. No es solo una herramienta de infra; es disciplina de desarrollo.

    Dominar Docker te sitúa un nivel arriba en capacidad técnica y decisiones arquitectónicas. Empieza hoy; la próxima vez que surja el “funciona en mi máquina”, ya sabrás por qué dejó de ser excusa.

    Para proyectos relacionados con automatización, agentes o IA, puedes encontrar recursos y experimentos adicionales en Dominicode Labs. Es un complemento lógico si quieres poner en práctica stacks orquestados y pruebas reproducibles en escenarios de workflows y RAG.

    FAQ

    ¿Docker sustituye a las máquinas virtuales?

    No exactamente. Docker ofrece aislamiento ligero compartiendo el kernel del host, lo que lo hace más eficiente en arranque y consumo de recursos. Las VMs siguen siendo necesarias cuando necesitas un kernel distinto o aislamiento a nivel de hardware.

    ¿Debo usar Docker en todos mis proyectos?

    No es obligatorio, pero sí altamente recomendable cuando la reproducibilidad, el onboarding o los despliegues son relevantes. Para scripts puntuales o tareas muy simples puede ser excesivo; evalúa el coste-beneficio.

    ¿Cómo manejo secretos en imágenes?

    No empaquetes secretos en imágenes ni en el Dockerfile. Usa variables de entorno en tiempo de ejecución, secretos del orchestrator (Kubernetes Secrets) o herramientas como HashiCorp Vault para inyectar credenciales de forma segura.

    ¿Qué herramientas usar para escanear imágenes?

    Herramientas comunes: Docker Scout, Trivy. Además considera firmar artefactos con cosign para validar integridad y procedencia en registries.

    ¿Cómo integro Docker en CI/CD?

    En CI construyes la imagen con docker build, la subes a un registry (ECR/GCR/GHCR) y despliegas esa misma imagen en staging/producción. GitHub Actions, GitLab CI y Jenkins ofrecen pasos y acciones para build/push y despliegue.

    ¿Puedo ejecutar LLMs en contenedores localmente?

    Sí. Herramientas como Ollama usan contenedores o runtimes aislados para ejecutar modelos locales sin afectar el host. Es útil para pruebas y despliegues controlados, evitando dependencias nativas complejas.