06 — Configuración de nginx¶
Este SaaS necesita un único vhost nginx que sirve tres tipos de tráfico:
| Path | Destino | Comentario |
|---|---|---|
/static/* |
Filesystem (alias) |
Widget JS/CSS, uploads, assets del Django admin |
/media/* |
Filesystem (alias) |
Logos y avatares subidos por usuarios |
/api/*, /v3/*, /admin/*, /health |
proxy_pass http://127.0.0.1:8000 |
Django |
/ (resto) |
Filesystem (root + fallback /index.html) |
SPA del dashboard |
Vhost completo (copy-paste)¶
El template real está en infra/nginx-superbot.conf.example. Resumen del bloque de overrides (lo que va en un Plesk vhost) o del server { } completo (si es nginx puro):
server {
listen 443 ssl http2;
server_name app.tudominio.com widget.tudominio.com;
# SSL — ajusta paths de tus certificados
ssl_certificate /etc/letsencrypt/live/app.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.tudominio.com/privkey.pem;
client_max_body_size 64M;
# ── Estáticos del widget + uploads ──
# Importante: NO añadir try_files con alias (genera double-path 404).
location /static/ {
alias /home/blimxapp/saas/chatbot-v3-workspace/static/;
autoindex off;
expires 1h;
add_header Cache-Control "public, max-age=3600" always;
add_header Access-Control-Allow-Origin "*" always;
disable_symlinks off;
}
# ── Uploads de usuarios (logos, avatares) ──
location /media/ {
alias /home/blimxapp/saas/chatbot-v3-workspace/backend_django/media/;
autoindex off;
expires 7d;
add_header Cache-Control "public, max-age=604800" always;
}
# ── Endpoints públicos del widget ──
location /v3/ {
proxy_pass http://127.0.0.1:8000;
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;
proxy_read_timeout 60s;
proxy_connect_timeout 30s;
gzip on;
gzip_types application/json text/plain text/css application/javascript;
}
# ── API del SaaS ──
location /api/ {
proxy_pass http://127.0.0.1:8000;
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;
proxy_read_timeout 600s;
}
# ── Stripe webhook (sin rate limit, sin compresión) ──
location = /api/billing/stripe/webhook/ {
proxy_pass http://127.0.0.1:8000;
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;
proxy_read_timeout 600s;
}
# ── Django admin ──
location /admin/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ── Health check ──
location = /health {
proxy_pass http://127.0.0.1:8000;
access_log off;
}
# ── Dashboard SPA (React) ──
location / {
root /home/blimxapp/saas/chatbot-v3-workspace/apps/dashboard/dist;
try_files $uri $uri/ /index.html;
index index.html;
expires 5m;
}
}
# ── HTTP → HTTPS redirect ──
server {
listen 80;
server_name app.tudominio.com widget.tudominio.com;
return 301 https://$host$request_uri;
}
Por qué cada bloque importa¶
/static/ con alias y sin try_files¶
alias(noroot) porque queremos que/static/v3/foo.jsbusquestatic/v3/foo.js(sin duplicar el prefijo).- NO
try_files $uri =404— combinado conaliasproduce un bug donde nginx busca un path doblado y devuelve 404 en TODO. Si necesitas 404 explícito, omitetry_files(nginx ya lo hace). disable_symlinks offporque el path origen está fuera del docroot.
/v3/ y /api/ apuntan al mismo upstream¶
Ambos paths van a Django :8000. El routing interno lo hace blimx_admin/urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('api/auth/', include('authentication.urls')),
path('v3/', include('api.widget.urls')),
]
Mantenerlos separados en nginx permite aplicar timeouts distintos:
/v3/*→ 60s (queries rápidas, FAQs)/api/*→ 600s (Stripe, generación de PDF de facturas)
El endpoint /v3/faq devuelve únicamente categories[].faqs y no duplica una lista plana faqs[]. Esto reduce el payload móvil sin afectar al widget Help Center, que renderiza las preguntas desde sus categorías.
Webhook Stripe en location = (exact match)¶
Stripe espera respuesta en menos de 10s. El = (exact match) hace que nginx no evalúe otros location bloques y no pierde tiempo. También tener un bloque dedicado evita que un futuro rate-limit global bloquee a Stripe.
/media/ separado de /static/¶
/static/es inmutable (output de builds, mismo en todos los servidores)./media/es mutable (uploads de usuarios, distinto entre servidores).
Si haces backup, basta con respaldar /media/ y la BD; /static/ se regenera con un build.
Permisos de filesystem¶
Para que nginx pueda leer /static/ y /media/:
-
Identifica como qué usuario corre nginx:
-
Añade ese usuario al grupo de la app:
-
Verifica permisos del path raíz:
Si está en drwx------ (700), nginx no podrá entrar. Cambia a 750:
- Smoke test:
Despliegue en Plesk¶
Plesk genera el server { } automáticamente. No edites /etc/nginx/plesk.conf.d/vhosts/<dominio>.conf (Plesk lo regenera). En su lugar pon los bloques location en:
Plesk hace include de ese archivo dentro del server { }, así sobrescribe los defaults de Plesk con los nuestros.
Tras editar:
Verificación final¶
# 1) Frame del widget se sirve
curl -sI https://widget.tudominio.com/static/v3/dist/blimx-v3-chatbot-frame.html | head -3
# 2) Bundle JS se sirve
curl -sI https://widget.tudominio.com/static/v3/dist/blimx-chatbot.min.js | head -3
# 3) /v3/health responde JSON
curl -s https://widget.tudominio.com/v3/health
# 4) /api/health responde JSON
curl -s https://app.tudominio.com/api/health/
# 5) Dashboard SPA carga
curl -sI https://app.tudominio.com/ | head -3
Todos deben devolver 200 OK. Si alguno da 502, revisa que blimx-django.service esté active. Si da 403, problema de permisos (ver sección anterior).