diff --git a/Dockerfile b/Dockerfile index de383aa7..6e5839c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # syntax=docker/dockerfile:1.7-labs +ARG COMPANY ARG NODE_IMAGE=node:24-bookworm-slim ARG PNPM_VERSION=10.20.0 @@ -7,6 +8,10 @@ ARG PNPM_VERSION=10.20.0 # 0) Base común ######################## FROM ${NODE_IMAGE} AS base + +ARG COMPANY +ENV COMPANY=${COMPANY} + ENV CI=1 \ NODE_ENV=production RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate @@ -38,7 +43,7 @@ FROM base AS builder COPY --from=pruner /repo/out/full/ ./ COPY --from=pruner /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml -COPY --from=pruner /repo/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates ./templates +#COPY --from=pruner /repo/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates ./templates # Reutilizamos la store prefetch #COPY --from=installer /root/.local/share/pnpm/store /root/.local/share/pnpm/store @@ -92,11 +97,11 @@ COPY --from=builder /repo/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=builder /repo/pnpm-workspace.yaml ./pnpm-workspace.yaml COPY --from=builder /repo/apps/server/dist ./apps/server/dist -COPY --from=builder /repo/apps/server/.env.production ./apps/server/dist/.env +COPY --from=builder /repo/apps/server/.env.${COMPANY} ./apps/server/dist/.env COPY --from=builder /repo/apps/server/node_modules ./apps/server/node_modules COPY --from=builder /repo/apps/server/package.json ./apps/server/package.json -COPY --from=builder /repo/templates ./apps/server/dist/templates +#COPY --from=builder /repo/templates ./apps/server/dist/templates # Salud del contenedor (ajusta puerto/endpoint) diff --git a/apps/server/.env.acana b/apps/server/.env.acana new file mode 100644 index 00000000..61efaa69 --- /dev/null +++ b/apps/server/.env.acana @@ -0,0 +1,33 @@ +COMPANY=acana +DOMAIN=acana.factuges.app + +#WEB_VERSION=v0.0.1-20251031-200910 +#API_IMAGE=factuges-server:rodax-v0.0.1 +API_PORT=3002 + + + +NODE_ENV=production +HOST=0.0.0.0 +PORT=3002 +FRONTEND_URL=https://aana.factuges.app + +DB_DIALECT=mysql +DB_USER=acana +DB_PASS=r@U8%GJ+2e/AWR +DB_NAME=factuges_acana +DB_ROOT_PASS=cQF#qRM*JU+tDyf +DB_PORT=3306 +DB_LOGGING=false +DB_SYNC_MODE=alter + +APP_TIMEZONE=Europe/Madrid +TRUST_PROXY=0 + +JWT_SECRET=supersecretkey +JWT_ACCESS_EXPIRATION=1h +JWT_REFRESH_EXPIRATION=7d + +PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome + +TEMPLATES_PATH=/opt/factuges/acana/templates \ No newline at end of file diff --git a/apps/server/.env.development b/apps/server/.env.development index 64ab9e10..9ac99ca9 100644 --- a/apps/server/.env.development +++ b/apps/server/.env.development @@ -9,7 +9,7 @@ DB_HOST=localhost DB_PORT=3306 DB_NAME=uecko_erp DB_USER=rodax -DB_PASSWORD=rodax +DB_PASS=rodax DB_LOGGING=false DB_SYNC_MODE=alter diff --git a/apps/server/.env.example b/apps/server/.env.example index fd4feeec..1c4076da 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -23,7 +23,7 @@ DB_HOST=localhost DB_PORT=3306 DB_NAME=uecko_erp DB_USER=rodax -DB_PASSWORD=rodax +DB_PASS=rodax # Log de Sequelize (true|false) DB_LOGGING=false diff --git a/apps/server/.env.production b/apps/server/.env.production index cec87282..a2d05074 100644 --- a/apps/server/.env.production +++ b/apps/server/.env.production @@ -22,7 +22,7 @@ DB_HOST=localhost DB_PORT=3306 DB_NAME=uecko_erp DB_USER=rodax -DB_PASSWORD=rodax +DB_PASS=rodax DB_LOGGING=false DB_SYNC_MODE=alter diff --git a/apps/server/package.json b/apps/server/package.json index 17bdeca3..66bc5da5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@erp/factuges-server", - "version": "0.0.13", + "version": "0.0.14", "private": true, "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", @@ -69,4 +69,4 @@ "engines": { "node": ">=22" } -} +} \ No newline at end of file diff --git a/apps/server/src/config/database.ts b/apps/server/src/config/database.ts index e11a96a2..b3c434c3 100644 --- a/apps/server/src/config/database.ts +++ b/apps/server/src/config/database.ts @@ -1,5 +1,7 @@ import { Sequelize } from "sequelize"; + import { logger } from "../lib/logger"; + import { ENV } from "./index"; /** @@ -73,11 +75,11 @@ function buildSequelize(): Sequelize { if (!ENV.DB_DIALECT) { throw new Error("DB_DIALECT is required when DATABASE_URL is not provided"); } - if (!ENV.DB_NAME || !ENV.DB_USER) { + if (!(ENV.DB_NAME && ENV.DB_USER)) { throw new Error("DB_NAME and DB_USER are required when DATABASE_URL is not provided"); } - return new Sequelize(ENV.DB_NAME, ENV.DB_USER, ENV.DB_PASSWORD, { + return new Sequelize(ENV.DB_NAME, ENV.DB_USER, ENV.DB_PASS, { host: ENV.DB_HOST, port: ENV.DB_PORT, dialect: ENV.DB_DIALECT, diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts index 8e86fc00..5ff878ab 100644 --- a/apps/server/src/config/index.ts +++ b/apps/server/src/config/index.ts @@ -29,7 +29,7 @@ const DB_HOST = process.env.DB_HOST ?? "localhost"; const DB_PORT = asNumber(process.env.DB_PORT, 3306); const DB_NAME = process.env.DB_NAME ?? ""; const DB_USER = process.env.DB_USER ?? ""; -const DB_PASSWORD = process.env.DB_PASSWORD ?? ""; +const DB_PASS = process.env.DB_PASS ?? ""; const DB_LOGGING = asBoolean(process.env.DB_LOGGING, false); @@ -57,7 +57,7 @@ export const ENV = { DB_PORT, DB_NAME, DB_USER, - DB_PASSWORD, + DB_PASS, DB_LOGGING, DB_SYNC_MODE, APP_TIMEZONE, diff --git a/apps/server/src/health.ts b/apps/server/src/health.ts index 859f4412..7f0057c9 100644 --- a/apps/server/src/health.ts +++ b/apps/server/src/health.ts @@ -1,4 +1,4 @@ -import { Application, Request, Response } from "express"; +import type { Application, Request, Response } from "express"; import { DateTime } from "luxon"; /** @@ -8,9 +8,13 @@ Registra endpoints de liveness/readiness. /__ready : 200 si ready=true, 503 en caso contrario. */ -export function registerHealthRoutes(app: Application, getStatus: () => { ready: boolean }): void { +export function registerHealthRoutes( + app: Application, + baseRoutePath: string, + getStatus: () => { ready: boolean } +): void { // Liveness probe: indica que el proceso responde - app.get("/__health", (_req: Request, res: Response) => { + app.get(`${baseRoutePath}/__health`, (_req: Request, res: Response) => { // Información mínima y no sensible res.status(200).json({ status: "ok", @@ -19,7 +23,7 @@ export function registerHealthRoutes(app: Application, getStatus: () => { ready: }); // Readiness probe: indica que el servidor está listo para tráfico - app.get("/__ready", (_req: Request, res: Response) => { + app.get(`${baseRoutePath}/__ready`, (_req: Request, res: Response) => { const { ready } = getStatus(); if (ready) { return res.status(200).json({ diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index b7c0aba6..a6b1cf7c 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -120,9 +120,6 @@ registerModules(); const app = createApp(); -// ➕ Rutas de salud disponibles desde el inicio del proceso -registerHealthRoutes(app, () => ({ ready: isReady })); - // Crea el servidor HTTP const server = http.createServer(app); @@ -235,6 +232,9 @@ process.on("uncaughtException", async (error: Error) => { // initStructure(sequelizeConn.connection); // insertUsers(); + // ➕ Rutas de salud disponibles desde el inicio del proceso + registerHealthRoutes(app, API_BASE_PATH, () => ({ ready: isReady })); + await initModules({ app, database, diff --git a/apps/web/.env.acana b/apps/web/.env.acana new file mode 100644 index 00000000..450385fe --- /dev/null +++ b/apps/web/.env.acana @@ -0,0 +1 @@ +VITE_API_SERVER_URL=https://acana.factuges.app/api/v1 diff --git a/apps/web/.env.rodax b/apps/web/.env.rodax new file mode 100644 index 00000000..ddae38ec --- /dev/null +++ b/apps/web/.env.rodax @@ -0,0 +1 @@ +VITE_API_SERVER_URL=https://factuges.rodax-software.local/api/v1 diff --git a/apps/web/package.json b/apps/web/package.json index af791688..13a8143f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,11 +1,13 @@ { "name": "@erp/factuges-web", "private": true, - "version": "0.0.13", + "version": "0.0.14", "type": "module", "scripts": { "dev": "vite --host --clearScreen false", "build": "tsc && vite build", + "build:rodax": "tsc && vite build --mode rodax", + "build:acana": "tsc && vite build --mode acana", "preview": "vite preview", "clean": "rm -rf dist && rm -rf node_modules && rm -rf .turbo", "check:deps": "pnpm exec depcheck", @@ -48,4 +50,4 @@ "tw-animate-css": "^1.2.9", "vite-plugin-html": "^3.2.2" } -} +} \ No newline at end of file diff --git a/modules/auth/package.json b/modules/auth/package.json index 26cf2ae1..b8295f1d 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,15 +1,13 @@ { "name": "@erp/auth", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" }, - "exports": { ".": "./src/common/index.ts", "./api": "./src/api/index.ts", @@ -39,4 +37,4 @@ "react-router-dom": "^6.26.0", "react-secure-storage": "^1.3.2" } -} +} \ No newline at end of file diff --git a/modules/core/package.json b/modules/core/package.json index 2f0078b1..9955ae41 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, @@ -54,4 +54,4 @@ "sequelize": "^6.37.5", "zod": "^4.1.11" } -} +} \ No newline at end of file diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 90628187..88df6c2e 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, @@ -63,4 +63,4 @@ "sequelize": "^6.37.5", "zod": "^4.1.11" } -} +} \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts index 25e1c31e..1efc0b89 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts @@ -30,7 +30,6 @@ import { ItemAmount, ItemDiscount, ItemQuantity, - ItemTaxes, } from "../../../../domain"; /** diff --git a/modules/customers/package.json b/modules/customers/package.json index c157c5f5..31f61e48 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customers", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, @@ -51,4 +51,4 @@ "use-debounce": "^10.0.5", "zod": "^4.1.11" } -} +} \ No newline at end of file diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json index f28f9bb2..a4ae4404 100644 --- a/modules/doc-numbering/package.json +++ b/modules/doc-numbering/package.json @@ -1,10 +1,9 @@ { "name": "@erp/doc-numbering", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" @@ -30,4 +29,4 @@ "@repo/rdx-utils": "workspace:*", "@repo/rdx-logger": "workspace:*" } -} +} \ No newline at end of file diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index d5d18db9..0abd0910 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,20 +1,17 @@ { "name": "@repo/rdx-criteria", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" }, - "exports": { ".": "./src/defaults.ts", "./server": "./src/index.ts" }, - "devDependencies": { "@repo/typescript-config": "workspace:*", "rimraf": "^6.0.0", @@ -23,4 +20,4 @@ "dependencies": { "sequelize": "^6.37.5" } -} +} \ No newline at end of file diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index 0b89c72e..8061da5f 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,19 +1,16 @@ { "name": "@repo/rdx-ddd", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" }, - "exports": { ".": "./src/index.ts" }, - "devDependencies": { "@repo/typescript-config": "workspace:*", "@types/node": "^22.15.12", @@ -27,4 +24,4 @@ "shallow-equal-object": "^1.1.1", "zod": "^4.1.11" } -} +} \ No newline at end of file diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index aeb5fdf7..b234a0aa 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,19 +1,16 @@ { "name": "@repo/rdx-logger", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" }, - "exports": { ".": "./src/index.ts" }, - "devDependencies": { "typescript": "^5.9.3" }, @@ -22,4 +19,4 @@ "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" } -} +} \ No newline at end of file diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index 4b9a9a7c..d326dfe6 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,19 +1,16 @@ { "name": "@repo/rdx-utils", - "version": "0.0.13", + "version": "0.0.14", "private": true, "type": "module", "sideEffects": false, - "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit", "clean": "rimraf .turbo node_modules dist" }, - "exports": { ".": "./src/index.ts" }, - "devDependencies": { "@repo/typescript-config": "workspace:*", "@types/node": "^22.15.12", @@ -23,4 +20,4 @@ "joi": "^17.13.3", "uuid": "^11.0.5" } -} +} \ No newline at end of file diff --git a/scripts/Caddyfile b/scripts/Caddyfile new file mode 100644 index 00000000..7b58e2bd --- /dev/null +++ b/scripts/Caddyfile @@ -0,0 +1,16 @@ +{ + email soporte@rodax-software.com + auto_https disable_redirects +} + + +https://presupuestos.uecko.com:13001 { + reverse_proxy backend:3001 + encode gzip # Comprime las respuestas con gzip +} + +:443 { + root * /srv + file_server + try_files {path} /index.html # Esto asegura que las rutas en React funcionen correctamente +} \ No newline at end of file diff --git a/scripts/build-api.sh b/scripts/build-factuges.sh similarity index 77% rename from scripts/build-api.sh rename to scripts/build-factuges.sh index cde3469a..268e96c3 100755 --- a/scripts/build-api.sh +++ b/scripts/build-factuges.sh @@ -1,21 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -SCRIPT_VERSION="1.0.5" +SCRIPT_VERSION="1.2.0" # ===================================================== # FACTUGES Build Script # ----------------------------------------------------- -# Build + Export de la API y/o -# compilación de la Web (por compañía) +# Build + Export de la API y/o WEB (por compañía) # ===================================================== # Uso: -# ./scripts/build-api.sh [--api|web|all] [--load] +# ./build_factuges.sh [--api|web|all] [--load] # # Ejemplos: -# ./scripts/build-api.sh acme --api --load # solo API + carga en Docker local -# ./scripts/build-api.sh acme --web # solo web -# ./scripts/build-api.sh acme # API + web (por defecto) +# ./build_factuges.sh acme --api --load # solo API + carga en Docker local +# ./build_factuges.sh acme --web # solo web +# ./build_factuges.sh acme # API + web (por defecto) # # Funcionalidades: # - Detecta automáticamente el nombre, versión y puerto de la API @@ -27,12 +26,26 @@ SCRIPT_VERSION="1.0.5" # ===================================================== # --- Configuración base --- -COMPANY="${1:-}" +COMPANY="" MODE="all" # api | web | all LOAD=false +# --- Validar que el primer argumento existe y no es un flag --- +if [[ $# -eq 0 || "$1" == --* ]]; then + echo "❌ ERROR: Falta el parámetro " + echo "Uso: ./build_factuges.sh [--api|--web|--all] [--load]" + echo "Ejemplos:" + echo " ./build_factuges.sh acme --api" + echo " ./build_factuges.sh acme --web" + exit 1 +fi + +COMPANY="$1" + # --- Parseo de flags --- -for arg in "${@:2}"; do +shift # quitamos el , ahora solo quedan flags + +for arg in "$@"; do case "$arg" in --api) MODE="api" ;; --web) MODE="web" ;; @@ -51,7 +64,12 @@ OUT_API_DIR="${PROJECT_DIR}/out/${COMPANY}/api" OUT_WEB_DIR="${PROJECT_DIR}/out/${COMPANY}/web" if [[ -z "$COMPANY" ]]; then - echo "❌ Error: debes indicar la compañía. Ejemplo: ./scripts/build-api.sh acme [--api|--web|--all] [--load]" + echo "❌ Error: debes indicar la compañía. Ejemplo: ./build_factuges.sh acme [--api|--web|--all] [--load]" + exit 1 +fi + +if [[ $COMPANY =~ --.* ]]; then + echo "❌ Error: debes indicar la compañía. Ejemplo: ./build_factuges.sh acme [--api|--web|--all] [--load]" exit 1 fi @@ -94,8 +112,6 @@ rm -rf "${OUT_API_DIR:?}/"* rm -rf "${OUT_WEB_DIR:?}/"* -echo "" -echo "" echo "" echo "-------------------------------------------------------" echo " FACTUGES Build Script v${SCRIPT_VERSION}" @@ -120,7 +136,7 @@ if [[ "$MODE" == "web" || "$MODE" == "all" ]]; then # Puedes pasar variables específicas por compañía # Ejemplo: VITE_COMPANY=acme VITE_API_BASE=https://acme.localhost/api - VITE_COMPANY="${COMPANY}" pnpm build + VITE_COMPANY="${COMPANY}" pnpm build:${COMPANY} # Carpeta versionada VERSION_DIR="${OUT_WEB_DIR}/versions/v${WEB_VERSION}-${DATE}" @@ -156,11 +172,14 @@ fi # 2️⃣ API # ===================================================== if [[ "$MODE" == "api" || "$MODE" == "all" ]]; then + # Recopilar plantillas + ${SCRIPT_DIR}/build-templates.sh + cd "${PROJECT_DIR}" echo "🐳 Construyendo imagen Docker..." docker build --no-cache --debug -t "${IMAGE_TAG_V}" -t "${IMAGE_TAG_LATEST}" \ - --build-arg PORT="${PORT}" \ + --build-arg PORT="${PORT}" --build-arg COMPANY="${COMPANY}" \ -f "${PROJECT_DIR}/Dockerfile" "${PROJECT_DIR}" echo "✅ Imagen Docker construida correctamente" @@ -199,9 +218,14 @@ EOF echo "📦 API manifest generado en ${OUT_API_DIR}/manifest-v${API_VERSION}-${DATE}.json" if [[ "$LOAD" == true ]]; then - echo "📥 Cargando imagen en Docker local..." - docker load -i "${TAR_FILE_V}" - echo "✅ Imagen cargada en Docker local" + echo "📥 Cargando imagen en producción vps-2.rodax-software.com..." + [[ "$MODE" == "web" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_WEB_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/ + [[ "$MODE" == "api" || "$MODE" == "all" ]] && scp -r -P 49152 ${OUT_API_DIR} rodax@vps-2.rodax-software.com:/opt/factuges/${COMPANY}/ + + [[ "$MODE" == "api" || "$MODE" == "all" ]] && RESULT=$(ssh -p 49152 rodax@vps-2.rodax-software.com "docker load -i /opt/factuges/${COMPANY}/api/${TAR_FILE_LATEST}") + [[ "$MODE" == "api" || "$MODE" == "all" ]] && echo $RESULT + #docker load -i "${TAR_FILE_V}" + echo "✅ Imagen cargada en producción" fi fi diff --git a/scripts/build-templates.sh b/scripts/build-templates.sh index b7cd07ee..13726622 100755 --- a/scripts/build-templates.sh +++ b/scripts/build-templates.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash - -# Fail fast set -euo pipefail +echo "-------------------------------------------------------" +echo "[build-templates] Recopilando plantillas del proyecto..." + # Root directory (dir where the script lives, then go up) ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -28,4 +29,5 @@ for module in "$SOURCE_DIR"/*; do fi done -echo "[build-templates] Completed." +echo "✅ [build-templates] Terminado." +echo "-------------------------------------------------------" \ No newline at end of file diff --git a/scripts/docker-compose.caddymanager.yml b/scripts/docker-compose.caddymanager.yml new file mode 100644 index 00000000..0fc80b1e --- /dev/null +++ b/scripts/docker-compose.caddymanager.yml @@ -0,0 +1,78 @@ +services: + # MongoDB database for persistent storage (optional - SQLite is used by default) + mongodb: + image: mongo:8.0 + container_name: caddymanager-mongodb + restart: unless-stopped + environment: + - MONGO_INITDB_ROOT_USERNAME=mongoadmin + - MONGO_INITDB_ROOT_PASSWORD=someSecretPassword # Change for production! + ports: + - "27017:27017" # Expose for local dev, remove for production + volumes: + - mongodb_data:/data/db + networks: + - caddymanager + profiles: + - mongodb # Use 'docker-compose --profile mongodb up' to include MongoDB + + # Backend API server + backend: + image: caddymanager/caddymanager-backend:latest + container_name: caddymanager-backend + restart: unless-stopped + environment: + - PORT=3000 + # Database Engine Configuration (defaults to SQLite) + - DB_ENGINE=sqlite # Options: 'sqlite' or 'mongodb' + # SQLite Configuration (used when DB_ENGINE=sqlite) + - SQLITE_DB_PATH=/app/data/caddymanager.sqlite + # MongoDB Configuration (used when DB_ENGINE=mongodb) + - MONGODB_URI=mongodb://mongoadmin:someSecretPassword@mongodb:27017/caddymanager?authSource=admin + - CORS_ORIGIN=http://localhost:80 + - LOG_LEVEL=debug + - CADDY_SANDBOX_URL=http://localhost:2019 + - PING_INTERVAL=30000 + - PING_TIMEOUT=2000 + - AUDIT_LOG_MAX_SIZE_MB=100 + - AUDIT_LOG_RETENTION_DAYS=90 + - METRICS_HISTORY_MAX=1000 # Optional: max number of in-memory metric history snapshots to keep + - JWT_SECRET=your_jwt_secret_key_here # Change for production! + - JWT_EXPIRATION=24h + # Backend is now only accessible through frontend proxy + volumes: + - sqlite_data:/app/data # SQLite database storage + networks: + - caddymanager + + # Frontend web UI + frontend: + image: caddymanager/caddymanager-frontend:latest + container_name: caddymanager-frontend + restart: unless-stopped + depends_on: + - backend + environment: + - BACKEND_HOST=backend:3000 + - APP_NAME=Caddy Manager + - DARK_MODE=true + ports: + - "80:80" # Expose web UI + networks: + - caddymanager + +networks: + caddymanager: + driver: bridge + +volumes: + mongodb_data: # Only used when MongoDB profile is active + sqlite_data: # SQLite database storage + +# Notes: +# - SQLite is the default database engine - no additional setup required! +# - To use MongoDB instead, set DB_ENGINE=mongodb and start with: docker-compose --profile mongodb up +# - For production, use strong passwords and consider secrets management. +# - The backend uses SQLite by default, storing data in a persistent volume. +# - The frontend proxies all /api/* requests to the backend service. +# - Backend is not directly exposed - all API access goes through the frontend proxy. diff --git a/scripts/docker-compose.old b/scripts/docker-compose.old new file mode 100644 index 00000000..e65959ce --- /dev/null +++ b/scripts/docker-compose.old @@ -0,0 +1,115 @@ + +# ====================================================== +# STACK POR COMPAÑÍA +# - API Node.js +# - Web React (Nginx) +# - MariaDB +# - Integrado con Traefik +# ====================================================== + + +services: + # --- Base de datos MariaDB --- + db: + image: mariadb:lts-noble + container_name: factuges_${COMPANY}_db + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASS} + MARIADB_USER: ${DB_USER} + MARIADB_PASSWORD: ${DB_PASS} + MARIADB_DATABASE: ${DB_NAME} + volumes: + - /opt/factuges/${COMPANY}/volumes/db_data:/var/lib/mysql + networks: + - internal + - edge + ports: + - 3306:3306 + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 20s + timeout: 5s + retries: 10 + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: factuges_${COMPANY}_phpmyadmin + restart: always + environment: + PMA_HOST: db + PMA_USER: ${DB_USER} + PMA_PASSWORD: ${DB_PASS} + PMA_VERBOSES: "FactuGES Rodax" + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS} + UPLOAD_LIMIT: 64M + networks: + - internal + - edge + depends_on: + - db + ports: + - 8080:80 + labels: + traefik.enable: "true" + traefik.http.routers.factuges_rodax_phpmyadmin.rule: Host(`phpmyadmin.${DOMAIN}`) + traefik.http.routers.factuges_rodax_phpmyadmin.entrypoints: web + traefik.http.services.factuges_rodax_phpmyadmin.loadbalancer.server.port: "80" + + # --- API (imagen versionada generada por build-factuges.sh) --- + api: + image: ${API_IMAGE} + container_name: factuges_${COMPANY}_api + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: production + COMPANY: ${COMPANY} + PORT: ${SERVER_PORT:-3002} + DB_DIALECT: "mysql" + DB_HOST: "db" + DB_PORT: ${DB_PORT} + DB_NAME: ${DB_NAME} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASS} + FRONTEND_URL: ${FRONTEND_URL} + networks: + - internal + - edge + ports: + - ${SERVER_PORT:-3002}:${SERVER_PORT:-3002} + labels: + traefik.enable: "true" + traefik.http.routers.factuges_rodax_api.rule: Host(`${DOMAIN}`) && PathPrefix(`/api`) + traefik.http.routers.factuges_rodax_api.entrypoints: web + traefik.http.services.factuges_rodax_api.loadbalancer.server.port: "${API_PORT:-3002}" + + # --- Web estática (React compilado por build-factuges.sh) --- + web: + image: nginx:1.27-alpine + container_name: factuges_${COMPANY}_web + restart: unless-stopped + depends_on: + - api + volumes: + - /opt/factuges/${COMPANY}/web/versions/${WEB_VERSION}/dist:/usr/share/nginx/html:ro + networks: + - internal + - edge + labels: + traefik.enable: "true" + traefik.http.routers.factuges_rodax_web.rule: Host(`${DOMAIN}`) + traefik.http.routers.factuges_rodax_web.entrypoints: web + traefik.http.services.factuges_rodax_web.loadbalancer.server.port: "80" + +networks: + edge: + external: true # red pública manejada por Traefik + internal: + driver: bridge # red privada de la compañía + +volumes: + db_data: + \ No newline at end of file diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml new file mode 100644 index 00000000..baa011fc --- /dev/null +++ b/scripts/docker-compose.yml @@ -0,0 +1,133 @@ + +# ====================================================== +# STACK POR COMPAÑÍA +# - API Node.js +# - Web React (Nginx) +# - MariaDB +# - Integrado con Traefik +# ====================================================== + + +services: + # --- Base de datos MariaDB --- + db: + image: mariadb:lts-noble + container_name: factuges_${COMPANY}_db + restart: unless-stopped + environment: + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASS} + MARIADB_USER: ${DB_USER} + MARIADB_PASSWORD: ${DB_PASS} + MARIADB_DATABASE: ${DB_NAME} + volumes: + - /opt/factuges/${COMPANY}/volumes/db_data:/var/lib/mysql + networks: + - internal + - edge + ports: + - 3306:3306 + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 20s + timeout: 5s + retries: 10 + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: factuges_${COMPANY}_phpmyadmin + restart: always + environment: + PMA_HOST: db + PMA_USER: ${DB_USER} + PMA_PASSWORD: ${DB_PASS} + PMA_VERBOSES: "FactuGES Rodax" + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS} + UPLOAD_LIMIT: 64M + networks: + - internal + - edge + depends_on: + - db + ports: + - 8080:80 + labels: + traefik.enable: "true" + # Router + traefik.http.routers.${COMPANY}-phpmyadmin.rule: Host(`${PMA_DOMAIN}`) + traefik.http.routers.${COMPANY}-phpmyadmin.entrypoints: websecure + traefik.http.routers.${COMPANY}-phpmyadmin.tls.certresolver: cfresolver + # Servicio + traefik.http.services.${COMPANY}-phpmyadmin.loadbalancer.server.port: "80" + # Middleware: whitelist por IP + traefik.http.routers.${COMPANY}-phpmyadmin.middlewares: "${COMPANY}-phpmyadmin-ipwhitelist@docker" + traefik.http.middlewares.${COMPANY}-phpmyadmin-ipwhitelist.ipwhitelist.sourcerange: "79.116.183.41/32" + + # --- API (imagen versionada generada por build-factuges.sh) --- + api: + image: ${API_IMAGE} + container_name: factuges_${COMPANY}_api + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + NODE_ENV: production + COMPANY: ${COMPANY} + PORT: ${SERVER_PORT:-3002} + DB_DIALECT: "mysql" + DB_HOST: "db" + DB_PORT: ${DB_PORT} + DB_NAME: ${DB_NAME} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASS} + FRONTEND_URL: ${FRONTEND_URL} + networks: + - internal + - edge + ports: + - ${SERVER_PORT:-3002}:${SERVER_PORT:-3002} + labels: + traefik.enable: "true" + # Router + traefik.http.routers.${COMPANY}-api.rule: Host(`${API_DOMAIN}`) + traefik.http.routers.${COMPANY}-api.entrypoints: websecure + traefik.http.routers.${COMPANY}-api.tls.certresolver: cfresolver + # Servicio + traefik.http.services.${COMPANY}-api.loadbalancer.server.port: "${SERVER_PORT:-3002}" + + # --- Web estática (React compilado por build-factuges.sh) --- + web: + container_name: caddy + image: caddy:alpine + volumes: + - /opt/factuges/${COMPANY}/Caddyfile:/etc/caddy/Caddyfile # Monta el archivo de configuración + - caddy_data:/data # Almacena los certificados en este volumen + - caddy_config:/config # Configuración de Caddy + - /opt/factuges/${COMPANY}/web/latest/dist/:/srv + ports: + - 81:80 # Puerto HTTP (Caddy lo redirige automáticamente a HTTPS) + - 444:443 # Puerto HTTPS + - 13001:13001 # reverse proxy al backend + networks: + - internal + - edge + restart: on-failure + depends_on: + - api + labels: + traefik.enable: "true" + traefik.http.routers.factuges_rodax_web.rule: Host(`${DOMAIN}`) + traefik.http.routers.factuges_rodax_web.entrypoints: web + traefik.http.services.factuges_rodax_web.loadbalancer.server.port: "444" + +networks: + edge: + external: true # red pública manejada por Traefik + internal: + driver: bridge # red privada de la compañía + +volumes: + db_data: + caddy_data: + caddy_config: + \ No newline at end of file diff --git a/scripts/stack.env b/scripts/stack.env new file mode 100644 index 00000000..33954a8b --- /dev/null +++ b/scripts/stack.env @@ -0,0 +1,14 @@ +COMPANY=rodax +DOMAIN=rodax.factuges.rodax-software.local +FRONTEND_URL=rodax.factuges.rodax-software.local +WEB_VERSION=v0.0.4-latest +API_IMAGE=factuges-server:rodax-latest +SERVER_PORT=3002 +DB_HOST=db +DB_DIALECT=mysql +DB_PORT=3306 +DB_USER=rodax_usr +DB_PASS=supersecret +DB_NAME=rodax_db +DB_ROOT_PASS=verysecret +TRAEFIK_ENTRYPOINT=web \ No newline at end of file