feat: deploy en host LXC (nginx + systemd) y ajustes para mismo-origen
- deploy/nginx-host.conf: nginx del host sirve dist/ de las 4 apps y proxifica /api/ a 127.0.0.1:3000 (client_max_body_size 10m) - deploy/frasa-api.service: API Gateway como servicio systemd - landing: URLs relativas (/quote/, /client/, /pay/) - Dockerfiles de apps: ARG VITE_API_URL=/api para builds same-origin - docker-compose: NGINX_CONF parametrizado (nginx.http.conf sin SSL) - README: instrucciones para host LXC y nota de DDNS pendiente
This commit is contained in:
24
README.md
24
README.md
@@ -18,7 +18,20 @@ Cada app es un proyecto Vite independiente: `npm install && npm run dev` (o `npm
|
||||
|
||||
Variables de entorno: copiar `.env.example` a `.env` y completar (el `.env` nunca se commitea).
|
||||
|
||||
## Deploy (servidor Ubuntu + Docker)
|
||||
## Servidor actual (LXC sin Docker)
|
||||
|
||||
Esta máquina es un contenedor LXC sin nesting: Docker no puede correr contenedores.
|
||||
El stack corre directo en el host:
|
||||
|
||||
- **NGINX** (apt) sirve los `dist/` de las apps y proxifica `/api/` → `127.0.0.1:3000`.
|
||||
Config: `deploy/nginx-host.conf` → `/etc/nginx/sites-available/frasa`.
|
||||
- **API** como servicio systemd: `deploy/frasa-api.service` → `frasa-api.service`
|
||||
(`systemctl status frasa-api`, logs en `/var/log/frasa-api.log`).
|
||||
- Rebuild de apps tras cambios: `VITE_API_URL=/api npm run build` en cada app + `systemctl reload nginx` (no requiere restart).
|
||||
|
||||
El `docker-compose.yml` queda listo para cuando el stack se mude a una VM con Docker.
|
||||
|
||||
## Deploy con Docker (VM futura)
|
||||
|
||||
```bash
|
||||
git clone https://git.consultoria-as.com/consultoria-as/FRASA-CAS.git /opt/frasa-ecosystem
|
||||
@@ -34,13 +47,12 @@ docker compose logs -f api
|
||||
curl https://frasadesarrollos.ddns.net/api/health
|
||||
```
|
||||
|
||||
Nota primer arranque: mientras no existan los certs, comenta el bloque `server 443` de
|
||||
`nginx/nginx.conf` (o usa un cert autofirmado temporal); después de expedir, restaura y
|
||||
Modo local sin SSL (LAN): `.env` con `NGINX_CONF=nginx.http.conf` hace que NGINX
|
||||
sirva todo por HTTP en el puerto 80. Cuando el DDNS apunte a este servidor:
|
||||
`certbot certonly --webroot -w /var/www/certbot -d frasadesarrollos.ddns.net`,
|
||||
quita `NGINX_CONF` del `.env` (vuelve `nginx.conf` con 443) y
|
||||
`docker compose restart nginx`. Renovación: `certbot renew --deploy-hook "docker compose -f /opt/frasa-ecosystem/docker-compose.yml restart nginx"`.
|
||||
|
||||
Antes del deploy, cambiar en `apps/landing/src/data/modulos.ts` las URLs absolutas de
|
||||
preview por rutas relativas (`/quote/`, `/client/`, `/pay/`) y rehacer el build.
|
||||
|
||||
Backup: `scripts/backup.sh` (cron sugerido `17 2 * * *`).
|
||||
|
||||
## Monday.com
|
||||
|
||||
@@ -4,6 +4,8 @@ WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
RUN npm run build
|
||||
|
||||
# Etapa 2: servidor estático
|
||||
|
||||
@@ -4,6 +4,8 @@ WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
RUN npm run build
|
||||
|
||||
# Etapa 2: servidor estático
|
||||
|
||||
@@ -4,6 +4,9 @@ WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
# URL del API horneada en el build (detras de NGINX es same-origin)
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=$VITE_API_URL
|
||||
RUN npm run build
|
||||
|
||||
# Etapa 2: servidor estático
|
||||
|
||||
@@ -7,20 +7,19 @@ describe('MODULOS', () => {
|
||||
expect(MODULOS.map((m) => m.id)).toEqual(['cotizador', 'alta-clientes', 'cobranza'])
|
||||
})
|
||||
|
||||
it('cada módulo tiene nombre, descripción y URL de preview válida', () => {
|
||||
it('cada módulo tiene nombre, descripción y URL válida', () => {
|
||||
for (const modulo of MODULOS) {
|
||||
expect(modulo.nombre.length).toBeGreaterThan(0)
|
||||
expect(modulo.descripcion.length).toBeGreaterThan(0)
|
||||
expect(() => new URL(modulo.url)).not.toThrow()
|
||||
expect(new URL(modulo.url).protocol).toBe('http:')
|
||||
expect(modulo.url).toMatch(/^\/[a-z]+\/$/)
|
||||
}
|
||||
})
|
||||
|
||||
it('apunta a los puertos y prefijos de preview correctos', () => {
|
||||
it('apunta a los prefijos correctos (same-origin detrás de NGINX)', () => {
|
||||
const urls = Object.fromEntries(MODULOS.map((m) => [m.id, m.url]))
|
||||
expect(urls['cotizador']).toBe('http://192.168.10.123:3001/quote/')
|
||||
expect(urls['alta-clientes']).toBe('http://192.168.10.123:3002/client/')
|
||||
expect(urls['cobranza']).toBe('http://192.168.10.123:3003/pay/')
|
||||
expect(urls['cotizador']).toBe('/quote/')
|
||||
expect(urls['alta-clientes']).toBe('/client/')
|
||||
expect(urls['cobranza']).toBe('/pay/')
|
||||
})
|
||||
|
||||
it('no hay ids ni URLs duplicadas', () => {
|
||||
|
||||
@@ -5,25 +5,24 @@ export interface Modulo {
|
||||
url: string
|
||||
}
|
||||
|
||||
// TODO: tras montar el NGINX de producción cambiar las URLs absolutas de
|
||||
// preview por rutas relativas '/quote/', '/client/' y '/pay/'.
|
||||
// Rutas relativas: la landing se sirve desde el mismo NGINX que las apps.
|
||||
export const MODULOS: Modulo[] = [
|
||||
{
|
||||
id: 'cotizador',
|
||||
nombre: 'Cotizador',
|
||||
descripcion: 'Propuestas comerciales y PDF en tiempo real',
|
||||
url: 'http://192.168.10.123:3001/quote/',
|
||||
url: '/quote/',
|
||||
},
|
||||
{
|
||||
id: 'alta-clientes',
|
||||
nombre: 'Alta de clientes',
|
||||
descripcion: 'Expediente digital, firmas y formatos PLD',
|
||||
url: 'http://192.168.10.123:3002/client/',
|
||||
url: '/client/',
|
||||
},
|
||||
{
|
||||
id: 'cobranza',
|
||||
nombre: 'Cobranza',
|
||||
descripcion: 'Planes de enganche, vencidos y estados de cuenta',
|
||||
url: 'http://192.168.10.123:3003/pay/',
|
||||
url: '/pay/',
|
||||
},
|
||||
]
|
||||
|
||||
17
deploy/frasa-api.service
Normal file
17
deploy/frasa-api.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=FRASA API Gateway
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/root/frasa-ecosystem/api-gateway
|
||||
EnvironmentFile=/root/frasa-ecosystem/.env
|
||||
Environment=PATH=/root/tools/node22/bin:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/root/tools/node22/bin/node dist/index.js
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=append:/var/log/frasa-api.log
|
||||
StandardError=append:/var/log/frasa-api.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
39
deploy/nginx-host.conf
Normal file
39
deploy/nginx-host.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
# FRASA Ecosystem — nginx del host (modo HTTP, LAN)
|
||||
# Instalado en /etc/nginx/sites-available/frasa
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 10m;
|
||||
|
||||
# API Gateway (servicio systemd frasa-api en 127.0.0.1:3000)
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Apps estáticas (builds de producción)
|
||||
location /quote/ {
|
||||
alias /root/frasa-ecosystem/apps/cotizador/dist/;
|
||||
try_files $uri $uri/ /quote/index.html;
|
||||
}
|
||||
|
||||
location /client/ {
|
||||
alias /root/frasa-ecosystem/apps/alta-clientes/dist/;
|
||||
try_files $uri $uri/ /client/index.html;
|
||||
}
|
||||
|
||||
location /pay/ {
|
||||
alias /root/frasa-ecosystem/apps/cobranza/dist/;
|
||||
try_files $uri $uri/ /pay/index.html;
|
||||
}
|
||||
|
||||
# Landing (raíz)
|
||||
location / {
|
||||
root /root/frasa-ecosystem/apps/landing/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ services:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
# NGINX_CONF=nginx.http.conf para modo HTTP local (sin SSL todavia)
|
||||
- ./nginx/${NGINX_CONF:-nginx.conf}:/etc/nginx/nginx.conf:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro # certs siempre al día tras certbot renew
|
||||
- certbot_webroot:/var/www/certbot # challenge HTTP-01
|
||||
depends_on:
|
||||
|
||||
39
nginx/nginx.http.conf
Normal file
39
nginx/nginx.http.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
# FRASA Ecosystem — reverse proxy (modo HTTP, red local)
|
||||
# Se usa cuando aun no hay certificado SSL (DDNS sin apuntar a este servidor).
|
||||
# Cuando el dominio resuelva aqui: certbot --webroot y volver a nginx.conf (443).
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Uploads de hasta 10MB (expedientes de clientes)
|
||||
client_max_body_size 10m;
|
||||
|
||||
# Challenge de Let's Encrypt (para cuando se expida el cert)
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# API Gateway
|
||||
location /api/ {
|
||||
proxy_pass http://api:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Apps web (cada contenedor sirve su SPA con su base path)
|
||||
location /quote/ { proxy_pass http://cotizador:80; proxy_set_header Host $host; }
|
||||
location /client/ { proxy_pass http://alta-clientes:80; proxy_set_header Host $host; }
|
||||
location /pay/ { proxy_pass http://cobranza:80; proxy_set_header Host $host; }
|
||||
|
||||
# Landing (raíz)
|
||||
location / {
|
||||
proxy_pass http://landing:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user