← Blog

Despliega Next.js en DigitalOcean

Railyard

8 de junio de 2026 · 10 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

Tienes una app de Next.js que funciona en tu máquina local y quieres que corra en un droplet de DigitalOcean que tú controlas, en lugar de una plataforma que cobra según el uso. Esta guía te lleva desde un droplet vacío hasta una app en vivo en tu propio dominio: una imagen Docker pequeña construida a partir de la salida standalone de Next.js, Caddy por delante gestionando los certificados HTTPS y una rutina corta para publicar la siguiente versión.

Lo que necesitas

  • Un droplet de DigitalOcean. El tamaño Basic s-1vcpu-2gb (1 vCPU, 2 GB de memoria, 50 GB SSD, 2.000 GB de transferencia) cuesta 12 USD al mes y es suficiente para una app típica de Next.js. Si tu build es pesado, el tamaño s-2vcpu-4gb (2 vCPU, 4 GB, 80 GB SSD) cuesta 24 USD al mes. Los precios son de la página de precios de droplets de DigitalOcean.
  • Un nombre de dominio cuyo DNS puedas editar.
  • Una cuenta de DigitalOcean con una clave SSH agregada.
  • Una app de Next.js en un repositorio Git, con un lockfile incluido en el commit (package-lock.json, yarn.lock o pnpm-lock.yaml).

Esta guía usa Next.js 16.4.3 y Node.js 24, la línea LTS actual, a través de la imagen oficial node:24-slim.

Paso 1: Crea el droplet

En el panel de control de DigitalOcean, elige Create → Droplets. Selecciona Ubuntu 24.04 (LTS) x64, el plan Basic con un SSD Regular, y el tamaño de 12 USD. Elige una región cercana a tus usuarios (Frankfurt o Ámsterdam mantienen los datos en la UE), selecciona tu clave SSH y crea el droplet.

Cuando esté listo, copia su dirección IPv4 pública y conéctate:

ssh root@your-droplet-ip

Deberías ver el banner de bienvenida de Ubuntu y un prompt root@.

Ya que estás en la configuración DNS de tu dominio, crea un registro A que apunte el dominio (o un subdominio como app.example.com) a esa IP. El DNS puede tardar unos minutos en propagarse, y Caddy lo necesita listo antes de poder obtener un certificado en el Paso 5, así que conviene hacerlo ahora.

Paso 2: Instala Docker

Ubuntu no incluye Docker. El script de instalación oficial de Docker instala el motor y el plugin de Compose de una sola vez:

curl -fsSL https://get.docker.com | sh

Comprueba que ambos estén instalados:

docker --version
docker compose version

Deberías ver una línea de versión para cada uno y ningún error.

Paso 3: Activa la salida standalone

Next.js puede rastrear exactamente qué archivos necesita el servidor de producción y copiarlos en .next/standalone, junto con un pequeño server.js que inicia la app. Eso mantiene pequeña la imagen final, porque no carga toda tu carpeta node_modules.

Abre next.config.ts (o next.config.js) y agrega output: "standalone" al objeto de configuración, conservando las opciones que ya tengas:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

Haz commit del cambio. Deberías ver aparecer .next/standalone/server.js la próxima vez que ejecutes npm run build localmente.

Paso 4: Agrega un Dockerfile

Agrega este Dockerfile en la raíz del repositorio. Sigue la estructura del ejemplo oficial with-docker del repositorio de Next.js: una etapa instala las dependencias con el gestor de paquetes al que pertenece tu lockfile, otra construye la app, y la última ejecuta solo la salida standalone como el usuario sin privilegios node.

FROM node:24-slim AS dependencies
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; \
  elif [ -f yarn.lock ]; then corepack enable yarn && yarn install --frozen-lockfile --production=false; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm install --frozen-lockfile; \
  else echo "No lockfile found." && exit 1; \
  fi

FROM node:24-slim AS builder
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
ARG NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NODE_ENV=production
RUN if [ -f package-lock.json ]; then npm run build; \
  elif [ -f yarn.lock ]; then corepack enable yarn && yarn build; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm build; \
  fi

FROM node:24-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
COPY --from=builder --chown=node:node /app/public ./public
RUN mkdir .next && chown node:node .next
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Dos líneas merecen una explicación. ARG NEXT_PUBLIC_SITE_URL y la línea ENV que la sigue existen porque Next.js incrusta cada variable NEXT_PUBLIC_ en el bundle del navegador durante next build. Un valor que solo proporcionas al iniciar el contenedor llega demasiado tarde para esas variables. Agrega un par ARG y ENV por cada variable NEXT_PUBLIC_ que use tu app, o elimina el par si no usa ninguna. Las variables solo de servidor (URLs de bases de datos, secretos de API) no van aquí: se leen en tiempo de ejecución.

