1. services: roles y contenedores
Cuando abres por primera vez un compose.yaml, es fácil caer en la trampa: parece que services es solo una lista de contenedores que Compose va a «levantar». Pero es mejor pensar de otra manera: un service es una descripción de un rol en el entorno, y un contenedor es simplemente una «copia» concreta de ese rol. Es como una receta (service) y la pizza preparada (container): la receta se puede repetir, y la pizza… bueno, normalmente no vive mucho tiempo.
Service vs image vs container — mapa breve de conceptos
En nuestro curso ya tenemos las palabras básicas de Docker, pero Compose añade una más: service. Resumamos esto en una pequeña tabla para que el cerebro no se sobrecaliente:
| Término | Qué es | Cómo pensarlo en humano |
|---|---|---|
| Image | Plantilla/artefacto empaquetado | «Caja con la aplicación» (plantilla) |
| Container | Proceso ejecutado a partir de una image | «Caja ejecutándose» (instancia) |
| Service | Descripción de cómo crear contenedor(es) | «Instrucción: cómo ejecutar este rol en el entorno» |
Compose opera precisamente con descripciones. Lee el YAML y decide: «Vale, aquí está el servicio app, necesita tal image o build, tales variables, tales puertos y tales montajes de volume».
Service en YAML: ejemplo mínimo
Este es un servicio que toma una imagen ya preparada y publica el puerto:
services:
# Nombre del servicio (rol): es en lo que te apoyarás en Compose
app:
# Qué imagen ejecutar (si no se especifica build)
image: docker-java-catalog-service
# Publicación del puerto en la máquina host: host:container
ports:
- "8080:8080"
En estas 5 líneas hay una idea importante: app es el nombre del servicio, no el nombre del contenedor. Compose luego creará un contenedor para este servicio (y lo nombrará según sus propias reglas), pero tu punto de apoyo es el nombre del servicio.
Por qué container_name es un mal apoyo
Compose tiene la opción container_name, y para un principiante parece un salvavidas: «¡Oh, voy a poner un nombre fijo al contenedor y accederé a él!». Pero es como etiquetar cada tornillo de un mueble con un rotulador: una vez parece útil, pero luego no puedes montar el armario siguiendo las instrucciones.
Compose es bueno porque puede gestionar varias instancias de un servicio, recrear contenedores, cambiar el orden de creación y, al mismo tiempo, mantener un modelo predecible. Si haces de container_name un pilar obligatorio, rompes parte de esa flexibilidad y a menudo te creas futuros conflictos de nombres.
En nuestro curso seguimos la regla: el apoyo es el nombre del servicio, no el nombre manual del contenedor.
Por ahora, en nuestro compose.yaml solo hay un servicio, por lo que es fácil tomar la red como una teoría prematura. Pero el error de localhost aparece justo en el momento en que aparece al menos un vecino: una base de datos, una caché o un cliente interno. Es mejor asimilar este mínimo de redes ahora, sin el ruido de las dependencias, que tener que reaprenderlo después en un entorno multi-contenedor real.
2. networks: default network y comunicación entre servicios
Cuando tienes un solo contenedor, la red parece algo opcional: reenvías el puerto y ya está. Pero en cuanto aparecen al menos dos participantes en el entorno, se vuelve importante cómo se comunican. Compose lo resuelve así: crea una red para tu entorno (normalmente de forma automática), conecta los servicios a ella y les permite encontrarse entre sí por nombre. No es «magia», es simplemente una automatización útil de lo que podrías hacer manualmente.
Default network: la red por defecto
Si en compose.yaml no has definido networks, Compose igualmente creará una red del proyecto. Esto se llama default network: «red por defecto para este entorno de Compose». Dentro de ella, los servicios obtienen direcciones IP y también registros en el DNS integrado.
Un matiz importante para principiantes: esta red es interna, se trata de la comunicación entre contenedores. No se trata del «acceso desde tu máquina», para eso sirve ports.
ports es un puente hacia el host, no un «permiso para comunicarse internamente»
El comando y la clave YAML ports a menudo inducen a error, porque parece: «Si no especifiqué ports, entonces el servicio no está disponible». Esto solo es cierto para el acceso desde tu máquina host (por ejemplo, desde el navegador en localhost). Dentro de la red de Compose es diferente: los contenedores se comunican a través de los puertos internos.
Siente la diferencia:
ports: "8080:8080" significa «desde la máquina host se puede acceder al puerto 8080, y eso llegará al contenedor en el puerto 8080».
Para la comunicación dentro de la red, ports no es necesario en absoluto: los servicios pueden comunicarse directamente entre sí porque están en la misma red.
Un pequeño esquema para que quede claro en la mente:
flowchart LR
%% Acceso desde la máquina host: a través del puerto publicado (ports)
Host["Tu máquina (host)"] -->|localhost:8080| App["service: app (container)"]
%% Acceso entre contenedores: por nombre de servicio a través del DNS integrado
Client["service: internal-client (container)"] -->|http://app:8080| App
subgraph ComposeNetwork["default network (Compose)"]
App
Client
end
La idea es simple: «host → contenedor» va a través de ports, mientras que «contenedor → contenedor» va a través de la red y el DNS mediante el nombre del servicio.
Redes explícitas: cuándo escribir networks
Por ahora no estamos haciendo topologías complejas. Pero es útil ver que una red explícita en YAML es simplemente la declaración de un recurso y la conexión a él:
services:
app:
image: docker-java-catalog-service
networks:
# Conectamos explícitamente el servicio a la red indicada
- app-net
networks:
# Declaramos la red como un recurso del entorno de Compose
app-net:
Son 8 líneas que dicen: «Crea la red app-net y conecta app a ella». Para un solo servicio parece escribir de más, y a menudo lo es. Pero si quieres disciplinar el entorno o más adelante conectar varios servicios a la misma red de forma explícita, se convierte en una herramienta clara. Para el primer compose.yaml suele ser suficiente con la default network.
3. DNS de Compose: el nombre del servicio como hostname y la trampa de localhost
Con docker run nos acostumbramos: «Hay un contenedor, está aislado, pero puedo reenviar el puerto a localhost». En Compose, donde hay varios servicios, aparece un nuevo hábito: si quieres acceder a un servicio vecino, usas el nombre del servicio como nombre de host. Este será el reflejo ingenieril principal en cualquier entorno multi-contenedor, pero lo entrenamos ya ahora, mientras todo es simple y no duele.
El nombre del servicio como dirección: http://app:8080
Supongamos que tienes dos servicios: nuestra app (Catalog Service) y un cliente interno condicional que accede a ella por HTTP. En Compose se vería así:
services:
# Servicio que proporciona una API HTTP dentro de la red de Compose
app:
image: docker-java-catalog-service
# Cliente interno (también un contenedor) que accederá a app
internal-client:
image: curlimages/curl:8.7.1
environment:
# Importante: el nombre del servicio app se usa como hostname en el DNS de Compose
APP_BASE_URL: http://app:8080
Fíjate en http://app:8080. Aquí app no es un DNS de Internet ni el «hostname de tu máquina». Es un registro en el DNS integrado de la red de Compose: «el servicio app está disponible bajo el nombre app».
La dirección IP del contenedor puede cambiar (el contenedor se recrea y la IP cambia), pero el nombre del servicio permanece. Y esa es la principal victoria sobre el caos: dejamos de depender de la estabilidad de las IP y empezamos a vivir con nombres estables. Por la misma razón, una base de datos en Compose vive bajo un nombre como db, y no bajo una IP aleatoria ni bajo localhost.
localhost dentro de un contenedor es «yo mismo»
Uno de los errores más frecuentes de los principiantes: «Estoy dentro del contenedor, entonces localhost es mi máquina». No. localhost dentro de un contenedor es el mismo contenedor.
Si el contenedor internal-client intenta acceder a http://localhost:8080, buscará un servidor HTTP dentro de sí mismo. Y dentro de sí mismo, lo más probable es que no tenga nada más que curl y un silencioso desespero.
Mini-ilustración (no ejecutes esto como una «receta», solo siente la diferencia en las direcciones):
# localhost dentro del contenedor = este mismo contenedor (probablemente no tenga un servidor HTTP)
curl http://localhost:8080/actuator/health
# nombre del servicio app = contenedor vecino en la red de Compose (DNS integrado)
curl http://app:8080/actuator/health
Si mantienes esta regla en la cabeza, te ahorrarás horas de futuros «¿¡por qué no se conecta!?».
Un poco sobre estabilidad: el nombre en lugar de la IP
En teoría, puedes averiguar la IP del contenedor y acceder por ella. En la práctica, esto se rompe en cuanto el contenedor se recrea. Compose te hace un favor muy humano: «No pienses en IP, piensa en nombres». Es como una agenda telefónica: no memorizas que tu colega ayer tenía el número +1... y hoy +1... (bueno, eso espero), sino que lo buscas por su nombre.
Precisamente por eso en Compose son importantes los nombres de servicio «limpios» y con sentido: app, db, cache, broker, etc. En nuestra etapa actual, nos basta con comprender el principio: el nombre del servicio debe ser apto para usarse en una URL.
4. volumes: datos separados del contenedor
Un contenedor es algo «desechable» (en el buen sentido). Es fácil recrearlo, y eso es normal. Pero los datos que quieres conservar normalmente no deberían desaparecer junto con el contenedor. En días anteriores ya hablamos de la writable layer, los bind mounts y los named volumes. Ahora añadimos una capa importante: en Compose, los volúmenes son parte de la descripción del entorno y viven junto a los servicios en el mismo YAML.
Volúmenes en Compose: dos estilos
En Compose puedes conectar almacenamiento a un servicio mediante la clave volumes. Pero el almacenamiento en sí es de dos tipos: bind mount (ruta en la máquina host) y named volume (volumen gestionado por Docker). Su sintaxis es similar, y esto confunde a la gente con frecuencia, así que comparémoslos «en la práctica», no solo en YAML.
| Cuestión | Bind mount | Named volume |
|---|---|---|
| Qué va a la izquierda en la notación X:Y | Ruta en la máquina host | Nombre del volumen (recurso de Docker) |
| ¿Se ven los datos en el sistema de archivos del proyecto? | Sí, es un directorio normal | Normalmente no, es almacenamiento gestionado |
| Portabilidad entre máquinas | Peor (rutas, permisos) | Mejor (Docker lo gestiona) |
| Cuándo es conveniente | Exportar archivos al exterior, trabajar con código/configuraciones | Almacenar estado de servicios a los que «no les importa ser visibles» |
| ¿Es necesario declararlo en el nivel superior volumes:? | No | Sí (por lo general, para declarar explícitamente el recurso) |
Bind mount en Compose: nuestro escenario de «exportación»
Para Container-Ready Catalog Service tenemos una exportación del catálogo a archivos, y nos es importante ver el resultado en la máquina host en ./data/exports. Por lo tanto, el bind mount es la elección lógica:
services:
app:
image: docker-java-catalog-service
volumes:
# Bind mount: a la izquierda, ruta en el host; a la derecha, ruta en el contenedor
- ./data/exports:/data/exports
A la izquierda, ./data/exports es la ruta en el repositorio. A la derecha, /data/exports es la ruta dentro del contenedor. Es el mismo principio que con docker run -v ..., pero en formato YAML.
Named volume en Compose: cuando los datos son importantes pero «no se necesitan fuera»
A veces es importante que los datos se conserven entre reinicios, pero no necesitas abrir el directorio en el explorador y mirar los archivos manualmente. Entonces el named volume se vuelve más cómodo y menos «frágil» (especialmente entre plataformas):
services:
app:
image: docker-java-catalog-service
volumes:
# Named volume: a la izquierda, nombre del recurso Docker; a la derecha, ruta en el contenedor
- exports-data:/data/exports
volumes:
# Declaramos el named volume como un recurso del entorno de Compose
exports-data:
Observa: ha aparecido una sección de nivel superior volumes. Declara el recurso exports-data. Luego, el servicio simplemente lo «conecta».
Regla importante: un volume no conecta servicios, almacena datos
Un error lógico muy común suena así: «Si conecté un volume, los servicios se verán entre sí a través de él». No. Un volume trata de archivos (estado), una network trata de conexión (comunicación). Son dos ejes diferentes. Se podría decir que la red es un «teléfono» y el volumen es un «armario con carpetas». A través del armario no se puede llamar.
Si necesitas que los servicios se vean entre sí, usa la red y el nombre del servicio. Si necesitas conservar archivos, usa volume/bind mount.
5. Tres ejes del entorno: procesos, red, datos
En esta etapa, a menudo el principiante tiene la sensación: «Compose es solo un YAML con un montón de claves que hay que aprenderse». En realidad, es más útil pensar así: Compose describe el entorno en tres dimensiones. Están los procesos (services), la comunicación (networks) y el estado/archivos (volumes). Si aprendes a leer cualquier compose.yaml según estos tres ejes, el archivo dejará de parecer un pergamino chamánico y se convertirá en una configuración normal.
Mini-esquema de lectura de un archivo compose
Desglosemos «cómo leerlo» a nivel de pensamiento. Primero, localizas services y entiendes quién participa en el entorno. Luego, observas cómo se comunican (default network o networks explícitas). Y solo después analizas dónde viven los datos (volumes/bind mounts).
Aquí tienes un ejemplo compacto donde se ven los tres ejes, sin sobrecargar:
services:
# Proceso/rol en el entorno
app:
image: docker-java-catalog-service
# Acceso desde la máquina host al contenedor
ports:
- "8080:8080"
# Datos/archivos que deben vivir fuera del contenedor
volumes:
- ./data/exports:/data/exports
Aquí se describen explícitamente el service y el storage (bind mount), mientras que la network no se describe — por lo tanto, será la default network. Esto es normal. Incluso si por ahora tenemos un solo servicio, Compose igualmente crea un «entorno de contenedores», y luego es cómodo añadir vecinos sin reescribir el mundo.
El nombre del servicio ya es importante ahora, incluso con un solo servicio
Hoy, en la primera versión de compose.yaml, tendremos un solo servicio (nuestra app). Podría preguntarse: «¿Para qué necesito el DNS y el nombre del servicio si no hay con quién comunicarme?» La respuesta es simple: el nombre del servicio es parte de la disciplina. Nombra el servicio como quieras referenciarlo en el entorno. Aunque ahora no haya vecinos, no querrás que mañana (cuando aparezca un segundo participante en el entorno) tengas que renombrar todo y romper la configuración.
Si eliges el nombre app, es corto, neutral y legible. Si eliges very-important-catalog-service-that-i-love, el YAML se hará más largo y la vida no mejorará. Compose fomenta los nombres simples.
6. Errores típicos al trabajar con Compose
En Compose, los errores a menudo parecen «como si la aplicación se hubiera roto», aunque en realidad solo modelaste incorrectamente el entorno. La buena noticia: estos errores son típicos y se curan no con heroicidades, sino con las palabras adecuadas en la cabeza. Ahora repasaremos los más frecuentes para que en la lección 5 el compose.yaml se monte con tranquilidad, sin «por qué no se abre y por qué no duermo otra vez».
Error n.º 1: acceder a un servicio vecino a través de localhost.
Es un clásico. Dentro de un contenedor, localhost significa «yo mismo», no otro contenedor ni tu máquina host. En Compose, para acceder a un vecino se usa el nombre del servicio: http://app:8080, http://some-service:1234, etc. Si fijas en tu mente «service name = hostname», este error desaparece casi por completo.
Error n.º 2: confundir el puerto interno del contenedor con el puerto host publicado.
La notación ports: "9090:8080" significa «desde la máquina host accedemos al puerto 9090; dentro del contenedor, la aplicación escucha en el puerto 8080». Pero dentro de la red de Compose, otro contenedor debe dirigirse al vecino por el puerto interno, es decir, http://app:8080, no http://app:9090. El puerto interno es el puerto del proceso en el contenedor; el puerto host es solo una «entrada» externa desde tu máquina.
Error n.º 3: pensar que un service es «un contenedor concreto para siempre».
Un service es una descripción. Los contenedores se recrean, se actualizan, pueden reiniciarse tras cambios. Si empiezas a apegarse a un contenedor concreto como a un «personaje», aparecen expectativas extrañas como «¿por qué cambió la IP?» o «¿por qué se recreó el contenedor?». Compose trabaja precisamente con descripciones, y los contenedores son ejecutores reemplazables.
Error n.º 4: creer que un volume es «una forma de conectar servicios».
Un volume es almacenamiento. Resuelve el problema de los datos y archivos, pero no el problema de la comunicación en red. La comunicación es cosa de networks y DNS. Si quieres que los servicios se vean entre sí, piensa en la red y los nombres de servicio. Si quieres conservar archivos, piensa en bind mounts y named volumes. Mezclar estos modelos genera configuraciones muy extrañas.
Error n.º 5: usar container_name como fundamento de todo el entorno.
Al principio, parece cómodo nombrar manualmente el contenedor para «saber exactamente cómo acceder a él». Pero en Compose es más correcto apoyarse en el nombre del servicio y el DNS integrado. container_name a menudo rompe la escalabilidad, crea conflictos de nombres y convierte el entorno en un «zoológico manual» del que Compose precisamente quería salvarte.
GO TO FULL VERSION