Si ejecutas docker compose up y te aparece que la version de tu Compose está obsoleta, te enfrentas a un clásico problema de operaciones: una advertencia que parece inofensiva hasta que no lo es. Los equipos la ignoran durante meses y luego una actualización de portátil, un runner de CI renovado o un nuevo host de producción convierten esa advertencia en compilaciones rotas, redes distintas o contenedores que “funcionaban ayer” y ahora no.
Este tipo de advertencia no va sobre sintaxis. Va sobre expectativas. No solo estás editando YAML; estás negociando comportamiento entre tu archivo, la CLI de Compose, el motor y la “Compose Specification” que reemplazó las viejas versiones del formato de archivo. Modernicemos sin sorpresas.
Lo que realmente significa la advertencia (y lo que no significa)
El mensaje “version is obsolete” aparece más comúnmente cuando usas Docker Compose V2 (el subcomando docker compose) y tu compose.yaml aún incluye un encabezado version: "2" o version: "3.7". Compose V2 implementa la Compose Specification y ya no necesita esa línea para seleccionar un parser o modo de comportamiento. De hecho, la cabecera puede inducir a error a las personas pensando que fija el comportamiento. No lo hace.
Esta es la realidad operativa:
- Antes la “versión” del archivo Compose seleccionaba un conjunto de características (especialmente entre 2.x y 3.x).
- La Compose Spec pasó a un modelo por capacidades: la CLI decide lo que soporta; el archivo describe la intención.
- Tu archivo Compose aún puede comportarse distinto entre máquinas, pero la diferencia normalmente no está en la línea
version. Está en la versión del plugin Compose, la versión del engine y el entorno.
Entonces: eliminar version suele ser seguro. Lo inseguro es asumir que todo lo demás está bien. La migración es menos “borrar una línea” y más “verificar que la semántica no cambió”.
Una regla seca que te mantiene fuera de problemas: trata Compose como una herramienta de despliegue, no como un lenguaje de programación. YAML no es un SLA.
Compose V1 vs V2: por qué tu memoria muscular te engaña
Compose V1 era la antigua herramienta Python docker-compose. Compose V2 es un plugin en Go integrado en la CLI de Docker como docker compose. Muchas banderas parecen iguales. Algunos comportamientos son parecidos. Las situaciones límite son donde viven los incidentes.
Broma #1: YAML es donde la indentación provoca guerras religiosas, y Compose solo quiere que elijas un bando de forma consistente.
La cita (y por qué aplica)
Idea parafraseada — John Allspaw: la confiabilidad viene de cómo se comportan los sistemas bajo estrés, no de lo confiados que nos sentimos al leer la configuración.
Esta advertencia es un empujón para probar el comportamiento, no para ganar una discusión con un linter.
Breve línea de tiempo: por qué Compose dejó de depender de “version”
Un poco de contexto histórico hace que la advertencia parezca menos arbitraria y más como una limpieza de aristas heredadas. Aquí hay hechos concretos que explican la trayectoria:
- Los archivos Compose 2.x y 3.x no eran solo “más nuevos”; apuntaban a mundos distintos. 3.x se alineó con el diseño de la era Swarm (notablemente en torno a claves
deploy). - La sección
deployse introdujo para describir intención de orquestación, pero el Compose clásico (no-Swarm) ignoraba la mayor parte, lo que confundió a la gente durante años. - Compose V1 (Python) y Compose V2 (plugin en Go) tienen implementaciones distintas, lo que implica casos límite distintos aun con YAML idéntico.
- Docker integró Compose en la CLI central para reducir la proliferación de herramientas, pero eso también significó iteración más rápida y UX más consistente con otros comandos Docker.
- La Compose Specification se convirtió en una especificación neutral del proveedor dentro del ecosistema OCI, con el objetivo de estandarizar el comportamiento más allá de “lo que haga la herramienta Docker hoy”.
- Las cabeceras de versión antiguas se volvieron un hack de compatibilidad: se usaban para condicionar características en vez de describir intención. Eso es al revés para una spec.
- Características como profiles llegaron después y no encajan bien en la taxonomía antigua 2/3; el enfoque de la spec es más flexible.
- Muchos equipos usaron Compose como “un Kubernetes barato”, lo cual está bien hasta que mezclas entornos y asumes las mismas semánticas en todos lados.
La advertencia no es un juicio moral. Es Compose diciéndote: “Deja de fingir que una cadena controla el comportamiento. No lo hace”.
Principios para modernizar de forma segura
1) Normaliza la cadena de herramientas primero, no al final
Antes de editar YAML, captura lo que estás ejecutando hoy: versión del Docker Engine, versión del plugin Compose y la línea de comandos exacta usada en CI y en hosts de producción. Si esos difieren, tu archivo Compose ya es “multi-plataforma” aunque no lo hayas querido.
2) La configuración renderizada es la verdad
Compose soporta interpolación de variables, campos de extensión, múltiples archivos, profiles y valores por defecto. Los humanos leen YAML. Compose ejecuta el modelo totalmente resuelto. Tu migración debe comparar la configuración renderizada antes y después de los cambios.
3) “Funciona en mi portátil” a menudo es un tema de permisos de volúmenes
La modernización provoca reconstrucciones y recreación de contenedores. Ahí es cuando la propiedad de archivos, las etiquetas SELinux y los drivers de almacenamiento te recuerdan que existen. Modernizar Compose trata tanto del almacenamiento como del YAML.
4) Congela lo que debe estar congelado
No congeles todo (eso se vuelve un museo), pero sí fija las cosas que cambian comportamiento: tags o digests de imágenes, versión del plugin Compose en runners de CI y convenciones de nombres de proyecto.
5) Haz que “up” sea aburrido
Un archivo Compose correcto hace que docker compose up -d sea una operación de bajo drama. Si dependes de valores por defecto implícitos (redes, nombres de contenedores, contextos de build), tu yo futuro pagará intereses.
Plan de migración: de “version:” a Compose Spec
La migración básicamente es: quitar la clave version, asegurar que el nombre y la disposición del archivo se ajusten a las expectativas modernas, validar con docker compose config y confirmar el comportamiento mediante una recreación controlada.
Paso 1: Renombra y estandariza el nombre de archivo
Compose moderno por defecto usa compose.yaml. También leerá docker-compose.yml, pero estandarizar reduce la confusión de “¿por qué eligió ese archivo?”.
Paso 2: Elimina la línea version (pero no te quedes ahí)
Borra:
version: "2"version: "3"version: "3.8"(etc.)
Mantén todo lo demás. Luego valida la configuración renderizada. Tu objetivo es demostrar que el modelo resultante es el mismo.
Paso 3: Reemplaza patrones obsoletos por otros amigables con la spec
Ejemplos comunes:
links: legado; Compose moderno usa descubrimiento DNS basado en servicio en redes por defecto.depends_oncon condiciones: patrones antiguos usabancondition: service_healthy; V2 tiene soporte parcial según versiones. Prefiere healthchecks explícitos + lógica de espera en el entrypoint para dependencias críticas.container_nameen todas partes: parece ordenado, rompe el escalado y puede causar colisiones entre proyectos. Úsalo con moderación.external_links: normalmente es un olor a diseño; modela redes explícitamente en su lugar.
Paso 4: Audita las definiciones de almacenamiento como si importara
Si ejecutas bases de datos en Compose (probablemente sí), trata los volúmenes como estructuras de datos de producción. Nombra volúmenes explícitamente, elige bind mounts intencionalmente y entiende dónde viven los bytes en disco. Una modernización que recrea contenedores puede huir volúmenes o recrearlos con nombres nuevos si cambias el nombre del proyecto.
Paso 5: Compruébalo con una recreación controlada
No “simplemente ejecutes”. Usa una mentalidad de simulación: renderiza config, descarga imágenes y recrea en un entorno de staging o en una copia del host. Luego compara metadatos de contenedor (redes, montajes, variables de entorno, healthchecks).
Broma #2: “Solo cambié una advertencia” es el lema no oficial de las retrospectivas de incidentes.
Tareas prácticas (comandos, salidas, decisiones)
A continuación hay tareas concretas y ejecutables que puedes hacer en un host o runner de CI. Cada una incluye (1) un comando, (2) qué significa la salida y (3) la decisión que tomas a partir de ello. Esto es lo que evita que “debería ser igual” se convierta en una tormenta de tickets.
Task 1: Confirma que usas Compose V2 (plugin) y no V1
cr0x@server:~$ docker compose version
Docker Compose version v2.27.1
Qué significa: Estás ejecutando el plugin V2. La advertencia “version is obsolete” es esperada si tu archivo aún tiene una clave version:.
Decisión: Migra al estilo Compose Spec (elimina version) y valida con el comportamiento de V2.
Task 2: Busca uso legacy de V1 en scripts
cr0x@server:~$ command -v docker-compose || echo "docker-compose not found"
docker-compose not found
Qué significa: Scripts que llamen a docker-compose fallarán aquí; en otras máquinas podrían seguir funcionando. Esa inconsistencia es un riesgo de migración.
Decisión: Estandariza el uso de docker compose en automatización, o instala/fija V1 explícitamente donde sea necesario (rara vez justificado ahora).
Task 3: Captura la versión del Docker Engine (el comportamiento varía)
cr0x@server:~$ docker version --format '{{.Server.Version}}'
26.1.4
Qué significa: Las características del engine (opciones de driver de red, comportamiento de build, integración con iptables) varían según la versión.
Decisión: Si producción y CI difieren materialmente, o alineas versiones o pruebas en la versión más antigua soportada.
Task 4: Renderiza la configuración totalmente resuelta (la línea base)
cr0x@server:~$ docker compose -f docker-compose.yml config
name: app
services:
api:
environment:
NODE_ENV: production
image: registry.local/app/api:1.9.2
networks:
default: null
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
networks:
default:
name: app_default
Qué significa: Esto es lo que Compose aplicará realmente. Incluye valores por defecto, valores interpolados y campos normalizados.
Decisión: Guarda esta salida como tu referencia “conocida buena” antes de editar.
Task 5: Valida que tu archivo parsea sin la clave version
cr0x@server:~$ cp docker-compose.yml compose.yaml
cr0x@server:~$ sed -i '/^version:/d' compose.yaml
cr0x@server:~$ docker compose -f compose.yaml config >/dev/null && echo "config ok"
config ok
Qué significa: La sintaxis es válida bajo el modelo de Compose Spec.
Decisión: Procede a la comparación semántica (tarea siguiente). Pasar la verificación de parseo es necesario, no suficiente.
Task 6: Compara configuraciones renderizadas antes y después (detector de deriva semántica)
cr0x@server:~$ docker compose -f docker-compose.yml config > /tmp/before.yaml
cr0x@server:~$ docker compose -f compose.yaml config > /tmp/after.yaml
cr0x@server:~$ diff -u /tmp/before.yaml /tmp/after.yaml | head
Qué significa: No haber salida significa no hay diff en la configuración renderizada (ideal). Si hay diferencias, léelas como una revisión de cambios: entorno, montajes, redes y etiquetas son las áreas de mayor riesgo.
Decisión: Si existen diffs, corrige el archivo o documenta por qué el cambio es esperado y seguro.
Task 7: Comprueba si los profiles cambian silenciosamente qué se inicia
cr0x@server:~$ docker compose -f compose.yaml config --profiles
default
debug
Qué significa: Hay profiles. Ejecutar docker compose up sin --profile puede omitir algunos servicios. Eso puede parecer “Compose se rompió” cuando en realidad es “no activaste el profile”.
Decisión: En scripts de producción, sé explícito sobre profiles (o evítalos en archivos de prod).
Task 8: Verifica el nombre del proyecto (estabilidad de nombres de volúmenes/redes)
cr0x@server:~$ docker compose -f compose.yaml ls
NAME STATUS CONFIG FILES
app running(3) /srv/app/compose.yaml
Qué significa: Compose usa un “nombre de proyecto” para namespacear redes/volúmenes/contenedores. Cambiar el nombre del directorio, el nombre del archivo o usar -p puede cambiarlo.
Decisión: Fija el nombre del proyecto en la automatización (usa -p app o name: en la config) si la estabilidad importa.
Task 9: Inspecciona volúmenes para asegurar que los datos no van a desaparecer
cr0x@server:~$ docker volume ls
DRIVER VOLUME NAME
local app_dbdata
local app_redisdata
Qué significa: Estos volúmenes son objetos separados, no ligados al ciclo de vida del contenedor. Sobreviven a la recreación, pero pueden ser reemplazados si los renombras accidentalmente (a menudo por cambios en el nombre del proyecto).
Decisión: Si los nombres de volumen están prefijados por proyecto y vas a cambiar la nomenclatura, asigna nombres estables explícitamente a los volúmenes.
Task 10: Confirma qué está montado dónde (trampas de permisos y SELinux)
cr0x@server:~$ docker inspect app-db-1 --format '{{json .Mounts}}'
[{"Type":"volume","Name":"app_dbdata","Source":"/var/lib/docker/volumes/app_dbdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]
Qué significa: La base de datos escribe en un volumen gestionado por Docker. El modo z sugiere relabeling de SELinux (común en hosts Fedora/RHEL derivados).
Decisión: Si pasas a bind mounts, debes manejar propiedad y etiquetas SELinux explícitamente. Si te quedas con volúmenes nombrados, mantén nombres estables.
Task 11: Detecta el riesgo de recreación de contenedores antes de aplicar cambios
cr0x@server:~$ docker compose -f compose.yaml up -d --no-deps --dry-run
service api: would recreate
Qué significa: Compose predice que recreará contenedores. La recreación es donde descubres que olvidaste persistir datos o que fijaste el tag de imagen equivocado.
Decisión: Si servicios críticos se recrearían, programa una ventana, confirma montajes de volúmenes y asegúrate de poder revertir.
Task 12: Confirma que las imágenes están fijadas de forma sensata
cr0x@server:~$ docker compose -f compose.yaml images
CONTAINER REPOSITORY TAG IMAGE ID SIZE
app-api-1 registry.local/app/api 1.9.2 1a2b3c4d5e6f 312MB
Qué significa: Estás usando un tag específico. Si usas latest, la modernización es un gran momento para actualizaciones sorpresa.
Decisión: Fija tags; idealmente usa digests en producción si tu flujo con el registry lo permite.
Task 13: Valida que los healthchecks realmente se ejecuten y reporten lo esperado
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}' | head -n 5
NAMES STATUS
app-api-1 Up 2 minutes (healthy)
app-db-1 Up 2 minutes (healthy)
Qué significa: Los healthchecks están activos y reportan. Si los servicios están “Up” pero nunca “healthy”, tus suposiciones de orquestación de dependencias pueden estar equivocadas.
Decisión: Si tu secuencia de arranque depende de salud, asegura que existan healthchecks y se comporten de forma consistente entre entornos.
Task 14: Confirma el comportamiento efectivo de red y DNS
cr0x@server:~$ docker network inspect app_default --format '{{json .IPAM.Config}}'
[{"Subnet":"172.23.0.0/16","Gateway":"172.23.0.1"}]
Qué significa: Compose creó una red bridge por defecto. Si cambias el nombre del proyecto o declaras redes de forma distinta, el nombre/subred de la red puede cambiar y romper listas de control de acceso hardcodeadas.
Decisión: Si algo externo depende de subredes/nombres estables, declara la red explícitamente con un nombre estable (y evita hardcodear subredes salvo que sea necesario).
Task 15: Confirma interpolación de variables de entorno y variables faltantes (pistola silenciosa)
cr0x@server:~$ docker compose -f compose.yaml config 2>&1 | grep -i warning | head
WARN[0000] The "API_KEY" variable is not set. Defaulting to a blank string.
Qué significa: Compose sustituyó por una cadena vacía. Tu servicio puede iniciar y luego fallar de una forma que parezca no relacionada.
Decisión: Falla rápido: usa variables requeridas en CI, guárdalas en un gestor de secretos o proporciona un .env con valores por defecto seguros solo para desarrollo.
Task 16: Identifica contenedores huérfanos tras refactors
cr0x@server:~$ docker compose -f compose.yaml up -d
[+] Running 3/3
✔ Container app-api-1 Started
✔ Container app-db-1 Started
✔ Container app-redis-1 Started
cr0x@server:~$ docker compose -f compose.yaml ps --all
NAME IMAGE COMMAND SERVICE STATUS
app-api-1 registry.local/app/api:1.9.2 "node server.js" api running
app-db-1 postgres:16 "docker-entrypoint..." db running
app-redis-1 redis:7 "docker-entrypoint..." redis running
Qué significa: Lista limpia. Si Compose imprime “orphan containers”, probablemente renombraste servicios o cambiaste el nombre del proyecto y dejaste contenedores antiguos.
Decisión: Si ves huérfanos, elimínalos deliberadamente (docker compose down --remove-orphans) tras confirmar que no siguen recibiendo tráfico.
Guía de diagnóstico rápido
Cuando la modernización de Compose sale mal, necesitas un camino corto hacia el cuello de botella. Aquí está el orden que encuentra el culpable más rápido en sistemas reales.
Primero: identifica qué cadena de herramientas estás ejecutando
- Revisa
docker compose versionydocker version. - Confirma los archivos Compose usados (usar
-fen la automatización evita “lo que hay en el directorio”). - Renderiza config:
docker compose config. Si no puedes renderizar, nada más importa.
Segundo: detecta deriva de nombres (proyecto, redes, volúmenes)
- Revisa proyecto:
docker compose ls. - Lista volúmenes:
docker volume ls. - Inspecciona nombres de redes:
docker network lsydocker network inspect.
Si los nombres cambiaron, puedes haber “perdido” datos simplemente creando nuevos volúmenes bajo un nuevo namespace.
Tercero: verifica almacenamiento y permisos antes de perseguir bugs de la app
- Inspecciona los montajes en el contenedor.
- Revisa logs de contenedores por errores de permisos.
- En hosts con SELinux, busca operaciones denegadas al cambiar tipos de montaje.
Cuarto: comprueba orden de ejecución y salud
- Estado de salud y recuento de reinicios con
docker ps. - ¿La BD se vuelve healthy antes de que arranque la API?
- Si había suposiciones de
depends_on, verifica si aún se cumplen.
Quinto: solo entonces afina rendimiento
Si el sistema está lento tras la modernización, mide antes de optimizar: revisa latencia del sistema de archivos, comportamiento de overlay2, throttling de CPU y resolución DNS dentro de la red. Compose no era el cuello de botella hasta que lo demuestres.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “Mi base de datos está vacía después de la migración”
Causa raíz: Cambió el nombre del proyecto, así que el volumen nombrado se volvió otro objeto (p. ej., oldproj_dbdata vs newproj_dbdata). O cambiaste de volumen nombrado a bind mount sin migrar datos.
Solución: Nombra el volumen explícitamente y mantenlo estable. Verifica el mapeo de volúmenes con docker inspect. Si necesitas migrar, copia los datos desde la ruta del volumen antiguo a la nueva durante una ventana de mantenimiento controlada.
2) Síntoma: “Los servicios no se alcanzan entre sí; falla la resolución DNS”
Causa raíz: Eliminaste links y asumiste que creaba conectividad. En Compose moderno, la conectividad viene de redes compartidas; los nombres DNS vienen de los nombres de servicio.
Solución: Coloca ambos servicios en la misma red definida por el usuario (o en la por defecto) y usa DNS por nombre de servicio. Si necesitas conectividad cross-project, declara una red externa y adjunta ambos proyectos a ella.
3) Síntoma: “La advertencia desapareció, pero ahora CI se comporta distinto que mi portátil”
Causa raíz: CI usa una versión distinta del plugin Compose o del Docker Engine. La config es técnicamente válida pero las semánticas difieren (cache de build, comportamiento de pull, timing de healthchecks, rarezas de DNS).
Solución: Fija versiones de la cadena de herramientas en CI. Añade docker compose version a los logs de build. Trata la “deriva de la cadena de herramientas” como un cambio que requiere revisión.
4) Síntoma: “Los contenedores se recrean cada vez aunque nada cambió”
Causa raíz: No determinismo en variables de entorno, build args o config generada. O cambiaste etiquetas y Compose piensa que la definición del servicio difiere.
Solución: Normaliza las fuentes de variables de entorno. Renderiza config y difféala entre ejecuciones. Evita inyectar timestamps o valores “aleatorios” en el entorno.
5) Síntoma: “Usábamos depends_on para esperar la BD, ahora la app falla al arrancar”
Causa raíz: depends_on solo controla orden de arranque; no garantiza readiness. Herramientas antiguas daban una falsa sensación de seguridad combinadas con condiciones frágiles o timings.
Solución: Implementa healthchecks reales y reintentos/backoff en la app. Considera un pequeño script wait-for solo como medida de protección, no como arquitectura.
6) Síntoma: “Cambiaron los puertos / el tráfico empezó a llegar al contenedor equivocado”
Causa raíz: Cambiar el nombre del proyecto o del servicio cambió nombres y etiquetas que un proxy inverso o agente de monitorización usaba. A veces quitar container_name expone la dependencia.
Solución: Deja de basar enrutamiento/monitorización en nombres de contenedor. Usa etiquetas de forma intencional (p. ej., para un proxy) y mantenlas estables entre refactors.
7) Síntoma: “Permiso denegado en bind mounts tras la modernización”
Causa raíz: Pasaste de volúmenes nombrados (permisos gestionados por Docker) a bind mounts (permisos del host) sin alinear UID/GID o etiquetas SELinux.
Solución: Alinea los UID de los contenedores, establece la propiedad en la ruta host y usa flags de montaje SELinux correctos donde aplique. Valida con logs del contenedor e inspección de montajes.
Tres micro-historias corporativas desde las trincheras de Compose
Micro-historia 1: Un incidente causado por una suposición equivocada
La compañía tenía un stack “simple” en Compose: API, Postgres, Redis y un sidecar que enviaba logs. El docker-compose.yml empezaba con version: "3.7". El equipo vio la advertencia tras actualizar Docker Desktop y decidió modernizar quitando la línea version y renombrando el archivo a compose.yaml. Pareció una tarea de mantenimiento.
Desplegaron a una pequeña VM de producción usando un directorio copiado. No especificaron -p y el nombre del directorio en la VM difería del de staging. Compose creó un nuevo nombre de proyecto según el directorio y con ello un nuevo conjunto de redes y volúmenes. Postgres arrancó rápido. También estaba vacío. La API se conectó, ejecutó migraciones contra la base de datos nueva y empezó a servir. Nadie notó hasta que un cliente llamó por datos faltantes, porque el nuevo entorno parecía perfectamente saludable.
El monitoreo no alertó. La CPU estaba bien. La latencia estaba bien. El sistema estaba “up”. Simplemente apuntaba a los bytes equivocados. Finalmente alguien corrió docker volume ls y vio dos conjuntos de volúmenes con nombres similares. Los datos antiguos seguían ahí, silenciosamente adjuntos al proyecto antiguo.
La solución fue aburrida: fijaron el nombre del proyecto, nombraron volúmenes explícitamente y añadieron un paso al runbook para verificar attachments de volúmenes antes de cualquier refactor de Compose. Aprendieron que “Compose reutilizará mis datos” no es una garantía; es un efecto secundario de nombres estables.
Micro-historia 2: Una optimización que salió mal
Otra organización tenía el mandato de reducir el tiempo de despliegue. Su stack Compose construía tres imágenes localmente con docker compose build y luego las arrancaba. Alguien propuso acelerar: “Cambiemos a bind mounts para todo para no reconstruir tan seguido”. También quitaron la clave version mientras tocaban el archivo porque la advertencia molestaba en los logs.
En desarrollo se sintió genial. Los cambios eran instantáneos. En producción fue un fallo a cámara lenta. Los contenedores corrían como non-root (bien), pero los directorios del host estaban en manos de un UID/GID distinto (esperable, pero sin manejo). Los servicios arrancaron, escribieron algunos archivos y luego fallaron al no poder crear directorios. Siguieron bucles de reinicio. El equipo parcheó corriendo contenedores como root, lo que resolvió el error inmediato y creó el siguiente problema: ahora las rutas host se llenaron de archivos propiedad de root, rompiendo futuros despliegues non-root.
Luego vino la sorpresa del rendimiento de almacenamiento. El host estaba en un sistema de ficheros optimizado para durabilidad, no para churn de pequeños ficheros. Los patrones bind-mounted incrementaron operaciones de metadata; la latencia subió en picos de escritura. La “optimización” ahorró tiempo de build y lo devolvió diez veces en I/O wait y caos operativo.
Se recuperaron revirtiendo en producción a volúmenes nombrados para rutas stateful, manteniendo bind mounts solo para un pequeño conjunto de assets estáticos de solo lectura. También formalizaron una regla: los cambios en producción que afectan semántica de almacenamiento requieren revisión explícita del path de datos, no solo code review.
Micro-historia 3: Una práctica aburrida pero correcta que salvó el día
En una compañía del sector financiero, un equipo decidió modernizar sus archivos Compose en docenas de repos. Lo hicieron como adultos: una checklist de migración estándar y un job de CI que renderizaba la config de Compose y la almacenaba como artefacto. Cada pull request incluía un diff de la salida de docker compose config antes y después. No era glamoroso, pero era visible.
Durante una migración, el diff mostró un cambio sutil: el nombre de una red cambió porque alguien introdujo un campo name: a nivel superior mientras también cambiaba el nombre del directorio en el repo. El desarrollador quería mejorar consistencia. La config renderizada reveló que los contenedores existentes se recrearían en una red nueva. El reverse proxy, que descubrÍa targets por pertenencia a la red, dejaría de enrutar al servicio durante el rollout.
Como lo detectaron en CI, planearon el cambio: pre-crearon la red objetivo, actualizaron la configuración del proxy y desplegaron en una ventana de mantenimiento. La migración salió con un corte limpio y sin agujeros negros de tráfico. Nadie celebró. Ese es el punto.
Mantuvieron la práctica: render-and-diff se convirtió en parte de la definición de hecho para cualquier cambio en Compose. Les salvó nuevamente cuando una variable de entorno quedó por defecto vacía en un entorno; los artefactos renderizados hicieron obvio el valor faltante.
Listas de verificación / plan paso a paso
Checklist A: Modernización segura en un solo repo
- Inventario de la cadena de herramientas: registra versiones de Engine + plugin Compose en dev, CI y prod.
- Renderiza la config actual y almacénala como artefacto base (
docker compose config). - Elimina
version:y estandariza encompose.yaml. - Renderiza de nuevo y diff las salidas; resuelve cambios inesperados.
- Fija el nombre del proyecto si volúmenes/redes deben ser estables (
-poname:). - Audita volúmenes: asegura que servicios stateful usen volúmenes nombrados explícitos o bind mounts deliberados.
- Confirma variables de entorno: sin advertencias de valores faltantes; falla el CI si las hay.
- Recreación en modo dry-run para ver qué se reemplazaría y planear downtime si hace falta.
- Despliega a staging con rutas de datos similares a producción; verifica montajes, salud y conectividad.
- Despliega a prod con plan de rollback: archivo antiguo, nombre de proyecto antiguo y attachments de volúmenes conocidos.
Checklist B: Estandarización en muchos repos (la realidad corporativa)
- Elige un rango de versiones soportado del plugin Compose y estandariza runners de CI primero.
- Crea una plantilla de job de migración en CI que ejecute
docker compose configy guarde artefactos. - Introduce chequeos de política: rechaza tags
latest, detecta vars faltantes, marca el uso excesivo decontainer_name. - Define reglas de almacenamiento: qué servicios pueden usar bind mounts, cuáles deben usar volúmenes nombrados, cómo funcionan los backups.
- Haz explícito el nombre de proyecto donde importe la persistencia de datos.
- Capacita a la gente una vez: una guía interna corta vence al conocimiento tribal.
Ejemplo mínimo: estilo Compose Spec moderno (sin version)
No es un tutorial completo, solo un patrón que evita trampas comunes: nombres de volúmenes estables, redes explícitas y nomenclatura predecible.
cr0x@server:~$ cat compose.yaml
name: app
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 20
api:
image: registry.local/app/api:1.9.2
depends_on:
- db
environment:
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app
ports:
- "8080:8080"
volumes:
dbdata:
name: app_dbdata
Lo importante no es que sea elegante. Es que sea estable.
Preguntas frecuentes (FAQ)
1) ¿Debería eliminar la clave version?
Sí, si estás usando Compose V2. Es obsoleta en el sentido de que ya no controla el parsing ni el modo de características. Elíminala y luego valida mediante diffs de la configuración renderizada.
2) ¿Eliminar version cambiará el comportamiento?
A menudo no. A veces sí, pero generalmente de forma indirecta: la modernización desencadena renombres de archivos, cambios en el nombre del proyecto o actualizaciones del plugin Compose. Ahí es donde se esconden los cambios de comportamiento. Demuéstralo con docker compose config antes y después.
3) ¿Por qué Compose aún acepta version si es obsoleta?
Por compatibilidad hacia atrás. Compose sabe que hay una década de archivos en repositorios. Aceptar la clave mientras se advierte es el compromiso entre rigidez y practicidad.
4) ¿Cuál es la diferencia entre las versiones de archivo Compose 2 y 3?
Históricamente: 2.x se enfocaba en comportamiento Compose de un solo host; 3.x se alineó con campos de despliegue de Swarm. La sección deploy en 3.x confundió a muchos equipos porque Compose clásico ignoraba la mayor parte. La Compose Spec intenta unificar esto bajo un único modelo.
5) ¿Necesito renombrar docker-compose.yml a compose.yaml?
No es obligatorio, pero estandarizar ayuda. El riesgo no está en el nombre del archivo; está en la deriva accidental del nombre del proyecto y en que la gente adivine qué archivo se usó. Si renombrás, fija el nombre del proyecto y verifica volúmenes.
6) ¿Cómo me aseguro de que los volúmenes no se recrean?
Nombra explícitamente los volúmenes críticos (p. ej., name: app_dbdata) y mantén estable el nombre del proyecto. Antes de desplegar, lista volúmenes e inspecciona montajes para confirmar que el contenedor se adjunta al volumen esperado.
7) Nuestro stack usa depends_on. ¿Es fiable?
Es fiable para el orden de arranque, no para la readiness. Si tu app necesita que la BD esté lista, implementa reintentos y healthchecks. Trata depends_on como conveniencia, no como mecanismo de corrección.
8) ¿Por qué CI advierte sobre variables faltantes pero producción no?
Porque CI y prod cargan variables de forma distinta: archivos .env, entorno del shell, inyección de secretos o variables del CI. Renderiza la config en ambos lugares y compara. Los valores por defecto vacíos son un fallo clásico de “arrancó pero está roto”.
9) ¿Puedo mezclar múltiples archivos Compose durante la migración?
Sí. Usa el apilamiento con -f (base + override). Pero recuerda: el modelo renderizado es lo que importa. Siempre valida con docker compose -f base -f override config y guarda esa salida.
10) ¿Debemos fijar versiones de Docker y Compose?
Fíjalas en CI. En producción puedes fijar o llevar un ritmo de actualizaciones controlado. Lo que no debes hacer es “usar la versión que trae la imagen del VM”. Eso no es estrategia; es un generador de sorpresas.
Próximos pasos que puedes hacer hoy
La advertencia “version is obsolete” es una campanilla de alarma barata. Toma la señal gratis, corrige el archivo y usa el momento para reducir la deriva futura.
- Ejecuta
docker compose configsobre el archivo actual y guarda la salida. - Elimina
version:, renderiza de nuevo la config y difféala. El objetivo es no tener diferencias; explica cualquier diff que aceptes. - Fija el nombre del proyecto si tienes volúmenes stateful o dependencias externas en nombres de red.
- Audita volúmenes y montajes como si fueran datos de producción (porque lo son).
- Estandariza CI en una versión conocida del plugin Compose y regístrala en cada pipeline.
Las advertencias solo son molestas cuando las ignoras. Si las tratas como un recordatorio para verificar comportamiento, son uno de los pocos regalos que los sistemas de producción te dan gratis.