En la lección 03-06 aprendiste a usar volúmenes, bind mounts y tmpfs. Aquí verás qué hay debajo: cómo el daemon apila las capas de una imagen con overlay2, cuánto cuesta de verdad escribir en la capa de un contenedor, qué son los volume drivers y cómo montar una estrategia de copias de seguridad de Aurora Libros con restauración probada.

Contenido

  1. Qué es un storage driver y qué resuelve
  2. overlay2 por dentro: los cuatro directorios
  3. Inspección real de una imagen y de un contenedor
  4. Tabla comparativa de los storage drivers
  5. Consultar el driver activo y cambiarlo
  6. El coste del copy-on-write, medido
  7. Por qué las bases de datos exigen volúmenes
  8. El driver local y sus driver_opts
  9. Montar NFS y tmpfs como volumen
  10. Plugins de terceros y declaración en Compose
  11. Volúmenes external compartidos entre pilas
  12. Estrategia de copias de seguridad de Aurora Libros
  13. Script de copia con rotación y restauración verificada
  14. Espacio en disco y data-root
  15. Rendimiento y permisos (UID/GID)

  1. Qué es un storage driver y qué resuelve

Una imagen es una pila de capas de solo lectura (lección 01-05). Un contenedor añade encima una capa de escritura. El problema técnico es este: presentar todas esas capas como un único sistema de ficheros coherente, y hacerlo sin copiar gigabytes cada vez que arranca un contenedor.

De eso se encarga el storage driver del daemon. Sus responsabilidades son tres:

  • Apilar las capas de la imagen en un solo árbol de directorios visible desde dentro.
  • Aplicar copy-on-write: al modificar un fichero de una capa inferior, copiarlo a la capa superior y trabajar sobre la copia.
  • Compartir las capas comunes entre contenedores, para que diez instancias de node:22-alpine ocupen el espacio de una.

Es una pieza del daemon, no del contenedor: dentro solo se ve un sistema de ficheros normal.

  1. overlay2 por dentro: los cuatro directorios

overlay2 usa OverlayFS, un sistema de ficheros de unión incluido en el kernel de Linux. Trabaja con cuatro conceptos:

Directorio Qué contiene Modo
lowerdir Las capas de la imagen, apiladas y separadas por : Solo lectura
upperdir La capa de escritura del contenedor Lectura y escritura
merged La vista unificada: lo que el contenedor ve como / Lectura y escritura
workdir Espacio interno de trabajo del kernel para operaciones atómicas Uso interno

Las reglas de resolución son sencillas y explican todo el comportamiento que ya has observado:

  1. Leer un fichero: se busca en upperdir; si no está, se recorre lowerdir de arriba abajo. Gana la primera coincidencia.
  2. Escribir en un fichero que solo existe en lowerdir: se copia entero a upperdir (copy-up) y se escribe allí. El original no se toca.
  3. Borrar un fichero de lowerdir: no se puede borrar lo que es de solo lectura, así que se crea en upperdir un fichero especial de tipo whiteout que lo oculta. El original sigue ocupando espacio.

Esa tercera regla es la razón, por fin explicada, de que borrar ficheros en una capa posterior de un Dockerfile no reduzca el tamaño de la imagen. Lo veremos medido en la lección 05-04.

flowchart TB
  M["merged/  ← lo que ve el contenedor"]
  U["upperdir/  capa de escritura<br/>ficheros nuevos, copy-up y whiteouts"]
  L3["lowerdir 3 · COPY src/ (aurora-api)"]
  L2["lowerdir 2 · npm ci --omit=dev"]
  L1["lowerdir 1 · node:22-alpine"]
  U --> M
  L3 --> M
  L2 --> M
  L1 --> M

  1. Inspección real de una imagen y de un contenedor

Todo esto es visible en el disco del host. Cada capa vive en /var/lib/docker/overlay2/<id>/:

