Guía9 min

Compose Watch para un agente IA: sync no es bind mount

Resumen

Cómo desarrollar un agente IA en contenedor sin montar el repo entero: develop.watch de Compose 2.22+, acciones sync, rebuild y sync+restart, ignore de node_modules, initial_sync y docker compose up --watch. Distinto de volúmenes bind. Docs oficiales curl 7 de septiembre de 2026.

Docker
Dos bloques host y container unidos por una barra sync

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Un agente IA en Docker se desarrolla mal de dos maneras. La primera: docker compose up --build en cada guardado, y el inner loop se come minutos. La segunda: un bind mount del repo entero, node_modules nativos del host dentro del contenedor, y un crash silencioso en ARM vs x86. Compose Watch resuelve el hueco: el contenedor sigue construido, pero Compose copia o reconstruye solo lo que cambió.

Requiere Docker Compose 2.22.0 o posterior. Vive bajo develop.watch, no bajo volumes. No sustituye un bind mount: es el compañero para código local con atributo build. Si el servicio solo declara image: y no build:, Watch no rastrea nada.

Qué problema resuelve

El inner loop de un agente (prompt, tool, test, restart) no aguanta un rebuild de imagen por cada .py. Tampoco aguanta montar node_modules/ o un wheel con código nativo. Watch te deja tres palancas oficiales:

AcciónQué haceCuándo
syncCopia el archivo al target y deja el proceso vivoHot reload (Vite, Flask debug, tsx watch)
rebuildBuildKit + recrea el servicio (compose up --build)package.json, requirements.txt, Dockerfile
sync+restartSincroniza y reinicia el procesonginx.conf, .env de runtime, config del bot
restartSolo reinicia (Compose 2.32.0+)Cambio que no necesita copiar archivos
sync+execSincroniza y corre un comando (Compose 2.32.0+, exec en 2.32.2+)app reload sin matar el PID 1

La regla: código interpretado → sync. Dependencias o binario → rebuild. Config que el proceso lee al arrancar → sync+restart.

Cómo se configura

Los paths son relativos al directorio del proyecto. Los directorios se observan de forma recursiva. No hay globs en path. Sí hay ignore e include con sintaxis tipo .dockerignore. Las reglas de .dockerignore se cargan implícitas. .git y archivos temporales de Vim/Emacs/JetBrains se ignoran solos.

El contenedor necesita stat, mkdir y rmdir en el PATH, y el USER tiene que poder escribir en target. Si copiaste con COPY como root y luego cambias a USER app, el sync falla. Usa COPY --chown=app:app.

services:
  agent:
    build: .
    command: npm start
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          initial_sync: true
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
        - action: sync+restart
          path: ./nginx/default.conf
          target: /etc/nginx/conf.d/default.conf

ignore es relativo al path de esa regla, no a la raíz del proyecto. Si path es ./web, node_modules/ ignora web/node_modules/, no el de la raíz. initial_sync (solo en acciones sync+*) alinea el árbol antes de empezar a observar, útil si el contenedor ya existía.

include es el inverso: solo dispara si el archivo coincide. Un patrón que empieza con * hay que citarlo en YAML ("*.go"), si no YAML lo lee como alias.

Arranque: --watch vs compose watch

Dos entradas oficiales:

  • docker compose up --watch — levanta el proyecto y enciende Watch. Mezcla logs de app con eventos de sync/rebuild.
  • docker compose watch — solo observa. Flags: --no-up (no construye ni arranca antes), --prune (default true: borra la imagen dangling tras cada rebuild), --quiet (esconde el output de build).

Si no quieres que un rebuild llene el disco de capas huérfanas, deja --prune en true. Si estás debuggeando una capa, --prune=false.

No hace falta Watch en todos los servicios. El webhook en Python sí; Postgres no. Un frontend JS con HMR es el caso de libro; un worker que solo corre un binario Go compilado pide rebuild o ni siquiera Watch.

Watch versus bind mount

Compose ya sabe compartir un directorio del host. Watch no lo reemplaza. La diferencia práctica para un agente:

  • Bind mount: el mismo inode, permisos del host, node_modules cruzados, I/O alto con muchos archivos chicos.
  • Watch sync: granularidad por regla, ignore fino, no compartes artefactos compilados entre Darwin y Linux.

En Node, no sincronices node_modules/. Aunque JS se interprete, hay addons nativos. En Python, sincroniza .py y reconstruye con requirements.txt / uv.lock. En Go, casi todo es rebuild salvo templates.

Diagrama de sync versus rebuild en Compose Watch

Errores típicos en un agente

Servicio solo con image:. Watch está pensado para build:. Si tu bot usa image: ghcr.io/org/agent:latest sin contexto de build, no hay nada que observar.

USER sin escritura. El sync corre como el usuario del contenedor. Un USER node sobre archivos copiados como root deja el inner loop mudo: Compose “ve” el cambio y no puede escribir el target.

Globs en path. path: ./src/**/*.ts no es válido. Pon el directorio y filtra con include/ignore.

Watch en prod. develop es opcional y de inner loop local. En el compose de producción no va. Si necesitas apagar Adminer o migrate, eso es profiles, no Watch. Los tokens no van en environment: del inspect: van en secrets.

Rebuild lento. Watch no arregla un Dockerfile desordenado. El rebuild es un compose up --build. Ordena lockfile → deps → código, multi-stage, y un .dockerignore que no meta el .git ni datasets. Ver dockerignore y multi-stage.

Mezclar bind mount y sync del mismo árbol. Doble fuente de verdad. Elige uno por path.

Checklist

  1. Compose ≥ 2.22.0 (docker compose version).
  2. El servicio tiene build:, no solo image:.
  3. Imagen con stat/mkdir/rmdir y USER que escribe en target.
  4. COPY --chown si no eres root.
  5. sync para fuente; rebuild para lockfile; sync+restart para config.
  6. ignore: [node_modules/] (o vendor/, .venv/) relativo al path.
  7. docker compose up --watch en dev; develop: ausente en prod.
  8. --prune true salvo que estés cazando una capa.
  9. No globs en path. Cita "*.go" en include.
  10. Un servicio a la vez: el bot sí, la base no.

Operador eligiendo sync, rebuild o restart según el archivo

FAQ

¿Watch reemplaza volúmenes? No. Los volúmenes siguen para Postgres, uploads y WAL. Watch es código de desarrollo.

¿Puedo usarlo con un agente en VPS? Sí para el inner loop en esa máquina. No es el mecanismo de deploy. El self-hosting con Caddy vive en self-hosting Docker VPS.

¿sync+exec o sync+restart? Si el proceso tiene un comando de reload (nginx -s reload, app reload) y Compose ≥ 2.32.2, sync+exec. Si tiene que releer config al nacer, sync+restart.

¿Por qué no se dispara? Revisa .dockerignore, ignore, que el path sea relativo al proyecto, y que no estés editando un archivo fuera de path. Globs en path no existen.

¿Se va a producción con --watch? No. develop es local. En prod: imagen pinneada, restart policy, secrets, profiles vacíos.

Siguiente paso

Si el agente aún no tiene un compose local, empieza por el curso de instalar un agente y el hub de seguridad y coste. Watch entra cuando el contenedor ya arranca y el dolor es el inner loop, no el primer docker compose up.