Desplegar Next.js en EC2 / Servidor Independiente
Desplegar Next.js en un servidor Linux independiente (AWS EC2, DigitalOcean Droplet, Hetzner, etc.) usando next build && next start. Todo lo que Vercel maneja automáticamente -- CDN, SSL, escalado, despliegues de vista previa, variables de entorno -- ahora es tu responsabilidad. Esta guía te lleva a través de cada parte.
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
# En tu servidor (Ubuntu 22.04+)
# 1. Construir el bundle de producción
NODE_ENV=production npm ci
NODE_ENV=production npx next build
# 2. Iniciar con PM2 (gestor de procesos)
npm install -g pm2
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup
# 3. Proxy inverso Nginx (después de instalar nginx)
sudo apt install nginx certbot python3-certbot-nginx -y
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 4. Certificado SSL
sudo certbot --nginx -d myapp.com -d www.myapp.com
# 5. Verificar
curl -I https://myapp.comCuándo usarlo: Tu empresa requiere auto-hosting, necesitas controlar el entorno del servidor, estás desplegando en una VPC sin acceso público a internet, o quieres costos mensuales predecibles en lugar de facturación basada en el uso.
Ejemplo Funcional
Un recorrido de despliegue completo de un servidor Ubuntu nuevo a una aplicación Next.js lista para producción.
Paso 1: Aprovisionar el Servidor
Lanzar una instancia EC2 (o equivalente) con:
- SO: Ubuntu 22.04 LTS o 24.04 LTS
- Tipo de instancia:
t3.mediummínimo (2 vCPU, 4 GB RAM) para la mayoría de aplicaciones Next.js - Almacenamiento: 30 GB+ SSD (las compilaciones consumen espacio en disco)
Configura el grupo de seguridad / firewall:
# Si usas ufw directamente en el servidor
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP (Nginx)
sudo ufw allow 443/tcp # HTTPS (Nginx)
sudo ufw enableInstalar Node.js 20 vía nvm:
# Instalar nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
# Instalar Node.js 20 LTS
nvm install 20
nvm alias default 20
node -v # v20.x.xCrea un usuario dedicado (nunca ejecutes Node.js como root):
sudo adduser --disabled-password nextjs
sudo usermod -aG sudo nextjs
sudo su - nextjsPaso 2: Clonar y Compilar
# Clonar tu repositorio
git clone https://github.com/your-org/your-app.git /home/nextjs/app
cd /home/nextjs/app
# Instalar dependencias de producción
npm ci
# Compilar para producción
NODE_ENV=production npx next buildDespués de que la compilación se complete, el directorio .next/ contiene:
.next/
├── cache/ # Caché ISR, caché de optimización de imágenes, caché de compilación
├── server/ # Bundles del lado del servidor (páginas App Router, rutas API)
│ ├── app/ # Páginas App Router compiladas
│ ├── chunks/ # Chunks del servidor compartidos
│ └── pages/ # Páginas Pages Router compiladas (si las hay)
├── static/ # Bundles JS/CSS del lado del cliente (con fingerprint)
│ └── chunks/ # Bundles del cliente divididos por código
├── BUILD_ID # Identificador único de compilación
├── build-manifest.json # Mapea rutas a bundles del cliente
└── trace # Datos de rastreo de compilaciónInicia el servidor de producción para verificar:
NODE_ENV=production npx next start -p 3000
# Visita http://<server-ip>:3000 para verificar, luego Ctrl+CPaso 3: Variables de Entorno
Crea .env.production en la raíz de tu proyecto:
# .env.production
# Solo del lado del servidor (no expuesto al navegador)
DATABASE_URL="postgresql://user:pass@db-host:5432/mydb"
NEXTAUTH_SECRET="your-secret-key-here"
NEXTAUTH_URL="https://myapp.com"
# Del lado del cliente (incrustado en el bundle JS en tiempo de COMPILACIÓN)
NEXT_PUBLIC_API_URL="https://api.myapp.com"
NEXT_PUBLIC_POSTHOG_KEY="phc_xxxxxxxxxxxx"El prefijo NEXT_PUBLIC_ es crítico de entender:
| Prefijo | Disponible Donde | Cuándo Se Resuelve | El Cambio Requiere |
|---|---|---|---|
NEXT_PUBLIC_ | Servidor + Cliente (navegador) | Tiempo de compilación (insertado en el bundle JS) | Recompilación |
| Sin prefijo | Solo servidor | Tiempo de ejecución (se lee de process.env) | Reinicio |
Para variables de entorno gestionadas por PM2, usa un ecosystem.config.js:
// ecosystem.config.js
module.exports = {
apps: [
{
name: "myapp",
script: "node_modules/.bin/next",
args: "start -p 3000",
cwd: "/home/nextjs/app",
env: {
NODE_ENV: "production",
DATABASE_URL: "postgresql://user:pass@db-host:5432/mydb",
NEXTAUTH_SECRET: "your-secret-key-here",
NEXTAUTH_URL: "https://myapp.com",
},
},
],
};Paso 4: Gestión de Procesos con PM2
PM2 mantiene tu proceso Node.js vivo, lo reinicia si falla, y sobrevive a los reinicios del servidor.
# Instalar PM2 globalmente
npm install -g pm2Crea un ecosystem.config.js listo para producción con modo cluster:
// ecosystem.config.js
module.exports = {
apps: [
{
name: "myapp",
script: "node_modules/.bin/next",
args: "start -p 3000",
cwd: "/home/nextjs/app",
instances: "max", // Usa todos los núcleos de CPU disponibles
exec_mode: "cluster", // Modo cluster para equilibrio de carga
max_memory_restart: "512M", // Reinicia si la memoria excede 512MB
env: {
NODE_ENV: "production",
PORT: 3000,
},
// Registro
error_file: "/home/nextjs/logs/err.log",
out_file: "/home/nextjs/logs/out.log",
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
merge_logs: true,
},
],
};Inicia y persiste:
# Crear directorio de registros
mkdir -p /home/nextjs/logs
# Iniciar la aplicación
pm2 start ecosystem.config.js
# Guardar la lista de procesos (para que PM2 sepa qué reiniciar después del reinicio)
pm2 save
# Generar el script de inicio (ejecuta el comando que genera como root)
pm2 startup
# Copia-pega el comando generado, p.ej.:
# sudo env PATH=$PATH:/home/nextjs/.nvm/versions/node/v20.x.x/bin pm2 startup systemd -u nextjs --hp /home/nextjs
# Verificar que los procesos se están ejecutando
pm2 lsPaso 5: Proxy Inverso Nginx
Nginx se sitúa delante de Node.js para manejar terminación TLS, compresión gzip, caché de activos estáticos, y headers de seguridad.
sudo apt install nginx -yCrear la configuración del sitio:
# /etc/nginx/sites-available/myapp
upstream nextjs_upstream {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name myapp.com www.myapp.com;
# Redirigir todo HTTP a HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
# Certificados SSL (gestionados por certbot)
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Compresión Gzip
gzip on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
# Headers de seguridad
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Servir activos estáticos de Next.js directamente desde Nginx (omite Node.js)
location /_next/static/ {
alias /home/nextjs/app/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
# Servir activos del directorio público directamente
location /public/ {
alias /home/nextjs/app/public/;
expires 30d;
access_log off;
}
# Favicon y robots.txt
location = /favicon.ico {
alias /home/nextjs/app/public/favicon.ico;
access_log off;
}
# Proxy todas las otras solicitudes a Next.js
location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
# Headers requeridos
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Soporte WebSocket (requerido para streaming Server Actions, HMR en desarrollo)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Almacenamiento en búfer
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
}
}Habilita el sitio y prueba:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default # Eliminar sitio por defecto
sudo nginx -t # Probar configuración
sudo systemctl reload nginxPaso 6: SSL con Certbot
# Instalar certbot
sudo apt install certbot python3-certbot-nginx -y
# Obtener e instalar certificado (el plugin Nginx configura SSL automáticamente)
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Verificar que la renovación automática está configurada
sudo certbot renew --dry-run
# Certbot instala un temporizador systemd automáticamente; verifica que exista:
sudo systemctl list-timers | grep certbotPaso 7: Despliegues Sin Tiempo de Inactividad
Crea un script de despliegue que extrae el código más reciente, recompila, y recarga gracefully:
#!/bin/bash
# /home/nextjs/deploy.sh
set -euo pipefail
APP_DIR="/home/nextjs/app"
LOG_FILE="/home/nextjs/logs/deploy-$(date +%Y%m%d-%H%M%S).log"
echo "=== Deploy iniciado en $(date) ===" | tee "$LOG_FILE"
cd "$APP_DIR"
# Extraer código más reciente
echo "Extrayendo código más reciente..." | tee -a "$LOG_FILE"
git pull origin main 2>&1 | tee -a "$LOG_FILE"
# Instalar dependencias (ci para instalaciones limpias)
echo "Instalando dependencias..." | tee -a "$LOG_FILE"
npm ci 2>&1 | tee -a "$LOG_FILE"
# Compilar la aplicación
echo "Compilando..." | tee -a "$LOG_FILE"
NODE_ENV=production npx next build 2>&1 | tee -a "$LOG_FILE"
# Recargar procesos PM2 gracefully (sin tiempo de inactividad)
echo "Recargando procesos PM2..." | tee -a "$LOG_FILE"
pm2 reload myapp 2>&1 | tee -a "$LOG_FILE"
echo "=== Deploy completado en $(date) ===" | tee -a "$LOG_FILE"¿Por qué pm2 reload en lugar de pm2 restart:
reload-- Inicia nuevos procesos worker primero, espera a que acepten conexiones, luego apaga gracefully los workers antiguos. Sin tiempo de inactividad.restart-- Mata todos los workers inmediatamente, luego inicia otros nuevos. Breve tiempo de inactividad mientras los nuevos procesos arrancan.
chmod +x /home/nextjs/deploy.shPaso 8: Comprobaciones de Salud
Crea un endpoint de comprobación de salud para monitoreo de balanceadores de carga (ALB, ELB, o servicios de monitoreo de tiempo de actividad):
// app/api/health/route.ts
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
return NextResponse.json({
status: "ok",
uptime: process.uptime(),
timestamp: new Date().toISOString(),
node_version: process.version,
memory: {
rss_mb: Math.round(process.memoryUsage().rss / 1024 / 1024),
heap_used_mb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
},
});
}Configura tu balanceador de carga o servicio de monitoreo para consultar https://myapp.com/api/health cada 30 segundos. Una respuesta 200 con "status": "ok" significa que el servidor está sano.
Paso 9: Registro
Configura la rotación de registros de PM2 para evitar que los registros consuman todo el espacio en disco:
# Instalar el módulo de rotación de registros
pm2 install pm2-logrotate
# Configurar los parámetros de rotación
pm2 set pm2-logrotate:max_size 50M # Rotar cuando el registro alcance 50MB
pm2 set pm2-logrotate:retain 30 # Mantener 30 archivos rotados
pm2 set pm2-logrotate:compress true # Comprimir registros antiguos con gzip
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ssPara enviar registros a un servicio centralizado:
Opción A: CloudWatch (AWS)
# Instalar el agente CloudWatch
sudo apt install amazon-cloudwatch-agent -y
# Configurarlo para monitorear archivos de registro de PM2
# /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json{
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/home/nextjs/logs/out.log",
"log_group_name": "myapp/stdout",
"log_stream_name": "{instance_id}"
},
{
"file_path": "/home/nextjs/logs/err.log",
"log_group_name": "myapp/stderr",
"log_stream_name": "{instance_id}"
}
]
}
}
}
}Opción B: Registro estructurado con Pino (recomendado para agregadores de registros como Datadog, Grafana Loki):
// lib/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});Análisis Profundo
Vercel vs. Auto-Hosting: Lo que Ahora Posees
| Capacidad | Vercel (Gestionado) | Auto-Hosting (Tu Responsabilidad) |
|---|---|---|
| CDN / Caché de borde | CDN global automático | Configura CloudFront, Cloudflare, o Fastly tú mismo |
| Revalidación ISR | Caché distribuida en nodos de borde | Funciona en servidor único; multi-servidor necesita caché compartida (Redis, S3) |
| Despliegues de vista previa | URLs de vista previa automáticas por PR | Configura tú mismo (proceso PM2 separado por rama, o omítelo) |
| Escalado sin servidor | Se escala automáticamente a cero y hacia arriba | Capacidad de servidor fija; escala con más instancias + ALB |
| Optimización de imágenes | next/image optimizada en borde | next/image funciona pero usa CPU de tu servidor; offload a un CDN |
| Analíticas / Web Vitals | Panel integrado | Añade las tuyas (Datadog, Grafana, PostHog, Vercel Speed Insights OSS) |
| Caché de compilación | Caché de compilación remota | .next/cache local; persiste en despliegues no eliminándolo |
| HTTPS | SSL automático | Gestiona certificados tú (cron de renovación automática de certbot) |
| Variables de entorno | UI del panel, por entorno | Sin panel; gestiona vía archivos .env, config PM2, o AWS SSM |
| Middleware | Se ejecuta en borde (V8 isolates) | Se ejecuta en Node.js (no en borde); API idéntica, características de tiempo de ejecución diferentes |
ISR en un Servidor Independiente
La Regeneración Estática Incremental funciona lista para usar en un servidor único porque las páginas regeneradas se escriben en .next/cache/ en disco local. Cuando se revalida una página, Next.js:
- Sirve la página obsoleta inmediatamente
- Regenera la página en segundo plano
- Escribe la nueva página en
.next/cache/ - Sirve la nueva página en la siguiente solicitud
El problema con múltiples servidores: Si escalas a 2+ servidores detrás de un balanceador de carga, cada servidor tiene su propio .next/cache/. El servidor A podría tener una página fresca mientras el servidor B sigue sirviendo una obsoleta. Los usuarios ven contenido inconsistente.
Soluciones:
- Sesiones adhesivas -- Enruta usuarios al mismo servidor vía afinidad de sesión ALB. Más simple pero reduce la efectividad del equilibrio de carga.
- Montaje NFS compartido -- Monta
.next/cache/desde un volumen EFS. Todos los servidores comparten la misma caché. Añade latencia pero asegura consistencia. - Controlador de caché personalizado -- Usa
incrementalCacheHandlerPathennext.config.tspara apuntar a una caché respaldada por Redis o S3:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheHandler: "./cache-handler.ts",
cacheMaxMemorySize: 0, // Deshabilitar caché en memoria, usar solo almacén externo
};
export default nextConfig;// cache-handler.ts
import { CacheHandler } from "next/dist/server/lib/incremental-cache";
import { createClient } from "redis";
const client = createClient({ url: process.env.REDIS_URL });
client.connect();
export default class RedisCacheHandler extends CacheHandler {
async get(key: string) {
const data = await client.get(key);
return data ? JSON.parse(data) : null;
}
async set(key: string, data: unknown, ctx: { revalidate?: number }) {
const ttl = ctx.revalidate ?? 60;
await client.set(key, JSON.stringify(data), { EX: ttl });
}
async revalidateTag(tag: string) {
// Escanear claves con esta etiqueta y eliminarlas
const keys = await client.keys(`*:${tag}:*`);
if (keys.length > 0) {
await client.del(keys);
}
}
}El Modo de Salida standalone
Configurar output: "standalone" en next.config.ts le indica al proceso de compilación que rastree las importaciones de tu aplicación y agrupe solo node_modules requerido en .next/standalone/:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;Después de compilar, el directorio .next/standalone/ contiene:
.next/standalone/
├── node_modules/ # Solo las dependencias que tu aplicación realmente usa (~50MB)
├── server.js # Punto de entrada mínimo del servidor Node.js
├── package.json
└── .next/
└── server/ # Bundles del servidor compiladosDebes copiar manualmente activos estáticos:
# Después de compilar con output: "standalone"
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/staticLuego inicia con:
cd .next/standalone
NODE_ENV=production node server.js| Escenario | Usar standalone | Usar salida por defecto |
|---|---|---|
| Contenedores Docker | Sí -- tamaño de imagen mínimo | No |
| Huella mínima del servidor | Sí -- ~50MB vs cientos | No |
| Lambda / sin servidor | Sí | No |
Servidor completo con node_modules disponible | No | Sí -- más simple |
| Monorepo con paquetes compartidos | Depende | Frecuentemente más fácil sin |
Monitoreo y Confiabilidad
Reinicio automático de PM2 en caso de fallo:
PM2 reinicia automáticamente procesos fallidos. Configura límites de memoria para atrapar fugas de memoria:
# Establecer límite de memoria (reinicia si se excede)
pm2 start ecosystem.config.js # max_memory_restart ya está configurado en la config
# Monitorear en tiempo real
pm2 monitEndpoint de comprobación de salud para ALB:
Configura la comprobación de salud del grupo objetivo ALB:
- Ruta:
/api/health - Intervalo: 30 segundos
- Umbral saludable: 2 éxitos consecutivos
- Umbral no saludable: 3 fallos consecutivos
- Timeout: 10 segundos
Gestión del espacio en disco:
El directorio .next/cache/ puede crecer sin límites, especialmente con ISR y optimización de imágenes:
# Trabajo cron para podar caché ISR más antigua de 7 días
# Agregar a crontab -e
0 3 * * * find /home/nextjs/app/.next/cache -type f -mtime +7 -delete 2>/dev/nullAjuste de memoria de Node.js:
Si tu aplicación procesa payloads o conjuntos de datos grandes, aumenta el límite de heap de V8:
// ecosystem.config.js
module.exports = {
apps: [
{
name: "myapp",
script: "node_modules/.bin/next",
args: "start -p 3000",
node_args: "--max-old-space-size=1024", // Límite de heap de 1 GB
max_memory_restart: "1200M", // Umbral de reinicio de PM2 por encima del límite de V8
// ... resto de config
},
],
};Trampas
-
Olvidar copiar
public/y.next/static/con salida standalone. El modooutput: "standalone"agrupa solo el código del servidor. Los activos estáticos (public/,.next/static/) deben copiarse en.next/standalone/manualmente, o Nginx servirá 404s para cada archivo CSS, JS e imagen. -
Ejecutar
next startcomo root. Si el proceso Node.js se ve comprometido, el atacante tiene acceso root. Siempre crea un usuarionextjsdedicado con permisos mínimos. PM2 se ejecuta como ese usuario, y Nginx (que necesita los puertos 80/443) se ejecuta como su propio usuariowww-data. -
No establecer
NODE_ENV=production. Next.js omite optimizaciones críticas en modo desarrollo: sin minificación, sin eliminación de código muerto, páginas de error verbose con mapas de fuente expuestos. Siempre estableceNODE_ENV=productionen tu config PM2 o entorno shell antes de compilar e iniciar. -
Exponer puerto 3000 directamente a internet. Nunca dejes que los usuarios lleguen directamente al proceso Node.js. Nginx proporciona terminación TLS, límite de velocidad, headers de seguridad, compresión gzip, y protección contra ataques slowloris. Tu grupo de seguridad solo debe permitir el puerto 3000 desde
127.0.0.1. -
Caché ISR creciendo sin límites. En sitios de alto tráfico con muchas páginas dinámicas (p.ej.,
/product/[id]con 100k productos),.next/cache/fetch-cache/e.next/cache/images/pueden llenar el disco. Monitorea el uso de disco y establece un trabajo cron para podar entradas de caché antiguas. -
Headers
Upgradefaltantes en Nginx. Sinproxy_set_header Upgrade $http_upgradeyproxy_set_header Connection "upgrade", las conexiones WebSocket fallan silenciosamente. Esto afecta respuestas streaming de Server Actions, streaming de React Server Components, y HMR en modo desarrollo. La conexión parece funcionar pero los datos nunca llegan. -
Renovación de Certbot no automatizada. Los certificados Let's Encrypt expiran cada 90 días. Aunque
certbotconfigura un temporizador systemd por defecto, verifica que esté activo:sudo systemctl list-timers | grep certbot. Si el temporizador falta, añade0 0 1 * * certbot renew --quieta crontab de root. -
Variables
NEXT_PUBLIC_insertadas en tiempo de compilación. Los desarrolladores que migran desde Vercel están acostumbrados a cambiar variables de entorno en un panel y que entren en efecto en la siguiente solicitud. En un servidor independiente, las variablesNEXT_PUBLIC_se incrustan en el bundle de JavaScript durantenext build. Cambiarlas requiere una recompilación y redespliegue completos, no solo un reinicio de PM2. Las variables solo del lado del servidor (sin el prefijoNEXT_PUBLIC_) sí entran en efecto después de un reinicio. -
Build fallando con OOM en instancias pequeñas.
next buildpuede consumir 2+ GB de RAM en aplicaciones grandes. Si estás compilando en unt3.micro(1 GB RAM), añade espacio de intercambio o compila en una instancia más grande y copia el directorio.next/encima. -
Olvidar persistir
.next/cache/en despliegues. Si tu script de despliegue ejecutarm -rf .nextantes de compilar, pierdes la caché de compilación e ISR. Las compilaciones toman más tiempo, y todas las páginas ISR deben regenerarse. En su lugar, solo elimina.next/server/y.next/static/si es necesario, preservando.next/cache/.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Vercel | Quieres cero ops, escalado automático, despliegues de vista previa | La política de la empresa requiere auto-hosting o cloud específico |
| Docker en ECS/EKS | Necesitas orquestación de contenedores, escalado automático, multi-región | Despliegue simple de servidor único |
| Exportación estática | Sitio completamente estático sin características del servidor | Usas SSR, ISR, middleware, o Server Actions |
| Coolify / Dokku | Quieres una PaaS auto-hospedada (tipo Heroku) en tu propio servidor | Necesidades a escala empresarial con orquestación compleja |
Preguntas Frecuentes
¿ISR (Regeneración Estática Incremental) aún funciona en un servidor independiente?
Sí. ISR funciona lista para usar en un servidor único porque las páginas regeneradas se escriben en .next/cache/ en disco local. La única complicación es configuraciones multi-servidor donde cada servidor tiene su propia caché. En ese caso, usa sesiones adhesivas, un montaje NFS compartido (AWS EFS), o un controlador de caché personalizado respaldado por Redis o S3.
¿Cómo hago despliegues de vista previa sin Vercel?
Tienes varias opciones: (1) Ejecuta un proceso PM2 separado por rama en un puerto diferente, con Nginx enrutando por subdominio (pr-123.preview.myapp.com). (2) Usa Coolify o Dokku, que proporcionan despliegues de vista previa automáticos. (3) Omite despliegues de vista previa y confía en entornos de ensayo. La mayoría de equipos eligen la opción 3 a menos que tengan un ingeniero de DevOps dedicado.
¿Qué hay sobre optimización de imágenes? ¿next/image aún funciona?
Sí, next/image funciona en un servidor independiente. La diferencia es que la optimización de imágenes (redimensionamiento, conversión de formato a WebP/AVIF) se ejecuta en la CPU de tu servidor en lugar de la red de borde de Vercel. Para sitios de alto tráfico, esto puede ser intensivo en CPU. Mitigación: pon un CDN (CloudFront, Cloudflare) delante de tu servidor para cachear imágenes optimizadas, o usa la prop loader para offload a un servicio como Cloudinary o Imgix.
¿Cómo retrocedo un despliegue deficiente?
Como estás desplegando vía git pull, retrocede comprobando el commit anterior y recompilando:
cd /home/nextjs/app
git log --oneline -5 # Encontrar el último commit bueno
git checkout <commit-hash> # Comprobar ese commit
npm ci && NODE_ENV=production npx next build
pm2 reload myappPara retrocesos más rápidos, mantén el directorio .next/ de compilación anterior como respaldo antes de cada despliegue.
¿Puedo usar Server Actions en un servidor independiente?
Sí. Server Actions funcionan idénticamente en un servidor independiente. Se ejecutan como solicitudes POST al mismo servidor Node.js. La única diferencia respecto a Vercel es que se ejecutan en un proceso Node.js de larga duración en lugar de una función sin servidor, así que sé consciente de las fugas de memoria en procesos de larga duración.
¿Necesito un balanceador de carga para un servidor único?
No. Una instancia EC2 única con Nginx como proxy inverso es suficiente. Solo necesitas un balanceador de carga (AWS ALB/NLB) cuando escalas a múltiples servidores. Sin embargo, incluso con un servidor, colocarlo detrás de un ALB te da comprobaciones de salud, terminación SSL fácil vía ACM (sin certbot necesario), y una ruta de migración más simple cuando escalas más tarde.
¿Cuánto cuesta esto comparado con Vercel?
Una instancia EC2 t3.medium (2 vCPU, 4 GB RAM) cuesta aproximadamente $30/mes con una instancia reservada o $34/mes bajo demanda. Esto puede manejar tráfico moderado que costaría $100+ en Vercel Pro. Sin embargo, estás pagando con tu tiempo por ops, monitoreo, y parches de seguridad. Para equipos pequeños, el servicio gestionado de Vercel es frecuentemente más barato cuando factorizas tiempo de ingeniería.
¿Debo usar el modo de salida standalone o el por defecto?
Usa output: "standalone" cuando despliegues en contenedores Docker o cuando quieras el artefacto de despliegue más pequeño posible (~50 MB). Usa la salida por defecto cuando despliegues con el directorio node_modules/ completo y quieras despliegues más simples (solo git pull && npm ci && next build && pm2 reload). Standalone añade un paso manual de copiar public/ y .next/static/.
¿Cómo manejo múltiples entornos (staging, production)?
Usa archivos .env.staging y .env.production separados. Next.js carga .env.production automáticamente cuando NODE_ENV=production. Para staging, o bien establece NODE_ENV=staging con una estrategia de carga de env personalizada, o usa archivos de ecosistema PM2 con bloques de env diferentes por entorno:
// ecosystem.config.js
module.exports = {
apps: [{
name: "myapp",
script: "node_modules/.bin/next",
args: "start",
env_production: { NODE_ENV: "production", API_URL: "https://api.myapp.com" },
env_staging: { NODE_ENV: "production", API_URL: "https://staging-api.myapp.com" },
}],
};
// Iniciar con: pm2 start ecosystem.config.js --env staging¿Middleware se ejecuta de la misma forma que en Vercel?
La API es idéntica, pero el tiempo de ejecución es diferente. En Vercel, Middleware se ejecuta en aislamientos de borde V8 (API Web limitada). En un servidor independiente, Middleware se ejecuta en el tiempo de ejecución completo de Node.js, lo que significa que tienes acceso a más APIs de Node.js pero pierdes el beneficio de ubicación de borde. Si tu Middleware es sensible a la latencia (p.ej., redirecciones de geolocalización), considera colocar un CDN delante de tu servidor.
¿Cómo configuro un pipeline de CI/CD para esto?
Usa GitHub Actions (o tu herramienta CI) para SSH al servidor y ejecutar el script de despliegue:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: nextjs
key: ${{ secrets.EC2_SSH_KEY }}
script: /home/nextjs/deploy.sh¿Qué pasa si mi compilación toma demasiado tiempo y causa tiempo de inactividad?
La compilación se ejecuta mientras la versión antigua sigue sirviendo tráfico (PM2 mantiene los procesos antiguos vivos hasta pm2 reload). No hay tiempo de inactividad durante la compilación misma. El único riesgo es si la compilación consume tanta CPU/RAM que degrada la aplicación en ejecución. Soluciones: (1) Compila en un servidor CI separado y rsync el directorio .next/ encima. (2) Usa una instancia más grande durante compilaciones. (3) Añade espacio de intercambio.
Relacionado
- Deployment -- Descripción general de opciones de despliegue de Next.js
- Environment Variables -- Gestionar variables de entorno en Next.js
- Deploy Next.js on ECS/EKS -- Despliegue basado en contenedores