docker image inspect auroralibros/aurora-api:1.2.0 --format '{{json .GraphDriver.Data}}' | tr ',' '\n'
mount | grep overlay | head -1 | tr ',' '\n' | grep -E 'lowerdir|upperdir|workdir' | cut -c1-70
{"LowerDir":"/var/lib/docker/overlay2/4a8e77/diff:/var/lib/docker/overlay2/c93b2e/diff"
"MergedDir":"/var/lib/docker/overlay2/7d1f0a/merged"
"UpperDir":"/var/lib/docker/overlay2/7d1f0a/diff"
"WorkDir":"/var/lib/docker/overlay2/7d1f0a/work"}
lowerdir=/var/lib/docker/overlay2/l/QW3F:/var/lib/docker/overlay2/l/K7RT
upperdir=/var/lib/docker/overlay2/9be21c/diff
workdir=/var/lib/docker/overlay2/9be21c/work

Fíjate en los enlaces cortos de overlay2/l/: existen porque la longitud de los argumentos de montaje del kernel está limitada, y una imagen con muchas capas superaría el límite con rutas completas.

Ahora la demostración que lo cierra todo. Escribe un fichero dentro del contenedor y míralo aparecer en el upperdir del host:

up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "prueba de capas" > /tmp/aurora.txt'
sudo cat "$up/tmp/aurora.txt" && sudo find "$up" -type f | head -2
prueba de capas
/var/lib/docker/overlay2/9be21c/diff/tmp/aurora.txt

El fichero está físicamente en el host, dentro del diff de ese contenedor y solo de ese contenedor. Es lo mismo que docker diff mostraba en la lección 03-03, pero ahora sabes dónde vive.

  1. Tabla comparativa de los storage drivers

Driver Estado en 2026 Requisitos Rendimiento Notas
overlay2 Estándar por defecto Kernel ≥ 4.0, ext4 o xfs con ftype=1 Muy bueno en lectura; copy-up costoso en ficheros grandes La opción correcta salvo motivo de peso
btrfs Soportado Sistema de ficheros Btrfs en /var/lib/docker Bueno; instantáneas nativas y muy baratas Útil si ya usas Btrfs; requiere mantenimiento
zfs Soportado ZFS instalado Bueno; compresión y snapshots Consumo de RAM alto (ARC); licencia fuera del kernel
devicemapper Eliminado Malo en modo loop-lvm Histórico; retirado en Docker 25. Si lo ves, migra
fuse-overlayfs Soportado Modo rootless en kernels antiguos Peor que overlay2: pasa por espacio de usuario Kernels ≥ 5.13 ya no lo necesitan
vfs Último recurso Ninguno Muy malo: copia entera cada capa Sin CoW; solo para pruebas o entornos anidados

Regla práctica: si docker info dice overlay2, no toques nada; si dice vfs en un servidor, tienes un problema de configuración que multiplicará por diez el espacio en disco.

  1. Consultar el driver activo y cambiarlo

docker info --format 'Driver: {{.Driver}} | Backing FS: {{index .DriverStatus 0 1}}'
Driver: overlay2 | Backing FS: extfs

Cambiar el driver se hace en /etc/docker/daemon.json con {"storage-driver": "overlay2"} y reiniciando el daemon.

Advertencia. Cambiar el storage driver hace que el daemon deje de ver todas las imágenes y contenedores existentes: siguen en el disco, bajo el directorio del driver antiguo, pero son inaccesibles hasta que vuelvas atrás. Antes de tocarlo, publica tus imágenes en un registro, guarda las que no estén publicadas con docker save y haz copia de seguridad de los volúmenes. Los volúmenes, eso sí, no dependen del driver: viven en /var/lib/docker/volumes/ y sobreviven al cambio.

  1. El coste del copy-on-write, medido

La teoría dice que el copy-up copia el fichero entero la primera vez que lo tocas. Vamos a medirlo con un fichero de 512 MB metido en una imagen.

