← Blog

Fazendo deploy de Next.js na DigitalOcean

Railyard

8 de junho de 2026 · 10 min de leitura

Siga isso no seu próprio servidor. Comece grátis, sem cartão

Você tem uma aplicação Next.js que funciona localmente, e quer ela rodando num droplet da DigitalOcean que você controla em vez de uma plataforma que cobra por uso. Este guia te leva de um droplet vazio até uma aplicação no ar no seu próprio domínio: uma imagem Docker pequena construída a partir da saída standalone do Next.js, Caddy na frente cuidando dos certificados HTTPS, e uma rotina curta pra lançar a próxima release.

O que você vai precisar

  • Um droplet da DigitalOcean. O tamanho Basic s-1vcpu-2gb (1 vCPU, 2 GB de memória, 50 GB SSD, 2.000 GB de transferência) custa US$12 por mês e roda uma aplicação Next.js típica. Se seu build é pesado, o tamanho s-2vcpu-4gb (2 vCPU, 4 GB, 80 GB SSD) custa US$24 por mês. Os preços são da página de preços de droplets da DigitalOcean.
  • Um nome de domínio cujo DNS você consiga editar.
  • Uma conta DigitalOcean com uma chave SSH adicionada.
  • Uma aplicação Next.js num repositório Git, com um lockfile commitado (package-lock.json, yarn.lock ou pnpm-lock.yaml).

Este guia usa Next.js 16.4.3 e Node.js 24, a linha de release LTS atual, através da imagem oficial node:24-slim.

Passo 1: criar o droplet

No painel de controle da DigitalOcean, escolha Create → Droplets. Escolha Ubuntu 24.04 (LTS) x64, o plano Basic com um SSD Regular, e o tamanho de US$12. Escolha uma região próxima dos seus usuários (Frankfurt ou Amsterdã mantêm os dados na UE), selecione sua chave SSH, e crie o droplet.

Quando estiver pronto, copie seu endereço IPv4 público e conecte:

ssh root@your-droplet-ip

Você deve ver o banner de boas-vindas do Ubuntu e um prompt root@.

Enquanto estiver nas configurações de DNS do seu domínio, crie um registro A que aponte o domínio (ou um subdomínio como app.example.com) pra esse IP. O DNS pode levar alguns minutos pra propagar, e o Caddy precisa disso em vigor antes de conseguir um certificado no Passo 5, então vale fazer isso agora.

Passo 2: instalar o Docker

O Ubuntu não vem com Docker. O script de conveniência oficial do Docker instala o engine e o plugin do Compose de uma vez:

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

Cheque se os dois estão lá:

docker --version
docker compose version

Você deve ver uma linha de versão pra cada um e nenhum erro.

Passo 3: ativar a saída standalone

O Next.js consegue rastrear exatamente quais arquivos o servidor de produção precisa e copiá-los pra .next/standalone, junto de um server.js pequeno que inicia a aplicação. Isso mantém a imagem final pequena, porque ela não carrega toda a sua pasta node_modules.

Abra next.config.ts (ou next.config.js) e adicione output: "standalone" ao objeto de configuração, mantendo quaisquer opções que você já tenha:

import type { NextConfig } from "next";

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

export default nextConfig;

Faça commit da mudança. Você deve ver .next/standalone/server.js aparecer na próxima vez que você rodar npm run build localmente.

Passo 4: adicionar um Dockerfile

Adicione este Dockerfile na raiz do repositório. Ele segue a estrutura do exemplo oficial with-docker no repositório do Next.js: um estágio instala dependências com o gerenciador de pacotes que combina com o seu lockfile, um constrói a aplicação, e o último roda só a saída standalone como o usuário sem privilégios 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"]

Duas linhas precisam de uma palavra de explicação. ARG NEXT_PUBLIC_SITE_URL e a linha ENV depois dela existem porque o Next.js embute cada variável NEXT_PUBLIC_ no bundle do navegador durante o next build. Um valor que você só fornece quando o container inicia chega tarde demais pra essas. Adicione um par ARG e ENV pra cada variável NEXT_PUBLIC_ que sua aplicação usa, ou remova o par se ela não usar nenhuma. Variáveis só-servidor (URLs de banco de dados, secrets de API) não pertencem aqui; elas são lidas em tempo de execução.