Junto a él, agrega un .dockerignore para que tu node_modules local, el resultado del build y los archivos de entorno nunca entren en el build:

.git
.next
node_modules
.env*

Excluir .env* importa: sin eso, COPY . . copiaría tus secretos dentro de una capa del build.

Paso 5: Agrega Compose y Caddy

La guía de autoalojamiento de Next.js recomienda poner un proxy inverso delante del servidor de Node en lugar de exponerlo directamente. Caddy cumple esa función y además obtiene y renueva un certificado de Let’s Encrypt por su cuenta.

Crea compose.yaml en el repositorio:

services:
  app:
    build:
      context: .
      args:
        NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL}
    restart: unless-stopped
    env_file: .env

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - app

volumes:
  caddy-data:
  caddy-config:

Solo Caddy publica puertos. La app escucha en el puerto 3000 dentro de la red de Compose, donde Caddy la alcanza por su nombre de servicio. El volumen caddy-data guarda tus certificados, de modo que sobreviven a los reinicios.

Luego un Caddyfile junto a él, con tu propio dominio:

app.example.com {
	reverse_proxy app:3000
}

Haz commit de ambos archivos.

Paso 6: Coloca el código y los secretos en el droplet

En el droplet, clona el repositorio. Para un repositorio privado, agrega primero una deploy key de solo lectura al repositorio, o clónalo por HTTPS con un token:

git clone https://github.com/you/your-app.git /opt/your-app
cd /opt/your-app

Crea ahí el archivo .env. Nunca entra a Git (el .gitignore por defecto de create-next-app ya ignora los archivos .env*), así que permanece en el servidor entre despliegues:

cat > .env <<'EOF'
NEXT_PUBLIC_SITE_URL=https://app.example.com
DATABASE_URL=postgres://user:password@db-host:5432/app
EOF
chmod 600 .env

Compose usa este mismo archivo dos veces: llena ${NEXT_PUBLIC_SITE_URL} en compose.yaml para el build, y env_file entrega cada variable a la app en ejecución.

Paso 7: Construye e inicia

docker compose up -d --build

El primer build tarda unos minutos. Luego revisa los contenedores:

docker compose ps

Deberías ver app y caddy, ambos con estado Up. Sigue el log de Caddy mientras obtiene el certificado:

docker compose logs -f caddy

Deberías ver una línea que indique que el certificado se obtuvo correctamente. Abre https://app.example.com y tu app cargará con un certificado válido.

Paso 8: Publica actualizaciones

Cada versión son los mismos tres comandos en el droplet:

cd /opt/your-app
git pull
docker compose up -d --build app

Compose reconstruye la imagen y reemplaza el contenedor app. Caddy sigue corriendo y conserva su certificado. Espera unos segundos de errores mientras arranca el nuevo contenedor, porque solo hay una copia de la app: si eso te importa, despliega en un horario de poco tráfico.

Para revertir, haz checkout del commit anterior y reconstruye:

git log --oneline -5
git checkout <previous-commit>
docker compose up -d --build app

Ejecuta git checkout main (o el nombre de tu rama) antes del siguiente despliegue normal.

Paso 9: Copias de seguridad

La app en sí no tiene estado: su código vive en Git y su imagen se puede reconstruir en cualquier momento. Lo que no puedes reconstruir es el archivo .env, así que guarda una copia en algún lugar distinto del droplet, como un gestor de contraseñas. Si la app usa una base de datos, esa base de datos es lo que realmente hay que respaldar: tanto las copias de seguridad de droplets de DigitalOcean como un pg_dump programado y copiado fuera del servidor funcionan.

Next.js guarda su caché de páginas y datos en el disco del contenedor por defecto. Eso funciona para una sola instancia como esta, y la caché simplemente empieza vacía después de cada despliegue, ya que el contenedor se reemplaza.

El atajo: hazlo con Railyard

La ruta manual funciona, y ahora conoces cada parte de ella. Railyard hace el mismo trabajo sin el Dockerfile, el archivo Compose ni el Caddyfile. Consulta Despliega Next.js en DigitalOcean para ver la guía completa.

  1. En el panel de control de DigitalOcean, abre API y genera un token de acceso personal con acceso de lectura y escritura.
  2. En Railyard, ve a Servers → Add a server → Create on your cloud, elige DigitalOcean, una región y un tamaño (s-1vcpu-2gb es el predeterminado). El droplet se crea en tu propia cuenta y DigitalOcean te factura directamente, sin recargo. También puedes traer un droplet que ya tengas: cualquier servidor Ubuntu 22.04 o 24.04 con sudo sin contraseña y los puertos 22, 80 y 443 abiertos.
  3. Despliega el repositorio, o abre https://app.railyard.run/deploy?repo=https://github.com/you/your-app.git con la URL de tu propio repositorio.

