Volver al blogGuías

Cómo desplegar Next.js en un hosting compartido con cPanel (sí, se puede)

farp.dev corre en un hosting compartido de $10 al mes con Next.js 16. Esta es la guía completa del setup real: Passenger, el server.js que necesitas, por qué NO usar output standalone, y cómo verificar que desplegaste bien.

FR
Francisco Reyes
CDO · Director Digital Externo
19 de julio de 202612 min de lectura

Cada vez que digo que esta web —farp.dev, Next.js 16, App Router, la que estás leyendo— corre en un hosting compartido con cPanel, alguien me mira raro. "Next.js es para Vercel", me dicen. (Si todavía estás decidiendo la tecnología y no el despliegue, esa discusión previa está en WordPress o Next.js para una empresa.) Y sí, Vercel es el camino cómodo. Pero yo ya pagaba un hosting compartido para otras cosas, y me negaba a pagar otra plataforma para servir una web que un servidor normal puede servir perfectamente.

Desarrolladora trabajando de noche frente a dos monitores durante un despliegue de Next.js en un servidor cPanel

Me costó varios 503, un par de noches y una conversación con el soporte de FastComet entenderlo del todo. Esta guía es el resultado: el proceso completo, con los errores que ya cometí yo para que no los cometas tú.

En pocas palabras: vas a desplegar una app Next.js en un hosting compartido con cPanel usando "Setup Node.js App" (Passenger), con build local y un entry point propio. Tiempo estimado: 1 a 2 horas la primera vez, 10 minutos los deploys siguientes. Nivel: intermedio — necesitas manejar terminal básico y tener una app Next.js que ya buildea sin errores.

Antes de empezar

Requisitos:

  • Un hosting cPanel con "Setup Node.js App" (a veces aparece como "Application Manager"). No todos los planes compartidos lo incluyen — revisa antes de empezar o pregunta al soporte. Yo uso FastComet; SiteGround, A2 y varios más también lo traen.
  • Node.js 18+ disponible en el hosting (para Next.js 16, idealmente 20+). La versión se elige al crear la app en cPanel.
  • Tu app buildeando en local sin errores. Si npm run build no pasa en tu máquina, arréglalo antes; el servidor no va a arreglarlo por ti.
  • Acceso SSH o al File Manager para subir archivos.

Y seamos honestos sobre cuándo tiene sentido esto y cuándo no:

Tiene sentido si: ya pagas el hosting (costo extra: $0), tu tráfico es moderado (una web corporativa, un blog, una landing con miles de visitas al mes, no millones), y tu app es mayormente SSR/SSG estándar.

NO tiene sentido si: dependes de funciones edge o de serverless específico de Vercel (ISR con revalidación agresiva, middleware pesado en edge, next/image con optimización bajo demanda intensiva), esperas picos de tráfico grandes, o necesitas escalar horizontal. Un compartido tiene límites de CPU y memoria estrictos (CloudLinux te corta si te pasas) y ahí no hay configuración que te salve: para eso, Vercel o un VPS. No fuerces la herramienta.

Paso 1: entender el truco central — Passenger necesita su propio entry point

Esto es lo que nadie te cuenta y la razón de que la mayoría de intentos mueran en un 503.

Cuando creas una app en "Setup Node.js App", cPanel no ejecuta npm start como harías tú. Usa Phusion Passenger, un gestor de aplicaciones que se integra con Apache. Passenger arranca tu app cargando un startup file — un archivo JS concreto — y espera que ese archivo levante un servidor HTTP. next start no encaja en ese modelo: Passenger no va a ejecutar un comando de CLI, va a hacer require() de tu archivo.

La solución es un server.js mínimo en la raíz del proyecto que arranque Next.js programáticamente:

const next = require("next");
const http = require("http");

const app = next({ dev: false });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  http
    .createServer((req, res) => handle(req, res))
    .listen(process.env.PORT || 3000);
});

Eso es todo. Passenger inyecta el PORT que le corresponde (en realidad Passenger gestiona el socket por ti, pero el patrón process.env.PORT || 3000 cubre ambos casos y te sirve también para probar en local con node server.js después de un build).

Commitea este archivo al repo. Es parte del proyecto, no un parche del servidor.

Paso 2: next.config.js SIN output standalone

Segundo error clásico, y este lo pagué con horas de 503 intermitentes: no uses output: 'standalone' con Passenger.

/** @type {import('next').NextConfig} */
const nextConfig = {
  // NO pongas esto en un hosting con Passenger:
  // output: 'standalone',
  reactStrictMode: true,
};

module.exports = nextConfig;

El modo standalone genera un mini-servidor autocontenido en .next/standalone con su propia copia parcial de node_modules, pensado para contenedores Docker. Con Passenger, el require("next") de tu server.js y el servidor standalone se pisan entre sí, las rutas de archivos estáticos no resuelven donde deben, y el resultado son 503 que aparecen y desaparecen sin patrón. Lo cuento porque lo sufrí en producción: la web caía, reiniciaba, volvía, caía de nuevo. El día que quité output: 'standalone' y redesplegué, se acabó.

Si tu app viene de un template que lo trae puesto (muchos lo traen "para Docker por si acaso"), quítalo.

Paso 3: build local y subida de archivos

El build se hace en tu máquina, no en el servidor. Un compartido con CloudLinux tiene límites de CPU/RAM que hacen que un next build sea lentísimo o directamente muera a medias. Además, buildear en producción significa que si el build falla, tu web ya está rota.

npm run build

Lo que tiene que llegar al servidor:

  • .next/ (el build — sin .next/cache, ver abajo)
  • public/
  • package.json (y package-lock.json)
  • next.config.js
  • server.js
  • src/ no es necesario para servir, pero si despliegas por git irá igual; no molesta.

Para subirlo tienes tres vías; te digo cuál uso yo y por qué:

Opción A — tarball por scp (mi día a día, y la que recomiendo):

tar -czf build.tgz .next public package.json package-lock.json next.config.js server.js --exclude=".next/cache"

scp -P 22 build.tgz tuusuario@tudominio.com:~/miapp/

# ya en el servidor (terminal de cPanel o SSH):
cd ~/miapp && tar -xzf build.tgz && rm build.tgz

Excluir .next/cache no es opcional por elegancia: es la diferencia entre subir 30 MB y subir 400.

Opción B — git: commitear .next/ al repo y hacer git pull en el servidor. Funciona, pero va contra la costumbre y ensucia el historial con miles de archivos de build; yo lo hice durante un tiempo y acabé volviendo al tarball. Si lo eliges, excluye .next/cache en .gitignore (.next/cache/ puede pesar cientos de MB y es basura regenerable) y, si alguna vez usaste standalone, .next/standalone/ también.

Opción C — File Manager de cPanel: sube el build.tgz por el navegador y usa "Extract" desde la interfaz. Funciona igual; es la opción si no tienes SSH.

Paso 4: crear la app en cPanel

Ahora sí, cPanel → Setup Node.js App → Create Application:

  • Node.js version: la más cercana a la que usas en local (20.x en mi caso). Buildear con Node 20 y servir con Node 16 es pedir problemas.
  • Application mode: Production.
  • Application root: la carpeta donde subiste todo, p. ej. miapp (relativa a tu home).
  • Application URL: el dominio o subdominio que servirá la app.
  • Application startup file: server.js — el del paso 1. Este campo es EL campo.

Guarda. cPanel crea la estructura de Passenger y un entorno virtual de Node para tu app.

Paso 5: npm install desde el botón de cPanel (no por SSH)

Dentro de la misma pantalla de la app verás un botón "Run NPM Install". Úsalo. No corras npm install por SSH.

La razón es específica de CloudLinux (que usan casi todos los compartidos): node_modules en tu carpeta no es una carpeta real, es un symlink a un entorno virtual que cPanel gestiona por app. Si corres npm install por SSH sin activar ese entorno, npm puede reemplazar el symlink por una carpeta real, y a partir de ahí Passenger y cPanel dejan de entenderse: instalaciones que "funcionan" pero la app no encuentra los módulos, o errores raros al reiniciar. Me pasó, y la solución fue borrar node_modules, recrear el symlink desde cPanel y reinstalar desde el botón.

Si necesitas sí o sí instalar por terminal, la propia pantalla de la app te muestra el comando para "entrar" al virtualenv (algo como source /home/tuusuario/nodevenv/miapp/20/bin/activate), y desde ahí sí puedes usar npm. Pero el botón hace exactamente eso sin riesgo de olvidarlo.

Paso 6: variables de entorno en la UI, no en el repo

En la misma pantalla de Setup Node.js App hay una sección Environment Variables. Ahí van tus GOOGLE_API_KEY, secrets de reCAPTCHA, contraseñas de servicios — todo lo que en local tienes en .env.local.

Dos reglas:

  1. Nunca subas .env ni .env.local al repo. Están en .gitignore por algo. Un repo con secrets es una brecha esperando fecha.
  2. Después de añadir o cambiar una variable, hace falta Restart para que la app la vea. Variables que "no funcionan" tras añadirlas = te faltó reiniciar, el 99% de las veces.

Ten en cuenta que las variables NEXT_PUBLIC_* se incrustan en el build, en tu máquina — cambiarlas en cPanel no hace nada si no rebuildeas y resubes. Solo las variables de servidor (las que lees en API routes y SSR) se pueden cambiar desde la UI sin rebuild.

Paso 7: Restart y verificación

Botón Restart Application. Espera unos segundos y abre tu dominio.

¿Cargó? Bien, pero no cantes victoria todavía: verifica que lo que se está sirviendo es el build que subiste. El truco que uso siempre es comparar el BUILD_ID, un archivo que Next.js genera en cada build con un identificador único:

# en tu máquina:
cat .next/BUILD_ID

# en el servidor:
cat ~/miapp/.next/BUILD_ID

Si coinciden, el servidor tiene tu build exacto. Si no, estás sirviendo un build viejo — probablemente el pull/subida no llegó o extraíste en la carpeta equivocada. Este check de 10 segundos me ha ahorrado muchos "pero si yo lo subí" a lo largo del tiempo.

Última verificación: navega 3 o 4 rutas distintas, incluyendo alguna API route si tienes, y mira que los estilos carguen (si la web sale "en texto plano", los assets de .next/static no están llegando — ver problemas comunes).

El flujo resumido (para los deploys siguientes)

Una vez montado, cada deploy es:

npm run build          # local
tar -czf build.tgz .next public package.json package-lock.json next.config.js server.js --exclude=".next/cache"
scp build.tgz tuusuario@tudominio.com:~/miapp/
# en el servidor: extraer el tarball
# cPanel → Restart Application
# verificar BUILD_ID

Diez minutos, sin sorpresas. Yo incluso tengo parte de esto delegado a una IA con un canal SSH restringido: cómo conectar una IA a tu hosting cPanel.

Problemas comunes

503 Service Unavailable después del deploy. Los tres sospechosos, en orden de probabilidad: (1) tienes output: 'standalone' en next.config.js — quítalo, rebuildea, resube; (2) te faltó el Restart después de subir — Passenger sigue sirviendo el proceso viejo, que ahora apunta a archivos que cambiaron debajo suyo; (3) el startup file está mal configurado o server.js no está donde cPanel espera. Para diagnosticar, revisa stderr.log en la raíz de la app — Passenger escribe ahí los errores de arranque, y ese archivo te dice la verdad.

Assets 404 (la web carga "sin estilos", imágenes rotas). Subiste .next incompleto — típicamente falta .next/static. Pasa cuando el tarball se hizo mal o cuando el File Manager corta la subida. Rehaz el tarball verificando su contenido con tar -tzf build.tgz | head -50 antes de subir.

Sigo viendo la versión vieja después del Restart. Antes de volverte loco: es tu navegador. Prueba en ventana de incógnito o con Ctrl+Shift+R. Los assets de Next llevan hashes, pero el HTML puede quedar cacheado. Si en incógnito también sale la vieja, entonces sí: verifica BUILD_ID (paso 7) — probablemente el build nuevo nunca llegó.

Errores de permisos al extraer o al arrancar. Los archivos deben pertenecer a tu usuario de cPanel. Si subiste por métodos mezclados (FTP con otro usuario, root en un VPS), Passenger puede negarse a arrancar. ls -la para revisar; en compartido normal esto casi nunca pasa si siempre subes con el mismo usuario.

La app arranca pero muere a los segundos (o va lentísima). Mira el uso de recursos en cPanel (CloudLinux → LVE). Si estás topando memoria o CPU constantemente, tu app quedó grande para el plan. Es la señal honesta de que toca subir de plan o migrar a un VPS — no hay flag de configuración que fabrique RAM. Cómo distinguir ese caso del "la web va lenta pero la culpa no es del servidor" lo desarrollé en cuándo cambiar de hosting.

Preguntas frecuentes

¿Qué rendimiento puedo esperar? ¿Es mucho peor que Vercel? Para el tráfico de una pyme, va sobrado: en mi experiencia con farp.dev, respuestas SSR normales y Core Web Vitals en verde. No tienes la CDN global ni el edge de Vercel — si tu audiencia está repartida por el mundo, eso se nota; si es de un país, apenas. La respuesta honesta: es rendimiento "correcto", no "espectacular", y cuesta $0 extra si el hosting ya lo pagas. Ese es el trato.

¿Funcionan las API routes, el App Router y el SSR? Sí, todo: al final estás corriendo el servidor Node real de Next.js, no una versión recortada ni un export estático. Lo que no tienes es lo específico de plataformas serverless: funciones edge, ISR distribuido, colas de imagen. Si tu app usa next/image con muchas imágenes remotas, considera pre-optimizarlas tú (yo las convierto a WebP en el pipeline, antes del build).

¿Y las actualizaciones de Next.js? ¿Se rompe esto con cada versión mayor? El patrón del server.js programático es API pública de Next.js y lleva estable muchas versiones — yo he pasado por varias majors (hasta Next 16 hoy) sin tocar ese archivo. Lo que sí debes hacer en cada actualización: rebuildear en local, probar con node server.js en local antes de subir, y verificar que la versión de Node del hosting cumple el mínimo de la nueva versión de Next.


Este es, literalmente, el setup con el que se sirve la página que acabas de leer. Si lo intentas y te atascas en un 503 que no cede, o quieres decidir con datos si tu proyecto cabe en un compartido o necesita algo más, puedo verlo contigo en una sesión de consultoría 1:1 ($60, una hora): revisamos tu hosting y tu app en vivo y sales con el deploy funcionando o con un diagnóstico claro de por qué no.

Next.jscPanelhosting compartidodeployPassengerNode.js
Guía gratis en PDFTu primera web con Claude Code, publicada en Vercel

Newsletter

¿Te fue útil este artículo?

Suscríbete y te aviso cuando publique algo que valga la pena leer. Sin correo basura y sin una frecuencia forzada.

Responsable: Francisco Reyes (FARP). Uso tus datos para enviarte la newsletter y para nada más; los guardo hasta que te des de baja y no se los cedo a nadie. Puedes pedirme acceso, corrección o borrado cuando quieras escribiendo a francisco@laura.lat. Detalle completo en la Política de Privacidad.

FR
Francisco Reyes
Director Digital Externo · 11 años alineando marketing, ventas y tecnología en PYMEs de Chile, España y LATAM. Creador de FARP.
Conversar sobre esto

¿Una duda rápida?

Escríbeme y te contesto en menos de 24 horas hábiles.