Adios
BlogIngegneria

Ingegneria

Eseguire Next.js come server Node senza Lambda

Distribuisci Next.js come server Node con compilazioni standalone, systemd, NGINX, streaming, decisioni sulla cache condivisa e rilasci sicuri.

Team di AdiosAggiornato 26 settembre 202620 min di lettura

Next.js può funzionare come processo Node persistente: compila l’app, avvia il server di produzione e inviagli richieste HTTP. Rendering server, Route Handlers, Server Actions, ottimizzazione immagini e streaming possono funzionare senza un adattatore Lambda. Ecco come impacchettare il server, mantenerlo attivo e distribuire una seconda istanza senza problemi di cache o rilascio.

Cosa viene eseguito nel processo Node

Una richiesta in arrivo raggiunge il reverse proxy e poi un server Next.js. Il server può restituire una pagina precompilata, renderizzare per la richiesta corrente, eseguire un Route Handler o servire una risposta in cache. Un cache miss può chiamare il database o un’API. Non serve scrivere un wrapper Express perché funzioni.

Un processo persistente può riutilizzare pool del database e connessioni HTTP tra richieste. Servono comunque CPU e memoria sufficienti per i picchi, una politica di riavvio e una procedura di distribuzione. Un riavvio, una nuova replica o una cache vuota possono rallentare le richieste: eseguire Node non elimina quei costi.

Scegli l’output di produzione per l’applicazione
OutputCome lo eseguiCosa distribuire
Server Node standardnext build, poi next start.Compilazione, risorse pubbliche, metadati dei pacchetti e dipendenze runtime necessarie.
Server Node standaloneImposta output: standalone, compila e poi esegui il server.js generato.File runtime tracciati, più public e .next/static. È il percorso principale qui sotto.
Esportazione staticaServi i file esportati da un server HTTP o da un archivio oggetti.Solo file statici. Le funzioni server eseguite al momento della richiesta richiedono un altro 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.

Esegui prima la compilazione di produzione

Verifica che l’app funzioni con il server di produzione prima di configurare VM o container. Mantieni questi script in package.json e installa dal lockfile nel repository. Installa le dipendenze di compilazione prima di compilare: omettere devDependencies troppo presto può eliminare strumenti necessari al compilatore.

package.json scripts

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

Verifica la stessa modalità che distribuirai

Con l’output standard, i comandi seguenti avviano il server Node completo su loopback. Controlla pagina dinamica, richiesta autenticata e Route Handler oltre alla homepage. Una sessione next dev riuscita non mette alla prova prerendering di produzione, tracciamento delle dipendenze o comportamento della cache in produzione.

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/

Impacchetta un rilascio standalone

L’output standalone impacchetta i file tracciati da Next.js per il server e genera il punto di ingresso server.js. Aggiungi le opzioni seguenti al tuo next.config.mjs mantenendo le altre impostazioni dell’app. Assegna a ogni compilazione un identificatore di rilascio e riutilizza esattamente il suo output per ogni replica di quel rilascio.

next.config.mjs

const nextConfig = {
  output: "standalone",
  deploymentId: process.env.RELEASE_ID,
};

export default nextConfig;

Includi le risorse del browser

L’output standalone non copia automaticamente public o .next/static. Includili quando il server Node deve servire quei file. Senza questo passaggio, una pagina può restituire correttamente HTML mentre ogni script del browser restituisce 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

Prova l’artefatto lontano dall’albero dei sorgenti

Copia la directory standalone in un luogo pulito e avvia lì server.js. Questo rileva dipendenze che funzionavano solo perché era presente il checkout sorgente. In un monorepo esamina la struttura generata: outputFileTracingRoot e outputFileTracingIncludes possono servire per pacchetti condivisi o file aperti tramite percorsi dinamici.

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

Distingui valori di compilazione e segreti runtime

Una variabile d’ambiente solo server può essere letta al momento della richiesta. Una variabile NEXT_PUBLIC_ viene incorporata nel JavaScript del browser durante la compilazione. Cambiarla all’avvio del processo non aggiorna un bundle browser esistente. Anche l’output renderizzato dal server durante la compilazione può acquisire i valori disponibili in quel momento.

Quando entra in vigore la configurazione
ValoreQuando impostarloRisultato della distribuzione
NEXT_PUBLIC_API_URLCreazione di risorse del browser.Ricompila per cambiare un URL incorporato oppure esponi intenzionalmente una configurazione runtime pubblica tramite un tuo endpoint.
DATABASE_URL / credenziali APIAvvio del server, quando il codice li legge dinamicamente.Tienili fuori da variabili pubbliche, controllo di versione e livelli delle immagini.
deploymentIdCompilazione del rilascio.Usa un identificatore per tutte le repliche di quell’artefatto. Cambiarlo soltanto all’avvio non riscrive la compilazione.
PORT / HOSTNAMEAvvio di server.js.Usa loopback dietro un proxy sullo stesso host; usa 0.0.0.0 dentro un container o un carico di lavoro gestito.

