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 networks y configurar el reinicio con restart.

¿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.yml de cómo debe ejecutarse ese contenedor.

Cada servicio vive bajo la sección services, identificado por un nombre que tú eliges:

services:
  web:        # nombre del servicio
    image: nginx:latest
  db:         # otro servicio
    image: postgres:16
  • web y db son 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.

services:
  cache:
    image: redis:7-alpine

Explicación:

  • image: redis:7-alpine: usa la imagen redis, etiqueta 7-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 de latest, 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.

services:
  api:
    build: .
  • build: .: indica a Compose que construya la imagen usando el Dockerfile que se encuentra en el directorio actual (.).

También puedes usar la forma extendida, más explícita y flexible:

services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod

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 el Dockerfile).
  • dockerfile: Dockerfile.prod: permite usar un Dockerfile con 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
Reconstrucción No aplica Con docker compose build o up --build

Consejo: puedes combinar ambas. Si pones build y image juntos, Compose construye la imagen y le asigna como nombre el valor de image, 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.

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
      - "8443:443"

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 en http://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 ./html de 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.

services:
  web:
    image: nginx:latest
    depends_on:
      - db
  db:
    image: postgres:16

Explicación:

  • depends_on: - db: Compose arrancará primero el servicio db y luego web.
  • También controla el orden inverso al apagar: web se detiene antes que db.

Matiz muy importante: depends_on solo 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 con condition: 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.
  • web está solo en frontend; db solo en backend; api está en ambas y actúa de puente.
  • Resultado: web no puede hablar directamente con db, lo que mejora el aislamiento y la seguridad. Solo api tiene 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.
services:
  db:
    image: postgres:16
    restart: unless-stopped
  • 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 datos

Explicación global del archivo:

  • api: se construye desde tu código (build: ./api), publica el puerto 3000, depende de db y cache (que arrancarán antes), está en la red app_net y se reinicia si falla.
  • db: usa la imagen oficial de Postgres, recibe su configuración por variables de entorno, guarda los datos en el volumen datos_db y 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_env es 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: y networks: de nivel superior.
  • Confiar en depends_on para la disponibilidad: solo controla el orden de arranque, no que el servicio esté listo. Para eso necesitas healthchecks.
  • Usar latest en imágenes críticas: fija versiones concretas (postgres:16, redis:7-alpine) para que el entorno sea reproducible.
  • Mezclar image y build sin entenderlo: si solo quieres usar una imagen existente, usa image; si necesitas construir, usa build. 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_env

build.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.

© Copyright 2026. Todos los derechos reservados