Saltar al contenido principal

Proyecto final multi-contenedor

Este proyecto cierra Docker 0, Docker 1 y Docker 2. Recibirás una aplicación funcional y prepararás todo su recorrido Docker sin copiar una solución terminada.

código → Dockerfile → imagen → compose.yaml
├── app
├── db
├── red
├── volumen
└── healthchecks
↓
registry
↓
otro consumo

Debes tomar decisiones, comprobarlas y documentarlas. No basta con reunir comandos.

Starter de TasteMatch​

Descarga TasteMatch Docker 2 final.

dk2-08-final-project/
├── app.example.env
├── package.json
├── package-lock.json
├── README-starter.md
├── scripts/build.js
└── src/
├── server.js
└── http.js

No contiene Dockerfile, .dockerignore, compose.yaml, node_modules, dist, secretos ni solución.

La aplicación:

  • utiliza Node.js 22;
  • genera dist/server.js mediante esbuild al ejecutar npm run build;
  • necesita pg en runtime: esta dependencia queda fuera del bundle;
  • escucha en PORT;
  • conecta con PostgreSQL mediante variables;
  • crea automáticamente la tabla favorites si la base está vacía;
  • permite listar y crear favoritos;
  • expone /health con estado de base de datos;
  • atiende señales de parada.

Comprobar fuera de Docker​

Lee primero README-starter.md:

npm ci
npm run build

Para una ejecución funcional necesitas PostgreSQL y estas variables:

VariableUso
PORTPuerto HTTP, 3000 por defecto
DB_HOSTHost obligatorio
DB_PORTPuerto, 5432 por defecto
DB_NAMEBase obligatoria
DB_USERUsuario obligatorio
DB_PASSWORDContraseña obligatoria

app.example.env solo documenta valores. No se carga automáticamente y no contiene credenciales reales.

Contrato:

MétodoRutaResultado
GET/health200 con tabla preparada y consultable; 503 degradada
GET/favoritesFavoritos persistidos
POST/favoritesRecibe {"recipe":"tortilla"}

Arquitectura mínima​

HOST :8080
│
▼
app propia
│ DB_HOST=db · DB_PORT=5432
▼
PostgreSQL 17
│
▼
volumen de datos

Solo app necesita puerto publicado. db se comunica internamente por nombre de servicio. Utiliza postgres:17-alpine y comprueba la ruta de datos de esa versión antes de montar el volumen.

Fase 1 — Análisis previo​

Antes de crear archivos Docker, entrega una tabla con:

  • runtime y versión;
  • archivos necesarios para instalar dependencias;
  • comando y salida del build;
  • artefactos de runtime;
  • comando de inicio;
  • puerto interno;
  • variables obligatorias y opcionales;
  • ruta de datos de PostgreSQL;
  • recursos que deben persistir;
  • archivos que deben quedar fuera del contexto.

Dibuja el recorrido de una petición y predice qué ocurre si db no está disponible.

Fase 2 — Primera imagen​

Crea un Dockerfile funcional de una etapa. Debe:

  • partir de una imagen Node 22 razonada;
  • instalar con el lockfile;
  • ejecutar npm run build;
  • iniciar dist/server.js;
  • declarar únicamente decisiones útiles;
  • no incluir variables sensibles.

Construye con un tag propio, ejecuta con configuración de laboratorio y comprueba logs y /health. Explica por qué el build no inicia un contenedor.

Fase 3 — Contexto y caché​

Crea .dockerignore. Razona al menos:

node_modules
dist
.git
*.log
.env
Dockerfile y compose.yaml, según tu estrategia

No copies una plantilla sin comprobar qué necesita el build. Compara:

  1. primer build;
  2. build sin cambios;
  3. cambio solo en src/server.js;
  4. cambio controlado de una dependencia mediante npm, conservando sincronizados package.json y package-lock.json.

Registra qué pasos pudieron reutilizarse: un cambio de código debería permitir reutilizar la instalación de dependencias; un cambio de dependencias y lockfile debe invalidar esa capa. No edites únicamente package.json dejando un lockfile incompatible con npm ci. Después restaura ambos archivos a su versión inicial.

Fase 4 — Imagen mejorada​

Existe una fase real que produce dist/, por lo que diseña un multi-stage:

etapa build
→ package/lock + herramientas + src + scripts
→ npm ci + npm run build
→ dist

etapa runtime
→ dependencias necesarias para ejecutar + dist

No se proporciona el Dockerfile final. Debes decidir cómo instalar solo dependencias runtime y cómo copiar el artefacto.

Ejecuta como usuario no root. Si eliges una imagen oficial de Node, comprueba el usuario disponible y utiliza propietarios coherentes, por ejemplo mediante COPY --chown cuando corresponda. No uses chmod 777.

Compara imagen inicial y final: tamaño, contenido, usuario, herramientas, funcionamiento y capacidad de diagnóstico.

Fase 5 — compose.yaml​

Define al menos:

servicio app
servicio db
red interna
volumen PostgreSQL
configuración runtime
publicación HTTP

No utilices version, container_name ni IPs internas. Valida siempre:

docker compose config

La aplicación debe usar db:5432; PostgreSQL no debe publicarse al host sin necesidad.

Fase 6 — Persistencia​

Monta un volumen nombrado en la ruta adecuada de postgres:17-alpine.

Prueba obligatoria:

crear favorito
→ comprobarlo
→ docker compose down
→ docker compose up
→ comprobar que permanece

Documenta el nombre lógico y el recurso Docker real. Explica el efecto destructivo de down -v, pero ejecútalo solo con datos desechables.

Fase 7 — Dependencias y salud​

Añade un healthcheck real para PostgreSQL con pg_isready y otro razonado para la aplicación. Debes distinguir:

running ≠ ready ≠ healthy

Configura la dependencia de arranque de app mediante service_healthy cuando la versión de Compose utilizada lo admita y compruébalo con docker compose config.

No presentes esta condición como resistencia ante una caída posterior. Explica timeouts, reintentos y reconexión como responsabilidades de la aplicación.

Fase 8 — Aplicación completa​

Comprueba:

  • ambos servicios y sus estados;
  • respuesta /health;
  • creación y lectura de favoritos;
  • conexión por nombre;
  • ausencia de publicación de PostgreSQL;
  • persistencia tras recreación;
  • usuario efectivo de app;
  • logs útiles y parada limpia.

Guarda evidencias concretas, no capturas sin explicación.

Fase 9 — Tres incidencias aisladas​

Investiga una cada vez. Restaura el sistema antes de la siguiente.

Incidencia A: hostname incorrecto​

Configura temporalmente DB_HOST=localhost. Registra síntoma, logs, redes y prueba desde app. No se proporciona la causa resuelta: debes demostrarla.

Incidencia B: variable obligatoria ausente​

Omite una variable obligatoria, como DB_PASSWORD. Localiza estado y primer mensaje relevante. Corrige solo esa causa y vuelve a comprobar.

Incidencia C: healthcheck incorrecto​

Cambia temporalmente el destino del healthcheck a un puerto donde PostgreSQL no escucha: pg_isready -h 127.0.0.1 -p 65432 -U tastematch -d tastematch. Busca no response, código de salida 2, aumento de FailingStreak y estado running + unhealthy. Comprueba el efecto sobre la dependencia y restaura después el healthcheck correcto. Una base inexistente no sirve para provocar este fallo: pg_isready puede devolver 0 si el servidor acepta conexiones.

Para cada incidencia entrega:

síntoma → evidencia → hipótesis → prueba → causa → cambio → validación

Puedes añadir puerto o montaje incorrecto, pero no introduzcas varios fallos simultáneos.

Fase 10 — Registry local​

Utiliza registry:3.1.2 como laboratorio en localhost:5000:

  1. prueba la imagen final;
  2. etiqueta con repositorio y versión propios;
  3. inicia el registry;
  4. ejecuta push;
  5. observa tag y digest;
  6. elimina solo referencias creadas por el proyecto;
  7. ejecuta pull;
  8. prueba sin reconstruir.

Identifica con docker inspect el mount de /var/lib/registry: aunque no declares un volumen nombrado, la imagen oficial puede utilizar uno anónimo. Anota su nombre para la limpieza.

No requiere cuenta personal. No generalices este registry local como configuración HTTP para entornos remotos.

Fase 11 — Consumir con image:​

Conserva una variante de Compose que reemplace:

build: ...

por la referencia publicada:

image: localhost:5000/<repositorio>:<tag>

Demuestra que puede levantar la aplicación después del pull sin construir localmente. Explica la diferencia entre tag y digest.

Fase 12 — Limpieza consciente​

Haz inventario de contenedores, redes, volúmenes, imágenes y registry. Elimina únicamente recursos con nombres de tu proyecto.

Conserva el volumen mientras necesites datos. Antes de down -v, describe exactamente qué se perderá. Después de eliminar el registry, elimina únicamente su volumen anónimo identificado si sus datos ya no son necesarios. No utilices prune.

README obligatorio​

Incluye:

  • arquitectura y requisitos;
  • decisiones de Dockerfile y .dockerignore;
  • build y tags;
  • variables y ejemplo no sensible;
  • servicios, redes y puertos;
  • persistencia y ruta de datos;
  • healthchecks y dependencia;
  • ejecución con build:;
  • publicación y ejecución con image:;
  • endpoints y casos de prueba;
  • diagnóstico de incidencias;
  • limpieza y advertencias destructivas.

No conviertas el README en una transcripción completa de terminal.

Entregables​

  1. Dockerfile multi-stage razonado.
  2. .dockerignore justificado.
  3. compose.yaml de build.
  4. variante de consumo con image:.
  5. configuración de ejemplo sin secretos.
  6. diagrama de arquitectura.
  7. evidencia de usuario no root.
  8. prueba de persistencia.
  9. estados de salud.
  10. tres informes de incidencias.
  11. referencia publicada y digest observado.
  12. README y procedimiento de limpieza.

Casos de prueba​

ÁreaEvidencia esperada
Starternpm ci, build y ejecución fuera de Docker
Imagenbuild limpio, proceso y usuario correctos
Multi-stageruntime sin fuentes/herramientas innecesarias
Composeconfig válido y servicios creados
Redapp llega a db por nombre
Puertoshost llega a app; db no está publicada
Persistenciafavorito sobrevive a down/up
Saludestados observables y tests significativos
Diagnósticotres causas demostradas por evidencia
Registrypush, pull y ejecución sin rebuild
Distribuciónvariante image: funcional

Preguntas finales​

  1. ¿Qué pertenece al build y qué al runtime?
  2. ¿Qué excluye .dockerignore y por qué?
  3. ¿Qué queda en la etapa final?
  4. ¿Con qué usuario se ejecuta app?
  5. ¿Por qué DB_HOST es db?
  6. ¿Qué puertos están publicados?
  7. ¿Dónde viven los datos?
  8. ¿Qué diferencia hay entre down y down -v?
  9. ¿Qué comprueba cada healthcheck?
  10. ¿Qué no garantiza service_healthy?
  11. ¿Qué evidencias confirmaron las incidencias?
  12. ¿Qué hace docker tag?
  13. ¿Por qué un tag puede no identificar siempre el mismo contenido?
  14. ¿Cómo consumes la imagen sin rebuild?
  15. ¿Qué secretos comprobaste que no estaban presentes?

No necesitas Kubernetes, Swarm, CI/CD, alta disponibilidad, backups profesionales ni observabilidad avanzada para resolver el proyecto.