Railyard elige npm, pnpm, yarn o bun según tu lockfile, ejecuta tu script de build e inicia la app con npm start. La app escucha en $PORT, y HOST y variables similares se configuran por ti. Agrega Postgres o Redis desde la página de la app y la cadena de conexión se inyecta como variable. Los builds grandes de frontend se ajustan a la memoria del servidor de build, así que no tienes que gestionar tú mismo el swap ni el tamaño del heap de Node. Cada push vuelve a desplegar, y el HTTPS se gestiona por ti.

El plan Free cubre un servidor más un servidor de base de datos y tres apps, sin necesidad de tarjeta. Pro cuesta 19 USD al mes por equipo y Team cuesta 49 USD al mes por equipo. Los docs de Node.js detallan las reglas de detección, y el quickstart recorre un primer despliegue.

Solución de problemas

bind: address already in use en el puerto 80 o 443. Otro servidor web en el droplet ya tiene el puerto ocupado. sudo ss -tlnp | grep -E ':(80|443) ' muestra qué proceso es. Detenlo y desactívalo (sudo systemctl disable --now nginx, por ejemplo), y luego ejecuta docker compose up -d de nuevo.

El certificado nunca llega. Casi siempre es el DNS. Ejecuta dig +short app.example.com y compara la respuesta con la IP del droplet. Si difieren, corrige el registro A y espera: Caddy sigue reintentando por su cuenta. Si el droplet tiene un firewall en la nube, los puertos 80 y 443 deben estar abiertos, porque Let’s Encrypt valida a través de ellos.

Un valor NEXT_PUBLIC_ está vacío o desactualizado en el navegador. Faltaba en el momento del build. Comprueba que esté en .env, que compose.yaml lo pase bajo build.args, que el Dockerfile tenga el par ARG y ENV correspondiente, y reconstruye con docker compose up -d --build app. Cambiar .env y reiniciar no es suficiente para estos casos.

next build se interrumpe a la mitad. El droplet de 2 GB se quedó sin memoria y el kernel mató el build. Agrega swap y vuelve a construir:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Cambiar al tamaño de 24 USD también funciona, al igual que construir la imagen en otro lugar y descargarla desde un registro.

El login o las redirecciones apuntan a la URL equivocada. Tu librería de autenticación todavía tiene una URL antigua. Si usas Auth.js o NextAuth, revisa AUTH_URL o NEXTAUTH_URL en .env, y luego reinicia con docker compose up -d app.

Preguntas frecuentes

¿Esto funciona con pnpm o yarn? Sí. El Dockerfile elige el gestor de paquetes a partir del lockfile, igual que el ejemplo oficial de Next.js. Este Dockerfile no maneja un lockfile de bun: agrega una rama para él o incluye un package-lock.json.

¿Puedo correr más de una app en el mismo droplet? Sí. Dale a cada app su propio servicio en compose.yaml y su propio bloque de sitio en el Caddyfile (other.example.com { reverse_proxy other:3000 }). Un solo Caddy las sirve a todas y gestiona cada certificado.

¿Image Optimization y Proxy siguen funcionando? Según la guía de autoalojamiento de Next.js, tanto la optimización de next/image como Proxy (la convención de archivo antes llamada Middleware) funcionan sin configuración adicional cuando ejecutas el servidor de Next.js, que es justo lo que hace server.js.

¿Necesito una base de datos? Solo si tu app usa una. Apunta DATABASE_URL en .env a una base de datos administrada o a una que operes tú mismo, y respáldala con su propio calendario.

¿Debería desactivar la telemetría? Next.js recopila telemetría anónima por defecto. Agrega ENV NEXT_TELEMETRY_DISABLED=1 a las etapas builder y runner si prefieres desactivarla.

Una vez que esto está en marcha, una versión es un git pull y un comando de Compose, y la factura es un fijo de 12 USD al mes con 2.000 GB de transferencia incluidos, en lugar de un número que cambia con tu tráfico.

Por qué eligen Railyard para esto

  • Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
  • Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
  • Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
  • Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
  • Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.

Sáltate la configuración del droplet.

Conecta tu cuenta de DigitalOcean, pega la URL de tu repositorio y Railyard construye, despliega y sirve tu app de Next.js por HTTPS.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.