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).
|
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
|
```bash
|
||||||
git clone https://git.consultoria-as.com/consultoria-as/FRASA-CAS.git /opt/frasa-ecosystem
|
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
|
curl https://frasadesarrollos.ddns.net/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Nota primer arranque: mientras no existan los certs, comenta el bloque `server 443` de
|
Modo local sin SSL (LAN): `.env` con `NGINX_CONF=nginx.http.conf` hace que NGINX
|
||||||
`nginx/nginx.conf` (o usa un cert autofirmado temporal); después de expedir, restaura y
|
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"`.
|
`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 * * *`).
|
Backup: `scripts/backup.sh` (cron sugerido `17 2 * * *`).
|
||||||
|
|
||||||
## Monday.com
|
## Monday.com
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG VITE_API_URL=/api
|
||||||
|
ENV VITE_API_URL=$VITE_API_URL
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Etapa 2: servidor estático
|
# Etapa 2: servidor estático
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG VITE_API_URL=/api
|
||||||
|
ENV VITE_API_URL=$VITE_API_URL
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Etapa 2: servidor estático
|
# Etapa 2: servidor estático
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
COPY . .
|
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
|
RUN npm run build
|
||||||
|
|
||||||
# Etapa 2: servidor estático
|
# Etapa 2: servidor estático
|
||||||
|
|||||||
@@ -7,20 +7,19 @@ describe('MODULOS', () => {
|
|||||||
expect(MODULOS.map((m) => m.id)).toEqual(['cotizador', 'alta-clientes', 'cobranza'])
|
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) {
|
for (const modulo of MODULOS) {
|
||||||
expect(modulo.nombre.length).toBeGreaterThan(0)
|
expect(modulo.nombre.length).toBeGreaterThan(0)
|
||||||
expect(modulo.descripcion.length).toBeGreaterThan(0)
|
expect(modulo.descripcion.length).toBeGreaterThan(0)
|
||||||
expect(() => new URL(modulo.url)).not.toThrow()
|
expect(modulo.url).toMatch(/^\/[a-z]+\/$/)
|
||||||
expect(new URL(modulo.url).protocol).toBe('http:')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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]))
|
const urls = Object.fromEntries(MODULOS.map((m) => [m.id, m.url]))
|
||||||
expect(urls['cotizador']).toBe('http://192.168.10.123:3001/quote/')
|
expect(urls['cotizador']).toBe('/quote/')
|
||||||
expect(urls['alta-clientes']).toBe('http://192.168.10.123:3002/client/')
|
expect(urls['alta-clientes']).toBe('/client/')
|
||||||
expect(urls['cobranza']).toBe('http://192.168.10.123:3003/pay/')
|
expect(urls['cobranza']).toBe('/pay/')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('no hay ids ni URLs duplicadas', () => {
|
it('no hay ids ni URLs duplicadas', () => {
|
||||||
|
|||||||
@@ -5,25 +5,24 @@ export interface Modulo {
|
|||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: tras montar el NGINX de producción cambiar las URLs absolutas de
|
// Rutas relativas: la landing se sirve desde el mismo NGINX que las apps.
|
||||||
// preview por rutas relativas '/quote/', '/client/' y '/pay/'.
|
|
||||||
export const MODULOS: Modulo[] = [
|
export const MODULOS: Modulo[] = [
|
||||||
{
|
{
|
||||||
id: 'cotizador',
|
id: 'cotizador',
|
||||||
nombre: 'Cotizador',
|
nombre: 'Cotizador',
|
||||||
descripcion: 'Propuestas comerciales y PDF en tiempo real',
|
descripcion: 'Propuestas comerciales y PDF en tiempo real',
|
||||||
url: 'http://192.168.10.123:3001/quote/',
|
url: '/quote/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alta-clientes',
|
id: 'alta-clientes',
|
||||||
nombre: 'Alta de clientes',
|
nombre: 'Alta de clientes',
|
||||||
descripcion: 'Expediente digital, firmas y formatos PLD',
|
descripcion: 'Expediente digital, firmas y formatos PLD',
|
||||||
url: 'http://192.168.10.123:3002/client/',
|
url: '/client/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cobranza',
|
id: 'cobranza',
|
||||||
nombre: 'Cobranza',
|
nombre: 'Cobranza',
|
||||||
descripcion: 'Planes de enganche, vencidos y estados de cuenta',
|
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"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
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
|
- /etc/letsencrypt:/etc/letsencrypt:ro # certs siempre al día tras certbot renew
|
||||||
- certbot_webroot:/var/www/certbot # challenge HTTP-01
|
- certbot_webroot:/var/www/certbot # challenge HTTP-01
|
||||||
depends_on:
|
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