Ingeniería
Ejecuta Next.js como un servidor Node sin Lambda
Despliega Next.js como un servidor Node con compilaciones standalone, systemd, NGINX, streaming, decisiones sobre caché compartida y despliegues seguros de nuevas versiones.
Next.js puede ejecutarse como un proceso persistente de Node: compila la aplicación, inicia su servidor de producción y envíale solicitudes HTTP. El renderizado del servidor, Route Handlers, Server Actions, la optimización de imágenes y el streaming pueden funcionar allí sin un adaptador Lambda. Aquí se explica cómo empaquetar el servidor, mantenerlo en ejecución y desplegar una segunda instancia sin crear problemas de caché ni de versiones.
Qué se ejecuta dentro del proceso Node
Una solicitud entrante llega al proxy inverso y después a un servidor Next.js. Este puede devolver una página generada previamente, renderizar una página para la solicitud actual, ejecutar un Route Handler o servir una respuesta en caché. Si no hay datos en caché, puede consultar la base de datos o la API. No necesitas crear una capa Express para que funcione.
Un proceso persistente puede reutilizar grupos de conexiones a la base de datos y conexiones HTTP entre solicitudes. Aun así, necesitas CPU y memoria suficientes para los picos de tráfico, una política de reinicio y un procedimiento de despliegue. Un reinicio, una réplica nueva o una caché vacía pueden ralentizar las solicitudes; ejecutar Node no elimina esos costes.
| Salida | Cómo lo ejecutas | Qué desplegar |
|---|---|---|
| Servidor Node estándar | next build, después next start. | La compilación, los recursos públicos, los metadatos del paquete y las dependencias de ejecución necesarias. |
| Servidor Node standalone | Configura output: standalone, compila y ejecuta el server.js generado. | Archivos de ejecución rastreados, más public y .next/static. Esta es la ruta principal que se explica a continuación. |
| Exportación estática | Sirve los archivos exportados desde un servidor HTTP o un almacén de objetos. | Solo archivos estáticos. Las funciones del servidor ejecutadas al recibir solicitudes necesitan otro backend. |
The deployment we will build
Browser
|
v
NGINX :443 -- TLS, request limits, streaming passthrough
|
v
Next.js / Node :3001 -- rendering, routes, Server Actions
| |
v v
Database / API Next.js caches
systemd supervises the Node process.
Each release gets its own directory and configuration.Ejecuta primero la compilación de producción
Asegúrate de que la aplicación funciona con el servidor de producción antes de configurar una máquina virtual o un contenedor. Conserva estos scripts en package.json e instala desde el archivo de bloqueo confirmado en el repositorio. Instala las dependencias de compilación antes de compilar; omitir devDependencies demasiado pronto puede eliminar herramientas que necesita el compilador.
package.json scripts
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}Comprueba el mismo modo que vas a desplegar
Con la salida estándar, los comandos siguientes inician el servidor Node completo en loopback. Comprueba una página dinámica, una solicitud autenticada y un Route Handler, además de la página de inicio. Una sesión de next dev que funcione no prueba el prerenderizado de producción, el rastreo de dependencias ni el comportamiento de caché de producción.
Standard output: local production check
npm ci
npm run build
npm start -- --hostname 127.0.0.1 --port 3000
# In a second terminal:
curl --fail --show-error -I http://127.0.0.1:3000/Empaqueta una versión standalone
La salida standalone empaqueta los archivos que Next.js rastrea para el servidor y genera el punto de entrada server.js. Añade las opciones siguientes a tu next.config.mjs actual, conservando los demás ajustes de la aplicación. Asigna un identificador de versión a cada compilación y reutiliza su resultado exacto en todas las réplicas de esa versión.
next.config.mjs
const nextConfig = {
output: "standalone",
deploymentId: process.env.RELEASE_ID,
};
export default nextConfig;Incluye los recursos del navegador
La salida standalone no copia automáticamente public ni .next/static. Inclúyelos si el servidor Node va a servir esos archivos. Si omites este paso, una página puede devolver HTML correctamente mientras todos los scripts del navegador devuelven 404.
Build, package, and start
npm ci
RELEASE_ID=release-001 npm run build
mkdir -p .next/standalone/.next
cp -a .next/static .next/standalone/.next/static
if [ -d public ]; then
cp -a public .next/standalone/public
fi
HOSTNAME=127.0.0.1 PORT=3001 RELEASE_ID=release-001 \
node .next/standalone/server.jsPrueba el artefacto fuera del árbol de código fuente
Copia el directorio standalone a una ubicación limpia e inicia allí server.js. Así detectas dependencias que solo funcionaban porque estaba presente la copia del código fuente. En un monorepo, inspecciona la estructura generada: puedes necesitar outputFileTracingRoot y outputFileTracingIncludes para paquetes compartidos o archivos abiertos mediante rutas dinámicas.
A separate terminal, using a different port
release_dir=$(mktemp -d)
cp -a .next/standalone/. "$release_dir/"
cd "$release_dir"
HOSTNAME=127.0.0.1 PORT=3002 RELEASE_ID=release-001 node server.jsSepara los valores de compilación de los secretos de ejecución
Una variable de entorno exclusiva del servidor puede leerse al recibir la solicitud. Una variable NEXT_PUBLIC_ se incorpora al JavaScript del navegador durante la compilación. Cambiarla al iniciar el proceso no actualiza un paquete de navegador ya compilado. El contenido renderizado en el servidor durante la compilación también puede capturar los valores disponibles en ese momento.
| Valor | Establécelo cuando | Consecuencia del despliegue |
|---|---|---|
| NEXT_PUBLIC_API_URL | Compilación de los recursos del navegador. | Vuelve a compilar para cambiar una URL incorporada al código o expón la configuración pública de ejecución prevista mediante tu propio endpoint. |
| DATABASE_URL / credenciales de la API | Al iniciar el servidor, cuando el código los lee dinámicamente. | Mantenlos fuera de las variables públicas, el control de versiones y las capas de imagen. |
| deploymentId | Compilación de la versión. | Utiliza un identificador para todas las réplicas de ese artefacto. Un cambio realizado solo al iniciar el proceso no reescribe la compilación. |
| PORT / HOSTNAME | Inicio de server.js. | Utiliza loopback detrás de un proxy en el mismo host; utiliza 0.0.0.0 dentro de un contenedor o una carga de trabajo gestionada. |
Añade una ruta de estado sin caché
Esta ruta espera una solicitud antes de leer RELEASE_ID e informa del estado del proceso. No comprueba una base de datos. Si atender tráfico requiere una base de datos, añade una comprobación de disponibilidad separada con los tiempos de espera de consultas y conexiones del cliente de base de datos; devuelve 503 cuando falle esa ruta necesaria. No conviertas un servicio opcional de analítica en una dependencia de disponibilidad.
src/app/api/health/route.js
import { connection } from "next/server";
export async function GET() {
await connection();
return Response.json(
{ ok: true, release: process.env.RELEASE_ID || "unknown" },
{ headers: { "Cache-Control": "no-store" } },
);
}Mantén el servidor Node en ejecución con systemd
En una máquina virtual, ejecuta el servidor empaquetado con un usuario dedicado y deja que systemd lo reinicie si falla. Utiliza un directorio separado para cada versión, de modo que un despliegue no sobrescriba archivos que aún necesita un proceso en ejecución. Los comandos siguientes suponen un host nuevo de tipo Debian con Node instalado; comprueba command -v node y ajusta ExecStart si su ruta es distinta.
Install the already-built artifact on the target host
sudo useradd --system --home-dir /srv/next --shell /usr/sbin/nologin nextjs
sudo install -d -o nextjs -g nextjs /srv/next/releases/release-001
sudo cp -a .next/standalone/. /srv/next/releases/release-001/
sudo chown -R nextjs:nextjs /srv/next/releases/release-001
sudo install -d -m 700 /etc/next
sudo touch /etc/next/release-001.env
sudo chmod 600 /etc/next/release-001.env
sudoedit /etc/next/release-001.envConfigura el entorno de la versión
Utiliza estos valores en el archivo de entorno y añade los secretos de servidor necesarios mediante tu mecanismo de despliegue. El límite del heap es un punto de partida de ejemplo, no una estimación de capacidad. La memoria total de Node también incluye asignaciones nativas y buffers, así que deja margen por debajo del límite total de memoria del servicio.
/etc/next/release-001.env
PORT=3001
RELEASE_ID=release-001
NODE_OPTIONS=--max-old-space-size=768Inicia una versión identificada por nombre
Guarda esta plantilla como /etc/systemd/system/next@.service. El valor %i selecciona el directorio de la versión y el archivo de entorno. Una segunda versión puede usar su propio puerto y ejecutarse junto a la primera mientras la compruebas. Esta unidad supervisa el proceso; no retira un proceso no disponible de un balanceador de carga.
/etc/systemd/system/next@.service
[Unit]
Description=Next.js release %i
After=network.target
[Service]
Type=simple
User=nextjs
Group=nextjs
WorkingDirectory=/srv/next/releases/%i
Environment=NODE_ENV=production
Environment=HOSTNAME=127.0.0.1
EnvironmentFile=/etc/next/%i.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=30
MemoryMax=1G
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetInspecciona el inicio antes de añadir tráfico
La respuesta de estado debe identificar release-001. Revisa el registro de systemd para detectar módulos ausentes, configuración inválida o problemas de permisos en el directorio de caché. Mantén disponibles las rutas de caché de Next.js que permiten escritura; convertir todo el sistema de archivos en solo lectura sin un plan de caché puede romper la regeneración o la optimización de imágenes.
Start and inspect
sudo systemctl daemon-reload
sudo systemctl enable --now next@release-001
sudo journalctl -u next@release-001 -n 50 --no-pager
curl --fail --show-error http://127.0.0.1:3001/api/healthColoca NGINX delante y conserva el streaming
Mantén privado el puerto de Node. En la misma máquina virtual, NGINX puede terminar HTTPS y reenviar a 127.0.0.1:3001. El ejemplo supone que el DNS ya apunta al host y que existe un certificado válido en las rutas indicadas. Sustituye app.example.com y valida con nginx -t antes de recargar.
Deja inicialmente desactivada la caché del proxy y desactiva el almacenamiento intermedio de respuestas para que las respuestas en streaming lleguen al navegador a medida que se reciben. Conserva el host y el protocolo públicos para las redirecciones y las comprobaciones de origen. Esta configuración de cabeceras supone que NGINX es el perímetro público; si lo precede un balanceador de carga de confianza, configura explícitamente ese límite de confianza.
/etc/nginx/conf.d/next.conf, inside the http context
upstream next_app {
server 127.0.0.1:3001;
keepalive 32;
}
server {
listen 80;
server_name app.example.com;
return 301 https://app.example.com$request_uri;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 10m;
if ($host != app.example.com) { return 421; }
location / {
proxy_pass http://next_app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_buffering off;
proxy_cache off;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}Prueba dos fragmentos a través del nombre de host público
Añade este Route Handler de diagnóstico y vuelve a compilar la versión. Solicítalo directamente y a través de NGINX con curl --no-buffer. La primera línea debe llegar antes que la segunda. Si ambas llegan juntas solo por la ruta pública, inspecciona todos los proxies y CDN entre el navegador y Node para detectar almacenamiento intermedio de respuestas.
src/app/api/stream/route.js
import { connection } from "next/server";
export async function GET() {
await connection();
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode("first chunk\n"));
await new Promise((resolve) => setTimeout(resolve, 1000));
controller.enqueue(encoder.encode("second chunk\n"));
controller.close();
},
});
return new Response(body, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-store",
"X-Accel-Buffering": "no",
},
});
}Comprueba los límites en ambas capas
proxy_read_timeout es un tiempo de espera por inactividad entre lecturas del upstream. Los streams de larga duración necesitan tiempos de espera adecuados y, cuando corresponda, datos periódicos de actividad. También hay límites de carga en la aplicación: aumentar el límite del cuerpo en NGINX no cambia los límites de Server Actions de Next.js. Ajusta el límite concreto alcanzado en lugar de ampliar todos los límites globalmente.
Check proxy configuration and streaming
sudo nginx -t
sudo systemctl reload nginx
curl --fail --no-buffer http://127.0.0.1:3001/api/stream
curl --fail --no-buffer https://app.example.com/api/streamIdentifica qué caché estás cambiando
No existe una única opción de caché de Next.js que cubra todas las capas. Una página de producto obsoleta puede proceder de una caché de la aplicación, de una ruta generada, de una respuesta de la CDN o del estado de navegación del navegador. Identifica la capa antes de cambiar los TTL o añadir Redis.
| Capa | Qué contiene | Qué comprobar |
|---|---|---|
| Recursos de la compilación | JavaScript, CSS, y otros archivos bajo .next/static. | Despliega los recursos de la misma compilación y conserva los archivos que necesitan los navegadores que usan la versión anterior. |
| Caché de datos / ISR | Datos de fetch en caché y resultado regenerado de las rutas en el modelo de caché correspondiente. | El almacenamiento local debe permitir escritura. Con varias réplicas, debes coordinar explícitamente el almacenamiento y la invalidación. |
| Cache Components | Valores creados mediante use cache y directivas relacionadas. | El valor predeterminado es la memoria local del proceso. Una directiva remota necesita un gestor externo configurado para compartir datos. |
| Optimización de imagen | Variantes generadas por next/image. | Supervisa la CPU, el disco y la latencia de la primera solicitud. Un gestor de caché de datos no comparte automáticamente los archivos de imágenes optimizadas. |
| Proxy inverso / CDN | Respuestas HTTP admitidas por la política de caché perimetral. | Respeta Cache-Control y la variación de respuestas. Las páginas autenticadas y las respuestas públicas compartidas necesitan políticas distintas. |
Las dos API de gestores de caché son distintas
Para la caché incremental del servidor que utilizan ISR y los datos en caché, Next.js ofrece cacheHandler, en singular. Al configurar almacenamiento compartido para ese modelo, cacheMaxMemorySize: 0 permite desactivar la capa de memoria de cada proceso. Tu gestor debe implementar el almacenamiento y el comportamiento de etiquetas necesarios.
Cache Components utiliza cacheHandlers, en plural. Un gestor remoto configurado puede respaldar use cache: remote; sin él, esa directiva por sí sola no aprovisiona Redis ni crea una caché compartida. Coordina el estado de las etiquetas y los valores, incluido refreshTags cuando lo requiera la API del gestor. Elige la API adecuada para tu versión instalada de Next.js y tu modelo de caché.
En Next.js 16.2 y versiones posteriores, las imágenes optimizadas pueden utilizar cacheHandler mediante images.customCacheHandler: true. El gestor debe admitir entradas IMAGE, incluidos sus datos binarios y su caducidad. Configura y prueba esta opción explícitamente si quieres compartir las variantes de imagen entre réplicas.
No guardes todo el HTML en la caché perimetral
La navegación de Next.js puede solicitar datos de React Server Components además de HTML. Una CDN debe conservar las variaciones de solicitud y las claves de caché que requiere el framework. Una regla genérica que guarde todo en caché puede mezclar tipos de respuesta o exponer contenido personalizado. Empieza por las cabeceras de caché del framework y la guía oficial sobre CDN; después prueba por separado las solicitudes con sesión iniciada y sin ella.
Añade réplicas sin duplicar los problemas
Ejecuta el mismo artefacto en cada réplica de una versión, pero da a cada proceso su propia identidad de ejecución y rutas locales que permitan escritura. Guarda las cargas duraderas fuera del directorio de la versión. Las sesiones deben funcionar en cualquier réplica que atienda la siguiente solicitud, mediante cookies verificadas o un almacén de sesiones compartido, en lugar de un objeto local del proceso.
Calcula el presupuesto de conexiones a la base de datos para el conjunto de procesos. Cuatro procesos con un grupo de hasta diez conexiones cada uno pueden utilizar cuarenta conexiones; mantener vivos cuatro procesos anteriores durante un despliegue puede elevar el total a ochenta, antes de contar los workers y las conexiones administrativas. Ajusta los límites de los grupos a la capacidad de la base de datos y al mayor número temporal de réplicas.
Mide la CPU, el retraso del bucle de eventos, la memoria total del proceso, la latencia de las solicitudes y los errores con una carga representativa. Un proceso persistente de Node puede gestionar E/S concurrente, pero el JavaScript que consume mucha CPU puede bloquear solicitudes sin relación. Traslada el trabajo costoso en segundo plano a un worker y escala según los cuellos de botella medidos. Aumentar el límite del heap de V8 no resuelve un cuello de botella de CPU.
Despliega una versión nueva mientras la anterior sigue en uso
Inicia release-002 en su propio directorio y en el puerto 3002 mientras release-001 sigue atendiendo tráfico en el 3001. Comprueba el estado y la aplicación en el 3002. Después cambia el upstream de NGINX al 3002, valida la configuración y recárgala. Mantén disponible el proceso anterior mientras terminan las solicitudes existentes y verificas la versión nueva.
Un navegador puede conservar JavaScript y datos precargados de release-001. deploymentId permite a Next.js detectar una discrepancia y provocar una navegación completa, que puede descartar el estado no guardado de los componentes. El parámetro de consulta dpl no hace que Next.js dirija solicitudes a una versión anterior. Conserva los recursos antiguos y utiliza enrutamiento que tenga en cuenta la versión si los clientes deben seguir comunicándose con ella.
| Preocupación | Qué preservar | Fallo que comprobar |
|---|---|---|
| Recursos del navegador | Los archivos que necesitan tanto las páginas nuevas como las antiguas aún abiertas. | Abre una página antes de la promoción y después carga un componente importado de forma diferida. |
| Server Actions | Un único artefacto y una configuración compatible de cifrado de acciones en todas sus réplicas. | Envía un formulario cargado antes de la promoción después de cambiar el tráfico. |
| Esquema de la base de datos | Compatibilidad con ambas versiones durante la coexistencia y la reversión. | Ejecuta la versión anterior con el esquema migrado antes de declarar que la reversión está disponible. |
| Solicitudes en curso | Un período de drenaje medido antes de terminar el proceso anterior. | Cambia el tráfico durante una respuesta lenta o un stream y comprueba que termine. |
Mantén coherentes las claves de Server Actions
El cifrado de las variables capturadas por Server Actions utiliza una clave de compilación. Reutilizar un artefacto conserva su clave generada entre réplicas. Si gestionas explícitamente NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, inyecta la misma clave AES válida codificada en base64 en las compilaciones que la necesiten y protégela como un secreto. Compartir una clave no hace intercambiables los ID de acciones de compilaciones distintas.
Drena las conexiones, detén el proceso y conserva una vía de reversión
Retira la instancia anterior del tráfico nuevo antes de enviar SIGTERM. Deja que el trabajo en curso termine dentro del plazo de apagado; aumenta el límite de 30 segundos del ejemplo si lo exige el comportamiento medido de las solicitudes. Las funciones de retorno de after() no son una cola de trabajos duradera; el trabajo que deba sobrevivir a un fallo debe ir a una cola persistente con gestión de reintentos.
Si la versión nueva falla, vuelve a dirigir el tráfico al proceso que funciona y conserva los registros del proceso fallido. Las migraciones de base de datos y los efectos externos pueden hacer insegura esa reversión; por eso hay que probar la compatibilidad del esquema antes de promover la versión.
Despliega el mismo modelo de servidor en Adios
En Adios, declara en adios.yaml el comando de compilación, el comando de inicio standalone, el puerto de escucha y la ruta de estado. La pasarela pública reenvía a la carga de trabajo Node, que debe escuchar en 0.0.0.0. No necesitas la configuración de systemd ni de NGINX de la máquina virtual dentro de esa carga de trabajo gestionada.
Establece un RELEASE_ID único en build.env y en env para cada despliegue, de modo que la compilación y el proceso en ejecución identifiquen la misma versión. Incluye devDependencies durante la compilación aunque NODE_ENV sea production. Este ejemplo empieza con una réplica. Aumentar el número no configura automáticamente una caché compartida, sesiones compartidas ni capacidad de base de datos.
adios.yaml
name: web
region: de
replicas: 1
build_cmd: |
npm ci --include=dev
npm run build
mkdir -p .next/standalone/.next
cp -a .next/static .next/standalone/.next/static
if [ -d public ]; then cp -a public .next/standalone/public; fi
start_cmd: node .next/standalone/server.js
build:
env:
RELEASE_ID: release-001
env:
NODE_ENV: production
HOSTNAME: 0.0.0.0
PORT: "3000"
RELEASE_ID: release-001
runtime:
name: node@24
port: 3000
health_path: /api/health
memory_mb: 1024Comprueba estos fallos antes de añadir tráfico
Ejecuta las comprobaciones contra la versión de producción empaquetada y su nombre de host definitivo. Incluye el ID de versión en los registros de la aplicación para distinguir si un error pertenece a una réplica, a una compilación o a todo el servicio.
- —Un artefacto limpio se inicia sin acceso a la copia del código fuente.
- —Las comprobaciones de estado, las solicitudes autenticadas, los recursos, la gestión de imágenes y el streaming funcionan a través de HTTPS.
- —El proceso se recupera tras reiniciarse y una dependencia fallida produce la respuesta prevista de disponibilidad.
- —Una pestaña antigua del navegador se comporta correctamente tras la promoción, incluido el envío de formularios.
- —Varias réplicas coinciden en los datos tras la invalidación y la base de datos tiene capacidad para más conexiones.
- —La versión anterior y un esquema compatible siguen disponibles para la reversión.
| Síntoma | Dónde conviene investigar | Verificación |
|---|---|---|
| El HTML funciona; los scripts o estilos devuelven 404 | Falta de archivos .next/static o versiones mixtas. | Comprueba una URL de script del HTML real y confirma que su artefacto de versión contiene el archivo. |
| La URL pública devuelve 502 | Dirección de escucha, puerto incorrecto o proceso que ha fallado. | Solicita la ruta de estado directamente en el puerto de Node y después inspecciona los registros del proxy y del proceso. |
| El streaming llega todo a la vez | Almacenamiento intermedio de respuestas en el proxy inverso o la CDN. | Compara el endpoint de dos fragmentos directamente y a través del nombre de host público. |
| El navegador sigue usando una URL antigua de la API | Un valor NEXT_PUBLIC_ fijado en el paquete compilado. | Inspecciona el código compilado del navegador y vuelve a compilar con la configuración pública prevista. |
| Solo algunas solicitudes muestran datos obsoletos | Cachés independientes de las réplicas o una caché perimetral. | Comprueba directamente cada réplica y verifica tanto los valores almacenados como la invalidación de etiquetas. |
| Server Actions falla después de un despliegue | Desajuste entre versiones compiladas, claves de cifrado distintas o cabeceras de origen. | Compara los ID de versión, el artefacto usado por cada réplica y el reenvío de Host/Origin. |
| El proceso termina forzosamente bajo carga | La memoria total excede el límite de host o contenedor. | Comprueba RSS, la carga de optimización de imágenes, el crecimiento de la caché y el motivo de salida registrado por el supervisor. |