Subida a producción como Acana

This commit is contained in:
David Arranz 2025-11-27 20:07:30 +01:00
parent 46379e8881
commit 1783f630cf
30 changed files with 489 additions and 75 deletions

View File

@ -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)

33
apps/server/.env.acana Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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({

View File

@ -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,

1
apps/web/.env.acana Normal file
View File

@ -0,0 +1 @@
VITE_API_SERVER_URL=https://acana.factuges.app/api/v1

1
apps/web/.env.rodax Normal file
View File

@ -0,0 +1 @@
VITE_API_SERVER_URL=https://factuges.rodax-software.local/api/v1

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -30,7 +30,6 @@ import {
ItemAmount,
ItemDiscount,
ItemQuantity,
ItemTaxes,
} from "../../../../domain";
/**

View File

@ -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"
}
}
}

View File

@ -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:*"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

16
scripts/Caddyfile Normal file
View File

@ -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
}

View File

@ -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 <company> [--api|web|all] [--load]
# ./build_factuges.sh <company> [--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 <company>"
echo "Uso: ./build_factuges.sh <company> [--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 <company>, 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

View File

@ -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 "-------------------------------------------------------"

View File

@ -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.

115
scripts/docker-compose.old Normal file
View File

@ -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:

133
scripts/docker-compose.yml Normal file
View File

@ -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:

14
scripts/stack.env Normal file
View File

@ -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