Saltar a contenido

Docker Compose

Esta guía cubre qué es Docker Compose, cómo describir varios contenedores en un solo archivo y cómo usar nginx como puerta de entrada, igual que un Ingress de Kubernetes pero en una sola máquina.

Requisito previo: haber construido al menos una imagen con la guía de Docker.


¿Qué es y para qué sirve?

Una aplicación real nunca es un contenedor solo: son la API, la base de datos, a veces el frontend y un proxy. Levantarlos a mano significa repetir varios docker run con sus puertos, sus variables y su red, en el orden correcto y sin equivocarse.

Docker Compose es un archivo YAML que describe todos esos contenedores y un comando que los levanta juntos:

docker compose up -d
Sin Compose Con Compose
Varios docker run largos, escritos de memoria Un archivo docker-compose.yml versionado en el repositorio
Crear la red y conectar cada contenedor a mano Compose crea una red donde cada servicio es un nombre DNS
Cada integrante levanta el entorno distinto Todos levantan exactamente lo mismo
Apagar y limpiar contenedor por contenedor docker compose down

Compose es para una sola máquina

Compose coordina contenedores dentro de un servidor. Repartir carga entre varias máquinas, reemplazar pods sin cortar el servicio o recuperarse de la caída de un nodo es trabajo de Kubernetes. Compose es ideal para desarrollar en el portátil y para desplegar aplicaciones pequeñas en un único servidor.


1. Anatomía del archivo

El archivo se llama docker-compose.yml y vive en la raíz del proyecto.

docker-compose.yml
services:            # cada contenedor de la aplicación
  api:
    image: ghcr.io/docsjaveriana/cowork-api:latest
    ports:
      - "8080:8080"
    environment:
      DB_HOST: db
    depends_on:
      - db

volumes:             # datos que sobreviven a los contenedores
  datos:
Clave Para qué sirve
services Cada contenedor. El nombre del servicio (api, db) es además su nombre de red
image Imagen ya publicada que se descarga del registro
build Alternativa a image: construir desde un Dockerfile local
ports "host:contenedor". Lo que no se publica solo es visible dentro de Compose
environment Variables de entorno del contenedor
env_file Leer esas variables de un archivo (.env) que no se sube al repositorio
depends_on Orden de arranque: primero la base de datos, luego la API
volumes Carpetas que persisten aunque el contenedor se borre y se vuelva a crear
restart unless-stopped hace que el contenedor vuelva a levantarse con el servidor

2. Ejemplo sencillo: API y base de datos

docker-compose.yml
services:
  api:
    image: ghcr.io/docsjaveriana/cowork-api:latest
    ports:
      - "8080:8080"
    environment:
      DB_HOST: db              # el nombre del servicio es el host
      DB_PORT: 5432
      DB_NAME: cowork
      DB_USER: cowork
      DB_PASSWORD: ${DB_PASSWORD}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: cowork
      POSTGRES_USER: cowork
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - datos:/var/lib/postgresql/data
    restart: unless-stopped
    # Sin "ports": la base de datos solo es visible desde la red de Compose

volumes:
  datos:

Qué hace este archivo, línea a línea:

  1. Crea una red privada donde api puede conectarse a db por el nombre db, sin IPs ni localhost.
  2. Publica únicamente el puerto 8080: la base de datos no queda expuesta a internet.
  3. Crea el volumen datos, donde PostgreSQL guarda la información aunque se recreen los contenedores.
  4. Levanta primero db y después api.

Las contraseñas van en .env

.env
DB_PASSWORD=unaClaveQueNoSeSube
.gitignore
.env

Compose lee ese archivo automáticamente y reemplaza ${DB_PASSWORD}. Es el equivalente al Secret de Kubernetes: nunca se escribe la contraseña dentro del YAML.

depends_on no espera a que la base de datos esté lista

Solo controla el orden de arranque, no que PostgreSQL ya acepte conexiones. Si la API falla al iniciar, agrégale un healthcheck a db y usa depends_on: { db: { condition: service_healthy } }, o deja que la API reintente la conexión.


3. Comandos de referencia

docker compose up -d            # levantar todo en segundo plano
docker compose ps               # ver qué está corriendo
docker compose logs -f api      # seguir los logs de un servicio
docker compose exec api sh      # entrar a un contenedor
docker compose pull             # bajar las imágenes nuevas del registro
docker compose up -d --build    # reconstruir las imágenes locales y recrear
docker compose restart api      # reiniciar un servicio
docker compose stop             # parar sin borrar
docker compose down             # parar y borrar contenedores y red
docker compose down -v          # ... y además borrar los volúmenes (¡borra los datos!)
docker compose up -d --scale api=3   # tres copias del servicio api

down -v borra los datos

La bandera -v elimina los volúmenes. En un servidor con datos reales equivale a borrar la base de datos.


