Next.js genera páginas en el servidor, así que en producción necesita un proceso de Node.js en marcha además de los archivos estáticos. En este tutorial desplegarás una aplicación Next.js en Ubuntu 24.04: instalarás Node.js 24 desde el repositorio oficial de NodeSource, compilarás la aplicación, la ejecutarás con next start como servicio de systemd y pondrás Nginx delante para servir los estáticos con caché larga y gestionar HTTPS.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM. La compilación con next build es la parte que más memoria consume.
  • Un usuario no root con privilegios sudo. En los ejemplos se llama your_user y es también el usuario que ejecutará la aplicación.
  • Un dominio con un registro DNS A apuntando a la IP del servidor. En los ejemplos se usa your_domain.
  • Una aplicación Next.js en un repositorio Git, o las ganas de probar con un proyecto nuevo.

La aplicación de ejemplo se llama myapp y vive en /var/www/myapp.

Paso 1: Instalar Node.js 24

La versión de Node.js de los repositorios de Ubuntu 24.04 es demasiado antigua para las versiones actuales de Next.js. Instala Node.js 24 LTS desde el repositorio de NodeSource. Primero, descarga su clave de firma:

sudo apt update
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg

Añade el repositorio e instala Node.js, que incluye npm:

echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install nodejs

Comprueba las versiones:

node -v
npm -v
v24.x.x
11.x.x

Instala también Nginx y Git, y abre en el cortafuegos SSH, HTTP y HTTPS:

sudo apt install nginx git
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Paso 2: Descargar y compilar la aplicación

Crea el directorio de la aplicación con tu usuario como propietario:

sudo mkdir -p /var/www/myapp
sudo chown your_user:your_user /var/www/myapp

Si tienes la aplicación en un repositorio, clónala:

git clone https://github.com/your_account/myapp.git /var/www/myapp

Si solo quieres probar el despliegue, crea un proyecto nuevo en ese directorio y acepta las opciones por defecto del asistente:

npx create-next-app@latest /var/www/myapp

Si la aplicación usa variables de entorno en el servidor (claves de API, URL de la base de datos), guárdalas en .env.production.local, que Next.js carga en producción y que no se sube al repositorio:

nano /var/www/myapp/.env.production.local
DATABASE_URL=postgres://user:your_strong_password@localhost:5432/myapp

Instala las dependencias exactamente como están en package-lock.json y compila. npm ci instala también las dependencias de desarrollo, que hacen falta para compilar (TypeScript, por ejemplo):

cd /var/www/myapp
npm ci
npm run build
   Next.js 16.x.x

   Creating an optimized production build ...
   Compiled successfully
 ...

La compilación genera el directorio .next, que contiene el servidor compilado y los estáticos en .next/static.

Paso 3: Ejecutar Next.js con systemd

Crea un servicio de systemd que ejecute next start y lo reinicie si falla. Las opciones -H 127.0.0.1 -p 3000 hacen que solo escuche en local, de modo que únicamente Nginx puede llegar a él:

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=myapp Next.js server
After=network.target

[Service]
Type=simple
User=your_user
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/npm run start -- -H 127.0.0.1 -p 3000
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

El script start del package.json de un proyecto Next.js es next start, y -- le pasa el resto de argumentos. Activa e inicia el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp --no-pager
● myapp.service - myapp Next.js server
     Active: active (running) since ...

Comprueba que responde:

curl -I http://127.0.0.1:3000
HTTP/1.1 200 OK
X-Powered-By: Next.js
Content-Type: text/html; charset=utf-8
...

Los logs de la aplicación, incluidos los console.log del servidor, están en journalctl -u myapp.

Paso 4: Configurar Nginx con caché para los estáticos

Los archivos de /_next/static/ llevan un hash en el nombre, así que nunca cambian y el navegador puede guardarlos un año. Nginx los servirá directamente desde disco, sin pasar por Node.js, y enviará el resto de peticiones a la aplicación. Crea el bloque de servidor:

sudo nano /etc/nginx/sites-available/myapp
server {
    listen 80;
    listen [::]:80;
    server_name your_domain www.your_domain;

    location /_next/static/ {
        alias /var/www/myapp/.next/static/;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        include proxy_params;
    }
}

