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:
| 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.
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¶
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:
- Crea una red privada donde
apipuede conectarse adbpor el nombredb, sin IPs nilocalhost. - Publica únicamente el puerto 8080: la base de datos no queda expuesta a internet.
- Crea el volumen
datos, donde PostgreSQL guarda la información aunque se recreen los contenedores. - Levanta primero
dby despuésapi.
Las contraseñas van en .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")]
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:
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:
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¶
- Documentación oficial: docs.docker.com/compose
- Referencia del archivo: Compose file reference
- nginx como proxy: docs.nginx.com
- Guías relacionadas: Docker, Kubernetes, GitHub Actions