Docker Compose a fondo

Avatar de Apuntes Hacking

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"
Bash

Guárdalo como docker-compose.yml y lanza:

docker compose up -d
Bash

Tienes 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:
    # ...
Bash

Si 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:alpine
Bash

Define 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"
Bash

Sintaxis: «<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"
Bash

O usar un rango:

ports:
  - "8080-8082:80"
Bash

Esto 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@ssw0rd
Bash

Estas 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:
Bash

Esto 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/html
Bash

Eso 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:
      - db
Bash

Pero 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_healthy
Bash

Esto 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: 10
Bash
  • 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-stopped
Bash

Polí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: bridge
Bash

Esto 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.10
Bash

El 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"
Bash

Esto 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=milab
Bash

Y en tu docker-compose.yml:

environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  MYSQL_DATABASE: ${MYSQL_DATABASE}
Bash

Compose reemplazará ${VAR} con el valor del archivo .env. Añade .env a tu .gitignore y ya puedes compartir el laboratorio sin regalar credenciales.


Etiquetas:

Avatar de Apuntes Hacking

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *