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.jsmediante esbuild al ejecutarnpm run build; - necesita
pgen runtime: esta dependencia queda fuera del bundle; - escucha en
PORT; - conecta con PostgreSQL mediante variables;
- crea automáticamente la tabla
favoritessi la base está vacía; - permite listar y crear favoritos;
- expone
/healthcon 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:
| Variable | Uso |
|---|---|
PORT | Puerto HTTP, 3000 por defecto |
DB_HOST | Host obligatorio |
DB_PORT | Puerto, 5432 por defecto |
DB_NAME | Base obligatoria |
DB_USER | Usuario obligatorio |
DB_PASSWORD | Contraseña obligatoria |
app.example.env solo documenta valores. No se carga automáticamente y no contiene credenciales reales.
Contrato:
| Método | Ruta | Resultado |
|---|---|---|
| GET | /health | 200 con tabla preparada y consultable; 503 degradada |
| GET | /favorites | Favoritos persistidos |
| POST | /favorites | Recibe {"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:
- primer build;
- build sin cambios;
- cambio solo en
src/server.js; - cambio controlado de una dependencia mediante npm, conservando sincronizados
package.jsonypackage-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:
- prueba la imagen final;
- etiqueta con repositorio y versión propios;
- inicia el registry;
- ejecuta
push; - observa tag y digest;
- elimina solo referencias creadas por el proyecto;
- ejecuta
pull; - 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
- Dockerfile multi-stage razonado.
.dockerignorejustificado.compose.yamlde build.- variante de consumo con
image:. - configuración de ejemplo sin secretos.
- diagrama de arquitectura.
- evidencia de usuario no root.
- prueba de persistencia.
- estados de salud.
- tres informes de incidencias.
- referencia publicada y digest observado.
- README y procedimiento de limpieza.
Casos de prueba
| Área | Evidencia esperada |
|---|---|
| Starter | npm ci, build y ejecución fuera de Docker |
| Imagen | build limpio, proceso y usuario correctos |
| Multi-stage | runtime sin fuentes/herramientas innecesarias |
| Compose | config válido y servicios creados |
| Red | app llega a db por nombre |
| Puertos | host llega a app; db no está publicada |
| Persistencia | favorito sobrevive a down/up |
| Salud | estados observables y tests significativos |
| Diagnóstico | tres causas demostradas por evidencia |
| Registry | push, pull y ejecución sin rebuild |
| Distribución | variante image: funcional |
Preguntas finales
- ¿Qué pertenece al build y qué al runtime?
- ¿Qué excluye
.dockerignorey por qué? - ¿Qué queda en la etapa final?
- ¿Con qué usuario se ejecuta
app? - ¿Por qué
DB_HOSTesdb? - ¿Qué puertos están publicados?
- ¿Dónde viven los datos?
- ¿Qué diferencia hay entre
downydown -v? - ¿Qué comprueba cada healthcheck?
- ¿Qué no garantiza
service_healthy? - ¿Qué evidencias confirmaron las incidencias?
- ¿Qué hace
docker tag? - ¿Por qué un tag puede no identificar siempre el mismo contenido?
- ¿Cómo consumes la imagen sin rebuild?
- ¿Qué secretos comprobaste que no estaban presentes?
No necesitas Kubernetes, Swarm, CI/CD, alta disponibilidad, backups profesionales ni observabilidad avanzada para resolver el proyecto.