Aggiungi una route di integrità senza cache

Questa route attende una richiesta prima di leggere RELEASE_ID e segnala l’integrità del processo. Non verifica un database. Se per servire traffico serve un database, aggiungi un controllo distinto di disponibilità con timeout di query e connessione del client; restituisci 503 quando quel percorso necessario fallisce. Non rendere un servizio facoltativo di analisi una dipendenza della disponibilità.

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" } },
  );
}

Mantenere il server Node in esecuzione con systemd

Su una VM, esegui il server impacchettato con un utente dedicato e lascia che systemd lo riavvii dopo un arresto anomalo. Usa una directory per ogni rilascio, così la distribuzione non sovrascrive file ancora necessari a un processo attivo. I comandi seguenti presuppongono un nuovo host di tipo Debian con Node installato: verifica command -v node e adatta ExecStart se il percorso differisce.

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

Imposta l’ambiente del rilascio

Usa questi valori nel file d’ambiente e aggiungi i segreti server necessari all’app attraverso il meccanismo di distribuzione. Il limite dell’heap è un punto di partenza esemplificativo, non una stima di capacità. La memoria totale di Node comprende allocazioni native e buffer: lascia quindi margine sotto il limite totale del servizio.

/etc/next/release-001.env

PORT=3001
RELEASE_ID=release-001
NODE_OPTIONS=--max-old-space-size=768

Avvia un rilascio identificato

Salva questo modello come /etc/systemd/system/next@.service. Il valore %i sceglie directory di rilascio e file d’ambiente. Un secondo rilascio può usare una propria porta e funzionare accanto al primo durante i controlli. Questa unità supervisiona il processo; non rimuove dal bilanciatore un processo non funzionante.

/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

Controlla l’avvio prima di aggiungere traffico

La risposta di integrità deve identificare release-001. Controlla il journal per moduli mancanti, configurazione non valida o permessi delle directory di cache. Mantieni disponibili i percorsi scrivibili della cache Next.js: rendere tutto il filesystem di sola lettura senza un piano per la cache può interrompere rigenerazione o ottimizzazione immagini.

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

Metti NGINX davanti al server e conserva lo streaming

Mantieni privata la porta Node. Sulla stessa VM, NGINX può terminare HTTPS e inoltrare a 127.0.0.1:3001. L’esempio presuppone che il DNS punti già all’host e che un certificato valido esista nei percorsi indicati. Sostituisci app.example.com, poi verifica con nginx -t prima di ricaricare.

All’inizio lascia disattivata la cache del proxy e disabilita il buffering delle risposte, così quelle in streaming arrivano al browser man mano. Conserva host pubblico e schema per reindirizzamenti e controlli di origine. Questa configurazione delle intestazioni presuppone che NGINX sia l’edge pubblico; se è preceduto da un bilanciatore fidato, configura esplicitamente quel confine di fiducia.

/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;
  }
}

Prova due blocchi attraverso l’hostname pubblico

Aggiungi questo Route Handler diagnostico e ricompila il rilascio. Richiedilo direttamente e attraverso NGINX con curl --no-buffer. La prima riga deve arrivare prima della seconda. Se arrivano insieme soltanto dalla route pubblica, controlla il buffering in ogni proxy e CDN tra browser e Node.

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",
    },
  });
}

Controlla i limiti di entrambi i livelli

proxy_read_timeout è un timeout di inattività tra letture dall’upstream. Gli stream lunghi richiedono timeout adatti e, dove opportuno, dati heartbeat. L’app ha anche limiti di caricamento: aumentare quello del corpo in NGINX non cambia i limiti delle Server Actions Next.js. Adatta il limite specifico raggiunto invece di aprirli tutti 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

Sappi quale cache stai modificando

Non esiste un unico interruttore di cache Next.js che copra ogni livello. Una pagina prodotto vecchia può provenire dalla cache applicativa, da una route generata, da una risposta CDN o dallo stato di navigazione del browser. Individua il livello prima di cambiare TTL o aggiungere Redis.

