3.3Ejemplo hilado : aplicación web + base de datos
Lo esencial en 30 segundos
- Tres servicios, una red : api (build local), bdd (postgres + volumen), adminer (consola dev) — la api alcanza bdd:5432 por nombre, sin IP en duro.
- La URL de conexión viaja en variable : DATABASE_URL leída por la api — ningún host en duro en el código, la config cambia sin tocar la imagen.
- depends_on ordena, el reintento fiabiliza : la base arranca más lenta — la api reintenta su conexión al arrancar, si no el primer lanzamiento falla.
Mini-caso hilado — Idir cablea la maqueta del equipo
Idir, devops junior en Argel, debe proporcionar una maqueta API + base que cada uno lance en un comando : su primer intento pone la IP de la base en duro y falla una de cada dos veces al arrancar. Pasa al cableado por nombres (la api alcanza bdd:5432), a la URL en variable DATABASE_URL, al volumen persistente — y añade un bucle de reintento en la api. Resultado : up -d --build, la api responde en :3000, adminer en :8081, la base solo es accesible desde dentro ; down luego up : la tabla creada la víspera sigue ahí.
Este mismo caso vuelve en el curso, el esquema, el guion y el reto 48h.
1. Tres servicios, una red, nombres — no IPs
Una app real es api (SU imagen, build local desde ./api), bdd (postgres + volumen nombrado) y adminer (consola web ligera de administración SQL, publicada en dev). Compose las conecta en la misma red : la api alcanza bdd:5432 por nombre de servicio — las IP cambian en cada recreación, los nombres permanecen. Nunca IP en duro en el código : es la regla que hace portable el stack.
La api lee DATABASE_URL en variable (host = bdd, base, credenciales) : la config viaja, el código queda idéntico entre dev y prod. Se publica la api (:3000) y adminer (:8081, solo dev) ; la base nunca se publica — accesible solo desde dentro. Superficie expuesta mínima : solo sale lo que debe salir.
2. El orden no basta : reintento obligatorio, persistencia probada
depends_on lanza la base antes que la api, pero no su DISPONIBILIDAD : postgres tarda varios segundos en aceptar conexiones. Sin bucle de reintento al arrancar, la api falla en el primer lanzamiento y luego « funciona » en el segundo — el síntoma clásico. La diferencia entre una demo y un stack que sobrevive a los reinicios es ese reintento con retardo en la aplicación (el healthcheck además ordena, nivel 4.2).
La prueba de que todo aguanta : down luego up — la tabla creada vía adminer debe sobrevivir, porque vive en el volumen, no en el contenedor. Idir lo convierte en su test de recepción sistemático : lo que no sobrevive al reinicio no está persistido, punto. Base persistida, nunca publicada, reintento al arrancar : el trío del stack sano.
🔎 Ejemplo concreto — Stack API + Postgres + Adminer, cableado y persistido
Idir escribe el compose de la maqueta : variables, volumen, red, adminer opt-in en dev.
services:
api:
build: ./api
ports: ["3000:3000"]
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on: [bdd]
networks: [appnet]
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes: [pgdata:/var/lib/postgresql/data]
networks: [appnet]
adminer:
image: adminer:4.8
profiles: [dev]
ports: ["8081:8080"]
networks: [appnet]
volumes:
pgdata:
networks:
appnet:
# Lanzar : docker compose --profile dev up -d --build
Lectura : la api y adminer están publicados, la base no — accesible desde dentro.
Lección : abra :3000 (api) y :8081 (adminer) ; down/up prueba que la tabla sobrevive.
Vocabulario de la sección
- DATABASE_URL
- La URL de conexión completa pasada en variable : host = nombre del servicio, nunca IP en duro.
Ejemplo sencillo: La api de Idir lee ${DATABASE_URL} hacia bdd.
Lo que no es: la IP encontrada ayer en inspect, copiada en el código.
Reflejo: host = nombre del servicio, config = variable.
- Reintento
- El bucle de reconexión al arrancar : la api reintenta hasta que la base acepta.
Ejemplo sencillo: El reintento que cura el fallo del primer arranque.
Lo que no es: una conexión única que falla si la base es lenta.
Reflejo: toda conexión al arrancar = reintento con retardo.
- Adminer
- Consola web ligera de administración SQL, publicada en dev para ver y rellenar la base.
Ejemplo sencillo: Idir crea la tabla de prueba en :8081.
Lo que no es: la base publicada en producción para « depurar ».
Reflejo: consola solo en dev, nunca en prod.
- Build local
- Servicio construido desde una carpeta (./api) en lugar de una imagen remota : su código contenerizado.
Ejemplo sencillo: La imagen api construida en el up --build.
Lo que no es: un código montado a mano en un contenedor nginx.
Reflejo: --build tras cada cambio de código.
- Superficie expuesta
- El conjunto de puertos publicados : se minimiza — solo sale lo necesario.
Ejemplo sencillo: api + adminer (dev) publicados, bdd interna.
Lo que no es: postgres publicado en el host « por si acaso ».
Reflejo: cada puerto publicado debe justificar su existencia.
En la práctica — Montar la mini-app cableada y persistida
- Cree ./api (Dockerfile + servidor que lee DATABASE_URL con reintento al arrancar).
- Escriba el compose de arriba (URL en variable, base no publicada) y lance up -d --build.
- Verifique la api (:3000) y adminer (:8081, perfil dev), cree una tabla.
- Reinicie todo (down luego up) : la tabla debe sobrevivir — si no, el volumen está mal montado.
Guion listo para decir — entregar la maqueta al equipo (30 s)
« Un comando : up -d --build con el perfil dev. La api responde en 3000, la consola base en 8081, la base solo es accesible desde dentro. La URL viaja en variable, el reintento absorbe el arranque, y down/up no pierde nada — probado. »
Para decir al entregar la maqueta ; mostrar el down/up con la tabla intacta.
Modelo copiar-pegar — API + base + consola (válido)
services:
api:
build: ./api
ports:
- "${PORT:-3000}:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
depends_on:
- bdd
networks:
- appnet
bdd:
image: postgres:16.4
environment:
POSTGRES_USER: ${POSTGRES_USER:-app}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-appdb}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- appnet
adminer:
image: adminer:4.8
profiles:
- dev
ports:
- "8081:8080"
networks:
- appnet
volumes:
pgdata:
networks:
appnet:
# .env local (NO commiteado) : DATABASE_URL, POSTGRES_PASSWORD
Independiente
Cableado por nombres + URL en variable y su maqueta arranca en cada cliente.
PYME en Argel
Idir entrega la maqueta en un comando : api en 3000, consola en 8081, base persistida.
Equipo de proyecto
Estandarizar DATABASE_URL + reintento al arrancar para todos los servicios del equipo.
Principiante
Esta semana : api + base cableadas por nombres, tabla creada, down/up que prueba la persistencia.
✓ Buenas prácticas
- Una por nombres de servicio, configure por variables de entorno.
- Añada un reintento de conexión al arrancar en cada servicio cliente de la base.
- Mantenga la consola de admin solo en dev (perfil dev), superficie mínima en prod.
✗ Errores frecuentes
- IP en duro en el código : cambia en cada recreación.
- Creer que depends_on = listo : sin reintento, el primer arranque falla.
- Publicar la base en producción : superficie de ataque ampliada para nada.
Puntos clave
- UNA RED, nombres, una URL en variable : bdd:5432 vía DATABASE_URL, nunca IP.
- BASE persistida (volumen nombrado) y NUNCA publicada : solo interna.
- REINTENTO obligatorio al arrancar : depends_on ordena, el reintento espera de verdad.
- DOWN/UP prueba la persistencia : la tabla debe sobrevivir al reinicio.
Reto 48h — aplique mañana
Monte api + base cableadas por nombres con reintento, y demuestre la persistencia con un down/up ante un colega.
- Mis servicios se alcanzan por nombres vía variables.
- Mi base está persistida y no publicada.
- Mi api reintenta su conexión al arrancar.
Preguntas frecuentes
¿La api falla en el primer arranque y luego funciona : normal ?
SÍ, si no reintenta : la base arranca más lenta que la api (depends_on ordena sin esperar). Añada un bucle de reintento con retardo al arrancar la api — el fallo desaparece. Complemento nivel 4 : healthcheck en la base + condición de arranque en la api para ordenar limpiamente.
¿Dónde poner la contraseña y la URL de verdad ?
En DEV : .env no commiteado junto al compose (documentado por un .env.example ficticio). En PROD : secreto gestionado (archivo montado, CI, orquestador). NUNCA en claro en el compose commiteado, nunca en la imagen. Y la base sigue no publicada : el secreto solo transita por la red interna de Compose.
La api debe alcanzar postgres en la red Compose. ¿Qué URL sana ?