diff --git a/README.md b/README.md index 8c54b962..956de442 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,21 @@ python -c "from core.database import init_db; init_db()" - **Frontend**: http://localhost:5173 - **Keycloak Admin**: http://localhost:8080 +## 🛠️ Produccion + +``` +docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend +docker build -t dev.aduanasoft.com/anexo76/frontend:latest -f ./frontend/Dockerfile.prod ./frontend +``` + +Publica en el registro (ajusta el registry si corresponde): + +``` +docker login dev.aduanasoft.com +docker push dev.aduanasoft.com/anexo76/backend:latest +docker push dev.aduanasoft.com/anexo76/frontend:latest +``` + ## 🛠️ Desarrollo Local ### Backend diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..8a6b8f8e --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,260 @@ +services: + # PostgreSQL - Base de datos core (app) + postgres-a76: + image: postgres:18-alpine + container_name: anexo76-postgres-a76 + environment: + POSTGRES_DB: anexo76_core + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8" + ports: + - "5432:5432" + volumes: + - postgres_app_data:/var/lib/postgresql/data + - ./scripts/postgres-app-entrypoint.sh:/docker-entrypoint-initdb.d/init-app.sh:ro + networks: + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d anexo76_core || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 20s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + shm_size: 128mb + + # PostgreSQL - Base de datos Keycloak + postgres-keycloak: + image: postgres:18-alpine + container_name: anexo76-postgres-keycloak + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8" + ports: + - "5433:5432" + volumes: + - postgres_keycloak_data:/var/lib/postgresql/data + - ./scripts/postgres-keycloak-entrypoint.sh:/docker-entrypoint-initdb.d/init-keycloak.sh:ro + networks: + - auth-net + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d keycloak || exit 1"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 20s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + shm_size: 128mb + + # Keycloak - Servidor de autenticación + keycloak: + image: quay.io/keycloak/keycloak:26.4 + container_name: anexo76-keycloak + environment: + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} + KC_DB: postgres + KC_DB_URL_HOST: postgres-keycloak + KC_DB_URL_PORT: "5432" + KC_DB_URL_DATABASE: keycloak + KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/keycloak + KC_DB_USERNAME: postgres + KC_DB_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + KC_DB_SCHEMA: public + KC_HOSTNAME: localhost + KC_HTTP_ENABLED: "true" + KC_HOSTNAME_STRICT: "false" + KC_HOSTNAME_STRICT_HTTPS: "false" + KC_PROXY_HEADERS: "xforwarded" + KC_HEALTH_ENABLED: "true" + KC_METRICS_ENABLED: "true" + KC_LOG_LEVEL: INFO + JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true" + command: + - start-dev + - --db=postgres + - --db-url-host=postgres-keycloak + - --db-url-port=5432 + - --db-url-database=keycloak + - --db-username=postgres + - --db-password=${POSTGRES_KEYCLOAK_PASSWORD:-postgres} + - --http-enabled=true + - --hostname-strict=false + - --proxy-headers=xforwarded + ports: + - "8080:8080" + - "9000:9000" + depends_on: + postgres-keycloak: + condition: service_healthy + volumes: + - keycloak_data:/opt/keycloak/data + networks: + - auth-net + - backend-net + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 90s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 768M + reservations: + memory: 512M + + # Backend - FastAPI + backend: + image: dev.aduanasoft.com/anexo76/backend:latest + container_name: anexo76-backend + environment: + - DEBUG=${DEBUG:-True} + - ENVIRONMENT=${ENVIRONMENT:-development} + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + - CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76} + - CORE_DB_PORT=${CORE_DB_PORT:-5432} + - CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core} + - CORE_DB_USER=${CORE_DB_USER:-postgres} + - CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres} + - KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080} + - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} + - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} + ports: + - "8000:8000" + depends_on: + postgres-a76: + condition: service_healthy + keycloak: + condition: service_healthy + volumes: + - ./backend:/app + - backend_cache:/app/__pycache__ + - ./scripts/backend-entrypoint.sh:/entrypoint.sh:ro + networks: + - backend-net + - frontend-net + restart: unless-stopped + entrypoint: ["/entrypoint.sh"] + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload", "--log-level", "info"] + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + + # Frontend - SvelteKit + frontend: + image: dev.aduanasoft.com/anexo76/frontend:latest + container_name: anexo76-frontend + environment: + - NODE_ENV=${NODE_ENV:-development} + - VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/} + - INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/} + - VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080} + - VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master} + - VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend} + - KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080} + - KEYCLOAK_REALM=${KEYCLOAK_REALM:-master} + - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend} + - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9} + ports: + - "5173:5173" + depends_on: + backend: + condition: service_healthy + entrypoint: ["/frontend-entrypoint.sh"] + volumes: + - ./frontend:/app + - frontend_node_modules:/app/node_modules + - ./scripts/frontend-entrypoint.sh:/frontend-entrypoint.sh:ro + networks: + - frontend-net + - auth-net + restart: unless-stopped + command: ["pnpm", "run", "dev", "--", "--host", "0.0.0.0"] + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5173/ || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 45s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + +volumes: + postgres_app_data: + driver: local + postgres_keycloak_data: + driver: local + keycloak_data: + driver: local + frontend_node_modules: + driver: local + backend_cache: + driver: local + +networks: + backend-net: + driver: bridge + auth-net: + driver: bridge + frontend-net: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c13ae35e..18215907 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -162,7 +162,7 @@ services: - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret} - CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000} ports: - - "5050:8000" + - "8000:8000" depends_on: postgres-a76: condition: service_healthy diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod new file mode 100644 index 00000000..1c5a55e6 --- /dev/null +++ b/frontend/Dockerfile.prod @@ -0,0 +1,56 @@ +# ========================== +# Etapa de build +# ========================== +FROM node:22-alpine AS build + +# Directorio de trabajo +WORKDIR /app + +# Configurar npm para trabajar con certificados autofirmados e instalar pnpm +RUN npm config set strict-ssl false && \ + npm install -g pnpm + +# Copiar archivos de dependencias +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +# Instalar dependencias con pnpm +RUN pnpm install --frozen-lockfile + +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} + +# Copiar el resto del código +COPY . . + +# Construir el proyecto +RUN pnpm run build + + +# ========================== +# Etapa de ejecución con Node.js +# ========================== +FROM node:22-alpine AS runtime + +WORKDIR /app + +# Crear usuario no-root para seguridad antes de copiar con --chown +RUN addgroup -g 1001 -S nodejs +RUN adduser -S svelte -u 1001 + +# Copiar solo archivos necesarios para producción y aplicar propietario en la copia +COPY --from=build --chown=svelte:nodejs /app/build ./build +COPY --from=build --chown=svelte:nodejs /app/package.json ./ +COPY --from=build --chown=svelte:nodejs /app/node_modules ./node_modules + +USER svelte + +# Puerto para SvelteKit con adapter-node +EXPOSE 3000 + +# Variables de entorno +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOST=0.0.0.0 + +# Ejecutar aplicación con Node.js +CMD ["node", "build"] \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 5845afea..af6b62ab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "keycloak-js": "^26.2.1", - "lucide-svelte": "^0.553.0" + "lucide-svelte": "^0.553.0", + "svelte-sonner": "^1.0.7" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 35a56541..bfafc5e5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: lucide-svelte: specifier: ^0.553.0 version: 0.553.0(svelte@5.40.2) + svelte-sonner: + specifier: ^1.0.7 + version: 1.0.7(svelte@5.40.2) devDependencies: '@eslint/compat': specifier: ^1.4.0 @@ -1675,6 +1678,11 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.28.0: + resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==} + peerDependencies: + svelte: ^5.7.0 + runed@0.35.1: resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} peerDependencies: @@ -1761,6 +1769,11 @@ packages: svelte: optional: true + svelte-sonner@1.0.7: + resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==} + peerDependencies: + svelte: ^5.0.0 + svelte-toolbelt@0.10.6: resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -3379,6 +3392,11 @@ snapshots: dependencies: queue-microtask: 1.2.3 + runed@0.28.0(svelte@5.40.2): + dependencies: + esm-env: 1.2.2 + svelte: 5.40.2 + runed@0.35.1(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2): dependencies: dequal: 2.0.3 @@ -3460,6 +3478,11 @@ snapshots: optionalDependencies: svelte: 5.40.2 + svelte-sonner@1.0.7(svelte@5.40.2): + dependencies: + runed: 0.28.0(svelte@5.40.2) + svelte: 5.40.2 + svelte-toolbelt@0.10.6(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2): dependencies: clsx: 2.1.1