Docker compose y el firewall
Módulo 9: Docker y docker compose · Tiempo de lectura: 12 minutos
Objetivo de aprendizaje: Después de esta lección podrá describir servicios en un archivo compose, iniciarlos, actualizarlos y detenerlos, y sabrá explicar por qué los puertos publicados por Docker esquivan ufw y cómo evitarlo.
Conceptos clave: compose.yaml, services, image, ports, volumes, environment, .env, restart, networks, depends_on, healthcheck, docker compose up -d, pull, logs, down, version: obsoleto, Docker y ufw, DOCKER-USER, publicar en 127.0.0.1, secretos, docker.sock
¿Por qué compose?
Con docker run (lección 9.2) inicia un único contenedor con una larga línea de opciones. Una aplicación real suele constar de varios contenedores: un programa, una base de datos, un proxy inverso. Recordar esas líneas y volver a teclearlas después de un reinicio o una actualización acaba saliendo mal algún día.
Docker Compose describe todos los contenedores de una aplicación en un solo archivo: compose.yaml. Usted coloca ese archivo en una carpeta propia, por ejemplo /srv/status/compose.yaml, y lo gestiona todo con docker compose. El archivo es a la vez su documentación: qué se ejecuta, qué imagen, qué puertos, dónde están los datos. Incluya esa carpeta en su copia de seguridad (lección 8.1) y podrá restaurar la aplicación en un servidor nuevo en pocos minutos.
El complemento se llama docker compose (con un espacio, versión 2). El antiguo programa independiente docker-compose (con guion) ha sido sustituido; todavía lo verá en guías antiguas. También el campo version: "3.8" al principio de los archivos antiguos está obsoleto: Compose utiliza siempre el esquema más reciente y con version: solo muestra una advertencia.
Leer un archivo compose
Un ejemplo con un programa y una base de datos PostgreSQL:
# /srv/notities/compose.yaml
services:
app:
image: ghcr.io/example/notities:1.4
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
environment:
DB_HOST: db
DB_USER: notities
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: notities
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notities"]
interval: 10s
retries: 5
volumes:
db-data:Paso a paso:
- services: cada servicio se convierte en un contenedor. El nombre (
app,db) es también el nombre de host con el que los contenedores se encuentran entre sí. Compose crea una red propia para la aplicación, de modo queappse conecta a la base de datos endb:5432. - image: siempre con una etiqueta de versión (lección 9.1).
- restart: lo que hace Docker cuando el contenedor se detiene o el servidor se reinicia.
unless-stoppedlo vuelve a iniciar, salvo que usted lo haya detenido a propósito. Sin política de reinicio, su aplicación no vuelve tras un reinicio (lección 4.2). - ports: solo la aplicación publica un puerto, y únicamente en
127.0.0.1. La base de datos no publica nada: la aplicación llega a ella a través de la red interna. Una base de datos conports: - "5432:5432"es uno de los errores más frecuentes (véase más abajo). - environment: ajustes para el programa. La contraseña no aparece literalmente, sino como
${DB_PASSWORD}. - volumes: el volumen con nombre
db-dataconserva la base de datos cuando se sustituye el contenedor. - depends_on con healthcheck: la aplicación solo arranca cuando la base de datos está realmente lista, no simplemente iniciada.
Secretos en un archivo .env
Compose lee variables como ${DB_PASSWORD} de un archivo .env en la misma carpeta:
# /srv/notities/.env
DB_PASSWORD=Kies-hier-een-lang-willekeurig-wachtwoordanna@web1:/srv/notities$ sudo chmod 600 .envAsí la contraseña no figura en el archivo compose que quizá guarde en Git o pegue en un foro para pedir ayuda. Añada .env a .gitignore. Genere la contraseña con un gestor de contraseñas o con openssl rand -base64 24.
Uso diario
Todos los comandos se ejecutan en la carpeta que contiene el archivo compose:
anna@web1:/srv/notities$ sudo docker compose config # comprobar el archivo, con las variables sustituidas
anna@web1:/srv/notities$ sudo docker compose up -d # iniciar todo (o actualizar según el archivo)
anna@web1:/srv/notities$ sudo docker compose ps # estado por servicio
anna@web1:/srv/notities$ sudo docker compose logs -f app
anna@web1:/srv/notities$ sudo docker compose pull # descargar las imágenes nuevas
anna@web1:/srv/notities$ sudo docker compose up -d # sustituir los contenedores por las imágenes nuevas
anna@web1:/srv/notities$ sudo docker compose down # detener y eliminar los contenedoresdown deja sus volúmenes intactos. down -v elimina también los volúmenes de la aplicación, y con ellos su base de datos. Escríbalo solo si de verdad quiere empezar de cero.
docker compose config es el equivalente de nginx -t y sshd -t: muestra cómo entiende Compose su archivo, con todas las variables sustituidas, antes de iniciar nada. Tenga cuidado de no pegar esa salida en ningún sitio, porque contiene las contraseñas.
Docker esquiva ufw
Llegamos al punto de seguridad más importante de este módulo. En la lección 5.2 configuró ufw como "denegar todo, excepto SSH, 80 y 443". Cabría esperar que un contenedor con ports: - "5432:5432" no fuera accesible. No es así.
La documentación de Docker lo dice literalmente: Docker y ufw usan las reglas del firewall de una manera incompatible entre sí. Cuando usted publica un puerto, Docker añade sus propias reglas al firewall del kernel. Esas reglas redirigen el tráfico al contenedor (traducción de direcciones, DNAT) antes de que se apliquen las reglas de ufw. El paquete, por tanto, rodea ufw. ufw status no muestra nada especial, mientras el puerto es accesible desde todo internet. Lo mismo ocurre con firewalld.
Además, sin dirección Docker publica en 0.0.0.0 y [::], es decir, en IPv4 e IPv6 (lección 9.1).
Tres formas de hacerlo bien, de la más sencilla a la más compleja:
- No publique nada que no deba ser público. Las bases de datos, las cachés y los servicios internos no llevan
ports:. Los contenedores de la misma aplicación compose se comunican a través de la red interna. - Publique en loopback.
"127.0.0.1:8081:8080". Así solo el propio servidor puede acceder, y usted coloca delante un proxy inverso (nginx o Caddy, módulo 7) que escucha en el 443. Docker también puede tener esto como valor predeterminado: un ajuste del daemon hace que todos los puertos publicados sin dirección queden en127.0.0.1. - Reglas en la cadena DOCKER-USER. Docker crea una cadena vacía
DOCKER-USERque se recorre antes de sus propias reglas. Ahí puede añadir sus propias reglas, por ejemplo "solo esta dirección IP puede acceder a los puertos publicados". Es un trabajo avanzado con iptables o nftables, y es fácil equivocarse. Para la mayoría de los servidores pequeños bastan las opciones 1 y 2.
Compruébelo siempre desde fuera, como en la lección 5.1: nc -vz -w 3 <ip> 5432 desde su portátil, o la comprobación del curso del ejercicio Z5.1 y Z10.4.
Otras dos cosas que no debe hacer
- No monte
/var/run/docker.socken un contenedor, a menos que sepa exactamente por qué. Un contenedor con acceso al socket de Docker puede iniciar contenedores por su cuenta, incluso uno con todo el disco del host (lección 9.2). Algunas herramientas (paneles, actualizadores automáticos) lo piden; en la práctica, eso equivale a dar root a ese programa. - No use
privileged: trueninetwork_mode: hostsolo porque lo diga una guía. Ambos eliminan en gran parte el aislamiento del contenedor. Utilícelos únicamente si el autor explica por qué son necesarios.
Resumen
- Un archivo compose (
compose.yaml) describe todos los contenedores de una aplicación; es a la vez documentación y debe formar parte de su copia de seguridad. - Use etiquetas de versión, una política de reinicio, volúmenes con nombre, un healthcheck cuando sea posible, y secretos en un
.envcon permisos 600. docker compose config,up -d,ps,logs,pull,down;down -velimina también los volúmenes.- Los puertos publicados esquivan ufw y firewalld. Publique solo lo que deba ser público y, en otro caso, en
127.0.0.1con un proxy inverso delante. - Nada de docker.sock,
privilegedninetwork_mode: hostsin una buena razón.
Cuestionario
Ejercicio
Ejercicio Z9.3: Revisar un archivo compose. El archivo de ejemplo compose-nakijken.txt contiene (como archivo de texto) el archivo compose de una pequeña tienda online con una aplicación, una base de datos, una caché y una herramienta de administración. Contiene seis errores tratados en esta lección o en las lecciones 9.1 y 9.2. Indique para cada bloque de líneas numerado si es correcto o incorrecto, y elija para cada error la corrección adecuada. El programa compara sus respuestas con la clave fija. Si tiene su propio archivo compose, revíselo después siguiendo los mismos puntos.
Para saber más
- Documentación de Docker, How Compose works (compose.yaml): https://docs.docker.com/compose/intro/compose-application-model/
- Documentación de Docker, Version and name top-level elements: https://docs.docker.com/reference/compose-file/version-and-name/
- Documentación de Docker, Packet filtering and firewalls (Docker and ufw): https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Documentación de Docker, Port publishing and mapping: https://docs.docker.com/engine/network/port-publishing/
- Documentación de Docker, Interpolation (variables y .env): https://docs.docker.com/reference/compose-file/interpolation/
