Persistencia y bases de datos
En la unidad anterior conectamos app y db por nombre. Ahora queremos conservar los favoritos de TasteMatch aunque eliminemos y volvamos a crear los contenedores. ¿Basta con que PostgreSQL esté guardando archivos?
contenedor reemplazable
│
▼
¿dónde viven los datos importantes?
En Docker 1 ya utilizaste volúmenes y bind mounts. Ahora expresaremos ese almacenamiento dentro de compose.yaml y comprobaremos que su ciclo de vida puede ser distinto del contenedor.
Un PostgreSQL concreto
El laboratorio utiliza:
postgres:17-alpine
Fijar la versión mayor evita que un pull cambie silenciosamente a otra versión mayor. La variante Alpine es adecuada para este laboratorio, no una recomendación universal.
Para PostgreSQL 17 en la imagen oficial, montaremos los datos en:
/var/lib/postgresql/data
En esta imagen, PGDATA apunta a esa ruta y la imagen declara VOLUME /var/lib/postgresql/data. Si no proporcionamos un montaje, Docker crea allí un volumen anónimo: los datos no se guardan en la capa escribible del contenedor.
Esta ruta debe comprobarse de nuevo si cambia la versión o variante de imagen.
Empezar sin volumen nombrado
Crea una carpeta nueva dk204-persistencia y copia en ella el app.js y el Dockerfile de DK2-02, como hicimos en DK2-03. Así separas estos datos de laboratorio de los de otros proyectos. En esa carpeta, utiliza este compose.yaml:
services:
app:
build: .
ports:
- "8080:3000"
environment:
DB_HOST: db
DB_PORT: 5432
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: tastematch
POSTGRES_USER: tastematch
POSTGRES_PASSWORD: laboratorio
Son credenciales ficticias. environment configura el contenedor; no es un gestor profesional de secretos.
Antes de levantar, observa los volúmenes existentes para no confundirlos con los de esta práctica:
docker volume ls
Valida y levanta:
docker compose config
docker compose up -d
docker compose ps
docker compose logs db
Solo publicamos app en localhost:8080. PostgreSQL no tiene ports: app llega a db:5432 por la red del proyecto, como en DK2-03.
Comprueba esa conexión desde app:
docker compose exec app node -e "const s=require('node:net').connect(Number(process.env.DB_PORT),process.env.DB_HOST,()=>{console.log('db:5432 accesible');s.end()});s.on('error',e=>{console.error(e.message);process.exitCode=1})"
Si PostgreSQL aún se está inicializando, consulta sus logs y vuelve a intentarlo cuando acepte conexiones. Estudiaremos la disponibilidad durante el arranque en DK2-05; aquí no añadimos condiciones de dependencia.
Comprueba también la ruta utilizada y el montaje real:
docker compose exec db printenv PGDATA
docker compose exec db psql -U tastematch -d tastematch -c "SHOW data_directory;"
docker compose ps -q db
docker inspect <id-del-contenedor-db> --format "{{json .Mounts}}"
docker volume ls
Las dos primeras comprobaciones deben indicar /var/lib/postgresql/data. En Mounts, anota el Name del volumen cuyo Destination coincide con esa ruta: es el volumen anónimo de este laboratorio. Usa el ID obtenido con ps -q db, sin adivinar el nombre del contenedor.
Cuando PostgreSQL acepte conexiones, crea un dato reconocible:
docker compose exec db psql -U tastematch -d tastematch -c "CREATE TABLE favoritos (nombre text PRIMARY KEY);"
docker compose exec db psql -U tastematch -d tastematch -c "INSERT INTO favoritos VALUES ('tortilla');"
docker compose exec db psql -U tastematch -d tastematch -c "SELECT * FROM favoritos;"
psql forma parte de la imagen de PostgreSQL. Creamos y consultamos los datos directamente para que el experimento de persistencia no dependa del código de app: la prueba con Node comprueba TCP, no consultas SQL.
Elimina los contenedores y observa qué queda:
docker compose down
docker volume ls
docker volume inspect <volumen-anonimo-anotado>
docker compose up -d
El volumen anónimo anterior permanece tras down, pero el nuevo up no lo reutiliza automáticamente. Repite la inspección de Mounts con el nuevo ID de db: verás otro volumen. Anota también su nombre.
Cuando PostgreSQL acepte conexiones, comprueba si existe la tabla:
docker compose exec db psql -U tastematch -d tastematch -c "SELECT to_regclass('public.favoritos') IS NULL AS tabla_ausente;"
El resultado t significa verdadero: la tabla no está en esta base nueva. No hemos demostrado que Docker borrase los datos anteriores, sino que el contenedor nuevo no está usando aquel almacenamiento.
postgres:17-alpine declara VOLUME /var/lib/postgresql/data
→ sin montaje en Compose, Docker crea un volumen anónimo
→ down elimina el contenedor y deja ese volumen
→ nuestra definición no tiene una identidad estable para reutilizarlo
→ el siguiente up monta otro volumen anónimo
Cierra esta primera prueba y elimina solo los dos volúmenes anónimos cuyos nombres anotaste, que contienen datos desechables de este laboratorio:
docker compose down
docker volume rm <primer-volumen-anotado> <segundo-volumen-anotado>
Declarar no es montar
Mantén app y db, añade un volumen raíz y móntalo en db. Empezaremos con una base nueva; este cambio no copia los datos de un volumen anónimo:
services:
app:
build: .
ports:
- "8080:3000"
environment:
DB_HOST: db
DB_PORT: 5432
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: tastematch
POSTGRES_USER: tastematch
POSTGRES_PASSWORD: laboratorio
volumes:
- datos-db:/var/lib/postgresql/data
volumes:
datos-db:
Hay dos decisiones distintas:
volumes.datos-db
→ declara almacenamiento lógico del proyecto
services.db.volumes
→ lo monta en la ruta donde PostgreSQL escribe
datos-db es el nombre lógico del archivo. Docker suele crear un nombre real prefijado por el proyecto. La aplicación no debe depender de adivinarlo.
Relacionar definición y recurso real
docker compose config
docker compose up -d
docker volume ls
Localiza el volumen del proyecto e inspecciónalo:
docker volume inspect <volumen-real>
No manipules directamente la ruta interna de Docker. La inspección sirve para comprobar nombre, driver y etiquetas. Repite también la inspección de Mounts del contenedor db: su Name debe ser este volumen y su Destination, /var/lib/postgresql/data.
Experimento central: el dato sobrevive
1. Crear el dato
docker compose exec db psql -U tastematch -d tastematch -c "CREATE TABLE favoritos (nombre text PRIMARY KEY);"
docker compose exec db psql -U tastematch -d tastematch -c "INSERT INTO favoritos VALUES ('tortilla');"
2. Comprobarlo
docker compose exec db psql -U tastematch -d tastematch -c "SELECT * FROM favoritos;"
3. Eliminar contenedores
docker compose down
Comprueba que el volumen continúa en docker volume ls.
4. Recrear
docker compose up -d
docker compose ps
Espera a que PostgreSQL acepte conexiones; todavía no hemos estudiado healthchecks ni condiciones de dependencia.
5. Volver a consultar
docker compose exec db psql -U tastematch -d tastematch -c "SELECT * FROM favoritos;"
Si aparece tortilla, el contenedor cambió pero el volumen conservó los archivos de la base.
down → elimina contenedor
volumen → permanece
up → nuevo contenedor monta el mismo volumen
Aplicación y base de datos
La arquitectura que hemos mantenido es:
host :8080 → app :3000
│ db:5432
▼
db
│ /var/lib/postgresql/data
▼
datos-db
Después de recrear los contenedores, repite la prueba TCP desde app. La conexión interna sigue usando db:5432, no localhost ni un puerto del host. El volumen cambia dónde se conservan los datos; no cambia cómo se comunican los servicios.
Eliminar también el volumen
down -v es destructivo:
docker compose down -v
Antes de ejecutarlo, confirma que solo contiene datos de laboratorio. Después:
docker volume ls
docker compose up -d
Comprueba que el volumen desaparece de la lista tras down -v. El siguiente up crea otro con el mismo nombre lógico, pero almacenamiento nuevo. Cuando PostgreSQL acepte conexiones:
docker compose exec db psql -U tastematch -d tastematch -c "SELECT to_regclass('public.favoritos') IS NULL AS tabla_ausente;"
El resultado debe ser t: la tabla y tortilla anteriores ya no están.
No conviertas down -v en la limpieza habitual. Decide conscientemente si quieres conservar o destruir datos.
Variables de inicialización y un volumen existente
POSTGRES_DB, POSTGRES_USER y POSTGRES_PASSWORD intervienen en la inicialización cuando el directorio de datos está vacío. Cambiarlas en compose.yaml no reescribe mágicamente una base ya inicializada.
volumen vacío → inicialización
volumen con base → reutilización de datos existentes
Si esperabas otra configuración, primero comprueba qué volumen se montó y qué contiene. No uses down -v para ocultar el diagnóstico si los datos importan.
Volumen o bind mount
Volumen nombrado:
services:
db:
volumes:
- datos-db:/var/lib/postgresql/data
volumes:
datos-db:
Docker gestiona su ubicación. Es la opción del laboratorio para datos de PostgreSQL.
Bind mount:
services:
app:
volumes:
- ./config:/app/config:ro
Relaciona una ruta concreta del host. :ro impide escribir mediante ese montaje. Puede ser útil para configuración o código durante desarrollo, pero introduce rutas, permisos y diferencias entre sistemas.
Un bind mount puede ocultar archivos que ya existían en el destino del contenedor. No lo uses como sustituto automático de cualquier volumen.
Persistencia no es backup ni alta disponibilidad
Un volumen separa datos y contenedor. No crea por sí solo:
- copias históricas;
- recuperación ante corrupción;
- almacenamiento fuera del host;
- replicación;
- failover;
- un servicio disponible durante fallos.
persistencia → sobreviven datos al reemplazo del contenedor
backup → copia recuperable
HA → diseño para mantener disponibilidad
Son problemas diferentes.
Diagnóstico
Si no aparecen los datos esperados:
- comprueba
docker compose psy logs; - revisa la configuración resuelta;
- confirma el volumen montado y su destino;
- identifica el volumen real;
- comprueba si se inicializó uno nuevo;
- revisa si cambiaste variables después de la primera inicialización.
Errores habituales:
- usar
localhostdesdeapp; - publicar PostgreSQL sin necesidad;
- montar en una ruta distinta de la usada por la imagen;
- creer que
downsiempre borra volúmenes; - ejecutar
down -vsin evaluar la pérdida; - confundir bind mount y volumen;
- considerar el volumen un backup;
- esperar reinicialización al cambiar variables;
- guardar credenciales reales en el repositorio.
Mini reto
Parte del primer compose.yaml de esta unidad: app y db, con postgres:17-alpine, sin volumen nombrado explícito.
- identifica dónde están los datos y por qué el volumen anónimo no resuelve la reutilización tras
downyup; - declara
datos-reto; - móntalo en
/var/lib/postgresql/datadentro dedb; - valida con
docker compose config; - levanta el proyecto;
- comprueba desde
appla conexión adb:5432; - crea una tabla y un dato reconocible mediante
psql; - ejecuta
docker compose down; - comprueba que el volumen nombrado permanece;
- vuelve a levantar el proyecto;
- demuestra mediante una consulta que el dato permanece;
- inspecciona el volumen y relaciona su nombre real con
datos-retoy la ruta montada; - explica por qué
dbno necesita publicar5432; - con datos únicamente de laboratorio, ejecuta
docker compose down -vy confirma que elimina el volumen; - vuelve a levantar;
- comprueba que hay almacenamiento nuevo y que la tabla anterior ya no existe;
- explica la diferencia entre persistencia, backup y alta disponibilidad.
Al terminar, limpia los recursos de este reto. Usa down -v solo tras confirmar que sus datos son desechables. No elimines volúmenes de otros proyectos.
La siguiente unidad estudiará otro problema: un contenedor puede estar running mientras PostgreSQL todavía se está inicializando.