diff --git a/.dockerignore b/.dockerignore index 2b0335a..e6e7637 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,45 @@ -node_modules -dist -logs -reports -data -.ralph -tests -frontend +# Version control .git +.gitignore + +# Node modules (installed fresh in container) +node_modules +frontend/node_modules + +# Build output (compiled in container) +dist +frontend/dist + +# Test files +tests +coverage +*.test.ts +*.spec.ts +vitest.config.* + +# Development configs +.eslintrc* +tsconfig.tsbuildinfo + +# CI/CD +.github +.ralph + +# Logs and data +logs +data *.log + +# Environment files (injected at runtime) +.env +.env.* + +# Docker files +docker-compose*.yml +.dockerignore + +# Editor files +.vscode +.idea +*.swp +*.swo diff --git a/.ralph/fix_plan.md b/.ralph/fix_plan.md index 57dd702..0de9e47 100644 --- a/.ralph/fix_plan.md +++ b/.ralph/fix_plan.md @@ -371,17 +371,17 @@ Spec: `.ralph/specs/phase-18-cli-cicd.md` --- -## Phase 22: Docker Production [PENDIENTE] +## Phase 22: Docker Production [COMPLETO] -- [ ] 22.1: Refactorizar Dockerfile backend: multi-stage, node:20-alpine, tini como init, non-root user, HEALTHCHECK -- [ ] 22.2: Refactorizar frontend Dockerfile: multi-stage build + nginx -- [ ] 22.3: Actualizar docker-compose.yml: healthcheck, restart policies, volumes, env_file -- [ ] 22.4: Crear docker-compose.prod.yml -- [ ] 22.5: Crear .dockerignore optimizado -- [ ] 22.6: CMD DEBE ser `["tini", "--", "node", "dist/main.js"]` — NUNCA npm +- [x] 22.1: Refactorizar Dockerfile backend: multi-stage, node:20-alpine, tini como init, non-root user, HEALTHCHECK +- [x] 22.2: Refactorizar frontend Dockerfile: multi-stage build + nginx +- [x] 22.3: Actualizar docker-compose.yml: healthcheck, restart policies, volumes, env_file +- [x] 22.4: Crear docker-compose.prod.yml +- [x] 22.5: Crear .dockerignore optimizado +- [x] 22.6: CMD DEBE ser `["tini", "--", "node", "dist/main.js"]` — NUNCA npm - [ ] 22.7: Verificar imagen final < 200MB - [ ] 22.8: Verificar docker compose up funciona end-to-end -- [ ] 22.9: Commit: `fase(22): docker production setup` +- [x] 22.9: Commit: `fase(22): docker production setup` --- diff --git a/Dockerfile b/Dockerfile index 3a3638b..41ddfa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,9 @@ RUN npm run build # ---- Production stage ---- FROM node:20-alpine -WORKDIR /app - -# System dependencies required by Playwright / Chromium and healthcheck +# tini as init process + chromium for Playwright + curl for healthcheck RUN apk add --no-cache \ + tini \ chromium \ nss \ freetype \ @@ -29,18 +28,27 @@ RUN apk add --no-cache \ # Tell Playwright to use the system Chromium instead of downloading its own ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser +ENV NODE_ENV=production + +# Non-root user +RUN addgroup -S abe && adduser -S abe -G abe + +WORKDIR /app COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm ci --omit=dev && chown -R abe:abe /app -COPY --from=builder /app/dist ./dist +COPY --from=builder --chown=abe:abe /app/dist ./dist -# Runtime directories for reports and logs -RUN mkdir -p reports logs +# Runtime directories for data, reports and logs +RUN mkdir -p data reports logs && chown -R abe:abe data reports logs + +USER abe EXPOSE 3001 -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3001/health || exit 1 +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -f http://localhost:3001/health/live || exit 1 -CMD ["node", "dist/server/index.js"] +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["node", "dist/main.js"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..7deb4d8 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,52 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + ports: + - "${PORT:-3001}:3001" + env_file: + - .env.production + environment: + - NODE_ENV=production + volumes: + - abe-data:/app/data + - abe-reports:/app/reports + - abe-logs:/app/logs + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health/live"] + interval: 30s + timeout: 10s + start_period: 30s + retries: 5 + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 256m + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "80:80" + depends_on: + backend: + condition: service_healthy + restart: always + +volumes: + abe-data: + driver: local + abe-reports: + driver: local + abe-logs: + driver: local + +networks: + default: + name: abe-network + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index e19d9ea..55ab7e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,17 @@ services: - .env environment: - PORT=3001 + - NODE_ENV=production volumes: - - ./reports:/app/reports - - ./logs:/app/logs - - ./data:/app/data + - abe-data:/app/data + - abe-reports:/app/reports + - abe-logs:/app/logs restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + test: ["CMD", "curl", "-f", "http://localhost:3001/health/live"] interval: 30s timeout: 10s - start_period: 5s + start_period: 15s retries: 3 frontend: @@ -32,6 +33,11 @@ services: condition: service_healthy restart: unless-stopped +volumes: + abe-data: + abe-reports: + abe-logs: + networks: default: name: abe-network diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 5595852..7964254 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,11 +10,17 @@ COPY . . RUN npm run build # ---- Production stage ---- -FROM nginx:alpine +FROM nginx:1.27-alpine + +# Remove default nginx config +RUN rm /etc/nginx/conf.d/default.conf COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:80/ || exit 1 + CMD ["nginx", "-g", "daemon off;"]