Ao lado dele, adicione um .dockerignore pra que seu node_modules local, a saída de build e arquivos de ambiente nunca entrem no build:

.git
.next
node_modules
.env*

Excluir .env* importa: sem isso, o COPY . . copiaria seus secrets pra uma camada do build.

Passo 5: adicionar Compose e Caddy

O guia de self-hosting do Next.js recomenda colocar um reverse proxy na frente do servidor Node em vez de expô-lo diretamente. O Caddy faz esse trabalho e também consegue e renova um certificado Let’s Encrypt por conta própria.

Crie compose.yaml no repositório:

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:

Só o Caddy publica portas. A aplicação escuta na porta 3000 dentro da rede do Compose, onde o Caddy a alcança pelo nome do serviço dela. O volume caddy-data guarda seus certificados, então eles sobrevivem a restarts.

Depois um Caddyfile ao lado dele, com seu próprio domínio:

app.example.com {
	reverse_proxy app:3000
}

Faça commit dos dois arquivos.

Passo 6: colocar o código e os secrets no droplet

No droplet, clone o repositório. Pra um repositório privado, adicione uma deploy key só-leitura nele primeiro, ou clone via HTTPS com um token:

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

Crie o arquivo .env ali. Ele nunca vai pro Git (o .gitignore padrão do create-next-app já ignora arquivos .env*), então ele continua no servidor entre deploys:

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

O Compose usa esse único arquivo duas vezes: preenche ${NEXT_PUBLIC_SITE_URL} em compose.yaml pro build, e env_file entrega cada variável pra aplicação em execução.

Passo 7: construir e iniciar

docker compose up -d --build

O primeiro build leva alguns minutos. Depois cheque os containers:

docker compose ps

Você deve ver app e caddy ambos com status Up. Acompanhe o log do Caddy enquanto ele consegue o certificado:

docker compose logs -f caddy

Você deve ver uma linha dizendo que o certificado foi obtido com sucesso. Abra https://app.example.com e sua aplicação carrega com um certificado válido.

Passo 8: lançar atualizações

Toda release é os mesmos três comandos no droplet:

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

O Compose reconstrói a imagem e substitui o container app. O Caddy continua rodando e mantém o certificado dele. Espere alguns segundos de erros enquanto o container novo inicia, porque existe só uma cópia da aplicação; se isso importa, faça o deploy num momento tranquilo.

Pra fazer rollback, dê checkout no commit anterior e reconstrua:

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

Rode git checkout main (ou o nome da sua branch) antes do próximo deploy normal.

Passo 9: backups

A aplicação em si não tem estado: o código dela vive no Git e a imagem dela pode ser reconstruída em qualquer momento. O que você não consegue reconstruir é o arquivo .env, então mantenha uma cópia em algum lugar além do droplet, como um gerenciador de senhas. Se a aplicação usa um banco de dados, esse banco de dados é a coisa real pra fazer backup; os backups de droplet da DigitalOcean ou um pg_dump agendado copiado pra fora do servidor funcionam dos dois jeitos.

O Next.js mantém o cache de página e de dados no disco do container por padrão. Isso funciona pra uma única instância como essa, e o cache simplesmente começa vazio depois de cada deploy, já que o container é substituído.

O caminho mais curto: fazer isso com o Railyard

O caminho manual funciona, e agora você conhece cada peça dele. O Railyard faz o mesmo trabalho sem o Dockerfile, o arquivo do Compose ou o Caddyfile. Veja Deploy Next.js to DigitalOcean pro passo a passo completo.

  1. No painel de controle da DigitalOcean, abra API e gere um token de acesso pessoal com acesso de leitura e escrita.
  2. No Railyard, vá em Servers → Add a server → Create on your cloud, escolha a DigitalOcean, uma região e um tamanho (s-1vcpu-2gb é o padrão). O droplet é criado na sua própria conta e a DigitalOcean cobra você direto, sem markup. Você também pode trazer um droplet que já tem: qualquer servidor Ubuntu 22.04 ou 24.04 com sudo sem senha e portas 22, 80 e 443 abertas.
  3. Faça deploy do repositório, ou abra https://app.railyard.run/deploy?repo=https://github.com/you/your-app.git com a URL do seu próprio repositório.

