Introducción
Ya tienes Docker instalado y sabes lanzar contenedores individuales con docker run. Ahora imagina que tu laboratorio necesita dos servicios: una aplicación web vulnerable y una base de datos donde guarda sus credenciales. Con docker run tendrías que crear una red manualmente, exponer puertos, pasar variables de entorno y asegurarte de que la base de datos esté lista antes que la aplicación. Un coñazo.
Docker Compose convierte ese caos en un archivo de texto. Le dices qué servicios quieres, cómo se comunican y dónde guardan sus datos, y con un solo comando levantas todo el chiringuito.
En los próximos posts desplegarás DVWA y Juice Shop con esta herramienta. Pero antes de copiar y pegar archivos a ciegas, vamos a entender cada directiva, cada opción y cada truco de un docker-compose.yml para que puedas escribir el tuyo propio cuando enfrentes retos que no tengan imagen oficial.
¿Qué es Docker Compose y por qué te importa?
Docker Compose es el director de orquesta de tus contenedores. Lees un archivo YAML donde defines servicios, redes y volúmenes, y él se encarga de crearlos, conectarlos y mantenerlos vivos.
Para un laboratorio de pentesting esto significa:
- Un solo comando para empezar a hackear: docker compose up -d.
- Entornos complejos en un archivo: una app web + base de datos + un servidor de correo falso para probar inyecciones.
- Redes internas a medida: simula segmentos de cliente como hacías con las VLANs en VirtualBox.
- Reset instantáneo: rompiste la base de datos con un SQLi, docker compose down -v && docker compose up -d y vuelta a empezar.
Anatomía de un docker-compose.yml mínimo
Creamos un archivo de ejemplo para empezar a despiezarlo:
services:
web:
image: nginx:alpine
ports:
- "8080:80"BashGuárdalo como docker-compose.yml y lanza:
docker compose up -dBashTienes un Nginx funcionando en http://localhost:8080. Con tres líneas. Ahora vamos a expandirlo hasta tener una plantilla completa para cualquier laboratorio.
Las directivas una a una
services — el corazón
Todo servicio que quieras levantar va bajo esta clave. El nombre que le des se convierte automáticamente en su hostname dentro de las redes internas de Docker.
services:
web:
# ...
db:
# ...BashSi la aplicación necesita conectarse a la base de datos, usará db como dirección del servidor, sin IPs mágicas. Docker se encarga de resolver ese nombre.
image — la plantilla base
image: nginx:alpineBashDefine qué imagen usar. El formato es nombre:tag. Algunos ejemplos relevantes para tu lab:
- mysql:8.0 → base de datos MySQL.
- php:apache → Apache con PHP listo para servir apps vulnerables.
- vulnerables/web-dvwa → DVWA empaquetada.
- bkimminich/juice-shop → Juice Shop.
- alpine:latest → distribución Linux de 5 MB para montar cualquier cosa.
Si la imagen no existe localmente, Compose la descarga automáticamente al hacer up.
ports — abriendo ventanas al contenedor
ports:
- "8080:80"BashSintaxis: «<puerto_host>:<puerto_contenedor>«. El puerto de la izquierda es el de tu máquina anfitriona, el de la derecha el del servicio dentro del contenedor.
Puedes mapear varios:
ports:
- "8080:80"
- "443:443"BashO usar un rango:
ports:
- "8080-8082:80"BashEsto mapea los puertos 8080, 8081 y 8082 de tu host al 80 de tres instancias del servicio (si escalas). Para pentesting, con un par de puertos vas sobrado.
environment — variables de entorno
Muchas imágenes usan variables para configurarse. MySQL, por ejemplo, pide contraseñas:
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: dvwa
MYSQL_USER: dvwa
MYSQL_PASSWORD: p@ssw0rdBashEstas variables se pasan al contenedor como si las hubieras exportado en su shell. La propia imagen las lee en su script de entrada para crear bases de datos, usuarios, etc.
Puedes también cargarlas desde un archivo .env externo (luego lo vemos) para no tener contraseñas hardcodeadas en el YAML.
volumes — persistencia (o no)
Sin volúmenes, los datos del contenedor mueren con él. Para que la base de datos sobreviva a un reinicio:
services:
db:
image: mysql:8.0
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:BashEsto crea un volumen con nombre db_data gestionado por Docker. Los datos de MySQL se escriben ahí. Si haces docker compose down, el volumen se conserva. Si haces docker compose down -v, se destruye (vuelta a cero total).
También puedes montar directorios del host, ideal para meter tu propio código:
volumes:
- ./mi_app_vulnerable:/var/www/htmlBashEso monta la carpeta mi_app_vulnerable de tu sistema dentro del contenedor en /var/www/html. Así editas el código con tu IDE favorito y los cambios se reflejan en vivo.
depends_on — orden de arranque
No quieres que la web arranque antes que la base de datos. depends_on establece dependencias:
services:
web:
depends_on:
- dbBashPero depends_on solo espera a que el contenedor se haya iniciado, no a que la aplicación dentro esté lista. Para MySQL necesitas un healthcheck.
Versión robusta con condición:
depends_on:
db:
condition: service_healthyBashEsto obliga a que el servicio db pase su healthcheck antes de arrancar web. Evita errores de conexión al iniciar el laboratorio.
healthcheck — ¿está realmente vivo?
Un contenedor puede estar «Up» pero la aplicación interna aún no acepta conexiones. El healthcheck verifica el estado real:
services:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
interval: 5s
timeout: 5s
retries: 10Bash- test: comando que se ejecuta para comprobar la salud. Debe devolver 0 si está bien. Aquí usamos mysqladmin ping.
- interval: cada cuánto se repite la comprobación.
- timeout: tiempo máximo de espera del comando.
- retries: intentos fallidos consecutivos antes de declarar el servicio como unhealthy.
Combinado con depends_on: condition: service_healthy, tienes un laboratorio que se levanta sin errores de timing.
restart — qué hacer si se muere
restart: unless-stoppedBashPolíticas disponibles:
- no: no reiniciar (por defecto).
- always: reiniciar siempre que se pare, incluso si lo paras tú manualmente (se reinicia solo al arrancar Docker).
- unless-stopped: reiniciar siempre excepto si tú lo paraste explícitamente con docker compose stop.
- on-failure: solo reiniciar si el contenedor falla (código de salida distinto de 0).
Para laboratorio, unless-stopped es lo más cómodo
networks — creando segmentos a medida
Docker crea una red por defecto, pero puedes definir las tuyas propias para aislar servicios como hacías con las VLANs de VirtualBox:
services:
web:
networks:
- red_interna
db:
networks:
- red_interna
networks:
red_interna:
driver: bridgeBashEsto mete web y db en una red aislada. Si añades otro servicio en otra red, no podrá hablar con ellos.
Puedes incluso asignar IPs fijas (útil si necesitas emular una topología concreta):
networks:
red_interna:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
gateway: 172.20.0.1
services:
web:
networks:
red_interna:
ipv4_address: 172.20.0.10BashEl post 1.3.6 se dedicará exclusivamente a redes con Docker Compose, así que no profundizo más aquí.
extra_hosts — modificando el /etc/hosts del contenedor
A veces necesitas que el contenedor resuelva ciertos nombres a IPs externas (como si tuvieras un AD en otra máquina virtual):
extra_hosts:
- "dc01.milab.local:192.168.56.20"BashEsto añade una entrada en /etc/hosts dentro del contenedor. Muy práctico para integrar contenedores con VMs.
Variables con archivo .env (buenas prácticas)
No quieres contraseñas en claro en tu YAML si vas a compartirlo. Crea un archivo .env en el mismo directorio:
# .env
MYSQL_ROOT_PASSWORD=SuperSecreto123
MYSQL_DATABASE=milabBashY en tu docker-compose.yml:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}BashCompose reemplazará ${VAR} con el valor del archivo .env. Añade .env a tu .gitignore y ya puedes compartir el laboratorio sin regalar credenciales.


Deja una respuesta