mkdir -p /tmp/cow && cd /tmp/cow
printf 'FROM alpine:3\nRUN dd if=/dev/zero of=/grande.bin bs=1M count=512\n' > Dockerfile
docker build -q -t prueba-cow .

# Un byte sobre un fichero que vive en una capa inferior
docker run --rm prueba-cow sh -c 'time dd if=/dev/zero of=/grande.bin bs=1 count=1 conv=notrunc 2>/dev/null'
# El mismo byte, sobre un fichero que ya está en la capa de escritura
docker run --rm prueba-cow sh -c 'cp /grande.bin /c.bin; time dd if=/dev/zero of=/c.bin bs=1 count=1 conv=notrunc 2>/dev/null'

# Y el espacio que ha costado ese byte
docker run -d --name cow prueba-cow sh -c 'dd if=/dev/zero of=/grande.bin bs=1 count=1 conv=notrunc; sleep 30'
sleep 3 && docker ps -s --filter name=cow --format '{{.Names}}\t{{.Size}}' && docker rm -f cow
real    0m 1.94s        <- primera escritura: copy-up de 512 MB
real    0m 0.00s        <- el fichero ya estaba en upperdir
cow    512MB (virtual 528MB)

Casi dos segundos para escribir un byte, y 512 MB de capa de escritura consumidos.

Ese es el coste real del copy-on-write, y explica tres reglas que ya habías aplicado sin conocer el porqué:

Regla Motivo real
No modificar ficheros grandes de la imagen Cada primera escritura copia el fichero completo
Usar COPY --chown en vez de un RUN chown -R posterior chown toca los metadatos de cada fichero y dispara un copy-up masivo
Las bases de datos van en volúmenes Escriben constantemente en ficheros grandes

  1. Por qué las bases de datos exigen volúmenes

PostgreSQL escribe sin parar en ficheros de datos de decenas o cientos de megabytes. Si /var/lib/postgresql/data estuviera en la capa de escritura, cada modificación de una página dispararía un copy-up del fichero entero la primera vez, y el rendimiento sería catastrófico.

Un volumen evita OverlayFS por completo: es un bind mount a un directorio del host (/var/lib/docker/volumes/<nombre>/_data), montado sobre el punto correspondiente. Las escrituras van directas al sistema de ficheros del host, sin capas ni copy-up.

docker compose exec aurora-db sh -c 'mount | grep -E "postgresql| / "'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...)
/dev/sda2 on /var/lib/postgresql/data type ext4 (rw,relatime)

Ahí está la prueba: mientras / es overlay, /var/lib/postgresql/data es directamente el sistema de ficheros del host. Por eso las imágenes oficiales de bases de datos declaran ese punto como VOLUME: si te olvidas de montarlo, al menos evitan la penalización creando un volumen anónimo.

  1. El driver local y sus driver_opts

El driver de volúmenes por defecto se llama local y admite opciones que casi nadie usa, aunque son sorprendentemente potentes: son las mismas del comando mount de Linux.

# Volumen sobre un directorio concreto del host (bind mount con nombre)
docker volume create --driver local \
  --opt type=none --opt device=/srv/aurora/datos --opt o=bind aurora-en-srv

# Volumen en memoria, limitado a 64 MB
docker volume create --driver local \
  --opt type=tmpfs --opt device=tmpfs --opt o=size=64m,uid=1000 aurora-temporal
Opción Significado
type Tipo de sistema de ficheros: none (bind), tmpfs, nfs, cifs
device Origen: ruta del host, tmpfs, o :/ruta/exportada en NFS
o Opciones de montaje separadas por comas: bind, ro, size, uid, addr

La diferencia con un bind mount corriente es de gestión: un volumen con nombre aparece en docker volume ls, admite etiquetas, se puede declarar external y no depende de la ruta relativa del proyecto.

  1. Montar NFS y tmpfs como volumen

