====== 4. Docker en el proyecto ====== Objetivo del tema: cada worktree tiene **su propia aplicación completa funcionando** (base de datos, backend y frontend), sin chocar con las de los demás worktrees. Al acabar, ''/git-flow start'' deja el worktree con sus puertos asignados y ''./app.sh start'' levanta la aplicación del issue con datos. Lo que ya sabéis y aquí no se repite: imágenes y contenedores ([[clase:daw:daw:1eval:tema03|Despliegue 3]]), Dockerfile ([[clase:daw:daw:1eval:tema04|Despliegue 4]]), Docker Compose ([[clase:daw:daw:1eval:tema04.b|Despliegue 4.b]]) y redes y nginx-proxy ([[clase:daw:daw:1eval:tema04.c|Despliegue 4.c]]). Automatizar Maven y Node: [[clase:daw:proyecto:1eval:tema03|Proyecto 3]]. ===== Tres contenedores ===== La aplicación son tres contenedores, uno por caja del diagrama C4 de contenedores: ^ Servicio ^ Imagen ^ Qué hace ^ | ''db'' | ''mariadb'' | La base de datos. Solo accesible desde el backend | | ''backend'' | Construida: Maven → JRE | El JAR de Spring Boot en el puerto 8080 | | ''frontend'' | Construida: Angular → Nginx | Sirve el Angular compilado y hace de proxy de ''/api'' al backend | El navegador solo habla con ''frontend''. Todo lo que empieza por ''/api'' lo reenvía Nginx al backend, así el frontend no necesita saber en qué puerto está el backend y no hay problemas de CORS. Los dos Dockerfile son **multi-stage** y los escribe la IA. Lo único que hay que vigilar: FROM maven:3.9-eclipse-temurin-25 AS build WORKDIR /app COPY pom.xml . RUN --mount=type=cache,target=/root/.m2 mvn -q dependency:go-offline COPY src ./src RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests package FROM eclipse-temurin:25-jre RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* COPY --from=build /app/target/*.jar /app.jar ENTRYPOINT ["java", "-jar", "/app.jar"] El ''curl'' es para el ''healthcheck'' de abajo. El ''--mount=type=cache'' sobre ''.m2'' es lo que hace que reconstruir la imagen tarde segundos y no minutos: sin él, cada ''build'' vuelve a descargar todas las dependencias. FROM node:22 AS build WORKDIR /app COPY package*.json . RUN npm ci COPY . . RUN npx ng build --configuration production FROM nginx:alpine COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html server { listen 80; root /usr/share/nginx/html; location /api/ { proxy_pass http://backend:8080/api/; } location / { try_files $uri $uri/ /index.html; } } El ''try_files'' es obligatorio en una aplicación Angular: sin él, recargar el navegador en ''/clientes/12'' da un 404 de Nginx. ===== Producción y desarrollo con los mismos ficheros ===== Docker Compose carga ''compose.yaml'' y, si existe, ''compose.override.yaml'' encima. Eso se usa así: * ''compose.yaml'' **es producción**: imágenes construidas, la base de datos en una red interna sin puertos publicados, el frontend en la red de nginx-proxy con su ''VIRTUAL_HOST''. * ''compose.override.yaml'' **cambia lo de desarrollo**. Está en el repositorio y Compose lo carga solo. En el servidor, el workflow de despliegue (tema 7) fija ''COMPOSE_FILE=compose.yaml'' y el override no se aplica. services: db: image: mariadb:11 environment: MARIADB_DATABASE: proyecto MARIADB_USER: proyecto MARIADB_PASSWORD: ${DB_PASSWORD} MARIADB_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - db-data:/var/lib/mysql networks: - interna healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 5s retries: 20 backend: build: ./backend environment: SPRING_DATASOURCE_URL: jdbc:mariadb://db:3306/proyecto SPRING_DATASOURCE_USERNAME: proyecto SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} depends_on: db: condition: service_healthy networks: - interna healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/hola"] interval: 5s retries: 20 frontend: build: ./frontend environment: VIRTUAL_HOST: .proyectosdaw.cip.fpmislata.com depends_on: backend: condition: service_healthy networks: - interna - proxy volumes: db-data: networks: interna: proxy: external: true name: nginx-proxy-network Los ''healthcheck'' con ''depends_on: condition: service_healthy'' hacen que el backend no arranque hasta que MariaDB acepte conexiones, y el frontend hasta que el backend responda. Sin eso, el backend muere al arrancar porque la base de datos aún no está lista. services: db: ports: - "${DB_PORT:-3306}:3306" backend: ports: - "${BACKEND_PORT:-8080}:8080" environment: SPRING_PROFILES_ACTIVE: ${PERFIL:-datos-prueba} frontend: build: target: build command: npx ng serve --host 0.0.0.0 --proxy-config proxy.conf.json volumes: - ./frontend:/app - /app/node_modules ports: - "${FRONTEND_PORT:-4200}:4200" Lo que cambia en desarrollo: * **Se publican los puertos**, para entrar desde el navegador y para que Maven y Playwright lleguen a la base de datos y al frontend desde el host. * **El frontend pasa a ''ng serve''** con la carpeta local ''frontend/'' montada dentro: al guardar un fichero, el navegador se recarga. ''proxy.conf.json'' manda ''/api'' a ''%%http://backend:8080%%''. El volumen anónimo sobre ''node_modules'' evita que los módulos instalados en la imagen se tapen con los del host. * **El backend no tiene recarga en caliente**: cada cambio en Java es ''docker compose up --build backend''. Con la caché de Maven son unos segundos. Y no pasa nada. * **Datos de prueba** activados (abajo). La red ''nginx-proxy-network'' es externa: en el servidor la crea nginx-proxy, pero en el portátil hay que crearla una vez, ''docker network create nginx-proxy-network'', o Compose se niega a arrancar. ''app.sh start'' lo hace si no existe. ===== Un compose por worktree ===== Dos worktrees con el mismo ''compose.yaml'' crearían contenedores con el mismo nombre y pedirían los mismos puertos. Lo resuelve un fichero ''.env'' **en cada worktree**, ignorado por git, que Compose lee solo: COMPOSE_PROJECT_NAME=mi-proyecto-12-alta-clientes DB_PORT=33112 BACKEND_PORT=8112 FRONTEND_PORT=4212 DB_PASSWORD=dev * ''COMPOSE_PROJECT_NAME'' es el prefijo de contenedores, redes y volúmenes de ese worktree. Dos proyectos distintos no se tocan. * Los puertos los busca ''/git-flow start'' al crear el worktree: prueba a partir de una base y se queda con los libres. * Las variables llevan valor por defecto, ''${BACKEND_PORT:-8080}'', para cuando no hay ''.env'': en CI (tema 7). ''/git-flow finish'' termina con ''docker compose down -v'': se van los contenedores **y el volumen** de la base de datos de ese worktree. No queda nada. ===== La primera tabla ===== Hasta ahora el backend no tiene base de datos. Se añaden ahora, con un prompt directo al agente (el flujo de trabajo de verdad llega en el tema 5; esto es solo para tener algo que ver): * Dependencias ''spring-boot-starter-data-jpa'', ''mariadb-java-client'' y ''flyway-mysql''. * Una migración ''%%backend/src/main/resources/db/migration/V1__clientes.sql%%'' con una tabla. * Un ''GET /api/clientes'' que la lista y una página de Angular que la muestra. El esquema **siempre** va en migraciones de Flyway, ''%%V__descripcion.sql%%'', y nunca con ''ddl-auto''. Así cada worktree, cada runner de CI y el servidor construyen la misma base de datos desde cero. ===== Datos de prueba ===== Cada worktree arranca con la base de datos vacía: no hay nada que ver en la aplicación ni se pueden escribir tests E2E que dependan de que exista un cliente. Los datos de prueba se separan del esquema: backend/src/main/resources/db/ ├── migration/ <- esquema: V1__clientes.sql, V2__pedidos.sql ... └── datos-prueba/ <- datos: R__clientes.sql, R__pedidos.sql ... Los de ''datos-prueba/'' son migraciones **repetibles** de Flyway (''%%R__%%''): se ejecutan después de todas las versionadas, y como la base de datos de un worktree siempre nace vacía, se ejecutan una vez. Flyway solo mira esa carpeta cuando está activo el perfil ''datos-prueba'': spring.flyway.locations=classpath:db/migration,classpath:db/datos-prueba * En desarrollo el perfil lo activa ''compose.override.yaml''. Para probar la aplicación vacía: ''PERFIL=default docker compose up -d --build backend''. * En producción ''compose.yaml'' no activa el perfil: los datos de prueba **nunca llegan al servidor**. * Para volver al estado inicial (antes de pasar los E2E, por ejemplo): ''docker compose down -v && docker compose up -d''. Cada entidad nueva trae su ''%%R__.sql%%'' con unos cuantos registros; el steering de cc-sdd (tema 6) obliga a que cada CRUD lo traiga. ===== El script app.sh ===== Operar la aplicación es siempre lo mismo y no necesita ninguna inteligencia, así que no es una skill: es un script de bash en la raíz del repositorio, con cuatro subcomandos y la gramática del tema 1. Bash está en los tres sistemas (en Windows lo trae Git, tema 3). Y es el mismo script que se usa en el servidor (tema 7): no hay una forma de operar en local y otra en producción. #!/bin/bash # La aplicación de este worktree: start | stop [-v] | status | logs set -e cd "$(dirname "$0")" case "$1" in start) docker network inspect nginx-proxy-network >/dev/null 2>&1 || docker network create nginx-proxy-network docker compose up -d --build --wait [ -f .env ] && . ./.env echo "Aplicación en http://localhost:${FRONTEND_PORT:-4200}" ;; stop) shift; docker compose down "$@" ;; status) docker compose ps ;; logs) docker compose logs --tail=200 ;; *) echo "uso: $0 start | stop [-v] | status | logs"; exit 1 ;; esac * ''./app.sh start'' vale para todo: la primera vez, después de cambiar el backend (reconstruye la imagen y la vuelve a levantar) y al día siguiente tras un ''stop''. ''--wait'' espera a los ''healthcheck'', así que cuando escribe la URL la aplicación ya responde. * ''./app.sh stop'' para los contenedores y conserva la base de datos. ''./app.sh stop -v'' la borra también: es lo que hace ''/git-flow finish'' al cerrar el issue, y lo que se hace para volver a los datos de prueba iniciales. * ''./app.sh status'' y ''./app.sh logs'' son lo primero que se mira cuando algo no responde. * Trabaja sobre la carpeta en la que está y lee su ''.env'', así que cada worktree opera el suyo. En el servidor no hay ''.env'': valen los valores por defecto. Los scripts se guardan con finales de línea de Linux o bash en Windows no los entiende. Una línea en ''.gitattributes'' lo garantiza para todos: ''*.sh text eol=lf''. ===== Dónde corren Maven y los tests ===== Las imágenes son de **ejecución**: el backend es un JRE sin Maven y el frontend es Nginx. Así que compilar y pasar tests se hace **en el host**, que para eso están instalados el JDK, Maven y Node: ^ Qué ^ Dónde ^ Contra qué ^ | ''mvn test'' (unitarios y ArchUnit) | ''backend/'' del worktree | Nada: no necesitan la aplicación levantada | | ''npm run test:api'' (API) | ''frontend/'' del worktree | ''%%http://localhost:$FRONTEND_PORT/api%%'' | | ''npm run test:e2e'' (E2E) | ''frontend/'' del worktree | ''%%http://localhost:$FRONTEND_PORT%%'' | Son los tres comandos de tests del proyecto. Maven y npm funcionan igual en Linux, Windows y Mac, y son lo que ejecutan el alumno, el agente y CI (tema 7). ''mvn test'' forma parte del build y corre en cada compilación. Los de API y los E2E **no forman parte del build**: se lanzan aparte, cuando se quiere, con los otros dos comandos. Los dos son de Playwright y atacan la aplicación levantada del worktree (tema 6). En el tema 6 la skill ''/test'' los envuelve con la gramática del tema 1 (''/test api run'') y explica lo que sale en rojo; CI los ejecuta tal cual. ==== npm run ==== ''npm run '' ejecuta un comando guardado en la sección ''scripts'' del ''package.json''. Es la forma de tener en el proyecto los comandos largos con un nombre corto, sin depender del sistema operativo: npm los ejecuta desde la carpeta del ''package.json'' y con los programas de ''node_modules/.bin'' en el PATH, así que ''playwright'' se escribe sin ''npx''. "scripts": { "start": "ng serve", "build": "ng build", "test:api": "docker compose down -v && docker compose up -d --wait && playwright test --project=api", "test:e2e": "docker compose down -v && docker compose up -d --wait && playwright test --project=e2e" } Los dos scripts resetean la base de datos del worktree, esperan a que la aplicación esté sana y pasan los tests de un proyecto de Playwright (''api'' o ''e2e'', tema 6). El ''&&'' lo entienden igual ''cmd'' y bash. ''npm run'' sin nombre lista los scripts que hay. ''start'' y ''build'' ya los trae Angular: son los que ejecutan ''npm start'' y ''npm run build''. Esto va en ''AGENTS.md'' tal cual, porque es lo que el agente ejecuta para comprobar su trabajo. Si no lo sabe, intentará ''mvn test'' dentro del contenedor y fallará, o ni probará. ===== Ejercicios ===== ==== Ejercicio 1 ==== * Crea un issue «Aplicación en Docker» e inícialo con ''/git-flow''. * Pide al agente los dos ''Dockerfile'', ''compose.yaml'', ''compose.override.yaml'', ''frontend/nginx.conf'' y ''frontend/proxy.conf.json'' siguiendo este tema. Revisa que el backend tiene la caché de ''.m2'', que ''db'' no publica puertos en ''compose.yaml'' y que ''nginx.conf'' tiene el ''try_files''. * Añade ''.env'' al ''.gitignore'' y crea uno a mano en el worktree con ''COMPOSE_PROJECT_NAME'', los tres puertos y ''DB_PASSWORD''. * ''docker network create nginx-proxy-network'' y ''docker compose up -d --build''. Comprueba con ''docker compose ps'' que los tres servicios están //healthy//. * Entra en ''%%http://localhost:%%'' y comprueba que se ve el ''/api/hola''. ==== Ejercicio 2 ==== * En el mismo worktree, pide al agente que añada JPA, MariaDB y Flyway, la migración ''%%V1__clientes.sql%%'', un ''GET /api/clientes'' y una página de Angular que liste los clientes. * Crea ''application-datos-prueba.properties'' y ''db/datos-prueba/%%R__clientes.sql%%'' con cinco clientes. * ''docker compose up -d --build backend'' y comprueba que la lista muestra los cinco clientes. * Arranca con ''PERFIL=default'' tras un ''down -v'' y comprueba que la lista sale vacía. Vuelve a dejarlo con datos. * Cambia un texto de la página de Angular, guarda y comprueba que el navegador se recarga solo. ==== Ejercicio 3 ==== * Añade a ''application.properties'' la URL con ''${DB_PORT:3306}'' y ejecuta ''mvn test'' desde ''backend/''. Tiene que pasar sin nada levantado. * Escribe en ''AGENTS.md'': que la aplicación se opera con ''./app.sh start | stop | status | logs'', que los tests se lanzan en el host con ''mvn test'', ''npm run test:api'' y ''npm run test:e2e'', y que el esquema va siempre en migraciones. * ''/git-flow commit'' y ''/git-flow finish''. ==== Ejercicio 4 ==== * Escribe ''app.sh'' y el ''.gitattributes''. Pruébalos: arrancar, cambiar un texto del backend, volver a arrancar y ver el cambio; parar, arrancar y ver que los datos siguen; parar con ''-v'', arrancar y ver los datos de prueba de nuevo. * Amplía ''/git-flow'': ''start'' escribe el ''.env'' del worktree buscando puertos libres y dice cómo arrancar; ''finish'' ejecuta ''./app.sh stop -v'' antes de borrar el worktree. * Finaliza el issue. ==== Ejercicio 5 ==== * Crea dos issues cualesquiera, inícialos y arranca los dos con ''./app.sh start''. Comprueba que cada worktree tiene su ''.env'' con puertos distintos y que las dos aplicaciones están levantadas a la vez (''docker ps''). * Cambia los datos de prueba en uno de los dos y comprueba que el otro no se entera. * Finaliza los dos y comprueba con ''docker ps -a'' y ''docker volume ls'' que no queda nada de ellos.