El archivo proxy_params de Ubuntu añade las cabeceras Host, X-Real-IP, X-Forwarded-For y X-Forwarded-Proto, que Next.js usa para construir URL absolutas y en los redirects.

Activa el sitio, desactiva el sitio por defecto y comprueba la sintaxis:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Comprueba que los estáticos salen de Nginx con la cabecera de caché. Toma el nombre de un archivo JavaScript de la página principal:

curl -s http://your_domain | grep -o '/_next/static/[^"]*\.js' | head -1
/_next/static/chunks/0a1b2c3d4e5f6a7b.js

Y pide ese archivo, sustituyendo la ruta por la que obtuviste:

curl -I http://your_domain/_next/static/chunks/0a1b2c3d4e5f6a7b.js
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: application/javascript
Cache-Control: public, max-age=31536000, immutable

Paso 5: Activar HTTPS con Let's Encrypt

Instala Certbot con su plugin para Nginx y solicita el certificado. Certbot añade la configuración TLS y la redirección de HTTP a HTTPS:

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain -d www.your_domain

Comprueba que la renovación automática funciona:

sudo certbot renew --dry-run

Abre https://your_domain en el navegador y deberías ver tu aplicación servida por HTTPS.

Paso 6: Ajustes de rendimiento

Con esta configuración, Next.js ya aplica las optimizaciones principales: prerenderiza las páginas estáticas durante la compilación, comprime las respuestas con gzip y optimiza las imágenes de next/image con sharp. Hay dos ajustes que merece la pena revisar en un servidor propio.

Caché de imágenes y de ISR. Las imágenes optimizadas y las páginas regeneradas con ISR se guardan en .next/cache. El servicio se ejecuta con tu usuario, propietario del directorio, así que puede escribir en él sin cambiar permisos. Comprueba que se llena tras visitar páginas con imágenes:

du -sh /var/www/myapp/.next/cache

Comprimir en Nginx en vez de en Node.js. Si prefieres que la compresión la haga Nginx, desactívala en Next.js añadiendo compress: false a next.config.js (o next.config.ts):

const nextConfig = {
  compress: false,
};

Después activa gzip para los tipos de texto en /etc/nginx/nginx.conf (Ubuntu ya trae gzip on; en el bloque http, y gzip_types y las demás directivas comentadas), vuelve a compilar y reinicia el servicio.

Actualizar la aplicación

Para desplegar cambios, descarga el código, instala dependencias, compila y reinicia el servicio:

cd /var/www/myapp
git pull
npm ci
npm run build
sudo systemctl restart myapp

Durante npm run build el directorio .next se reemplaza mientras el servidor sigue en marcha, y algunas peticiones pueden fallar durante unos segundos. Si necesitas despliegues sin cortes, compila en un directorio nuevo y cambia un enlace simbólico al terminar, o compila en un pipeline de CI.

Solución de problemas

  • JavaScript heap out of memory o el proceso muere al compilar: el servidor no tiene memoria suficiente para next build. Añade swap o compila en una máquina con más RAM.
  • 502 Bad Gateway: el servicio no está en marcha. Revisa sudo journalctl -u myapp -n 50; si ves Could not find a production build, ejecuta npm run build antes de arrancar.
  • 404 en archivos de /_next/static/: la ruta de alias no coincide con el directorio real o falta la barra final en alias o en location.
  • Una variable NEXT_PUBLIC_ no cambia: se incrusta en la compilación. Vuelve a ejecutar npm run build y reinicia el servicio.

Conclusión

Tu aplicación Next.js se ejecuta como servicio de systemd con Node.js 24, escucha solo en local y Nginx la publica por HTTPS sirviendo los estáticos con caché de un año. Como siguientes pasos puedes:

  • Usar output: "standalone" en next.config.js para generar un servidor mínimo que no necesite node_modules completo en producción.
  • Compilar en un pipeline de CI y copiar solo el resultado al servidor para evitar cortes durante el despliegue.
  • Poner una CDN delante de /_next/static/ si tienes tráfico de varias regiones.