3.3 · Ejemplo hilado : aplicación web + base de datos

Nivel 3 · Docker Compose

3.3Ejemplo hilado : aplicación web + base de datos

Objetivo: hacer funcionar una verdadera mini-aplicación (API + base) cableada por nombres.
Tiempo estimado: 15 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: « depends_on basta para la disponibilidad, y publicar la base ayuda al debug ». No, dos veces. depends_on ordena sin esperar : sin reintento, el primer arranque falla — es normal y previsible, no un misterio. Y publicar postgres en el host (sobre todo en producción) amplía la superficie de ataque por una comodidad ilusoria : se depura vía adminer en dev y exec en prod, nunca exponiendo la base.
App cableada, persistida, sobria Nombres, variable, reintento, superficie mínima Cablear por nombresapi → bdd:5432, URL en DATABASE_URL+Persistir + reintentoVolumen nombrado, bucle de reconexión+Exponer lo mínimoapi y adminer (dev) publicados, base interna down/up prueba la persistencia ; el reintento prueba la robustez.
Nombres, variable, reintento, superficie mínima
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) Cablear : Idir hace alcanzar bdd:5432 por nombre vía DATABASE_URL en lugar de una IP en duro. 2) Fiabilizar : reintento al arrancar en la api (depends_on ordena, el reintento espera de verdad) + volumen nombrado para la base. 3) Exponer : api y adminer publicados, base solo interna — down/up prueba que la tabla sobrevive. Lección : nombres, variable, reintento, superficie mínima.

🔎 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

  1. Cree ./api (Dockerfile + servidor que lee DATABASE_URL con reintento al arrancar).
  2. Escriba el compose de arriba (URL en variable, base no publicada) y lance up -d --build.
  3. Verifique la api (:3000) y adminer (:8081, perfil dev), cree una tabla.
  4. Reinicie todo (down luego up) : la tabla debe sobrevivir — si no, el volumen está mal montado.
Una verdadera app multiservicio funciona, cableada por nombres, persistida y de superficie mínima.

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.

Prudencia: Contraseñas y URL de conexión vía el .env local, nunca en duro : ${DATABASE_URL} y ${POSTGRES_PASSWORD} en el compose, valores reales solo en el .env no commiteado. En producción : secreto gestionado (archivo, CI, orquestador) — nunca el compose commiteado en claro, nunca la base publicada.

✓ 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.

Lista de salida de la sección
  • Mis servicios se alcanzan por nombres vía variables.
  • Mi base está persistida y no publicada.
  • Mi api reintenta su conexión al arrancar.
Autoevaluación 1-5 : ¿queda aún una IP en duro en mi código o mi compose ?¿mi stack sobrevive a un down/up sin pérdida ?

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.

Comprueba tu comprensión

La api debe alcanzar postgres en la red Compose. ¿Qué URL sana ?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 3Preguntas inéditas y más difíciles para validar tus conocimientos