Apariencia
Deploy del frontend a cPanel (develop)
Esta página documenta el flujo real que estamos usando para desplegar el frontend Next.js a un hosting cPanel (develop.humae.com.mx en host.prohospedame.com). Es el camino "build local + zip + extract en server" que evita las limitaciones de memoria del cPanel compartido.
TL;DR — 3 comandos por deploy
bash
# 1. En tu Mac
cd humae_frontend && npm run deploy:zip
# 2. Subir el zip por File Manager a:
# /home2/humaecom/develop.humae.com.mx/
# 3. En SSH
bash ~/develop.humae.com.mx/deploy.shPor qué este flujo
Intentamos primero buildear directamente en el servidor (npm ci && npm run build por SSH), pero el plan de cPanel compartido no tiene memoria suficiente: npm ci se cuelga o tira OOM al transpilar con Turbopack en Next 16. Solución pragmática: buildear en local, empaquetar el .next/standalone/ y subirlo.
Hay dos scripts que automatizan todo el proceso:
| Script | Dónde corre | Qué hace |
|---|---|---|
humae_frontend/scripts/build-deploy-zip.sh | Tu Mac | Limpia .next/, hace npm ci, next build, copia static/ y public/ adentro del standalone, verifica rutas críticas, empaqueta ~/Desktop/humae-frontend-standalone.zip. |
humae_frontend/scripts/server-deploy.sh | SSH cPanel | Verifica el zip, respalda .env, borra build viejo, extrae nuevo, restaura .env, mata Node, fuerza restart de Passenger y corre smoke tests. |
Setup inicial (una sola vez)
1. Crear la app de Node.js en cPanel
cPanel → Setup Node.js App → Create Application:
| Campo | Valor |
|---|---|
| Node.js version | 20.20.0 (recommended) |
| Application mode | Production |
| Application root | develop.humae.com.mx/.next/standalone |
| Application URL | develop.humae.com.mx |
| Application startup file | server.js |
| Passenger log file | (vacío) |
Click Create.
Si ves "No such application or it's broken"
Pasa cuando se cambia el Application root y cPanel deja el nodevenv en una ruta vieja. Solución: DESTROY la app desde la misma pantalla y vuelve a crearla con los mismos valores. No se borran tus archivos, solo el nodevenv que está en /home2/humaecom/nodevenv/....
2. Variables de entorno
En la misma pantalla, scroll abajo a Environment variables y agrega:
env
NODE_ENV=production
HOSTNAME=0.0.0.0
PORT=3000
NEXT_PUBLIC_API_URL=https://develop.backend-v1.humae.com.mx/api/v1
NEXT_PUBLIC_SITE_URL=https://develop.humae.com.mxPassenger asigna su propio puerto/socket internamente; el PORT solo lo usa Next como hint.
3. Subir el script de deploy del server
Vía cPanel File Manager, sube humae_frontend/scripts/server-deploy.sh a:
/home2/humaecom/develop.humae.com.mx/deploy.shLo renombramos a
deploy.shpara que el comando final sea más corto.
Dale permisos 755 desde el File Manager (Right click → Permissions).
4. Generar y subir el primer zip
En tu Mac:
bash
cd humae_frontend
npm run deploy:zipEso produce ~/Desktop/humae-frontend-standalone.zip (~15 MB).
Súbelo vía cPanel File Manager a:
/home2/humaecom/develop.humae.com.mx/5. Primer deploy
bash
ssh humaecom@host.prohospedame.com
bash ~/develop.humae.com.mx/deploy.shAl final del script verás el smoke test. Si alguna ruta da 502, ve a cPanel → Setup Node.js App → RESTART manual.
Deploys subsecuentes (flujo rápido)
Cada vez que cambia algo en humae_frontend/:
En tu Mac
bash
cd humae_frontend
npm run deploy:zipEl script imprime al final:
✅ Zip listo: /Users/.../Desktop/humae-frontend-standalone.zip (15M)Sube el zip
cPanel File Manager → /home2/humaecom/develop.humae.com.mx/ → Upload → arrastra ~/Desktop/humae-frontend-standalone.zip. Sobrescribe si ya hay uno.
En SSH
bash
bash ~/develop.humae.com.mx/deploy.shEl script:
- Verifica que el zip nuevo trae las rutas críticas (
success/page.js,cancel/page.js). - Respalda
.envy.env.productiondel standalone actual. - Borra
~/develop.humae.com.mx/.next/completo. - Extrae el zip nuevo en
.next/standalone/. - Restaura los
.env. - Mata el proceso Node viejo (
pkill -f "node.*server.js"). - Fuerza Passenger restart (
touch tmp/restart.txt). - Espera 4 segundos para que respawnee.
- Corre smoke test sobre 5 rutas:
/ → 200 /login → 200 /membership/success → 200 /membership/success?cs=test → 200 /membership/cancel → 200
Si todos dan 200, el deploy fue exitoso.
Estructura final en el server
Después de un deploy exitoso:
/home2/humaecom/develop.humae.com.mx/
├── .htaccess
├── deploy.sh ← script de deploy
├── humae-frontend-standalone.zip ← último zip subido
└── .next/standalone/
├── server.js ← Passenger startup
├── package.json
├── .env ← env vars de runtime
├── .env.production
├── node_modules/ ← incluido en standalone
├── public/ ← copiado desde public/
└── .next/
├── static/ ← copiado desde .next/static
└── server/
└── app/
├── (app)/
│ ├── dashboard/
│ ├── membership/
│ │ ├── cancel/page.js
│ │ └── success/page.js ← clave
│ └── ...
└── ...No buildees en el server
El .next/ raíz (fuera del standalone) no debe existir en el server después del deploy. El standalone es autocontenido y eso es lo que Passenger necesita. Si ves un .next/ raíz, es residuo de un intento de build local que falló — bórralo.
⚠️ Gotcha crítico — WAF / mod_security bloquea ?session_id=
Síntoma
Una request a /membership/success?session_id=cs_test_xxx devuelve 404 con headers x-nextjs-prerender: 1,1 y x-nextjs-cache: HIT aunque el build sí tenga la ruta y todas las demás funcionen.
cPanel viene con mod_security activo. Una de las reglas (CRS / Comodo / Atomicorp) flagea el query param session_id como SQL injection / token leak y devuelve 404 antes de pasar la request a Passenger. Los headers x-nextjs-cache: HIT no son de Next — son del 404 prerenderizado global que Apache devuelve como página de bloqueo.
Cómo confirmar que es WAF
bash
curl -s -o /dev/null -w "%{http_code}\n" "https://develop.humae.com.mx/membership/success"
# → 200
curl -s -o /dev/null -w "%{http_code}\n" "https://develop.humae.com.mx/membership/success?foo=bar"
# → 200
curl -s -o /dev/null -w "%{http_code}\n" "https://develop.humae.com.mx/membership/success?session_id=test"
# → 404 ← solo este fallaSi el patrón coincide (la misma ruta funciona con/sin query genérica pero falla con ?session_id=), es WAF.
Solución aplicada — usar ?cs=
Renombramos el query param que Stripe nos devuelve:
- Backend (
MembershipService.php:38) construyesuccess_urlcon?cs={CHECKOUT_SESSION_ID}. - Frontend (
success-client.tsx) leecscon fallback asession_idpara sesiones viejas:tsxconst sessionId = params.get("cs") ?? params.get("session_id");
Si en el futuro otra integración usa un param flagged
Tienes 2 opciones:
- Renombrar el param (lo que hicimos aquí — más rápido).
- Pedir a
prohospedameque deshabiliten la regla específica de mod_security para este dominio. Para identificar el rule ID, mira/usr/local/apache/logs/modsec_audit.logo~/develop.humae.com.mx/error_logjusto después de un request bloqueado — busca[id "..."].
Configuración del Stripe webhook en develop
En el Stripe Dashboard (modo Test, porque STRIPE_SECRET en el server es sk_test_…):
- Developers → Webhooks → Add endpoint.
- Endpoint URL:
https://develop.backend-v1.humae.com.mx/api/v1/webhooks/stripe. - Events to send:
checkout.session.completed(es el único que elStripeWebhookControllermaneja hoy). - Click Add endpoint → copia el Signing secret (
whsec_…). - En el
.envdel backend de develop:STRIPE_WEBHOOK_SECRET=whsec_<el nuevo signing secret> - Limpia config:bash
cd ~/develop.backend-v1.humae.com.mx /opt/cpanel/ea-php83/root/usr/bin/php artisan config:clear /opt/cpanel/ea-php83/root/usr/bin/php artisan config:cache - Verifica con Send test webhook desde el Dashboard → debe responder 200 y aparecer
Event processed.en~/develop.backend-v1.humae.com.mx/storage/logs/laravel-*.log.
Deshabilita endpoints viejos
Si hay un endpoint apuntando a localhost o a un dominio antiguo, deshabilítalo desde el Dashboard para no duplicar entregas.
Configuración de URLs en correos para develop
Todos los enlaces que se envían por correo (verify-email, reset password, notificaciones de membresía/entrevistas/asignaciones) se construyen con dos variables:
| Variable | Lee de | Impacto |
|---|---|---|
FRONTEND_URL | config('app.frontend_url') | URLs del frontend (verify, reset, dashboards) |
APP_URL | Laravel core | URLs absolutas del backend (verify callback firmado) |
En ~/develop.backend-v1.humae.com.mx/.env:
env
APP_ENV=staging
APP_URL=https://develop.backend-v1.humae.com.mx
FRONTEND_URL=https://develop.humae.com.mx
SANCTUM_STATEFUL_DOMAINS=develop.humae.com.mxY limpia caché:
bash
cd ~/develop.backend-v1.humae.com.mx
/opt/cpanel/ea-php83/root/usr/bin/php artisan config:clear
/opt/cpanel/ea-php83/root/usr/bin/php artisan cache:clear
/opt/cpanel/ea-php83/root/usr/bin/php artisan config:cache
/opt/cpanel/ea-php83/root/usr/bin/php artisan queue:restart # si hay queue:work corriendoVerificación rápida (sin mandar correo) vía tinker:
bash
/opt/cpanel/ea-php83/root/usr/bin/php artisan tinker --execute="echo config('app.frontend_url').PHP_EOL.config('app.url').PHP_EOL.url('/').PHP_EOL;"Esperado:
https://develop.humae.com.mx
https://develop.backend-v1.humae.com.mx
https://develop.backend-v1.humae.com.mxSMTP autenticado vs Postfix local
Originalmente el backend mandaba mail vía Postfix local (MAIL_HOST=127.0.0.1 puerto 25). Funciona pero la entregabilidad depende de la reputación de humae.com.mx y de DKIM/SPF/DMARC del cPanel.
Una vez que en cPanel → Email Deliverability los dominios develop.humae.com.mx, develop.backend-v1.humae.com.mx y humae.com.mx aparecen como Valid, lo cleaner es mandar por SMTP autenticado del cPanel:
env
MAIL_MAILER=smtp
MAIL_HOST=mail.develop.humae.com.mx
MAIL_PORT=465
MAIL_USERNAME=no-reply@develop.humae.com.mx
MAIL_PASSWORD="<password del buzón>"
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS="no-reply@develop.humae.com.mx"
MAIL_FROM_NAME="${APP_NAME}"
MAIL_REPLY_TO="reclutamiento@humae.com.mx"
MAIL_EHLO_DOMAIN=develop.humae.com.mx(Crear el buzón no-reply@develop.humae.com.mx antes en cPanel → Email Accounts.)
Si 465/SSL bloqueado, usa STARTTLS:
env
MAIL_PORT=587
MAIL_ENCRYPTION=tlsSmoke test:
bash
/opt/cpanel/ea-php83/root/usr/bin/php artisan tinker --execute="Mail::raw('test', fn(\$m) => \$m->to('andresj172020@gmail.com')->subject('HUMAE develop test'));"Verifica los headers del correo recibido (Gmail → "Mostrar original"):
SPF: PASS (mailfrom: develop.humae.com.mx)DKIM: PASS (d=develop.humae.com.mx)← claveDMARC: PASS
Troubleshooting frontend cPanel
| Síntoma | Causa | Fix |
|---|---|---|
404 con ?session_id=... | mod_security bloquea | Renombrar a ?cs= (ver §gotcha arriba) |
404 con x-nextjs-cache: HIT | Build viejo cacheado en Passenger | pkill -f "node.*server.js" && touch .next/standalone/tmp/restart.txt (lo hace deploy.sh) |
| Deploy ejecutado pero sigue 404 | Zip subido era el viejo | ls -lh humae-frontend-standalone.zip — fecha debe ser de minutos. Si no, vuelve a subir. |
| cPanel: "No such application or it's broken" | nodevenv desincronizado | DESTROY + recrear app de Node.js (no se borran archivos) |
success/page.js existe pero ruta da 404 | Next prerenderizó como 404 (el bug que tuvimos al cambiar "use client" al page.tsx) | Mantener page.tsx como server component que importa el client component hijo. Ver success/page.tsx + success/success-client.tsx. |
cp: cannot stat '.next/static' | Alguien intentó buildear en server o el zip no traía static | Re-buildear local con npm run deploy:zip (el script copia static al standalone antes del zip) |
npm ci se cuelga / OOM | Memoria limitada en cPanel shared | No buildees en el server. Usa npm run deploy:zip en local. |
| Passenger 502 después de deploy | App no arrancó | cPanel → Setup Node.js App → revisar Passenger log. Usualmente falta output: 'standalone' en next.config.ts o no se copiaron static//public/ |
Cambios en .env no se reflejan | Worker viejo en memoria | pkill -f "node.*server.js" y/o restart desde cPanel |
Apéndice: contenido de los scripts
build-deploy-zip.sh (corre en local)
bash
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ZIP_OUT="${ZIP_OUT:-$HOME/Desktop/humae-frontend-standalone.zip}"
cd "$ROOT_DIR"
echo "→ Limpiando build anterior"
rm -rf .next
echo "→ Instalando dependencias (npm ci)"
npm ci
echo "→ Build de producción"
NODE_ENV=production npm run build
echo "→ Copiando assets al standalone"
cp -R .next/static .next/standalone/.next/static
cp -R public .next/standalone/public
echo "→ Verificando que el build incluya rutas críticas"
test -f ".next/standalone/.next/server/app/(app)/membership/success/page.js" \
|| { echo "ERROR: falta success/page.js en el build"; exit 1; }
test -f ".next/standalone/.next/server/app/(app)/membership/cancel/page.js" \
|| { echo "ERROR: falta cancel/page.js en el build"; exit 1; }
echo "→ Empaquetando zip"
rm -f "$ZIP_OUT"
(cd .next/standalone && zip -rq "$ZIP_OUT" . -x ".env" ".env.production" ".env.local")
echo "✅ Zip listo: $ZIP_OUT"Se invoca con npm run deploy:zip (registrado en package.json).
server-deploy.sh (renombrado a deploy.sh en el server)
Maneja todo el ciclo de extracción + restart + verificación. Ver el archivo en humae_frontend/scripts/server-deploy.sh.
Referencias
- Checklist maestro de deploy — pasos pre-producción
- Variables de entorno — lista completa de env vars
- Stripe (pagos) — flujo de checkout y webhooks
- SMTP local — configuración de Postfix/SMTP
- Backend troubleshooting

