¿Por qué Nginx delante?
Express sabe escuchar en un puerto, pero Nginx termina el TLS (costoso para Node), sirve /docs como archivos estáticos sin despertar a Node, cachea las métricas y corta el tráfico abusivo antes de que consuma un worker.
DESPLIEGUE EN PRODUCCIÓN · SERVIDOR DE APLICACIÓN
Guía paso a paso para levantar integracorp-api en un servidor Linux dedicado, con la base de datos MySQL alojada en otro servidor. Incluye el porqué de cada decisión, no sólo los comandos.
00 · CONTEXTO
Este servidor es sólo aplicación: Node.js y Nginx. La base de datos MySQL vive en otro servidor, así que aquí no se instala ningún motor de base de datos.
Internet → Nginx (443, TLS, caché, rate limit) → Node cluster (8 workers, :4000) → MySQL remoto
Express sabe escuchar en un puerto, pero Nginx termina el TLS (costoso para Node), sirve /docs como archivos estáticos sin despertar a Node, cachea las métricas y corta el tráfico abusivo antes de que consuma un worker.
Node es monohilo. Con 10 vCPU, un solo proceso usaría un núcleo y dejaría nueve parados. src/cluster.js levanta varios procesos que comparten el puerto 4000 y el kernel reparte las conexiones.
En la red hacia MySQL. Con la base de datos fuera, cada consulta paga un tiempo de ida y vuelta. Toda la configuración de esta guía gira alrededor de reducir ese costo.
| Componente | RAM | vCPU | Notas |
|---|---|---|---|
| Node.js (8 workers) | ~3–4 GB | 8 | Alrededor de 400 MB por worker en carga normal. |
| Nginx + sistema operativo | ~1 GB | 2 | Nginx es muy liviano; el resto es caché de página del kernel. |
| Libre para picos | ~5 GB | — | Colchón para campañas de correo y generación de PDFs. |
00 · ANTES DE EMPEZAR
integracorp-api.tudrgroup.com apuntando a la IP pública del servidor (necesario para emitir el certificado TLS).integracorp-api.INTEGRACORP_API_KEY, JWT_SECRET, claves de base de datos, SMTP o Ultramsg. Todos los ejemplos usan marcadores de posición a propósito.PASO 1
adduser --system --group --home /var/www --shell /bin/bash deploy mkdir -p /var/www/storage/uploads chown -R deploy:deploy /var/www
root. Si algún día una vulnerabilidad permite ejecutar código —por ejemplo a través de un archivo subido por un paciente—, el daño queda limitado a lo que puede tocar el usuario deploy. Esta medida no te protege a ti como administrador: te protege de un atacante externo./var/www/storage/uploads queda fuera del directorio del repositorio a propósito: así un despliegue nuevo o un git pull nunca puede borrar documentos de pacientes.
PASO 2
apt update && apt upgrade -y
apt install -y build-essential git curl ufw sysstat htop \
unattended-upgrades logrotate mysql-client mtr-tiny
timedatectl set-timezone America/Caracas
dpkg-reconfigure --priority=low unattended-upgrades
| Paquete | Para qué |
|---|---|
build-essential | Algunas dependencias npm compilan código nativo al instalarse; sin compilador, npm ci falla. |
mysql-client | No instala el servidor, sólo el comando mysql para probar a mano la conexión remota. Imprescindible para diagnosticar. |
mtr-tiny | Mide latencia y pérdida de paquetes hacia la base de datos. Te dice si un problema es de red o de consultas. |
sysstat | Trae iostat y sar para observar saturación de CPU y disco. |
unattended-upgrades | Aplica parches de seguridad del sistema automáticamente. |
CURDATE() y comparan «mes actual vs mes anterior». Si este servidor está en UTC y el de base de datos en -04:00, los rangos de fecha se calculan desplazados cuatro horas: no verás ningún error, sólo números ligeramente equivocados en las primeras y últimas horas de cada mes. Anota la zona elegida y aplica exactamente la misma en el servidor de base de datos.timedatectl debe mostrar Time zone: America/Caracas.PASO 3
ufw default deny incoming ufw default allow outgoing ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw enable ufw status verbose
deny incoming por defecto sólo existen 22, 80 y 443, y el 4000 queda accesible únicamente desde localhost, que es exactamente lo que Nginx necesita.PASO 4
fallocate -l 4G /swapfile chmod 600 /swapfile mkswap /swapfile && swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab
vm.swappiness = 10 para que el kernel recurra al swap sólo cuando no le queda alternativa, no de forma preventiva.free -h debe mostrar 4 GB de swap y ~10 GB de RAM.PASO 5
Crea /etc/sysctl.d/99-integracorp.conf:
vm.swappiness = 10 fs.file-max = 200000 net.core.somaxconn = 4096 net.core.netdev_max_backlog = 5000 net.ipv4.tcp_max_syn_backlog = 4096 net.ipv4.ip_local_port_range = 10240 65535 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_slow_start_after_idle = 0 net.ipv4.tcp_keepalive_time = 120 net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_keepalive_probes = 5
Cola de conexiones. somaxconn define cuántas conexiones puede tener el kernel esperando a que la aplicación las acepte. Evita que en un pico de tráfico el kernel empiece a rechazar conexiones mientras los workers están ocupados.
Puertos efímeros. ip_local_port_range y tcp_tw_reuse importan porque este servidor abre muchas conexiones salientes: al MySQL remoto, a la API de Ultramsg y al servidor SMTP. Cada conexión cerrada deja un puerto en estado TIME_WAIT durante segundos. Ampliar el rango a unos 55.000 puertos y permitir reutilizarlos evita el error EADDRNOTAVAIL bajo carga sostenida, un fallo desconcertante porque parece de la aplicación y es del kernel.
Keepalive TCP. Este bloque es específico de tener la base de datos remota. Las conexiones del pool pasan largos ratos ociosas de madrugada, y los routers NAT o firewalls intermedios cortan conexiones inactivas sin avisar a nadie: la aplicación se entera cuando intenta usarlas y recibe PROTOCOL_CONNECTION_LOST. Con tcp_keepalive_time = 120 el kernel envía un paquete cada dos minutos y la conexión nunca parece muerta. El pool de la API ya trae enableKeepAlive: true a nivel de mysql2; esto lo refuerza a nivel de sistema.
Ahora los descriptores de archivo, en /etc/security/limits.d/99-nofile.conf:
* soft nofile 65535 * hard nofile 65535 root soft nofile 65535 root hard nofile 65535
EMFILE: too many open files. Ojo: systemd no respeta este archivo para los servicios que gestiona, por eso en el paso 11 repetimos el límite con LimitNOFILE=.sysctl --system
sysctl net.ipv4.tcp_keepalive_time # debe responder 120
PASO 6
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt install -y nodejs node -v && npm -v
package.json exige >=18, pero Node 18 ya salió de soporte. La 20 es LTS madura y trae fetch global estable, que es lo que usa el cliente de Ultramsg. La 22 también sirve; evita las versiones impares (21, 23), que no son LTS y dejan de recibir parches en meses.apt install nodejs a secas): trae una versión antigua. El script de NodeSource añade el repositorio oficial y así apt upgrade te mantiene actualizado dentro de la rama 20.PASO 7
Esta es la decisión técnica más importante del despliegue.
src/config/database.js) no tiene TLS configurado, así que las consultas y sus resultados —incluidos datos de historia clínica— viajarían en texto plano. Un túnel resuelve ambas cosas sin tocar código.Si ambos servidores están en el mismo datacenter y el proveedor ofrece red privada, usa esa IP privada y sáltate WireGuard. Si no:
apt install -y wireguard
cd /etc/wireguard && umask 077
wg genkey | tee privkey | wg pubkey > pubkey
cat pubkey # esta clave pública se entrega al servidor de base de datos
Archivo /etc/wireguard/wg0.conf:
[Interface] PrivateKey = <contenido de /etc/wireguard/privkey> Address = 10.8.0.2/24 [Peer] PublicKey = <clave pública del servidor de base de datos> Endpoint = <ip-publica-bd>:51820 AllowedIPs = 10.8.0.1/32 PersistentKeepalive = 25
systemctl enable --now wg-quick@wg0 wg show
Qué significa cada línea. Address = 10.8.0.2 es la IP privada de este servidor dentro del túnel; el de base de datos será 10.8.0.1. AllowedIPs restringe el túnel a esa única IP: no estás enrutando todo tu tráfico por ahí, sólo lo que va a la base de datos. PersistentKeepalive = 25 envía un paquete cada 25 segundos para que el NAT del proveedor no cierre el túnel en periodos de inactividad.
ping -c 5 10.8.0.1 mtr -rwc 30 10.8.0.1 time mysql -h 10.8.0.1 -u <usuario> -p -e "SELECT 1"
RTT + tiempo_de_consulta sirve 1000 / (RTT + t) consultas por segundo. Con un RTT de 20 ms y consultas de 5 ms, cada conexión rinde apenas 40 consultas por segundo; para sostener 400 necesitas unas 10 conexiones trabajando en paralelo.| RTT medido | Qué significa | DB_CONNECTION_LIMIT por worker |
|---|---|---|
| < 1 ms | Red privada del mismo datacenter — ideal | 10 |
| 1 – 5 ms | Mismo datacenter o muy cerca | 15 |
| 5 – 30 ms | Regiones distintas, funcional | 20 |
| > 30 ms | Duele; ver los ajustes finales | 25 + caché agresiva |
Con 8 workers, el total de conexiones contra MySQL será 8 × ese número, y el max_connections del servidor de base de datos tendrá que superarlo con margen. Juega a tu favor que los controladores de métricas usen queryAll(), que lanza las consultas en paralelo: un endpoint con seis consultas paga aproximadamente un RTT, no seis.
PASO 8
su - deploy cd /var/www git clone <URL_DEL_REPOSITORIO> integracorp-api cd integracorp-api npm ci --omit=dev
npm ci y no npm install: ci instala exactamente las versiones fijadas en package-lock.json, borrando node_modules antes. install puede subir dependencias a versiones más nuevas que cumplan el rango del package.json, y descubrir en producción que una dependencia menor cambió de comportamiento es la clase de sorpresa que no quieres. --omit=dev se salta nodemon, que sólo sirve en desarrollo.PASO 9
cd /var/www/integracorp-api
cp .env.example .env
openssl rand -hex 32 # ejecútalo dos veces: API key y JWT_SECRET
nano .env
chmod 600 .env
NODE_ENV=production PORT=4000 WEB_CONCURRENCY=8 DB_HOST=10.8.0.1 DB_PORT=3306 DB_USER=integracorp_api DB_PASSWORD=<clave> DB_NAME=<nombre_de_la_base> DB_CONNECTION_LIMIT=15 DB_QUEUE_LIMIT=100 DB_CONNECT_TIMEOUT_MS=15000 INTEGRACORP_API_KEY=<32 bytes aleatorios> JWT_SECRET=<otros 32 bytes aleatorios> JWT_EXPIRES_IN=8h CORS_ORIGINS=https://portal.tudrgroup.com,https://integracorp.tudrgroup.com RATE_LIMIT_WINDOW_MS=60000 RATE_LIMIT_MAX=30 LOGIN_RATE_LIMIT_MAX=20 STORAGE_ROOT=/var/www/storage/uploads UPLOAD_MAX_BYTES=10485760 MAIL_HOST=<servidor smtp> MAIL_PORT=587 MAIL_USERNAME=<usuario> MAIL_PASSWORD=<clave> MAIL_FROM_ADDRESS=<remitente> MAIL_FROM_NAME=Integracorp EMAIL_BATCH_CONCURRENCY=1 EMAIL_SEND_PAUSE_MS=1500 ULTRAMSG_INSTANCE_ID=<instancia> ULTRAMSG_TOKEN=<token> ULTRAMSG_MAX_BATCH_SIZE=50 ULTRAMSG_MESSAGE_DELAY_MS=4500
| Variable | Por qué importa |
|---|---|
NODE_ENV=production | No es cosmético. Activa el modo producción de Express, hace que los errores 500 oculten el mensaje interno al cliente, aplica de verdad la lista de CORS_ORIGINS (en desarrollo el código permite cualquier origen) y añade Cache-Control a la documentación. Olvidarlo es el error de despliegue más común y más caro. |
WEB_CONCURRENCY=8 | Con 10 vCPU deja dos para Nginx y el sistema. Poner 10 no rinde más: los workers compiten entre sí y con Nginx, y aumentan las conexiones contra la base de datos sin ganancia real. |
DB_CONNECTION_LIMIT | Según la tabla del paso 7. Cuidado con subirlo «por si acaso»: si las consultas son lentas, más conexiones sólo consiguen que el servidor de base de datos se ahogue más rápido. |
DB_CONNECT_TIMEOUT_MS | El default del código es 10 s, pensado para base de datos local. Con red de por medio conviene dar más margen antes de declarar fallida la conexión. |
RATE_LIMIT_MAX=30 | Es por worker, no global. Los contadores viven en memoria de cada proceso, así que con 8 workers el límite efectivo por IP es 8 × 30 ≈ 240 peticiones por minuto. Para un número exacto, usa el límite de Nginx del paso 12. |
CORS_ORIGINS | Lista exacta de los frontends que consumen la API, sin barra final. Si el portal del paciente no carga datos y la consola del navegador se queja de CORS, el problema está aquí. |
src/config/env.js carga dotenv con override: true, así que .env gana sobre cualquier variable definida en systemd o en el shell. Configura todo en .env y no te pelees con Environment=.PASO 10
Antes de convertirlo en servicio, arráncalo a mano para ver los errores en pantalla.
cd /var/www/integracorp-api npm start
Deberías ver MySQL conectado: … y integracorp-api escuchando en http://localhost:4000. Desde otra terminal:
curl -s localhost:4000/api/health curl -s localhost:4000/api/health/db curl -s "localhost:4000/api/agents?page=1" -H "X-API-Key: <tu-api-key>"
/api/health sólo confirma que el proceso vive. El importante es /api/health/db, que hace ping al pool y devuelve la latencia: ese número es tu RTT real más el overhead del pool, y será tu métrica de salud diaria./api/health/db.Detén el proceso con Ctrl+C cuando funcione.
PASO 11
Archivo /etc/systemd/system/integracorp-api.service:
[Unit] Description=integracorp-api After=network-online.target wg-quick@wg0.service Wants=network-online.target [Service] Type=simple User=deploy WorkingDirectory=/var/www/integracorp-api ExecStart=/usr/bin/node src/cluster.js Restart=always RestartSec=3 LimitNOFILE=65535 Environment=NODE_OPTIONS=--max-old-space-size=768 Environment=UV_THREADPOOL_SIZE=8 StandardOutput=journal StandardError=journal SyslogIdentifier=integracorp-api [Install] WantedBy=multi-user.target
| Directiva | Qué resuelve |
|---|---|
After=… wg-quick@wg0 | Arranca después del túnel, para que el primer intento de conexión a MySQL no falle por una carrera al reiniciar el servidor. |
ExecStart=… src/cluster.js | Modo cluster: crea WEB_CONCURRENCY workers y marca sólo al primero con WHATSAPP_WORKER=1. |
Restart=always | Si el proceso muere, systemd lo revive en 3 segundos. Junto al reinicio de workers del propio cluster, son dos niveles de recuperación. |
LimitNOFILE=65535 | Repite el límite del paso 5, porque systemd ignora /etc/security/limits.conf para sus servicios. |
--max-old-space-size=768 | Techo de heap por worker: 8 × 768 MB ≈ 6 GB en el peor caso. En operación normal cada worker ronda 200–400 MB; el techo evita que una fuga se coma toda la RAM. |
UV_THREADPOOL_SIZE=8 | Hilos de libuv para lo que no es asíncrono a nivel de sistema: archivos, criptografía y generación de PDFs con PDFKit. El default de 4 se satura si dos pacientes piden su PDF a la vez. |
WHATSAPP_WORKER=1. Si todos los workers la tuvieran, un lote de 50 mensajes se enviaría ocho veces y Ultramsg podría banear la instancia. No pongas PM2 en modo cluster encima de esto ni dupliques el servicio.systemctl daemon-reload systemctl enable --now integracorp-api systemctl status integracorp-api journalctl -u integracorp-api -n 50
journalctl -u integracorp-api | grep -c whatsapp-queue debe devolver exactamente 1. Si devuelve más, hay más de una instancia corriendo y las campañas saldrán duplicadas.PASO 12
apt install -y nginx certbot python3-certbot-nginx mkdir -p /var/cache/nginx/metrics chown -R www-data:www-data /var/cache/nginx
listen 443 ssl antes de que exista el certificado: nginx -t falla. La secuencia correcta es configuración HTTP mínima → certbot → configuración completa.Archivo /etc/nginx/conf.d/integracorp.conf (upstream, caché y zonas de rate limit; van fuera de cualquier server):
cat > /etc/nginx/conf.d/integracorp.conf <<'EOF'
upstream integracorp_api {
server 127.0.0.1:4000;
keepalive 64;
}
proxy_cache_path /var/cache/nginx/metrics levels=1:2 keys_zone=metrics:20m
max_size=500m inactive=30m use_temp_path=off;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=10r/m;
EOF
keepalive 64 mantiene 64 conexiones abiertas hacia Node en lugar de abrir y cerrar una por petición. Requiere proxy_http_version 1.1 y proxy_set_header Connection "" en el sitio, o no surte efecto.
cat > /etc/nginx/sites-available/integracorp-api <<'EOF'
server {
listen 80;
server_name integracorp-api.tudrgroup.com;
location / { proxy_pass http://integracorp_api; }
}
EOF
ln -sf /etc/nginx/sites-available/integracorp-api /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
# comprueba que responde por HTTP antes de pedir el certificado
curl -s http://integracorp-api.tudrgroup.com/api/health
certbot --nginx -d integracorp-api.tudrgroup.com
Certbot edita ese mismo archivo: añade el bloque listen 443 ssl, las líneas ssl_certificate y ssl_certificate_key, y la redirección de 80 a 443. También instala una renovación automática por temporizador de systemd; no hay que hacer nada más con el certificado.
cat > /etc/nginx/sites-available/integracorp-api <<'EOF'
server {
listen 80;
server_name integracorp-api.tudrgroup.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name integracorp-api.tudrgroup.com;
ssl_certificate /etc/letsencrypt/live/integracorp-api.tudrgroup.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/integracorp-api.tudrgroup.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 12m;
proxy_http_version 1.1;
proxy_set_header Connection "";
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_connect_timeout 5s;
proxy_read_timeout 120s;
# Login: freno contra fuerza bruta antes de tocar Node
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://integracorp_api;
}
# Métricas: caché de 120s, la mayor mejora con base de datos remota
location /api/metrics/ {
limit_req zone=api burst=20 nodelay;
proxy_cache metrics;
proxy_cache_key "$request_uri|$http_x_api_key";
proxy_cache_valid 200 120s;
proxy_cache_use_stale error timeout updating;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://integracorp_api;
}
# Documentación estática servida por nginx, sin pasar por Node
location /docs/ {
alias /var/www/integracorp-api/public/;
expires 1h;
try_files $uri $uri/ @node;
}
location @node { proxy_pass http://integracorp_api; }
location / {
limit_req zone=api burst=40 nodelay;
proxy_pass http://integracorp_api;
}
}
EOF
nginx -t && systemctl reload nginx
Cabeceras X-Forwarded-*. Sin ellas, Express vería todas las peticiones como provenientes de 127.0.0.1 y el rate limit trataría a todo internet como un solo cliente. El código ya trae app.set('trust proxy', 1), que le indica a Express que confíe en exactamente un proxy delante: encaja con este montaje.
client_max_body_size 12m. El límite de subida de la API es 10 MB (UPLOAD_MAX_BYTES); los 2 MB extra cubren el overhead del multipart. Con el default de 1 MB, Nginx rechaza las subidas de documentos con un 413 antes de que Node se entere, y el mensaje confunde.
proxy_read_timeout 120s. Coincide con el requestTimeout que el código fija en el servidor HTTP. Si Nginx cortara antes, verías 504 en operaciones legítimamente lentas como generar un PDF.
La caché de /api/metrics/. Con la base de datos en otro servidor, es la mejora individual más grande. Los endpoints de métricas hacen agregaciones pesadas cuyos resultados cambian poco: si el dashboard llama a veinte de ellos al abrirse y hay cinco personas mirando, pasas de cien consultas remotas por minuto a unas veinte. proxy_cache_lock on evita la estampida —cuando la caché expira, sólo una petición va a Node mientras las demás esperan ese resultado— y proxy_cache_use_stale error timeout hace que, si el enlace con la base de datos se cae, el dashboard siga mostrando los últimos datos conocidos en lugar de errores.
$http_x_api_key para no servirle a un cliente lo cacheado para otro. Y la caché se aplica sólo a /api/metrics/: jamás a /api/me, /api/documents, /api/clinical-history, /api/appointments ni /api/cases, que devuelven datos por paciente.Rate limit de login aparte. 10r/m sobre /api/auth/login frena la fuerza bruta contra las claves del portal en Nginx, antes de consumir un worker y una conexión a la base de datos. La API ya tiene su propio limitador, pero este actúa antes y es global, no por worker.
/docs/ servido por Nginx. La documentación son archivos estáticos; que los sirva Nginx libera a Node. El try_files … @node deja pasar hacia la aplicación las rutas dinámicas como /docs/api-catalog.json, que sí genera Express.
include de options-ssl-nginx.conf y el ssl_dhparam sólo existen si certbot los creó; si nginx -t se queja de que faltan, coméntalos. Y a partir de nginx 1.25, listen 443 ssl http2; se escribe como listen 443 ssl; más una línea http2 on;. En el nginx 1.24 de Ubuntu 24.04 la forma de arriba es la correcta.curl -sI http://integracorp-api.tudrgroup.com/api/health # 301 → https curl -s https://integracorp-api.tudrgroup.com/api/health curl -sI https://integracorp-api.tudrgroup.com/docs/endpoints.html # 200 sin pasar por Node
PASO 13 · OPCIONAL
El endpoint POST /api/appointments/:id/service-order/whatsapp no genera el PDF en Node: ejecuta php artisan operation-service-orders:ensure-pdf <id> dentro del proyecto Laravel de Integracorp.
400 con un mensaje claro y el resto de la API funciona con normalidad. No es un fallo del despliegue.apt install -y php8.3-cli php8.3-mbstring php8.3-xml php8.3-gd php8.3-mysql
Y en .env:
INTEGRACORP_APP_PATH=/var/www/integracorp PHP_BINARY=/usr/bin/php INTEGRACORP_STORAGE_BASE_URL=https://integracorp.tudrgroup.com/storage
El usuario deploy necesita permiso de escritura sobre el storage/ de ese Laravel, porque ahí se deposita el PDF generado. Después: systemctl restart integracorp-api.
PASO 14
su - deploy cd /var/www/integracorp-api git pull npm ci --omit=dev exit systemctl restart integracorp-api curl -s https://integracorp-api.tudrgroup.com/api/health/db
El reinicio corta las peticiones en vuelo. Si más adelante necesitas despliegues sin corte, se puede implementar recarga progresiva de workers; con el volumen actual, un reinicio de dos segundos de madrugada no afecta a nadie.
journalctl -u integracorp-api -f # en vivo
journalctl -u integracorp-api --since "1 hour ago" | grep -i error
tail -f /var/log/nginx/error.log
Limita el tamaño del journal para que no llene el disco con el tiempo: en /etc/systemd/journald.conf fija SystemMaxUse=1G y reinicia systemd-journald.
La base de datos la respalda el otro servidor, pero los archivos subidos viven aquí y sólo aquí:
mkdir -p /var/backups/storage cat > /usr/local/bin/backup-storage.sh <<'EOF' #!/bin/bash tar czf /var/backups/storage/uploads-$(date +%F).tar.gz -C /var/www/storage uploads find /var/backups/storage -name '*.tar.gz' -mtime +14 -delete EOF chmod +x /usr/local/bin/backup-storage.sh ( crontab -l 2>/dev/null; echo "0 4 * * * /usr/local/bin/backup-storage.sh" ) | crontab -
| Comando | Qué te dice |
|---|---|
curl -s localhost:4000/api/health/db | Latencia a la base de datos. Es el indicador principal: si sube, todo se siente lento. |
htop | Si los 8 workers están al 100 % de forma constante, sube WEB_CONCURRENCY. |
free -h | Los workers deberían sumar bastante menos de 4 GB. Un crecimiento sostenido indica fuga de memoria. |
mtr -rwc 30 10.8.0.1 | Pérdida de paquetes o jitter hacia la base de datos. |
curl -sI … | grep X-Cache-Status | Si siempre responde MISS, la caché de métricas no está funcionando. |
Monta además un chequeo externo (UptimeRobot o similar) contra https://integracorp-api.tudrgroup.com/api/health: es lo único que te avisa cuando el servidor entero deja de responder.
PASO 15
systemctl is-enabled integracorp-api nginx wg-quick@wg0 # los tres: enabled ufw status # sólo 22, 80, 443 curl -s https://integracorp-api.tudrgroup.com/api/health curl -s https://integracorp-api.tudrgroup.com/api/health/db curl -s https://integracorp-api.tudrgroup.com/api/endpoints -H "X-API-Key: $KEY" | head -c 200 curl -sI https://integracorp-api.tudrgroup.com/docs/endpoints.html curl -sI https://integracorp-api.tudrgroup.com/api/metrics/dashboard/venezuela-by-state \ -H "X-API-Key: $KEY" | grep X-Cache-Status # MISS, y HIT al repetir journalctl -u integracorp-api | grep -c whatsapp-queue # exactamente 1 reboot # y repite todo lo anterior
reboot final no es paranoia: es la única forma de comprobar que el túnel, el servicio y Nginx levantan solos y en el orden correcto tras un corte de energía.WEB_CONCURRENCY a 10.DB_CONNECTION_LIMIT. Es síntoma de consultas lentas acumulándose, no de falta de conexiones.proxy_cache_valid a 300s.my.cnf con InnoDB afinado, usuario integracorp_api@10.8.0.2, wait_timeout coherente con el pool, la misma zona horaria e índices para las consultas de métricas.