Skip to content

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.sh

Por 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:

ScriptDónde correQué hace
humae_frontend/scripts/build-deploy-zip.shTu MacLimpia .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.shSSH cPanelVerifica 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 AppCreate Application:

CampoValor
Node.js version20.20.0 (recommended)
Application modeProduction
Application rootdevelop.humae.com.mx/.next/standalone
Application URLdevelop.humae.com.mx
Application startup fileserver.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.mx

Passenger 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.sh

Lo renombramos a deploy.sh para 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:zip

Eso 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.sh

Al final del script verás el smoke test. Si alguna ruta da 502, ve a cPanel → Setup Node.js AppRESTART manual.


Deploys subsecuentes (flujo rápido)

Cada vez que cambia algo en humae_frontend/:

En tu Mac

bash
cd humae_frontend
npm run deploy:zip

El 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.sh

El script:

  1. Verifica que el zip nuevo trae las rutas críticas (success/page.js, cancel/page.js).
  2. Respalda .env y .env.production del standalone actual.
  3. Borra ~/develop.humae.com.mx/.next/ completo.
  4. Extrae el zip nuevo en .next/standalone/.
  5. Restaura los .env.
  6. Mata el proceso Node viejo (pkill -f "node.*server.js").
  7. Fuerza Passenger restart (touch tmp/restart.txt).
  8. Espera 4 segundos para que respawnee.
  9. 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 falla

Si 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) construye success_url con ?cs={CHECKOUT_SESSION_ID}.
  • Frontend (success-client.tsx) lee cs con fallback a session_id para sesiones viejas:
    tsx
    const sessionId = params.get("cs") ?? params.get("session_id");

Si en el futuro otra integración usa un param flagged

Tienes 2 opciones:

  1. Renombrar el param (lo que hicimos aquí — más rápido).
  2. Pedir a prohospedame que deshabiliten la regla específica de mod_security para este dominio. Para identificar el rule ID, mira /usr/local/apache/logs/modsec_audit.log o ~/develop.humae.com.mx/error_log justo 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_…):

  1. Developers → Webhooks → Add endpoint.
  2. Endpoint URL: https://develop.backend-v1.humae.com.mx/api/v1/webhooks/stripe.
  3. Events to send: checkout.session.completed (es el único que el StripeWebhookController maneja hoy).
  4. Click Add endpoint → copia el Signing secret (whsec_…).
  5. En el .env del backend de develop:
    STRIPE_WEBHOOK_SECRET=whsec_<el nuevo signing secret>
  6. 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
  7. 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:

VariableLee deImpacto
FRONTEND_URLconfig('app.frontend_url')URLs del frontend (verify, reset, dashboards)
APP_URLLaravel coreURLs 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.mx

Y 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 corriendo

Verificació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.mx

SMTP 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=tls

Smoke 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)clave
  • DMARC: PASS

Troubleshooting frontend cPanel

SíntomaCausaFix
404 con ?session_id=...mod_security bloqueaRenombrar a ?cs= (ver §gotcha arriba)
404 con x-nextjs-cache: HITBuild viejo cacheado en Passengerpkill -f "node.*server.js" && touch .next/standalone/tmp/restart.txt (lo hace deploy.sh)
Deploy ejecutado pero sigue 404Zip subido era el viejols -lh humae-frontend-standalone.zip — fecha debe ser de minutos. Si no, vuelve a subir.
cPanel: "No such application or it's broken"nodevenv desincronizadoDESTROY + recrear app de Node.js (no se borran archivos)
success/page.js existe pero ruta da 404Next 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 staticRe-buildear local con npm run deploy:zip (el script copia static al standalone antes del zip)
npm ci se cuelga / OOMMemoria limitada en cPanel sharedNo buildees en el server. Usa npm run deploy:zip en local.
Passenger 502 después de deployApp 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 reflejanWorker viejo en memoriapkill -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

Manual de usuario HUMAE · Uso interno