1. Varios archivos de Compose: un único stack
Cuando en un proyecto aparece compose.dev.yaml, en la mente de un desarrollador principiante suele surgir una idea muy humana: «Ajá, esto es como un segundo stack independiente: uno para normal, otro para dev». Y aquí Compose rompe las expectativas sin piedad: en realidad no son dos stacks, sino un único modelo de aplicación, construido a partir de varios archivos que se superponen unos sobre otros como láminas transparentes. El problema no está en su lógica; el problema es que el cerebro tiende a pensar «dos archivos = dos ejecuciones», mientras que Compose piensa «dos archivos = un resultado».
Internamente, Compose toma el primer archivo como base, luego lee el segundo archivo y realiza cambios sobre lo que ya ha leído. Si en ambos archivos se describe un servicio con el mismo nombre (por ejemplo, app), entonces no son dos servicios diferentes, sino el mismo servicio app, simplemente con ajustes adicionales. Si en el segundo archivo aparece un servicio nuevo que no estaba en la base, se añadirá al modelo final.
Una analogía muy útil para quienes ya han trabajado con la configuración de Spring Boot: es similar a cómo application.yml se complementa y sobrescribe con application-postgres.yml, o cómo los valores de las variables de entorno anulan los valores del archivo. Solo que aquí «superponemos» capas no de propiedades de Spring, sino de YAML de Compose.
Un pequeño esquema para fijar el modelo mental:
flowchart TD A["compose.yaml base"] --> C["docker compose config fusión"] B["compose.dev.yaml anulación"] --> C C --> D["merged config modelo final"] D --> E["docker compose up ejecución"]
La conclusión principal de esta sección: no se puede «a simple vista» saber qué se va a ejecutar realmente si solo se lee un archivo. La realidad se encuentra en la configuración fusionada (merged).
2. El orden de -f: base y anulación (override)
Una vez que se acepta que Compose «pega» los archivos, surge el siguiente nivel: el orden importa. No es un caso en el que «el comando simplemente toma una lista de archivos y luego ya se las arregla». Compose lee los archivos de izquierda a derecha, y los archivos posteriores tienen derecho a sobrescribir lo que se declaró antes. Por lo tanto, el comando básico del día parece aburrido, pero es muy importante:
# A la izquierda — base, a la derecha — anulación: el archivo derecho «parchea» al izquierdo
docker compose -f compose.yaml -f compose.dev.yaml config
Aquí compose.yaml es la base, y compose.dev.yaml es un parche sobre la base. Precisamente por eso llamamos al segundo archivo «anulación de desarrollo», y no «segundo archivo principal».
Si accidentalmente se invierte el orden, Compose «pegará» honestamente «hacia el otro lado», y se obtendrá una configuración que contradice lógicamente la idea de separación de roles. La opción más peligrosa es cuando el comando funciona formalmente, pero el resultado se vuelve inesperado: aparentemente «se activó el archivo de desarrollo», pero en realidad fue anulado por el archivo base.
Para demostrar la diferencia (y verlo una vez para no repetirlo), comparemos:
# Correcto: base -> anulación (los ajustes de desarrollo pueden anular los básicos)
docker compose -f compose.yaml -f compose.dev.yaml config
# Casi siempre incorrecto: anulación -> base (la base sobrescribirá los ajustes de desarrollo)
docker compose -f compose.dev.yaml -f compose.yaml config
El segundo comando no «fallará» automáticamente. Simplemente hará que el archivo base comience a sobrescribir los ajustes de desarrollo. Como resultado, se pueden perder 20 minutos intentando entender por qué el puerto de desarrollo «no se reenvía», aunque existe en el archivo de desarrollo. Spoiler: porque usted mismo lo «borró» con el orden de los archivos.
Otro punto muy práctico: si se utiliza un conjunto de -f ... -f ... para up, es razonable usar el mismo conjunto para config. De lo contrario, estará mirando una configuración y ejecutando otra, y luego se sorprenderá de que la realidad no coincida con las expectativas. Es como leer un application.yml y ejecutar la aplicación con otro perfil.
3. Reglas de combinación (merge) en Compose
Cuando se dice «Compose combina archivos», suena demasiado abstracto hasta que no se comprende qué tipos de ajustes es capaz de combinar y cómo. El YAML interno de Compose no es solo «texto». Es una estructura: en algunos lugares hay mapas (map) del tipo clave: valor, y en otros hay listas (list) del tipo - elemento. Y estos tipos se comportan de manera diferente durante la combinación. La buena noticia: para el trabajo diario de un desarrollador Java, bastan un par de reglas, y no es necesario convertirse en un experto en alquimia de YAML.
A continuación, una tabla práctica de «qué esperar» (simplificada, pero honesta para la mayoría de los escenarios del curso):
| Dónde en Compose | Cómo se ve en YAML | Qué suele hacer la combinación | Cómo se siente en la práctica |
|---|---|---|---|
| environment (forma de mapa) | KEY: valor | combina por claves; el archivo posterior puede sobrescribir el valor | «añadimos una variable sin romper las demás» |
| labels, annotations | k: v | lo mismo: combinación por clave | «pegamos otra etiqueta» |
| ports (listas) | - "8080:8080" | a menudo añade elementos del segundo archivo | «al puerto base se le añadió otro» |
| build.target, image, command (escalares) | target: ... | el archivo posterior reemplaza el valor | «cambiamos de modo» |
Pero no todos los campos similares a listas deben recordarse como «simplemente se añaden». Con ports suele ser así, pero los escenarios de montaje/volumen es más seguro verificarlos mediante el docker compose config final.
Y otra aclaración importante sobre los ejemplos de instantáneas: a continuación se mostrará el mismo fragmento simplificado de full-stack con un SPRING_PROFILES_ACTIVE fijo. Solo es necesario para ver la mecánica de la combinación. Para un inicio parcial por escenarios, ese valor ya es mejor sobrescribirlo, no considerarlo una constante eterna.
Veamos un ejemplo sencillo y muy real: en el compose.yaml base queremos mantener los ajustes normales de app (puertos, perfiles, conexiones a servicios), y en compose.dev.yaml añadir una variable de entorno exclusiva para desarrollo.
# compose.yaml (fragmento)
services:
app:
environment:
# Para un ejemplo compacto de full-stack, fijamos el conjunto completo de perfiles
SPRING_PROFILES_ACTIVE: "postgres,cache,messaging"
# compose.dev.yaml (fragmento)
services:
app:
environment:
# Ajuste exclusivo para desarrollo: se añadirá a environment si la clave es única
JAVA_TOOL_OPTIONS: "-Dexample.flag=true"
Intuitivamente, se espera que al final estén ambas variables. Así será: environment se «pegará» por claves, no se «reemplazará por completo».
Ahora un ejemplo en el que la anulación realmente sobrescribe: si se define la misma clave en el segundo archivo, se aplicará «el último gana».
# compose.dev.yaml (fragmento)
services:
app:
environment:
# La misma clave que en la base: por ejemplo, redujimos el escenario a app + postgres
SPRING_PROFILES_ACTIVE: "postgres"
En la configuración fusionada quedará postgres, porque el archivo de desarrollo llegó después. A veces eso es lo que se necesita. A veces es una rotura accidental. Y aquí llegamos suavemente al héroe de nuestra lección: docker compose config, que permite ver lo que ha resultado, no adivinarlo.
Otro ejemplo frecuente son los ports. En la base tenemos HTTP, y en el segundo archivo se añade otro puerto (por ejemplo, cualquier puerto local adicional). Compose, por lo general, añadirá otro elemento a la lista de puertos.
# compose.yaml (fragmento)
services:
app:
ports:
# Puerto HTTP base de la aplicación
- "8080:8080"
# compose.dev.yaml (fragmento)
services:
app:
ports:
# Puerto adicional para escenarios de desarrollo (por ejemplo, depurador)
- "5005:5005"
El resultado será la presencia de dos publicaciones de puertos. Esto es cómodo, pero en estos lugares es fácil obtener duplicados si no se ha notado que en algún lugar se repitió el mismo puerto. Y un puerto duplicado ya no es cuestión de filosofía, sino un error de inicio.
4. Rutas relativas en Compose
Las rutas relativas son un tema pequeño que causa frustración desproporcionada a su «pequeñez». Se ve en compose.dev.yaml algo como ./local-debug:/tmp/debug y el cerebro piensa automáticamente: «Bueno, es una ruta relativa al archivo de desarrollo». Y Compose dice: «Ja. Bonito. Pero no». En la práctica, Compose interpreta las rutas relativas en la configuración que se está combinando con respecto al primer archivo (base) de Compose, es decir, con respecto a compose.yaml, si es el primero en -f.
Esta regla se nota especialmente en los montajes de volúmenes y en build.context. Por ejemplo, ha añadido un volumen exclusivo para desarrollo en compose.dev.yaml:
# compose.dev.yaml (fragmento)
services:
app:
volumes:
# Importante: ./local-debug se considerará relativo al PRIMER archivo en -f
- ./local-debug:/tmp/debug
Si compose.yaml está en la raíz del repositorio (como es canónico en nuestro curso), entonces ./local-debug es raíz-del-repositorio/local-debug. Incluso si compose.dev.yaml también está en la raíz, todo está bien y casi no se nota. Pero si empieza a mover archivos entre carpetas o a ejecutar comandos desde directorios inesperados, puede encontrarse con el clásico «la ruta existe en mi cabeza, pero no existe en el disco».
Por eso, en el proyecto de aprendizaje mantenemos ambos archivos en la raíz y asumimos que los comandos se ejecutan desde la raíz del repositorio. No es «rigidez por rigidez», sino una forma de no convertir el curso en una búsqueda del tesoro del tipo «encuentra de dónde sacó Compose esta ruta».
Una buena forma de verificar la realidad: pedir a Compose que muestre la configuración fusionada y observar cómo interpretó las rutas. A veces docker compose config muestra directamente la ruta ya normalizada, y se ve al instante: «Oh, buscó ./local-debug en un lugar completamente diferente al que pensaba».
5. docker compose config: YAML final
En el mundo de Docker y Compose es fácil caer en el modo de «vamos a hacer up directamente, si no funciona, miramos los logs». Es un instinto aceptable, pero empieza a fallar cuando se tienen varios archivos de Compose, variables de entorno, sustituciones de ${...}, valores condicionales y el entorno crece hasta cuatro servicios. En ese momento, uno desea tener un botón que diga «muéstrame exactamente lo que vas a ejecutar». Ese botón existe: es `docker compose config`.
El comando hace dos cosas importantes. Primero, combina los archivos de Compose exactamente como lo haría up. Segundo, imprime el resultado en forma de YAML «normalizado», donde ya se puede ver qué valores reales se obtuvieron después de la combinación y la sustitución. Esta es la fuente única de verdad (single source of truth), especialmente cuando se deja de confiar en la memoria: «¿realmente lo sobrescribí o me lo parece?».
Ejemplo básico para nuestro esquema «base + anulación de desarrollo»:
# Usamos el mismo conjunto -f que en la ejecución real con up
docker compose -f compose.yaml -f compose.dev.yaml config
¿Qué cosas útiles se verán en la salida? Por ejemplo, la combinación de environment para el servicio app (fragmento, simplificado):
services:
app:
environment:
SPRING_PROFILES_ACTIVE: postgres,cache,messaging
JAVA_TOOL_OPTIONS: -Dexample.flag=true
Incluso si escribió environment en diferentes archivos, en la vista final ve «cómo será» para el contenedor. Esto es especialmente valioso cuando no está seguro de quién sobrescribió a quién.
Un matiz importante: docker compose config a menudo muestra la configuración en una forma más «expandida» de como se escribió. Por ejemplo, los puertos pueden convertirse en «forma larga», y environment puede estar alineado y reordenado. Esto no significa que Compose haya «reinventado» algo. Significa que ha mostrado la misma configuración, pero en forma canónica. Piense en ello no como «un YAML diferente», sino como un «resultado formateado».
Y otro matiz que es mejor conocer de antemano: el comando realiza la sustitución de variables (interpolation). Es decir, si en compose.yaml había algo como:
environment:
# El valor se toma de la variable de entorno; si no existe, se usa el valor por defecto después de :-
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-catalog}
entonces docker compose config mostrará el valor final, por ejemplo:
environment:
# Aquí está el resultado ya «calculado» después de la sustitución (podría ser un secreto)
SPRING_DATASOURCE_PASSWORD: catalog
Esto es extremadamente útil para verificar los valores por defecto, pero al mismo tiempo es una razón para ser cuidadoso: si se están sustituyendo secretos a través de variables de entorno, config puede imprimirlos. No porque Compose sea malo, sino porque es honesto.
6. Lista de verificación para Spring Boot
La salida de docker compose config puede ser larga, y la primera reacción de un principiante suele ser: «Vaya, hay muchas letras. Solo quería comprobar un puerto». Es normal. El secreto está en no intentar «leerlo todo como una novela», sino mirar la configuración como una lista de verificación de la realidad. En el contexto de nuestro servicio de Spring Boot, casi siempre empezamos por el servicio app, porque es el que debe levantarse y conectarse correctamente a los demás.
Primero, es útil encontrar en la salida services: app y asegurarse de que Compose ha ensamblado realmente la variante que se deseaba. Si se esperaba que el archivo de desarrollo añadiera algo a la compilación, en la configuración debería verse: por ejemplo, en build pueden aparecer detalles adicionales y en ports, un nuevo puerto publicado. No es necesario adivinar mirando dos YAML por separado; es mejor ver el combinado.
Después, tiene sentido mirar el environment de app. Para Spring Boot, esto es casi siempre el «pulso» de la aplicación: perfiles activos (SPRING_PROFILES_ACTIVE), hosts de infraestructura (SPRING_DATA_REDIS_HOST, SPRING_RABBITMQ_HOST), URL de datasource a postgres por nombre de servicio. Si después de la combinación desapareció repentinamente alguna variable de entorno o cambiaron los perfiles, habrá atrapado el problema antes de ejecutar.
Luego, conviene comprobreve los ports. En modo normal, normalmente basta con el puerto HTTP, por ejemplo 8080:8080. En modo de desarrollo puede añadirse otro puerto. Es importante no confundir la situación de «el puerto está en el archivo de desarrollo» con «el puerto realmente está en la configuración fusionada». config muestra lo segundo.
Si en el proyecto hay montajes bind, especialmente nuestro escenario de aprendizaje con el directorio de exportación (./data/exports), entonces echar un vistazo a volumes en la configuración fusionada ayuda a comprender rápidamente si ha ocurrido el problema de «la ruta se resuelve hacia otro lado». Es una rotura típica que parece un problema de la aplicación («¿por qué no veo los archivos de exportación?»), aunque en el fondo es un problema de ruta y montaje.
Y finalmente, en nuestro stack hay dependencias con el modelo de readiness. Por lo tanto, es útil asegurarse de que en la configuración final no se hayan perdido healthcheck y depends_on.condition: service_healthy (si se usan en la base). Esto es especialmente importante cuando el segundo archivo ha sobrescrito accidentalmente una parte del servicio y «con las manos equivocadas» ha afectado una parte importante.
Si la salida es demasiado grande, una práctica absolutamente normal es guardar la configuración fusionada en un archivo y abrirla como un YAML normal:
# Truco útil: guardar la configuración fusionada y verla/buscarla como un YAML normal
docker compose -f compose.yaml -f compose.dev.yaml config > merged.compose.yaml
Normalmente no se quiere hacer commit de este archivo, pero es cómodo «ojearlo» o buscar líneas concretas.
7. Errores típicos al trabajar con Compose
Cuando el entorno ya es maduro, los errores no se vuelven «más graves», sino simplemente más astutos: a menudo no se rompe el servicio en sí, sino la idea que uno tiene de lo que está ejecutando. A continuación, los problemas más frecuentes que ocurren precisamente en la combinación de «varios archivos + merge».
Error n.º 1: pasar los archivos en el orden incorrecto.
Esto parece inofensivo: simplemente se intercambian -f compose.yaml y -f compose.dev.yaml. Pero para Compose esto cambia el significado: el archivo base comienza a sobrescribir el archivo de anulación, y los ajustes de desarrollo «desaparecen», aunque estén escritos físicamente. El hábito de tener en mente la regla «primero la base, luego la anulación» y de verificar con docker compose config normalmente resuelve el problema al instante.
Error n.º 2: mirar solo compose.dev.yaml y tratar de adivinar el resultado a partir de él.
El archivo de desarrollo casi siempre contiene solo fragmentos (como debe ser), por lo que por definición no cuenta toda la verdad. Al final, la persona ve: «ajá, aquí se añadió una variable», y luego se sorprende de que la conexión a Redis no funcione. Porque la conexión a Redis estaba definida en el archivo base, y ni siquiera miró la imagen fusionada. En cuanto se adquiere el hábito de «fusionamos → miramos config → ejecutamos», este tipo de sorpresas se reducen drásticamente.
Error n.º 3: pensar que las rutas en el archivo de desarrollo se consideran relativas a dicho archivo.
La rotura más molesta: se monta ./something, existe en el disco, pero el contenedor dice «no such file or directory». La razón es el punto de referencia de la ruta relativa. En nuestro curso disciplinamos la estructura (ambos archivos en la raíz), pero igualmente es útil recordar la regla y, en casos dudosos, verificar la configuración fusionada.
Error n.º 4: no usar el mismo conjunto de -f para config y para up.
El comando docker compose config sin -f por defecto solo lee compose.yaml. Si luego se ejecuta docker compose -f compose.yaml -f compose.dev.yaml up, primero se ha mirado una realidad y luego se ha ejecutado otra. No es un «error de Docker», es simplemente una desincronización en la mente. Se cura con disciplina: el mismo conjunto de -f dentro de un mismo escenario.
Error n.º 5: imprimir docker compose config y compartir accidentalmente secretos.
Incluso si hoy en el proyecto de aprendizaje las contraseñas son condicionales, en el trabajo real este comando puede mostrar fácilmente valores reales del .env o del entorno del shell. No es una razón para no usar config, es una razón para recordar: la salida del comando es casi un «pasaporte de ejecución» listo, y es mejor no copiarlo en chats públicos sin filtrarlo.
GO TO FULL VERSION