Responsabilità della cache in una distribuzione gestita autonomamente
LivelloCosa contieneCosa controllare
Risorse della compilazioneJavaScript, CSS, e altri file sotto .next/static.Distribuisci le risorse della stessa compilazione e conserva i file necessari ai browser che usano il rilascio precedente.
cache dei dati / ISRDati fetch in cache e output delle route rigenerato nel modello di cache pertinente.L’archiviazione locale deve essere scrivibile. Più repliche richiedono un coordinamento esplicito di archiviazione e invalidazione.
Cache ComponentsValori creati tramite use cache e direttive correlate.L’impostazione predefinita è la memoria locale al processo. Una direttiva remota richiede un handler esterno configurato per condividere i dati.
Ottimizzazione delle immaginiVarianti generate da next/image.Osserva CPU, disco e latenza della prima richiesta. Un handler della cache dati non condivide automaticamente i file delle immagini ottimizzate.
proxy inverso / CDNRisposte HTTP consentite dalla politica della cache edge.Rispetta Cache-Control e le variazioni della risposta. Pagine autenticate e risposte pubbliche condivise richiedono politiche diverse.

Le due API di cache-handler sono diverse

Per la cache incrementale del server usata da ISR e dati in cache, Next.js espone cacheHandler, al singolare. Configurando archiviazione condivisa per quel modello, cacheMaxMemorySize: 0 può disabilitare il livello di memoria di ogni processo. L’handler deve implementare il comportamento richiesto per archiviazione e tag.

Cache Components usa cacheHandlers, al plurale. Un handler remoto configurato può supportare use cache: remote; senza handler, quella direttiva da sola non attiva Redis né crea una cache condivisa. Coordina sia lo stato dei tag sia i valori, incluso refreshTags dove richiesto dall’API dell’handler. Scegli l’API adatta alla versione Next.js installata e al modello di cache.

Da Next.js 16.2, le immagini ottimizzate possono usare cacheHandler tramite images.customCacheHandler: true. L’handler deve supportare voci IMAGE, inclusi dati binari e scadenza. Configuralo e testalo esplicitamente se vuoi condividere varianti delle immagini tra repliche.

Non memorizzare tutto l’HTML nella cache edge

La navigazione Next.js può richiedere payload React Server Component oltre all’HTML. Una CDN deve conservare le variazioni di richiesta e le chiavi di cache richieste dal framework. Una regola generica che memorizza tutto può mescolare tipi di risposta o esporre contenuti personalizzati. Parti dalle intestazioni di cache del framework e dalle indicazioni ufficiali per le CDN, poi testa separatamente richieste con e senza accesso.

Aggiungi repliche senza duplicare i problemi

Esegui lo stesso artefatto su ogni replica del rilascio, ma assegna a ogni processo la propria identità runtime e percorsi locali scrivibili. Conserva i caricamenti persistenti fuori dalla directory di rilascio. Le sessioni devono funzionare con qualsiasi replica gestisca la richiesta successiva, tramite cookie verificati o un archivio condiviso, anziché un oggetto locale al processo.

Pianifica le connessioni al database sull’insieme dei processi. Quattro processi con pool massimo di dieci connessioni possono usarne quaranta; mantenere attivi anche quattro vecchi processi durante un rilascio può portarle a ottanta, prima di contare worker e connessioni amministrative. Imposta i limiti dei pool in base alla capacità del database e al massimo numero temporaneo di repliche.

Misura CPU, ritardo dell’event loop, memoria totale del processo, latenza delle richieste ed errori sotto carico rappresentativo. Un processo Node persistente gestisce I/O concorrente, ma JavaScript intenso sulla CPU può bloccare richieste non correlate. Sposta lavori di background costosi su un worker e scala in base ai colli di bottiglia misurati. Aumentare il limite dell’heap V8 non risolve un collo di bottiglia della CPU.

Distribuisci un nuovo rilascio mentre il precedente è ancora in uso

Avvia release-002 nella sua directory e sulla porta 3002 mentre release-001 serve ancora traffico su 3001. Esegui controlli di integrità e applicativi su 3002. Poi imposta l’upstream NGINX su 3002, verifica la configurazione e ricaricala. Mantieni disponibile il vecchio processo mentre si completano le richieste esistenti e verifichi il nuovo rilascio.

Un browser può conservare ancora JavaScript e dati precaricati di release-001. deploymentId permette a Next.js di rilevare un’incoerenza e attivare una navigazione completa, che può perdere lo stato dei componenti non salvato. Il parametro di query dpl non fa instradare a Next.js le richieste verso un vecchio rilascio. Conserva le vecchie risorse e usa routing consapevole delle versioni se i client devono continuare a comunicare con quel rilascio.

Stato di rilascio che deve rimanere coerente
PreoccupazioniCosa conservareGuasto da testare
Risorse del browserI file richiamati dalle pagine nuove e dalle vecchie ancora aperte.Apri una pagina prima della promozione, poi carica un componente importato in modo differito.
Server ActionsUn unico artefatto e configurazione di crittografia delle azioni compatibile tra le sue repliche.Dopo il cambio di traffico, invia un modulo caricato prima della promozione.
Schema del databaseCompatibilità con entrambe le versioni durante sovrapposizione e rollback.Esegui il vecchio rilascio sullo schema migrato prima di dichiarare disponibile il rollback.
Richieste in corsoUn periodo misurato per completare le richieste prima di terminare il vecchio processo.Cambia il traffico durante una risposta lenta o uno stream e verifica che si completi.

