En la lección anterior aprendiste a levantar un servicio mínimo con Docker Compose. Ahora vamos a abrir la caja y estudiar a fondo el bloque más importante del archivo: la sección services. Cada servicio describe un contenedor de tu aplicación, y Compose ofrece un conjunto rico de claves para configurarlo: qué imagen usar o cómo construirla, qué puertos publicar, qué volúmenes montar, de qué otros servicios depende, a qué redes pertenece y cómo debe reiniciarse si falla. Dominar estas claves es esencial para describir aplicaciones reales con precisión, así que esta lección las desglosa una a una con ejemplos comentados.
Objetivos de Aprendizaje
- Comprender qué es un servicio y cómo se define dentro de
services. - Diferenciar entre usar una imagen existente (
image) y construir una propia (build). - Configurar la publicación de puertos con
ports. - Montar datos persistentes y código con
volumes. - Controlar el orden de arranque con
depends_on. - Conectar servicios mediante
networksy configurar el reinicio conrestart.
¿Qué Es un Servicio?
Un servicio es la definición de un contenedor (o de varias réplicas del mismo contenedor) dentro de tu aplicación Compose. No confundas estos conceptos:
- Imagen: la plantilla de solo lectura a partir de la cual se crean contenedores.
- Contenedor: una instancia en ejecución de una imagen.
- Servicio: la descripción en el
docker-compose.ymlde cómo debe ejecutarse ese contenedor.
Cada servicio vive bajo la sección services, identificado por un nombre que tú eliges:
webydbson nombres de servicio arbitrarios. Estos nombres son importantes: más adelante verás que sirven también como nombre de host para que los servicios se comuniquen entre sí.
La Clave image: Usar una Imagen Existente
La forma más sencilla de definir un servicio es indicar qué imagen debe usar, normalmente una imagen oficial de Docker Hub.
Explicación:
image: redis:7-alpine: usa la imagenredis, etiqueta7-alpine. Compose la descarga de Docker Hub si no está disponible localmente.- La etiqueta tras los dos puntos fija la versión. Es buena práctica indicar una versión concreta (
7-alpine) en lugar delatest, para que tu aplicación sea reproducible y no cambie de versión sin avisar.
La Clave build: Construir tu Propia Imagen
Cuando tu servicio necesita una imagen personalizada (por ejemplo, tu propia aplicación con su código y dependencias), usas build en lugar de image. Compose construirá la imagen a partir de un Dockerfile.
build: .: indica a Compose que construya la imagen usando elDockerfileque se encuentra en el directorio actual (.).
También puedes usar la forma extendida, más explícita y flexible:
Explicación:
context: ./backend: el directorio que se envía a Docker como contexto de construcción (donde están el código y los ficheros que necesita elDockerfile).dockerfile: Dockerfile.prod: permite usar unDockerfilecon un nombre distinto al estándar, útil para tener variantes (desarrollo, producción).
image vs build
| Aspecto | image |
build |
|---|---|---|
| De dónde viene la imagen | Se descarga ya construida | Se construye localmente |
| Cuándo usarla | Software estándar (bases de datos, cachés) | Tu propia aplicación |
Requiere Dockerfile |
No | Sí |
| Reconstrucción | No aplica | Con docker compose build o up --build |
Consejo: puedes combinar ambas. Si pones
buildyimagejuntos, Compose construye la imagen y le asigna como nombre el valor deimage, lo que facilita reutilizarla o subirla a un registro.
La Clave ports: Publicar Puertos
Por defecto, los puertos de un contenedor solo son accesibles dentro de la red de Docker. Para acceder desde el host (tu máquina), debes publicarlos con ports.
Explicación:
ports:: abre la lista de mapeos de puertos."8080:80": el formato es"HOST:CONTENEDOR". El puerto 8080 del host se conecta al 80 del contenedor. Accederás enhttp://localhost:8080."8443:443": un segundo mapeo, para el puerto HTTPS.
Recuerda: el número de la izquierda es el que escribes en tu navegador (el del host); el de la derecha es donde escucha el programa dentro del contenedor. Es un error muy frecuente invertirlos.
La Clave volumes: Persistir Datos y Montar Código
La sección volumes de un servicio monta almacenamiento dentro del contenedor. Hay dos usos principales:
services:
db:
image: postgres:16
volumes:
- datos_db:/var/lib/postgresql/data # volumen con nombre
web:
image: nginx:latest
volumes:
- ./html:/usr/share/nginx/html # bind mount
volumes:
datos_db:Explicación detallada:
datos_db:/var/lib/postgresql/data: monta un volumen con nombre (datos_db) en la ruta interna donde Postgres guarda sus datos. Así los datos sobreviven aunque elimines el contenedor../html:/usr/share/nginx/html: es un bind mount: enlaza la carpeta./htmlde tu host con la carpeta del contenedor donde Nginx sirve los archivos. Cualquier cambio en tu host se refleja al instante, muy útil en desarrollo.- El bloque
volumes:de nivel superior (al final): debes declarar ahí todo volumen con nombre que uses, para que Compose lo cree y lo gestione.
| Tipo | Sintaxis | Uso típico |
|---|---|---|
| Volumen con nombre | nombre:/ruta/contenedor |
Datos persistentes (bases de datos) |
| Bind mount | ./ruta/host:/ruta/contenedor |
Código y configuración en desarrollo |
La Clave depends_on: Orden de Arranque
depends_on indica que un servicio necesita que otro se inicie antes que él. Compose respetará ese orden al arrancar.
Explicación:
depends_on: - db: Compose arrancará primero el serviciodby luegoweb.- También controla el orden inverso al apagar:
webse detiene antes quedb.
Matiz muy importante:
depends_onsolo garantiza el orden de arranque del contenedor, no que el servicio dependiente esté listo para aceptar conexiones. Una base de datos puede tardar unos segundos en estar operativa después de que su contenedor haya arrancado. Para esperar a que esté realmente lista, se usan healthchecks concondition: service_healthy, un tema que profundizaremos más adelante.
La Clave networks: Conectar Servicios
Por defecto, Compose crea una red única para tu aplicación y conecta todos los servicios a ella, de modo que ya pueden comunicarse entre sí por su nombre de servicio. Aun así, puedes definir redes personalizadas para aislar grupos de servicios.
services:
web:
image: nginx:latest
networks:
- frontend
api:
build: .
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:Explicación:
networks:dentro de cada servicio: lista las redes a las que se conecta ese servicio.webestá solo enfrontend;dbsolo enbackend;apiestá en ambas y actúa de puente.- Resultado:
webno puede hablar directamente condb, lo que mejora el aislamiento y la seguridad. Soloapitiene acceso a la base de datos. - El bloque
networks:de nivel superior declara las redes que usas.
La Clave restart: Política de Reinicio
restart define qué debe hacer Docker si el contenedor se detiene o falla. Es clave para la resiliencia en producción.
| Valor | Comportamiento |
|---|---|
no |
Nunca reinicia (valor por defecto). |
always |
Reinicia siempre que se detenga, incluso al reiniciar Docker. |
on-failure |
Reinicia solo si termina con un código de error. |
unless-stopped |
Reinicia siempre, salvo que lo hayas detenido manualmente. |
restart: unless-stopped: si el contenedor se cae por un error o se reinicia la máquina, Docker lo volverá a levantar automáticamente; pero si tú lo paras a propósito, respetará tu decisión. Es una opción muy recomendable para servicios que deben estar siempre activos.
Ejemplo Completo con Varios Servicios
Reunamos todo en un único archivo que define tres servicios: una API que construyes tú, una base de datos PostgreSQL y una caché Redis.
services:
api:
build: ./api # construye la imagen desde ./api/Dockerfile
ports:
- "3000:3000" # expone la API en el puerto 3000 del host
depends_on:
- db # arranca después de la base de datos
- cache # y después de la caché
networks:
- app_net
restart: on-failure # reinicia si la API falla
db:
image: postgres:16 # imagen oficial de PostgreSQL
environment:
POSTGRES_USER: app_user
POSTGRES_DB: app_db
POSTGRES_PASSWORD: changeme_in_real_env
volumes:
- datos_db:/var/lib/postgresql/data # persistencia de datos
networks:
- app_net
restart: unless-stopped
cache:
image: redis:7-alpine # caché ligera en memoria
networks:
- app_net
restart: unless-stopped
networks:
app_net: # red común para los tres servicios
volumes:
datos_db: # volumen con nombre para la base de datosExplicación global del archivo:
api: se construye desde tu código (build: ./api), publica el puerto 3000, depende dedbycache(que arrancarán antes), está en la redapp_nety se reinicia si falla.db: usa la imagen oficial de Postgres, recibe su configuración por variables de entorno, guarda los datos en el volumendatos_dby se reinicia salvo parada manual.cache: una instancia de Redis ligera, en la misma red.networks: app_net: conecta los tres servicios para que puedan comunicarse por su nombre (db,cache,api).volumes: datos_db: declara el volumen con nombre que usa la base de datos.
Advertencia de seguridad:
changeme_in_real_enves un valor de ejemplo. En proyectos reales nunca escribas contraseñas en texto plano dentro del YAML; usa variables de entorno y un gestor de secretos, como se explica en la lección de Variables de Entorno.
Errores Comunes y Consejos
- Invertir el orden de los puertos: en
"HOST:CONTENEDOR", la izquierda es tu máquina y la derecha el contenedor. Si los inviertes, no podrás acceder al servicio. - Olvidar declarar el volumen o la red de nivel superior: si usas un volumen con nombre o una red personalizada en un servicio, debes declararlos también en los bloques
volumes:ynetworks:de nivel superior. - Confiar en
depends_onpara la disponibilidad: solo controla el orden de arranque, no que el servicio esté listo. Para eso necesitas healthchecks. - Usar
latesten imágenes críticas: fija versiones concretas (postgres:16,redis:7-alpine) para que el entorno sea reproducible. - Mezclar
imageybuildsin entenderlo: si solo quieres usar una imagen existente, usaimage; si necesitas construir, usabuild. Combínalos solo cuando quieras nombrar la imagen construida. - Consejo: comenta tus servicios con
#, como en el ejemplo anterior. Hace el archivo mucho más legible para tu equipo.
Ejercicios
Ejercicio 1
Define un servicio llamado cache que use la imagen redis:7-alpine, se reinicie siempre salvo parada manual y esté conectado a una red personalizada llamada interna.
Ejercicio 2
Crea un servicio api que se construya a partir de un Dockerfile ubicado en la carpeta ./backend, publique el puerto 4000 del host hacia el 4000 del contenedor y dependa de un servicio db.
Ejercicio 3
Explica la diferencia entre montar un volumen con nombre (datos:/var/lib/data) y un bind mount (./local:/app), y di cuál usarías para una base de datos y cuál para desarrollar código.
Soluciones
Solución al Ejercicio 1
services:
cache:
image: redis:7-alpine
restart: unless-stopped
networks:
- interna
networks:
interna:restart: unless-stopped reinicia el contenedor salvo que lo pares tú; la red interna se declara también en el bloque networks: de nivel superior.
Solución al Ejercicio 2
services:
api:
build:
context: ./backend
ports:
- "4000:4000"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: changeme_in_real_envbuild.context: ./backend indica dónde está el Dockerfile; el mapeo "4000:4000" publica el puerto; depends_on: - db asegura que la base de datos arranque antes que la API.
Solución al Ejercicio 3
- Volumen con nombre (
datos:/var/lib/data): lo gestiona Docker en un área controlada, es portable y está pensado para datos persistentes. Es la opción ideal para una base de datos, porque sus datos sobreviven a la eliminación del contenedor sin depender de rutas concretas del host. - Bind mount (
./local:/app): enlaza una carpeta de tu host con el contenedor, reflejando los cambios al instante. Es ideal para desarrollar código, ya que puedes editar los archivos en tu editor y ver el efecto sin reconstruir la imagen.
En resumen: volumen con nombre para la base de datos; bind mount para el código en desarrollo.
Conclusión
En esta lección has aprendido a definir servicios con todo detalle: a elegir entre image y build, a publicar puertos con ports, a persistir datos y montar código con volumes, a controlar el orden de arranque con depends_on, a conectar y aislar servicios con networks y a configurar la resiliencia con restart. Con un ejemplo de tres servicios has visto cómo encajan todas estas piezas en un único archivo coherente.
En la siguiente lección, Comandos de Docker Compose, pasaremos de definir la aplicación a operarla: aprenderás los comandos esenciales (up, down, ps, logs, exec, build, stop/start, restart) y el ciclo de trabajo típico para gestionar tus aplicaciones día a día.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Registro y Monitoreo en Docker
Módulo 6: Docker en Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
