Terminaste el módulo 3 con un guion de bash de cincuenta líneas que levanta Aurora Libros. Funciona, pero describe cómo llegar al estado deseado: crea la red, crea el volumen, arranca la base de datos, espera con un bucle, arranca la caché, la API y la web, cada una con sus catorce líneas de opciones.

Docker Compose invierte el planteamiento. En lugar de una secuencia de órdenes, escribes un fichero que dice cuál es el estado deseado —cuatro servicios, una red, un volumen— y dejas que Compose calcule los pasos. El fichero se llama compose.yaml, vive en el repositorio junto al código, se revisa en una pull request y se levanta con un comando.

En esta lección verás qué problema resuelve exactamente Compose (y cuál no resuelve), el YAML mínimo imprescindible para no pelearte con la sintaxis, cómo Compose usa el concepto de proyecto como espacio de nombres, y escribirás tu primer compose.yaml con dos de los cuatro servicios de Aurora Libros.

Contenido

  1. De cincuenta líneas imperativas a cuarenta declarativas
  2. Imperativo frente a declarativo
  3. Qué es Docker Compose y qué no es
  4. Compose v2: un plugin de la CLI de Docker
  5. El mínimo de YAML que necesitas
  6. Errores de sintaxis YAML que verás seguro
  7. Anatomía de un compose.yaml
  8. El proyecto como espacio de nombres
  9. Tu primer compose.yaml: base de datos y caché
  10. up, ps, logs, down: el ciclo completo

  1. De cincuenta líneas imperativas a cuarenta declarativas

Recupera dos fragmentos del guion del módulo 3, el de la base de datos y el de la caché:

docker network create aurora-net
docker volume create aurora-datos

docker run -d --name aurora-db --network aurora-net \
  -e POSTGRES_USER=aurora -e POSTGRES_PASSWORD=aurora_secreta \
  -e POSTGRES_DB=aurora_libros \
  -p 127.0.0.1:5432:5432 \
  --mount type=volume,src=aurora-datos,dst=/var/lib/postgresql/data \
  --memory 512m --cpus 1.0 --restart unless-stopped \
  postgres:16-alpine

docker run -d --name aurora-cache --network aurora-net \
  --memory 256m --cpus 0.5 --restart unless-stopped \
  redis:7-alpine redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru

Y ahora la misma información en compose.yaml:

services:
  aurora-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: aurora
      POSTGRES_PASSWORD: aurora_secreta
      POSTGRES_DB: aurora_libros
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - aurora-datos:/var/lib/postgresql/data
    restart: unless-stopped

  aurora-cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru
    restart: unless-stopped

volumes:
  aurora-datos:

No hay docker network create: Compose crea una red para el proyecto automáticamente. No hay docker volume create: basta con declarar el volumen. No hay --name: el nombre del servicio ya identifica al contenedor. Y no hay orden de ejecución, porque el fichero no es una secuencia, es una descripción.

  1. Imperativo frente a declarativo

Aspecto Imperativo (docker run) Declarativo (compose.yaml)
Qué escribes La secuencia de pasos El estado final deseado
Quién calcula el camino Tú, en el orden correcto Compose, comparando lo real con lo declarado
Repetir la ejecución Falla: "el contenedor ya existe" Es idempotente: no cambia lo que ya coincide
Cambiar una variable Borrar el contenedor y reescribir sus 14 líneas Editar una línea y volver a ejecutar up -d
Parar todo Otro guion, con los nombres repetidos docker compose down
Revisar cambios Imposible en una pull request git diff de un fichero de texto
Fuente de verdad Lo que hay corriendo en la máquina El fichero versionado
Deriva de configuración Invisible: nadie sabe si alguien tocó algo a mano up -d reconcilia y devuelve al estado declarado

La palabra clave es reconciliación. Cuando ejecutas docker compose up -d por segunda vez, Compose no vuelve a crear nada: compara la configuración de cada contenedor en marcha con la que declara el fichero y solo recrea lo que ha cambiado. Si editas la memoria de la caché y ejecutas up -d, se recrea la caché y los otros tres servicios ni se enteran.

  1. Qué es Docker Compose y qué no es