O Railyard escolhe npm, pnpm, yarn ou bun a partir do seu lockfile, roda seu script de build e inicia a aplicação com npm start. A aplicação escuta na $PORT, e HOST e variáveis parecidas são definidas pra você. Adicione Postgres ou Redis a partir da página da aplicação e a connection string é injetada como variável. Builds grandes de front-end são encaixados na memória do build server, então você não precisa gerenciar swap ou o tamanho do heap do Node você mesmo. Todo push faz o deploy de novo, e o HTTPS é tratado pra você.

O plano Free cobre um servidor mais um servidor de banco de dados e três aplicações, sem cartão necessário. O Pro custa US$19 por mês por equipe e o Team custa US$49 por mês por equipe. Os docs de Node.js listam as regras de detecção, e o quickstart passa por um primeiro deploy.

Resolução de problemas

bind: address already in use pra porta 80 ou 443. Outro servidor web no droplet já está ocupando a porta. sudo ss -tlnp | grep -E ':(80|443) ' mostra qual processo. Pare e desative ele (sudo systemctl disable --now nginx, por exemplo), depois rode docker compose up -d de novo.

O certificado nunca chega. Quase sempre é DNS. Rode dig +short app.example.com e compare a resposta com o IP do droplet. Se forem diferentes, corrija o registro A e espere; o Caddy continua tentando de novo por conta própria. Se o droplet tem um firewall de nuvem, as portas 80 e 443 precisam estar abertas, porque o Let’s Encrypt valida através delas.

Um valor NEXT_PUBLIC_ está vazio ou desatualizado no navegador. Ele estava faltando no momento do build. Cheque se está no .env, se o compose.yaml passa ele em build.args, se o Dockerfile tem um par ARG e ENV correspondente, e reconstrua com docker compose up -d --build app. Mudar o .env e reiniciar não é suficiente pra isso.

O next build é morto na metade do caminho. O droplet de 2 GB ficou sem memória, e o kernel matou o build. Adicione swap e construa de novo:

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

Redimensionar pro tamanho de US$24 também funciona, assim como construir a imagem em outro lugar e puxar ela de um registry.

Login ou redirecionamentos apontam pra URL errada. Sua biblioteca de autenticação ainda tem uma URL antiga. Se você usa Auth.js ou NextAuth, cheque AUTH_URL ou NEXTAUTH_URL no .env, depois reinicie com docker compose up -d app.

Perguntas frequentes

Isso funciona com pnpm ou yarn? Sim. O Dockerfile escolhe o gerenciador de pacotes a partir do lockfile, assim como o exemplo oficial do Next.js faz. Um lockfile do bun não é tratado por esse Dockerfile; adicione um branch pra ele ou commite um package-lock.json.

Eu posso rodar mais de uma aplicação no mesmo droplet? Sim. Dê a cada aplicação seu próprio serviço no compose.yaml e seu próprio bloco de site no Caddyfile (other.example.com { reverse_proxy other:3000 }). Um único Caddy serve todas elas e gerencia cada certificado.

A Otimização de Imagem e o Proxy continuam funcionando? Segundo o guia de self-hosting do Next.js, tanto a otimização do next/image quanto o Proxy (a convenção de arquivo antigamente chamada de Middleware) funcionam sem nenhuma configuração quando você roda o servidor do Next.js, que é o que o server.js faz.

Eu preciso de um banco de dados? Só se sua aplicação usar um. Aponte DATABASE_URL no .env pra um banco gerenciado ou um que você mesmo roda, e faça backup dele na própria agenda.

Eu deveria desativar a telemetria? O Next.js coleta telemetria anônima por padrão. Adicione ENV NEXT_TELEMETRY_DISABLED=1 nos estágios builder e runner se preferir desativar.

Uma vez que isso está em vigor, uma release é um git pull e um comando do Compose, e a fatura é um valor fixo de US$12 por mês com 2.000 GB de transferência incluídos, em vez de um número que se move junto do seu tráfego.

Por que escolhem o Railyard para isso

  • Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
  • Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
  • Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
  • Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
  • Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.

Pule a configuração do droplet.

Conecte sua conta DigitalOcean, cole a URL do seu repositório, e o Railyard constrói, faz deploy e serve sua aplicação Next.js sobre HTTPS.

Plano grátis, sem cartão, e 14 dias de Pro para cada equipe nova. Um preço fixo por equipe.