Un volumen NFS permite que varios hosts compartan los mismos datos, algo imposible con un volumen local:

# compose.yaml (fragmento)
volumes:
  aurora-portadas:
    driver: local
    driver_opts:
      type: nfs
      o: "addr=10.0.0.20,rw,nfsvers=4,soft,timeo=30"
      device: ":/exportado/aurora/portadas"
docker compose exec aurora-api sh -c 'mount | grep portadas'
10.0.0.20:/exportado/aurora/portadas on /app/portadas type nfs4 (rw,relatime,vers=4.2)

Tres avisos de campo. El montaje ocurre al arrancar el contenedor: si el servidor NFS no responde, el contenedor no arranca. Usa soft y un timeo razonable para que un fallo de red produzca un error y no un proceso colgado para siempre en estado D. Y jamás pongas los datos de PostgreSQL en NFS: el bloqueo de ficheros por red no ofrece las garantías que una base de datos necesita, y la corrupción es cuestión de tiempo. Para ficheros estáticos —las portadas del catálogo— es perfecto.

  1. Plugins de terceros y declaración en Compose

Un volume driver es un plugin del daemon que atiende las peticiones de montaje. Docker trae local; el resto se instala:

docker plugin install --grant-all-permissions rclone/docker-volume-rclone:amd64
docker plugin ls          # ID  NAME  ENABLED -> 9f2c1a  rclone/docker-volume-rclone  true
Familia de plugin Ejemplos Para qué
Almacenamiento en red local con NFS/CIFS, netshare Compartir entre hosts de la misma red
Objetos en la nube rclone, s3fs S3, GCS o Azure Blob como si fueran directorios
Bloques en la nube Plugins de EBS, Cinder Discos que siguen al contenedor entre nodos
Instantáneas Plugins sobre ZFS o Btrfs Copias instantáneas y clonado

En Compose se declaran igual que cualquier otro volumen, con driver: rclone y sus driver_opts (remote: "s3aurora:copias"). El aviso importante: un plugin de volumen es software privilegiado dentro de tu daemon. Instala solo plugins de origen verificado y trátalos con el mismo criterio con el que aprobarías una dependencia de producción.

  1. Volúmenes external compartidos entre pilas

Compose antepone el nombre del proyecto a los volúmenes que crea (aurora-libros_aurora-datos). Cuando un volumen debe existir fuera del ciclo de vida del proyecto —o compartirse con otra pila—, se declara external:

docker volume create --label proyecto=aurora --label criticidad=alta aurora-datos
volumes:
  aurora-datos:
    external: true        # ya existe; Compose no lo crea ni lo borra

Dos consecuencias muy prácticas. La primera: docker compose down -v no borra un volumen external, lo que lo convierte en una red de seguridad barata para los datos que no puedes permitirte perder. La segunda: otra pila —por ejemplo, un proyecto separado de informes— puede montar el mismo volumen en modo :ro y leer los datos sin tocar la plataforma principal.

  1. Estrategia de copias de seguridad de Aurora Libros

Hay dos formas de copiar los datos de aurora-db, y no son intercambiables:

Aspecto Copia lógica (pg_dump) Copia de ficheros (tar del volumen)
Servicio parado No: en caliente , para que sea consistente
Qué produce SQL o formato propio de PostgreSQL Archivo tar con el directorio de datos
Tamaño Pequeño (comprime muy bien) Grande: incluye índices y espacio libre
Restauración Requiere un servidor arrancado Se vuelca y se arranca
Cambio de versión mayor : de PostgreSQL 16 a 17 sin problema No: formato binario atado a la versión
Restaura una sola tabla No
Velocidad en bases grandes Lenta Rápida

Copia lógica, en caliente:

docker compose exec -T aurora-db \
  pg_dump -U aurora -d aurora_libros --format=custom --compress=9 \
  > ~/copias/aurora-$(date +%Y%m%d-%H%M).dump      # -> aurora-20260805-2310.dump, 18K