Mantieni coerenti le chiavi delle Server Actions

La crittografia delle closure delle Server Actions usa una chiave di compilazione. Riutilizzare un artefatto conserva la chiave generata tra le repliche. Se gestisci esplicitamente NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, inserisci la stessa chiave AES valida codificata in base64 nelle compilazioni che la richiedono e proteggila come segreto. Una chiave condivisa non rende intercambiabili gli ID delle azioni di compilazioni diverse.

Completa le richieste, arresta il processo e conserva un percorso di rollback

Smetti di inviare nuovo traffico alla vecchia istanza prima di inviare SIGTERM. Lascia terminare il lavoro in corso entro il tempo previsto per l’arresto; aumenta il limite di 30 secondi dell’esempio se il comportamento misurato delle richieste lo richiede. I callback after() non sono una coda di lavori persistente: il lavoro che deve sopravvivere a un arresto anomalo richiede una coda persistente con gestione dei nuovi tentativi.

Se il nuovo rilascio fallisce, riporta il traffico al processo funzionante noto e conserva i log di quello fallito. Migrazioni del database ed effetti esterni possono rendere il rollback rischioso: per questo la compatibilità dello schema va verificata prima della promozione.

Distribuisci lo stesso modello di server su Adios

Su Adios, dichiara in adios.yaml comando di compilazione, comando di avvio standalone, porta di ascolto e percorso di integrità. Il gateway pubblico inoltra al carico Node, che deve quindi ascoltare su 0.0.0.0. La configurazione systemd e NGINX della VM non serve all’interno di quel carico gestito.

Imposta un RELEASE_ID univoco sia in build.env sia in env per ogni distribuzione, così compilazione e processo attivo identificano lo stesso rilascio. Includi devDependencies durante la compilazione anche con NODE_ENV impostato su production. L’esempio parte da una replica. Aumentarne il numero non configura automaticamente cache condivisa, sessioni condivise o capacità del database.

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

Verifica questi guasti prima di aggiungere traffico

Esegui i controlli sul rilascio di produzione impacchettato e sul suo hostname finale. Conserva l’ID del rilascio nei log applicativi, così puoi capire se l’errore riguarda una replica, una compilazione o tutto il servizio.

  • —Un artefatto pulito si avvia senza accesso al checkout sorgente.
  • —Integrità, richieste autenticate, risorse, gestione delle immagini e streaming funzionano tramite HTTPS.
  • —Il processo si riprende dopo un riavvio e una dipendenza guasta produce la risposta di disponibilità prevista.
  • —Una vecchia scheda del browser funziona correttamente durante la promozione, compreso l’invio dei moduli.
  • —Le repliche concordano sui dati dopo l’invalidazione e il database ha margine per le connessioni.
  • —La versione precedente e uno schema compatibile rimangono disponibili per il rollback.
Verifica della produzione e risoluzione dei problemi
SintomoPunto probabile da indagareVerifica
L’HTML funziona; script o stili restituiscono 404File .next/static mancanti o rilasci mescolati.Controlla un URL di script dall’HTML effettivo e verifica che il suo artefatto di rilascio contenga il file.
L'URL pubblico restituisce 502Indirizzo di ascolto, porta non corrispondente o processo terminato.Richiedi la route di integrità direttamente sulla porta Node, poi esamina i log del proxy e del processo.
Lo streaming arriva tutto insiemeBuffering nel proxy inverso o CDN.Confronta l’endpoint a due blocchi direttamente e attraverso l’hostname pubblico.
Il browser usa ancora un vecchio URL APIUn valore NEXT_PUBLIC_ congelato nel bundle.Esamina il codice browser compilato e ricompila con la configurazione pubblica prevista.
Solo alcune richieste mostrano dati vecchiCache indipendenti delle repliche o cache edge.Controlla direttamente ogni replica e verifica valori memorizzati e invalidazione dei tag.
Le Server Actions falliscono dopo un rilascioDifferenze tra compilazioni, chiavi di crittografia non corrispondenti o intestazioni di origine.Confronta gli ID di rilascio, l'artefatto utilizzato da ogni replica e l'inoltro di Host/Origin.
Il processo viene terminato sotto caricoLa memoria totale supera il limite dell'host o del contenitore.Controlla RSS, carico dell’ottimizzazione immagini, crescita della cache e motivo di uscita del supervisore.
Tutti gli articoli