Producción¶
Esta página cubre lo que cambia cuando una instancia deja de ser local y se expone a internet. Primero instala siguiendo la guía de instalación.
Modos de despliegue¶
El docker-compose.yml de la raíz es un enlace simbólico a uno de los dos modos de
deploy/. El instalador lo crea según elijas MongoDB local o externo.
local-full |
external-mongodb |
|
|---|---|---|
| Pensado para | Desarrollo, pruebas, instalaciones pequeñas | Producción |
| MongoDB | Contenedor mongo:8 con volumen mongodb_data, puerto 27017 publicado |
Externo, vía MONGO_URI (Atlas, servidor propio) |
| Kroki | Contenedor interno | Contenedor interno |
| Backend | APP_ENV=development: uvicorn con recarga en caliente, Swagger en /docs |
APP_ENV=production (por defecto de la imagen): gunicorn con WORKERS procesos, sin Swagger |
| Frontend | Target development: servidor de Vite con HMR y código montado |
Target production: build estático servido por Nginx sin root |
VITE_API_URL |
Fijo: http://localhost:5172 |
Desde el entorno o un .env junto al compose |
# Elegir modo manualmente
ln -sf deploy/external-mongodb/docker-compose.yml docker-compose.yml
docker-compose up -d --build
No publiques local-full tal cual
local-full expone MongoDB en el puerto 27017 sin autenticación y corre el backend
en modo desarrollo (Swagger abierto, trazas de error visibles). Úsalo detrás de un
firewall o cambia a external-mongodb.
Imagen de producción del frontend¶
El Dockerfile del frontend es multi-etapa. El target production:
- Construye el bundle con Vite y lo sirve con
nginxinc/nginx-unprivilegedcomo usuarionginx(sin root), escuchando en el puerto5173. - Genera
/config.jsal arrancar el contenedor a partir deVITE_API_URL,VITE_SENTRY_DSNyVITE_APP_ENV, así que cambiar la URL de la API solo requiere recrear el contenedor. - Envía
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-PolicyyPermissions-Policy(frontend/headers.conf). HSTS no se envía aquí: va en el proxy que termina TLS. - Nunca guarda en caché
index.htmlniconfig.js; los assets con hash se cachean un año. - Incluye un healthcheck sobre
http://127.0.0.1:5173/.
Reverse proxy y HTTPS¶
Pon un reverse proxy (Nginx, Traefik, Caddy) delante para terminar TLS y servir en los puertos 80/443. El enfoque recomendado usa un subdominio para la API:
flowchart LR
U([Usuario]) -->|"https://diagramas.ejemplo.com"| P[Reverse proxy<br/>TLS]
U -->|"https://api.diagramas.ejemplo.com"| P
P -->|":5173"| F[frontend]
P -->|":5172"| B[backend]
# Frontend
server {
listen 80;
server_name diagramas.ejemplo.com;
location / {
proxy_pass http://localhost:5173;
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;
}
}
# Backend API
server {
listen 80;
server_name api.diagramas.ejemplo.com;
# Las importaciones de proyecto aceptan hasta 20 MB (MAX_IMPORT_UPLOAD_BYTES).
# Nginx limita el cuerpo a 1 MB por defecto.
client_max_body_size 20m;
location / {
proxy_pass http://localhost:5172;
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;
}
}
Certificados gratuitos con Let's Encrypt:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d diagramas.ejemplo.com -d api.diagramas.ejemplo.com
IP real del cliente detrás del proxy¶
Los límites de peticiones (login, registro, MFA, enlaces compartidos, render), el bloqueo de
cuentas y el registro de auditoría usan la IP del cliente. Detrás de un proxy, el backend solo
ve la IP del proxy, salvo que confíe en sus encabezados X-Forwarded-For. Si no lo hace,
todos los usuarios comparten el mismo contador y unos pocos intentos fallidos bloquean el
login de toda la instancia.
Indica en FORWARDED_ALLOW_IPS (en backend/.env) la dirección desde la que el proxy se
conecta al backend. Solo esas conexiones pueden fijar la IP del cliente; cualquier otra
conserva la suya, así que un cliente no puede falsificarla enviando el encabezado.
Reinicia el backend (docker compose up -d backend) y verifica que el campo ip_address de
la colección audit_log muestre las IPs de los clientes y no la del proxy.
No uses *
FORWARDED_ALLOW_IPS=* acepta el encabezado de cualquiera: solo es seguro si el puerto
del backend es accesible únicamente desde el proxy (firewall). Si no, cualquiera puede
cambiar su IP y evadir los límites.
Límites de peticiones y workers¶
Los contadores de los límites de peticiones y del bloqueo de cuentas viven en la memoria de
cada proceso. En producción, gunicorn arranca WORKERS procesos (por defecto 2) y cada uno
lleva sus propios contadores, así que el límite efectivo por IP puede ser hasta WORKERS
veces el configurado (con 2 workers, hasta 20 intentos de login por minuto en lugar de 10).
Ten esto en cuenta antes de subir WORKERS.
Apuntar la instancia a tu dominio¶
Antes de abrirla al público, ajusta tres valores; si no, el navegador bloquea la API y
Stripe redirige a localhost:
| Variable | Dónde | Valor |
|---|---|---|
FRONTEND_URL |
backend/.env |
https://diagramas.ejemplo.com |
BACKEND_CORS_ORIGINS |
backend/.env |
https://diagramas.ejemplo.com (lista separada por comas, sin corchetes). Vacío = solo FRONTEND_URL. |
VITE_API_URL |
entorno del frontend | https://api.diagramas.ejemplo.com |
Detalle de cada una en Configuración.
Qué hace APP_ENV=production¶
| Efecto | Detalle |
|---|---|
| Documentación de la API desactivada | /docs, /redoc y /openapi.json devuelven 404. |
| Trazas ocultas | Una excepción no controlada responde {"detail": "Internal server error"}; la traza queda en los logs. |
| HSTS | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload. |
| CSP de la API | default-src 'none'; frame-ancestors 'none' (las respuestas son JSON). |
| Servidor | gunicorn con workers de uvicorn (WORKERS, por defecto 2), timeout de 120 s. |
Para consultar Swagger en una instancia de producción, levanta una copia con
APP_ENV=development en un entorno no público.
Límites en memoria
Los límites de peticiones y el bloqueo de cuentas se guardan en la memoria de cada proceso: se reinician al reiniciar el backend y cada worker de gunicorn lleva su propia cuenta. Un despliegue con varias réplicas necesitaría un almacén compartido.
Content-Security-Policy del frontend¶
La CSP del frontend está desactivada por defecto: una política demasiado estricta rompe el
editor (Mermaid inyecta estilos en línea y Monaco carga workers desde jsDelivr).
frontend/csp.conf.example trae una política probada como punto de partida:
- Reemplaza
API_ORIGINpor el origen real de tu API. - Agrega la línea
add_headerafrontend/headers.conf. - Reconstruye la imagen y verifica en la consola del navegador el editor, el render y las exportaciones.
Checklist de endurecimiento¶
-
JWT_SECRETaleatorio de 32+ caracteres yAI_ENCRYPTION_KEYgenerada con Fernet. - Ambos secretos guardados fuera del servidor, junto con los respaldos.
-
APP_ENV=productionen el backend y frontend con el targetproduction. - HTTPS en el proxy;
FRONTEND_URL,BACKEND_CORS_ORIGINSyVITE_API_URLcon los dominios públicos. - MongoDB no expuesto a internet (sin
27017público), con usuario y contraseña. - Los puertos
5172y5173accesibles solo desde el proxy (firewall). - Un motor de email activo, para que funcionen la recuperación de contraseña y el MFA por correo (Integraciones).
- La cuenta de administrador con MFA activado.
- Respaldos automáticos de la base de datos (Mantenimiento).
- (Opcional) CSP del frontend habilitada y validada.
- (Opcional) Sentry configurado para backend y frontend.
Más contexto sobre los controles de la plataforma en Seguridad.