El -T es obligatorio: sin él, exec asigna un pseudoterminal y corrompe el flujo binario con traducciones de fin de línea. Es el error clásico de esta operación y no da ningún aviso: el fichero parece correcto y falla al restaurar.

Copia del volumen a nivel de fichero:

docker compose stop aurora-db                   # imprescindible para la consistencia
docker run --rm \
  -v aurora-libros_aurora-datos:/datos:ro \
  -v "$HOME/copias":/salida \
  alpine:3 tar czf /salida/volumen-$(date +%Y%m%d).tar.gz -C /datos .
docker compose start aurora-db

Por qué hay que parar el servicio: PostgreSQL mantiene páginas modificadas en memoria y escribe de forma asíncrona. Un tar sobre un directorio de datos vivo captura unos ficheros en un instante y otros en otro, produciendo un conjunto incoherente que puede restaurar sin errores y corromperse días después. Con el servicio parado, el directorio está quieto y la copia es válida.

  1. Script de copia con rotación y restauración verificada

#!/usr/bin/env bash
# copia-aurora.sh — copia lógica con rotación y verificación de restauración
set -euo pipefail

DESTINO="${DESTINO:-/srv/copias/aurora}"
RETENCION_DIAS="${RETENCION_DIAS:-14}"
FICHERO="$DESTINO/aurora-$(date +%Y%m%d-%H%M).dump"
mkdir -p "$DESTINO"

echo "==> Copia lógica de aurora_libros"
docker compose exec -T aurora-db \
  pg_dump -U aurora -d aurora_libros --format=custom --compress=9 > "$FICHERO"

# Una copia de 0 bytes es un fallo silencioso: compruébalo siempre
[ -s "$FICHERO" ] || { echo "ERROR: copia vacía"; exit 1; }

echo "==> Verificando la restauración en una base de datos desechable"
docker run -d --rm --name verifica-copia \
  -e POSTGRES_USER=aurora -e POSTGRES_PASSWORD=verifica -e POSTGRES_DB=verifica \
  --tmpfs /var/lib/postgresql/data postgres:16-alpine >/dev/null

for i in $(seq 1 30); do
  docker exec verifica-copia pg_isready -U aurora -d verifica -q && break || sleep 1
done

docker exec -i verifica-copia pg_restore -U aurora -d verifica --no-owner < "$FICHERO"
TOTAL=$(docker exec verifica-copia psql -U aurora -d verifica -tAc 'SELECT count(*) FROM libros')
docker rm -f verifica-copia >/dev/null

[ "$TOTAL" -ge 9 ] || { echo "ERROR: la copia solo tiene $TOTAL libros"; exit 1; }
echo "==> Restauración verificada: $TOTAL libros"

echo "==> Rotación: eliminando copias de más de $RETENCION_DIAS días"
find "$DESTINO" -name 'aurora-*.dump' -mtime "+$RETENCION_DIAS" -print -delete
==> Copia lógica de aurora_libros
==> Verificando la restauración en una base de datos desechable
==> Restauración verificada: 9 libros
==> Rotación: eliminando copias de más de 14 días

Lo que convierte este script en algo serio no es la copia, es la verificación: cada ejecución restaura el fichero recién creado en un PostgreSQL efímero en tmpfs y cuenta las filas. Si la copia estaba vacía, truncada o corrupta, el script falla hoy, no el día del desastre.

Grábate la regla: una copia que nunca se ha restaurado no es una copia, es una carpeta con esperanza dentro.

  1. Espacio en disco y data-root

Todo lo que Docker guarda vive bajo /var/lib/docker:

Subdirectorio Contenido Cómo se limpia
overlay2/ Capas de imágenes y de contenedores docker image prune -a, container prune
volumes/ Volúmenes con nombre y anónimos docker volume prune (con cuidado)
containers/ Metadatos y logs en JSON Rotación de logs (lección 05-06)
buildkit/ Caché de construcción docker builder prune
image/, network/ Índices y metadatos Se gestionan solos
sudo du -sh /var/lib/docker/* 2>/dev/null | sort -rh | head -4
4.1G    /var/lib/docker/overlay2
1.3G    /var/lib/docker/volumes
612M    /var/lib/docker/buildkit
88M     /var/lib/docker/containers

Si containers/ es enorme, el culpable son los logs sin rotar (lo resolverás en 05-06). Si lo es overlay2/, son imágenes antiguas. Para mover el almacén a un disco mayor:

sudo systemctl stop docker
sudo rsync -aHAX /var/lib/docker/ /datos/docker/
printf '{\n  "data-root": "/datos/docker"\n}\n' | sudo tee /etc/docker/daemon.json
sudo systemctl start docker
docker info --format 'Docker Root Dir: {{.DockerRootDir}}'   # -> /datos/docker

Usa rsync -aHAX y no cp: hay que preservar enlaces duros, atributos extendidos y permisos, o las capas quedarán inservibles. Comprueba que todo funciona antes de borrar el directorio antiguo.

  1. Rendimiento y permisos (UID/GID)

Elección de montaje por rendimiento, de mejor a peor para escritura intensiva:

Montaje Rendimiento de escritura Uso recomendado
tmpfs Máximo (RAM) Datos efímeros, pruebas, ficheros temporales
Volumen con nombre Nativo del sistema de ficheros Bases de datos y todo dato persistente
Bind mount (Linux) Nativo Código en desarrollo, configuración
Bind mount (macOS/WSL 2 cruzando la frontera) Malo Evitar para muchos ficheros pequeños
Capa de escritura del contenedor El peor: copy-up Nada que se escriba de forma repetida

Los permisos son la otra mitad del problema. Un volumen vacío hereda el propietario del punto de montaje de la imagen; un volumen con datos conserva los suyos, y aurora-api corre como el usuario node (UID 1000), así que un volumen creado por root le resulta ilegible.

docker volume create aurora-adjuntos
docker run --rm -v aurora-adjuntos:/datos alpine:3 stat -c '%u:%g' /datos      # -> 0:0
docker run --rm -v aurora-adjuntos:/datos alpine:3 chown -R 1000:1000 /datos   # una sola vez
docker run --rm -u 1000 -v aurora-adjuntos:/datos alpine:3 sh -c 'touch /datos/ok && echo OK'

Con bind mounts la solución es distinta, porque el propietario lo fija el host: o alineas el UID del contenedor con el de tu usuario (user: "${UID}:${GID}" en Compose), o ajustas los permisos en el host. Y recuerda de la lección 03-06 que el kernel solo entiende números: el nombre node no significa nada fuera del contenedor.

Errores Comunes y Consejos

Escribir datos persistentes en la capa del contenedor. Rendimiento pésimo por copy-up y pérdida total al recrear.

Hacer tar de un volumen con la base de datos en marcha. La copia parece correcta y está incoherente. Para el servicio o usa pg_dump.

Olvidar -T en docker compose exec pg_dump. El pseudoterminal corrompe el volcado binario sin avisar.

No probar nunca una restauración. El día del incidente descubres que llevas meses guardando ficheros vacíos.

Ejecutar docker volume prune sin mirar. Borra todo volumen no usado por ningún contenedor, y un volumen de una pila parada entra en esa definición. Declara external lo crítico.

Cambiar el storage driver sin copia previa, o poner datos de PostgreSQL en NFS. En el primer caso desaparecen imágenes y contenedores del inventario; en el segundo, el bloqueo de ficheros por red no da las garantías que una base de datos necesita.

Consejo: etiqueta los volúmenes al crearlos (--label criticidad=alta) y filtra por esa etiqueta antes de cualquier limpieza: docker volume ls --filter label=criticidad=alta. Un minuto de etiquetado evita el borrado que no tiene vuelta atrás.

Ejercicios

Ejercicio 1. Demuestra el copy-on-write en dos direcciones: localiza el upperdir de aurora-api, crea un fichero dentro del contenedor y encuéntralo en el disco del host; después mide el coste de modificar un byte de un fichero de 256 MB que venga en la imagen, y compáralo con modificar un byte de un fichero ya presente en la capa de escritura.

Ejercicio 2. Monta el ciclo completo de copia y restauración de aurora-db: haz una copia lógica, destruye los datos borrando el volumen, restaura y comprueba que los nueve títulos vuelven a estar. Explica en qué punto exacto la plataforma habría quedado inservible sin la copia.

Ejercicio 3. Convierte aurora-datos en un volumen external, demuestra que docker compose down -v ya no lo borra y explica qué protección aporta esto y cuál es su contrapartida.

Soluciones

Solución 1.

up=$(docker inspect aurora-libros-aurora-api-1 --format '{{.GraphDriver.Data.UpperDir}}')
docker compose exec aurora-api sh -c 'echo "hola capas" > /tmp/prueba.txt'
sudo cat "$up/tmp/prueba.txt"
docker diff aurora-libros-aurora-api-1 | grep prueba
hola capas
A /tmp/prueba.txt
printf 'FROM alpine:3\nRUN dd if=/dev/zero of=/g.bin bs=1M count=256\n' > /tmp/Dockerfile.cow
docker build -q -t cow256 -f /tmp/Dockerfile.cow /tmp
docker run --rm cow256 sh -c 'time dd if=/dev/zero of=/g.bin bs=1 count=1 conv=notrunc 2>/dev/null'
docker run --rm cow256 sh -c 'cp /g.bin /c.bin; time dd if=/dev/zero of=/c.bin bs=1 count=1 conv=notrunc 2>/dev/null'
real    0m 0.98s
real    0m 0.00s

Un segundo frente a cero, para escribir exactamente el mismo byte. La diferencia es el copy-up: en el primer caso el fichero vive en un lowerdir de solo lectura y el kernel debe copiar los 256 MB completos al upperdir antes de permitir la escritura; en el segundo, cp ya lo había traído a la capa de escritura. Extrapola: PostgreSQL escribiendo sin parar en ficheros de datos sin un volumen pagaría ese peaje una y otra vez, además de multiplicar el espacio ocupado. Aquí se ve, en un segundo de reloj, por qué la decisión de la lección 03-06 no era una convención sino una necesidad física.

Solución 2.

docker compose exec -T aurora-db pg_dump -U aurora -d aurora_libros -Fc > /tmp/aurora.dump
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
docker compose down -v                                    # el desastre
docker volume ls --filter name=aurora-datos -q | wc -l
docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
9
0
9

Aquí hay un matiz que confunde a mucha gente: tras el down -v la base de datos vuelve con 9 libros sin haber restaurado nada, porque el volumen se ha recreado vacío y PostgreSQL ha ejecutado de nuevo db/init.sql. Eso es exactamente lo que hace peligrosa la situación: parece que no se ha perdido nada.

Lo que sí se ha perdido son los datos añadidos después de la inicialización, que es todo lo que importa en producción:

docker compose exec aurora-db psql -U aurora -d aurora_libros \
  -c "INSERT INTO libros (titulo, autor, anio, isbn) VALUES ('Trafalgar','B. Pérez Galdós',1873,'978-84-000001-0')"
docker compose exec -T aurora-db pg_dump -U aurora -d aurora_libros -Fc > /tmp/aurora10.dump
docker compose down -v && docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
docker compose exec -T aurora-db pg_restore -U aurora -d aurora_libros --clean --if-exists --no-owner < /tmp/aurora10.dump
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
9
10

Tras el desastre quedan 9 (los de init.sql); tras restaurar, los 10 reales. El punto exacto en el que la plataforma habría quedado inservible es el down -v: el -v borra los volúmenes, y con ellos el único lugar donde existían los datos añadidos por los usuarios. Sin /tmp/aurora10.dump, esa fila no se recupera de ningún sitio.

Solución 3.

docker compose down && docker volume create --label criticidad=alta aurora-datos-ext
docker run --rm -v aurora-libros_aurora-datos:/origen:ro -v aurora-datos-ext:/destino \
  alpine:3 sh -c 'cp -a /origen/. /destino/'
# compose.yaml
volumes:
  aurora-datos:
    name: aurora-datos-ext
    external: true
docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
docker compose down -v
docker volume ls --filter name=aurora-datos-ext --format '{{.Name}}'
docker compose up -d --wait
docker compose exec aurora-db psql -U aurora -d aurora_libros -tAc 'SELECT count(*) FROM libros'
10
aurora-datos-ext
10

El down -v no ha borrado el volumen y los datos siguen intactos: Compose se niega a destruir lo que no ha creado. La protección es real y cuesta dos líneas de YAML, así que para el volumen de datos de una plataforma en producción es una decisión casi automática.

La contrapartida tiene dos caras. La primera, operativa: el volumen deja de crearse solo, y quien despliegue por primera vez en un servidor nuevo debe ejecutar docker volume create antes del up, o Compose fallará con external volume not found. La segunda, conceptual: pierdes la reproducibilidad de un entorno de cero. En desarrollo eso molesta —el reset de la lección 04-07 deja de funcionar—, así que la práctica sensata es declarar external solo en compose.prod.yaml, dejando que el entorno local siga siendo desechable.

Conclusión

Ya sabes qué ocurre bajo -v. Un storage driver apila las capas de una imagen y aplica copy-on-write, y overlay2 lo hace con cuatro directorios que has visto en el disco: los lowerdir de solo lectura de la imagen, el upperdir donde aparece cada fichero que escribe el contenedor, el merged que es lo que el proceso ve como / y el workdir interno del kernel. De ahí salen las tres reglas de resolución, incluida la de los whiteouts que explica por qué borrar un fichero en una capa posterior no libera espacio. Conoces la tabla completa de drivers —btrfs, zfs, el retirado devicemapper, fuse-overlayfs para rootless y el lentísimo vfs— y sabes que cambiar de driver deja invisibles todas tus imágenes.

Y has medido el copy-on-write: casi dos segundos y 512 MB de capa para escribir un solo byte en un fichero que venía en la imagen. Esa cifra convierte en física lo que era una recomendación: una base de datos escribe constantemente en ficheros grandes, luego va en un volumen, que evita OverlayFS y escribe directo en el sistema de ficheros del host. Sabes exprimir el driver local con sus driver_opts —bind con nombre, tmpfs con tamaño, NFS para las portadas y nunca para PostgreSQL—, declarar plugins de terceros en Compose y usar volúmenes external para que down -v no pueda llevarse por delante los datos de producción. Te llevas además una estrategia de copias que es una estrategia y no una intención: la tabla que distingue la copia lógica en caliente con pg_dump de la copia de ficheros con tar —y por qué la segunda exige parar el servicio—, un script con rotación que restaura y cuenta filas en cada ejecución, la regla de que una copia que nunca se ha restaurado no es una copia, y el control del espacio en /var/lib/docker con data-root.

En la lección 05-03 llega el turno de la seguridad, y con ella el endurecimiento completo de Aurora Libros: el modelo de amenazas y sus cuatro superficies, por qué pertenecer al grupo docker equivale a ser root —con la escalada demostrada—, el modo rootless, imágenes mínimas y fijadas por digest, --read-only, capacidades del kernel, seccomp, escaneo de vulnerabilidades con Docker Scout y Trivy, SBOM y firma con Cosign, y un compose.prod.yaml endurecido línea a línea con su checklist accionable.

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