Compose es una herramienta para definir y ejecutar aplicaciones multicontenedor en un único host Docker. Su terreno natural:

  • El entorno de desarrollo de cualquier equipo: git clone y docker compose up -d.
  • Pruebas de integración en CI, donde necesitas una base de datos real y efímera.
  • Demostraciones y entornos de prueba reproducibles.
  • Despliegues pequeños en un solo servidor, cuando la alta disponibilidad no es un requisito.

Compose no es un orquestador de producción multinodo. No sabe repartir contenedores entre diez máquinas, no reprograma un servicio cuando muere el servidor que lo alojaba, no hace despliegues progresivos con vuelta atrás automática ni balanceo entre réplicas en máquinas distintas. Todo eso llega en el módulo 6 con Swarm y Kubernetes.

Necesidad Compose Orquestador (módulo 6)
Varios contenedores en un host Sí, es su razón de ser También, pero con más complejidad
Varios hosts en clúster No
Reprogramar si cae un nodo No
Escalado local de réplicas up --scale, con limitaciones Sí, con balanceo real
Despliegue progresivo y rollback No
Curva de aprendizaje Una tarde Semanas

La buena noticia es que el compose.yaml que escribas aquí no es trabajo desechable: los conceptos —servicio, imagen, variables, volúmenes, sondas de salud— son los mismos que verás en Kubernetes con otra sintaxis.

  1. Compose v2: un plugin de la CLI de Docker

Compose nació como un programa independiente escrito en Python que se invocaba con docker-compose (con guion) y leía un fichero docker-compose.yml. Esa es la sintaxis antigua. Desde Compose v2, reescrito en Go, es un plugin de la CLI de Docker y se invoca como subcomando: docker compose, sin guion. El nombre de fichero preferente hoy es compose.yaml, y la antigua clave version: "3.8" de la primera línea está obsoleta: si la pones, Compose la ignora y avisa. Olvídate de ella.

Comprueba tu instalación:

docker compose version
Docker Compose version v2.35.1

Si ese comando falla pero docker-compose version funciona, tienes la v1: instala el plugin (docker-compose-plugin en los repositorios de Docker) antes de seguir. Docker Desktop lo incluye de serie.

Compose busca el fichero en el directorio actual y hacia arriba, en este orden de preferencia:

Orden Nombre de fichero
1 compose.yaml
2 compose.yml
3 docker-compose.yaml
4 docker-compose.yml (nombre histórico)

Usa siempre compose.yaml. Los otros existen por compatibilidad.

  1. El mínimo de YAML que necesitas

YAML es un formato de serialización de datos pensado para que lo lean personas. Solo necesitas cinco ideas.

Indentación con espacios, nunca tabuladores. La jerarquía se expresa con sangrado. Dos espacios por nivel es la convención universal. Un tabulador es un error de sintaxis en YAML, sin excepciones.

Mapas (pares clave-valor), separados por dos puntos y un espacio:

image: postgres:16-alpine
restart: unless-stopped

Listas, con guion y espacio. Cada elemento al mismo nivel de sangrado:

volumes:
  - aurora-datos:/var/lib/postgresql/data
  - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro

Anidamiento: mapas dentro de mapas y listas dentro de mapas. Así se construye todo un compose.yaml: services es un mapa cuyas claves son nombres de servicio, y el valor de cada uno es otro mapa con image, ports, etc.

Escalares y comillas. YAML adivina el tipo: 5432 es un entero, true un booleano, 1.2 un decimal, postgres:16-alpine una cadena. Puedes usar comillas simples o dobles; las dobles interpretan secuencias de escape (\n), las simples no. La regla práctica en Compose:

Situación Escribe Por qué
Puertos "8080:80" Sin comillas, YAML 1.1 puede leerlo como número sexagesimal
Valores que empiezan por *, &, %, @, ` Entrecomillados Son caracteres reservados
Cadenas /no/on/off/true "true" Sin comillas se convierten en booleanos
Versiones tipo 1.2.0 "1.2.0" Sin comillas puede perder el dígito final
Nombres de imagen normales Sin comillas No hay ambigüedad

Comentarios: todo lo que sigue a # hasta el final de línea. Úsalos sin miedo, un compose.yaml bien comentado es documentación viva.

Para cadenas largas, YAML ofrece dos bloques: | conserva los saltos de línea y > los convierte en espacios. Los verás en command y en healthcheck.

  1. Errores de sintaxis YAML que verás seguro

Error Síntoma Solución
Tabulador para sangrar found character '\t' that cannot start any token Configura tu editor: 2 espacios
Falta el espacio tras : mapping values are not allowed here image: nginx, no image:nginx
Sangrado inconsistente Una clave "desaparece" o cambia de servicio Alinea todo el bloque al mismo nivel
8080:80 sin comillas El puerto publicado no es el que esperabas Entrecomilla siempre los puertos
Mezclar lista y mapa expected <block end> O todo con guiones, o todo con clave: valor
Duplicar una clave El segundo valor gana en silencio Revisa con docker compose config

Ese último comando es tu red de seguridad: docker compose config lee el fichero, lo valida, resuelve variables y te devuelve la configuración final tal y como la entiende Compose. Ejecútalo siempre que algo no cuadre.

  1. Anatomía de un compose.yaml

Un fichero de Compose tiene cuatro secciones de primer nivel de uso habitual:

services:     # OBLIGATORIA: los contenedores de la aplicación
  ...
volumes:      # volúmenes con nombre que gestiona Compose
  ...
networks:     # redes personalizadas
  ...
secrets:      # ficheros con datos sensibles (lección 04-05)
  ...

Solo services es obligatoria. Las demás existen porque un volumen o una red no pertenecen a un servicio: son recursos compartidos que varios servicios usan, y por eso se declaran aparte y se referencian desde dentro de cada servicio.

graph TD
    F["compose.yaml"] --> S["services:"]
    F --> V["volumes:"]
    F --> N["networks:"]
    S --> C1["contenedor<br/>aurora-libros-aurora-db-1"]
    S --> C2["contenedor<br/>aurora-libros-aurora-cache-1"]
    V --> VOL["volumen<br/>aurora-libros_aurora-datos"]
    N --> NET["red bridge<br/>aurora-libros_default"]
    C1 -.conectado a.-> NET
    C2 -.conectado a.-> NET
    C1 -.monta.-> VOL

Cada línea del fichero se convierte en objetos Docker reales de los que ya sabes todo: contenedores, redes bridge y volúmenes. Compose no inventa nada nuevo por debajo; habla con el mismo demonio y la misma API que usabas con docker run. Puedes seguir inspeccionando todo con docker ps, docker network inspect o docker volume ls.

  1. El proyecto como espacio de nombres

Compose agrupa todo lo que crea bajo un proyecto. El nombre del proyecto por defecto es el del directorio que contiene el fichero, en minúsculas y sin caracteres raros. Si tu fichero está en ~/aurora-libros/, el proyecto se llama aurora-libros.

Ese nombre se usa como prefijo:

Objeto Patrón de nombre Ejemplo
Contenedor <proyecto>-<servicio>-<número> aurora-libros-aurora-db-1
Red por defecto <proyecto>_default aurora-libros_default
Red declarada <proyecto>_<nombre> aurora-libros_trasera
Volumen con nombre <proyecto>_<nombre> aurora-libros_aurora-datos

Además, Compose etiqueta cada objeto con com.docker.compose.project=<proyecto> y com.docker.compose.service=<servicio>. Así es como sabe, en una máquina con veinte contenedores, cuáles son suyos.

El prefijado tiene una consecuencia práctica muy útil: dos proyectos distintos pueden usar el mismo compose.yaml sin pisarse. Tres formas de fijar el nombre del proyecto, de mayor a menor prioridad:

docker compose -p aurora-pruebas up -d          # 1. opción de la CLI
COMPOSE_PROJECT_NAME=aurora-pruebas docker compose up -d   # 2. variable de entorno
name: aurora-libros    # 3. clave de primer nivel en el propio fichero
services:
  ...

Fijar name: en el fichero es buena práctica: garantiza que el proyecto se llama igual aunque alguien clone el repositorio en una carpeta con otro nombre.

  1. Tu primer compose.yaml: base de datos y caché

Vas a empezar por lo pequeño: solo aurora-db y aurora-cache. La API y la web llegan en la lección 04-04, cuando ya sepas declarar dependencias y sondas de salud.

mkdir -p ~/aurora-libros && cd ~/aurora-libros
docker rm -f aurora-db aurora-cache aurora-api aurora-web 2>/dev/null
docker network rm aurora-net 2>/dev/null

Has borrado los contenedores del módulo 3 para que no haya conflictos de nombre ni de puertos. El volumen aurora-datos sigue intacto, pero Compose creará el suyo propio con prefijo, así que empezarás con una base de datos limpia poblada por db/init.sql.

Crea ~/aurora-libros/compose.yaml:

# compose.yaml — Aurora Libros S.L. (versión mínima: datos y caché)
name: aurora-libros

services:

  aurora-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: aurora
      POSTGRES_PASSWORD: aurora_secreta
      POSTGRES_DB: aurora_libros
    ports:
      - "127.0.0.1:5432:5432"     # solo accesible desde el propio host
    volumes:
      - aurora-datos:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    restart: unless-stopped

  aurora-cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru
    restart: unless-stopped

volumes:
  aurora-datos:

Tres detalles que merecen atención:

  • ./db/init.sql: las rutas relativas se resuelven desde el directorio del fichero, no desde tu cwd. Por eso desaparece el $HOME del guion de bash y el fichero se vuelve portable.
  • No hay networks:: Compose crea aurora-libros_default y conecta ahí ambos servicios, con DNS interno —exactamente igual que la red de usuario aurora-net que creaste a mano en la lección 03-05—.
  • No hay --name: no hace falta. El nombre del servicio es el nombre DNS.

Valida antes de arrancar:

docker compose config --quiet && echo "Sintaxis correcta"

  1. up, ps, logs, down: el ciclo completo

docker compose up -d
[+] Running 4/4
 ✔ Network aurora-libros_default        Created
 ✔ Volume "aurora-libros_aurora-datos"  Created
 ✔ Container aurora-libros-aurora-db-1     Started
 ✔ Container aurora-libros-aurora-cache-1  Started

Ahí está todo lo que promete el modelo declarativo: la red y el volumen se han creado solos, con su prefijo de proyecto, y los dos contenedores están en marcha. Ni una línea de docker network create.

docker compose ps
NAME                         IMAGE              SERVICE        STATUS         PORTS
aurora-libros-aurora-cache-1 redis:7-alpine     aurora-cache   Up 12 seconds  6379/tcp
aurora-libros-aurora-db-1    postgres:16-alpine aurora-db      Up 12 seconds  127.0.0.1:5432->5432/tcp

Fíjate en la columna SERVICE: ese es el nombre que usarás en todos los comandos, no el del contenedor. Comprueba que los objetos son Docker corriente:

docker network ls --filter name=aurora-libros
docker volume ls --filter name=aurora-libros
NETWORK ID     NAME                     DRIVER    SCOPE
b3f1a9c04e77   aurora-libros_default    bridge    local
DRIVER    VOLUME NAME
local     aurora-libros_aurora-datos

Los logs, agregados y coloreados por servicio:

docker compose logs --tail 3
aurora-db-1     | PostgreSQL init process complete; ready for start up.
aurora-db-1     | LOG:  database system is ready to accept connections
aurora-cache-1  | * Ready to accept connections tcp

Verifica que init.sql se ejecutó y que el DNS funciona entre servicios:

docker compose exec aurora-db psql -U aurora -d aurora_libros -c "SELECT count(*) FROM libros;"
docker compose exec aurora-cache redis-cli -h aurora-cache PING
 count
-------
     9
(1 row)
PONG

Nueve libros —los ocho originales más El Aleph— y la caché respondiendo por su nombre de servicio. Ahora la parte que ningún guion de bash te daba gratis:

docker compose down
[+] Running 3/3
 ✔ Container aurora-libros-aurora-cache-1  Removed
 ✔ Container aurora-libros-aurora-db-1     Removed
 ✔ Network aurora-libros_default           Removed

Un comando para la plataforma entera y limpia la red. El volumen no se ha borrado: down conserva los datos por diseño. Compruébalo y vuelve a levantar:

docker volume ls --filter name=aurora-libros --format "{{.Name}}"
docker compose up -d
docker compose exec aurora-db psql -U aurora -d aurora_libros -c "SELECT count(*) FROM libros;"
aurora-libros_aurora-datos
 count
-------
     9

Los nueve libros siguen ahí. Y si ahora ejecutas docker compose up -d una tercera vez, verás la reconciliación en acción:

 ✔ Container aurora-libros-aurora-db-1     Running
 ✔ Container aurora-libros-aurora-cache-1  Running

Running, no Recreated: nada ha cambiado en el fichero, así que Compose no toca nada. Eso es idempotencia.

Errores Comunes y Consejos

Usar tabuladores para sangrar. El error más frecuente y el más desconcertante, porque visualmente no se distingue. Configura tu editor para que inserte espacios en ficheros .yaml y activa la visualización de caracteres invisibles.

Escribir version: "3.8" en la primera línea. Está obsoleto desde Compose v2. Compose la ignora y emite un aviso. Bórrala.

Usar docker-compose con guion. Es la v1, sin mantenimiento desde 2023. Todo lo que aprendas aquí funciona con docker compose; a la inversa, no.

Creer que down borra los datos. No borra volúmenes con nombre... salvo que añadas -v. Ese -v es el comando más peligroso de Compose y aparece en detalle en la lección 04-03.

Ejecutar comandos desde otra carpeta. Compose busca el fichero desde el directorio actual hacia arriba. Si lanzas docker compose ps desde /tmp no encontrará nada, o peor, encontrará otro proyecto. Usa -f ~/aurora-libros/compose.yaml cuando no estés en el sitio.

Olvidar que el nombre del proyecto depende de la carpeta. Dos personas que clonan el repositorio en carpetas con nombres distintos obtienen proyectos distintos. Fija name: en el fichero.

No entrecomillar los puertos. - 8080:80 parece inofensivo, pero es la clase de fallo que te hace perder media hora. Comillas siempre.

Consejo: ejecuta docker compose config después de cada edición no trivial. Es gratis, instantáneo y te ahorra levantar una pila rota.

Ejercicios

Ejercicio 1. El siguiente fragmento tiene cinco errores (de sintaxis YAML o de uso de Compose). Encuéntralos, explica cada uno y escribe la versión corregida.

version: "3.8"
services:
  aurora-cache:
  image:redis:7-alpine
    ports:
      - 6379:6379
    restart: unless-stoped

Ejercicio 2. Sin borrar tu proyecto actual, levanta una segunda copia aislada de la misma pila con el nombre de proyecto aurora-pruebas, comprueba que sus contenedores, red y volumen son independientes, y explica por qué falla si no cambias nada más. Después, elimina la copia por completo, volúmenes incluidos.

Ejercicio 3. Demuestra con comandos que Compose no crea nada mágico: partiendo del proyecto levantado, obtén (a) las etiquetas que Compose ha puesto al contenedor de la caché, (b) el nombre real de la red y el rango de direcciones que asigna, y (c) la ruta del volumen en el host.

Soluciones

Solución 1.

# Error Explicación
1 version: "3.8" Clave obsoleta desde Compose v2; se ignora y genera un aviso
2 image:redis:7-alpine Falta el espacio tras los dos puntos: YAML no lo lee como mapa
3 image mal sangrado Está al mismo nivel que aurora-cache, no dentro del servicio
4 - 6379:6379 Puerto sin comillas: riesgo de interpretación numérica
5 unless-stoped Valor inválido, falta una p. Los cuatro válidos son no, always, on-failure, unless-stopped
name: aurora-libros
services:
  aurora-cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    restart: unless-stopped
docker compose config --quiet && echo OK

Solución 2.

cd ~/aurora-libros
docker compose -p aurora-pruebas up -d
Error response from daemon: driver failed programming external connectivity:
Bind for 127.0.0.1:5432 failed: port is already allocated

El aislamiento de Compose alcanza a contenedores, redes y volúmenes, pero no a los puertos del host: solo hay un 127.0.0.1:5432, y ya lo tiene el proyecto original. La solución más limpia es no publicar puerto en la copia; los servicios se hablan por la red interna igual.

# Copia del fichero sin la sección "ports"
sed '/127.0.0.1:5432/d; /ports:/d' compose.yaml > compose.pruebas.yaml
docker compose -p aurora-pruebas -f compose.pruebas.yaml up -d
docker ps --format "table {{.Names}}\t{{.Ports}}" | grep aurora
docker volume ls --format "{{.Name}}" | grep aurora
aurora-pruebas-aurora-db-1     5432/tcp
aurora-pruebas-aurora-cache-1
aurora-libros-aurora-db-1      127.0.0.1:5432->5432/tcp
aurora-libros-aurora-cache-1
aurora-libros_aurora-datos
aurora-pruebas_aurora-datos

Dos pilas completas en la misma máquina, cada una con su propio volumen prefijado, sin compartir un solo byte. Limpieza total de la copia:

docker compose -p aurora-pruebas -f compose.pruebas.yaml down -v
rm compose.pruebas.yaml

down -v sí borra los volúmenes con nombre del proyecto. Aquí es lo que quieres; en el proyecto real, casi nunca.

Solución 3.

# (a) etiquetas que Compose añade
docker inspect aurora-libros-aurora-cache-1 \
  --format '{{range $k, $v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}' \
  | grep com.docker.compose
com.docker.compose.project=aurora-libros
com.docker.compose.service=aurora-cache
com.docker.compose.container-number=1
com.docker.compose.oneoff=False
com.docker.compose.config-hash=7f3c1a...

Ese config-hash es la clave de la reconciliación: es el resumen de la configuración declarada. Cuando ejecutas up -d, Compose lo recalcula y solo recrea el contenedor si el hash ha cambiado.

# (b) red y subred
docker network inspect aurora-libros_default \
  --format '{{.Name}} | {{(index .IPAM.Config 0).Subnet}} | driver={{.Driver}}'
# (c) ruta del volumen en el host
docker volume inspect aurora-libros_aurora-datos --format '{{.Mountpoint}}'
aurora-libros_default | 172.20.0.0/16 | driver=bridge
/var/lib/docker/volumes/aurora-libros_aurora-datos/_data

Una red bridge de usuario y un volumen local en la ruta de siempre. Compose es una capa de conveniencia sobre la misma API que ya conocías: todo lo aprendido en el módulo 3 sigue siendo válido para inspeccionar y depurar.

Conclusión

Has cambiado de paradigma. Un guion de bash describe cómo llegar al estado deseado y falla si lo ejecutas dos veces; un compose.yaml describe cuál es ese estado, es idempotente y vive versionado en Git junto al código. Sabes que Compose v2 es un plugin de la CLI (docker compose, sin guion), que el fichero se llama compose.yaml y que version: está obsoleta. Dominas el YAML que necesitas —espacios y nunca tabuladores, mapas, listas, comillas en los puertos— y reconoces los seis errores de sintaxis que verás una y otra vez.

Entiendes que Compose organiza todo bajo un proyecto que actúa como espacio de nombres: prefija contenedores, redes y volúmenes, los etiqueta con com.docker.compose.* y permite que dos pilas idénticas convivan en la misma máquina. Y has comprobado con tus manos que la red y el volumen se crean y se destruyen solos, que down respeta los datos, y que up -d repetido no recrea nada porque compara el config-hash de lo declarado con lo que hay corriendo.

Tu compose.yaml de dos servicios funciona, pero solo usa un puñado de claves. En la lección siguiente, Definiendo Servicios en Docker Compose, verás la referencia práctica completa: cada bloque de un servicio —imagen y build, identidad, ejecución, red y puertos, datos, salud y dependencias, recursos y reinicio, etiquetas— con su equivalente exacto en las opciones de docker run que ya conoces, además de los bloques de nivel superior volumes: y networks: y las anclas de YAML para no repetir configuración. Al final de esa lección tendrás los cuatro servicios de Aurora Libros declarados en un solo fichero.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados