Adios
BlogIngeniería

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.

Equipo de AdiosActualizado 26 de septiembre de 202620 min de lectura

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.

Elige el formato de producción para la aplicación
SalidaCómo lo ejecutasQué desplegar
Servidor Node estándarnext 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 standaloneConfigura 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áticaSirve 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.js

Prueba 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.js

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

Cuándo surte efecto la configuración
ValorEstablécelo cuandoConsecuencia del despliegue
NEXT_PUBLIC_API_URLCompilació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 APIAl 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.
deploymentIdCompilació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 / HOSTNAMEInicio 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.env

Configura 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=768

Inicia 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.target

Inspecciona 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/health

Coloca 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/stream

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

Responsabilidades de caché en un despliegue autogestionado
CapaQué contieneQué comprobar
Recursos de la compilaciónJavaScript, 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 / ISRDatos 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 ComponentsValores 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 imagenVariantes 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 / CDNRespuestas 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.

Estado de la versión que debe mantenerse coherente
PreocupaciónQué preservarFallo que comprobar
Recursos del navegadorLos 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 ActionsUn ú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 datosCompatibilidad 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 cursoUn 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: 1024

Comprueba 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.
Verificación de la producción y solución de problemas
SíntomaDónde conviene investigarVerificación
El HTML funciona; los scripts o estilos devuelven 404Falta 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 502Direcció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 vezAlmacenamiento 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 APIUn 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 obsoletosCaché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 despliegueDesajuste 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 cargaLa 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.
Todos los artículos