4. nginx como reverse proxy

En Kubernetes, el Ingress mira el dominio y la ruta y reenvía al Service correcto. En Compose ese papel lo hace un contenedor de nginx: es el único que publica el puerto 80 y reparte el tráfico entre el frontend y la API.

flowchart LR
    I["Internet<br/>cowork.desarrolloweb.click"] --> P["nginx · proxy<br/>único con el puerto 80"]
    P -- "/" --> UI["ui:4000<br/>frontend Angular"]
    P -- "/api/cowork" --> API["api:8080<br/>backend Spring Boot"]
    API --> DB[("db:5432<br/>PostgreSQL")]
docker-compose.yml
services:
  proxy:                       # hace el papel del Ingress
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - ui
      - api
    restart: unless-stopped

  ui:                          # sin "ports": solo lo alcanza el proxy
    image: ghcr.io/docsjaveriana/cowork-ui:latest
    environment:
      PORT: "4000"
    restart: unless-stopped

  api:
    image: ghcr.io/docsjaveriana/cowork-api:latest
    environment:
      DB_HOST: db
      DB_PASSWORD: ${DB_PASSWORD}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: cowork
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - datos:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  datos:
nginx.conf
server {
    listen 80;
    server_name cowork.desarrolloweb.click;

    # El backend: todo lo que empiece por /api/cowork
    location /api/cowork {
        proxy_pass http://api:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # El frontend: todo lo demás
    location / {
        proxy_pass http://ui:4000;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Por qué funciona http://api:8080: dentro de la red de Compose, el nombre del servicio resuelve a la IP del contenedor. No hace falta publicar su puerto ni conocer la IP.

Ventaja de este montaje: el navegador llama siempre al mismo origen (cowork.desarrolloweb.click), así que no hay CORS entre el frontend y la API; el proxy reparte internamente.

Equivalencias con Kubernetes

Kubernetes Docker Compose
Ingress El servicio proxy con nginx y su nginx.conf
Service (nombre estable) El nombre del servicio en la red de Compose (api, ui, db)
Deployment · replicas: 3 docker compose up -d --scale api=3
PersistentVolumeClaim volumes:
Secret / ConfigMap Archivo .env o env_file
Namespace El nombre del proyecto Compose (la carpeta)

5. Usarlo en el proyecto semestral

Si el proyecto ya publica sus imágenes con GitHub Actions, desplegar con Compose en un servidor es:

# En el servidor, en la carpeta que tiene docker-compose.yml y .env
docker compose pull          # baja la imagen nueva publicada por el pipeline
docker compose up -d         # recrea los contenedores que cambiaron
docker compose logs -f api   # verificar

Esos dos comandos reemplazan al kubectl apply + rollout restart del despliegue con Kubernetes: el resto del pipeline (construir la imagen y publicarla en GHCR) es idéntico.

Desarrollo local: construir en vez de descargar

Durante el desarrollo conviene un archivo que construya desde el código en lugar de bajar la imagen publicada:

services:
  api:
    build: .            # usa el Dockerfile de esta carpeta
    ports:
      - "8080:8080"

Luego docker compose up -d --build reconstruye tras cada cambio.


6. Errores frecuentes

Síntoma Causa habitual
connection refused entre servicios Se usó localhost en vez del nombre del servicio (db, api)
La API arranca antes que la base de datos depends_on no espera a que esté lista: usar healthcheck o reintentos
Los datos desaparecen al recrear Falta el volumes: del servicio, o se ejecutó down -v
port is already allocated Otro proceso usa el puerto en el host: cambiar el lado izquierdo de ports
502 Bad Gateway en nginx El proxy_pass apunta a un puerto que el contenedor no escucha
La imagen no se actualiza Falta docker compose pull: con el tag :latest Compose reutiliza la que ya tiene
La contraseña quedó en el repositorio Se escribió en el YAML en vez de en .env (y .env sin .gitignore)

7. Comparación con Kubernetes

Docker Compose Kubernetes
Dónde corre Una sola máquina Un clúster de uno o varios nodos
Si un contenedor muere Lo reinicia (con restart) Crea otro para cumplir replicas
Escalar --scale api=3, en el mismo host replicas: 6, repartidas entre nodos
Entrada HTTP nginx configurado a mano Ingress declarado en YAML
Actualizar sin caída No, hay un instante sin servicio Sí, reemplazo gradual de pods
Complejidad Baja: un archivo y un comando Alta: muchos objetos que aprender
Cuándo usarlo Desarrollo local y servidores pequeños Producción con varias apps y alta disponibilidad

No son excluyentes: lo habitual es Compose en el portátil mientras se desarrolla y Kubernetes en el servidor. La imagen de Docker es exactamente la misma en los dos casos.

Referencias