Pular para o conteúdo

Publicar um projeto Next.js

O Next.js pode virar um site de arquivos prontos ou um servidor Node que roda o tempo todo (páginas renderizadas no servidor, rotas de API, middleware, ISR, otimização de imagens). O painel publica os dois, e ainda deixa você assumir o controle com um Dockerfile seu. Esta página mostra os três caminhos, quando usar cada um e o que o painel faz em cada um.

Se você usa um compose ou um Dockerfile seu, veja também as regras do compose.


O painel olha o repositório nesta ordem e para no primeiro que casa:

PrioridadeO que ele encontraCaminhoTipo na tela
1Um arquivo compose.yml / docker-compose.yml (ou variantes .prod)Seu compose é adotadocompose adotado
2Um Dockerfile na raiz e nenhum package.jsonSeu Dockerfile é usadodockerfile
3next nas dependências e output: "export" no next.configSite estático (pasta out/), mesmo que exista um Dockerfile: nesse caso o Dockerfile é ignoradoestático (Node)
4next nas dependências, sem output: "export" e sem DockerfileServidor automáticoNext.js (servidor)
5next nas dependências, sem output: "export", com Dockerfile e script build no package.jsonSeu Dockerfile é usadodockerfile

Atenção a dois casos: um projeto Next.js com output: "export" e um Dockerfile vira site estático (se você quer o seu Dockerfile, tire o output: "export"); e um projeto com Dockerfile mas sem script build no package.json não é reconhecido (tipo desconhecido): acrescente o script build.

Use quando o site não precisa de servidor: páginas que podem ser geradas no build, sem rotas de API, sem middleware, sem next/image otimizado em tempo real, sem ISR. O next build gera a pasta out/ e o painel a serve com um servidor web simples.

  • É o mais leve e o mais barato: não há processo Node rodando.
  • Variáveis de ambiente não existem em tempo de execução (não há servidor para lê-las). As NEXT_PUBLIC_* valem só se estiverem no código ou no build.
  • O painel lê o next.config.js, .mjs, .cjs, .ts ou .mts como texto (nunca executa o arquivo) e procura output: "export" escrito direto. Se o valor vier de uma expressão (process.env.X ? "export" : …), o painel não sabe qual é o resultado e trata o projeto como servidor: escreva o valor direto no arquivo.

Use quando o projeto usa o servidor do Next.js e você não quer escrever Dockerfile. É o caso de quase todo projeto criado com create-next-app que usa rotas de API, middleware.ts, Server Actions, páginas dinâmicas, revalidate (ISR) ou next/image.

O painel:

  1. gera o Dockerfile fora do seu repositório (nada é gravado no seu código; ele fica na pasta de trabalho do painel e é refeito a cada deploy). Nesse modo o .dockerignore do seu repositório é substituído pelo do painel (que deixa de fora .env, .git, node_modules e .next): o que você tinha nele não vale nesse build;
  2. constrói a imagem (multi-stage) com a instalação congelada pelo lockfile e roda o next build;
  3. sobe um container com a imagem final mínima, como usuário comum (não root), sem capabilities e sem publicar porta no servidor: o acesso é só pelo domínio, pelo Caddy, com HTTPS;
  4. confere se o site respondeu (health check) como nos outros tipos.

O que ele detecta sozinho:

DetecçãoDe onde vem
Gerenciador de pacotescampo packageManager do package.json, depois o lockfile (pnpm-lock.yaml, yarn.lock, package-lock.json), senão npm
Versão do Nodeengines.node do package.json, depois .nvmrc, depois .node-version; sem nada, Node 22 (LTS). O painel oferece 20, 22 e 24; outra versão volta ao 22 e avisa no log
Modo standaloneoutput: "standalone" escrito direto no next.config
Porta3000 por padrão; dá para trocar no assistente

Standalone ou next start?

  • Com output: "standalone" a imagem final leva só o necessário e roda node server.js. É a imagem bem menor (dezenas de MB em vez de centenas) e começa mais rápido. Recomendado.
  • Sem standalone, o painel roda next start com o projeto construído dentro da imagem. Funciona, só é maior.
  • Para ligar o standalone, acrescente ao next.config: output: "standalone".

Limites do servidor automático

  • O app precisa estar na raiz do repositório (o package.json com next na raiz). Monorepo com o app em apps/web: o painel avisa na detecção e você usa o caminho 3 (Dockerfile seu).
  • Com output: "standalone" em monorepo (outputFileTracingRoot), o caminho do server.js muda: o build para com uma mensagem explicando. Use o caminho 3.
  • Yarn Plug’n’Play (sem node_modules) não é suportado; use nodeLinker: node-modules.
  • Sem lockfile no repositório, a instalação não é reprodutível (cada deploy pode trazer versões novas): o painel avisa. Commite o lockfile.
  • O build não tem acesso à rede interna do projeto (por exemplo, ao banco no mesmo compose). Páginas geradas no build que leem banco precisam de um banco alcançável de fora ou devem ser geradas em tempo de execução.
  • O .git não entra no build (se o seu next.config roda git rev-parse para o generateBuildId, defina o id de outro jeito).

Use quando você precisa de controle: monorepo, pacotes do sistema (apk add), banco no mesmo compose, outro runtime, build em etapas diferentes. Coloque um Dockerfile (ou compose.yml) na raiz: ele tem prioridade sobre o servidor automático, e o painel o usa como está.

Isso inclui o caso comum de quem já tinha um Dockerfile do Next.js no repositório: nada muda para esse projeto.

SituaçãoCaminho
Site de conteúdo, sem API nem middleware, que pode ser pré-gerado1. Estático
App com rotas de API, middleware, Server Actions, ISR ou imagens, repositório de um app só2. Servidor automático
Monorepo, banco no mesmo compose, dependências do sistema, imagem própria3. Dockerfile/compose seu

Dúvida entre 1 e 2? Se o next build já gera tudo que você precisa em out/ com output: "export", fique no 1: é mais barato e mais simples de operar.


Na seção Variáveis do projeto:

  • Em tempo de execução (o servidor rodando), todas as variáveis do painel valem, como em qualquer projeto.
  • NEXT_PUBLIC_* precisam existir no build. O Next.js troca process.env.NEXT_PUBLIC_X pelo valor dentro do JavaScript que vai para o navegador. Por isso o painel passa as NEXT_PUBLIC_* ao build. Elas são públicas por definição: qualquer visitante lê o valor no código do site. Nunca coloque segredo numa NEXT_PUBLIC_*.
  • Mudou uma NEXT_PUBLIC_*? Faça um novo deploy: reiniciar o container não basta, porque o valor está dentro do build.
  • As demais variáveis (senhas, chaves, DATABASE_URL) também ficam disponíveis ao next build, caso uma página gerada no build precise delas. O painel as entrega por um segredo do BuildKit, montado só durante o passo do next build: elas não entram em nenhuma camada da imagem nem aparecem no docker history. O log do deploy lista só os nomes, nunca os valores.
  • Não entram no build (e valem só na execução): PORT, HOSTNAME, NODE_ENV, NODE_OPTIONS, PATH, HOME, LD_*, npm_* e nomes que não são identificadores válidos de shell; também valores com mais de 32 KB.
  • PORT nas Variáveis é ignorada: o servidor escuta na porta do projeto (3000 por padrão) e o painel entrega essa porta ao Next.js. Para mudar, use o campo de porta do projeto.
  • O painel sempre define HOSTNAME=0.0.0.0, NODE_ENV=production e desliga a telemetria do Next.js.

O next build é o passo mais pesado de um deploy. Em uma VPS pequena ele pode estourar a memória.

  • O painel limita o heap do Node no build com --max-old-space-size igual a 60% da RAM do servidor (o limite é por processo: o Next.js usa vários processos no build, então o total pode passar disso; para reduzir, limite experimental.cpus no next.config), com mínimo de 768 MB e máximo de 4096 MB (1 GB de RAM → 768 MB; 2 GB → 1229 MB; 4 GB → 2458 MB; 8 GB ou mais → 4096 MB). O valor aparece no log do deploy.
  • Se o build morrer por falta de memória, o log termina com “O build ficou sem memória” e o que fazer. O que costuma resolver, nesta ordem:
    1. Criar um arquivo de swap na VPS (2 GB já ajudam muito). Em Ubuntu: sudo fallocate -l 2G /swapfile, sudo chmod 600 /swapfile, sudo mkswap /swapfile, sudo swapon /swapfile, e a linha /swapfile none swap sw 0 0 em /etc/fstab para sobreviver ao reinício.
    2. Parar outros containers pesados durante o deploy.
    3. Usar uma VPS com mais RAM.
    4. No Next.js 15 ou mais novo, ligar experimental.webpackMemoryOptimizations no next.config para baixar o pico.
  • O site que já estava no ar continua no ar durante o build. Depois do build o container antigo é parado e guardado (não apagado) e o novo sobe; se o novo não subir ou o health check falhar, o painel liga o antigo de volta e o log diz isso. O antigo e a imagem anterior só são removidos depois que o novo responde. Depois de um deploy revertido, o botão Recriar (e a troca de fuso que recria containers) é recusado: a imagem mais recente é a que falhou. A saída é um novo deploy que fique saudável, que também libera o Recriar. Se a Porta do projeto mudou nesse deploy, o site segue na porta da versão antiga até lá.
  • Um deploy sem mudança nas dependências costuma ser bem mais rápido: o painel guarda o cache de instalação do gerenciador de pacotes por projeto, com um nome imprevisível (uma chave aleatória guardada na pasta de trabalho do projeto, fora do seu código): o cache de build do Docker é do servidor inteiro, e o nome imprevisível impede que o Dockerfile de outro projeto o monte, leia ou envenene.

A instalação é congelada: se o package.json e o lockfile não combinam, o log explica. Rode pnpm install (ou npm install / yarn install) no seu computador, commite o lockfile atualizado e faça o deploy de novo.


O cache de dependências e as camadas do build ficam no BuildKit do Docker e crescem com o uso.

  • Automático: depois de cada deploy que ficou saudável o painel remove a imagem anterior do projeto e o cache de build com mais de 7 dias.
  • Manual, quando precisar de espaço (na VPS):
    Janela do terminal
    docker system df
    docker builder prune --filter until=24h
    docker image prune
    Limpar o cache só deixa o próximo build mais lento; nada que está no ar é afetado.

O servidor automático segue as mesmas regras dos outros tipos:

  • As travas de segurança do deploy rodam sobre o seu código antes do build.
  • O container roda como node (não root), com cap_drop ALL e no-new-privileges, sem pastas do servidor montadas e sem porta publicada: só a rede interna do painel e o Caddy.
  • A imagem base é a oficial node:<versão>-alpine, fixada por digest.
  • O código dentro da imagem pertence ao root e é somente leitura para o processo; só .next/cache (cache de imagens do Next.js) é gravável pelo usuário node. Se o seu app grava arquivos em runtime, use um volume (Dockerfile ou compose seu).
  • Arquivos .env* do repositório não entram no build (nem o .env que o painel gravou quando o projeto era compose, que é apagado). As NEXT_PUBLIC_* vêm das Variáveis do projeto, não de .env.production versionado.
  • As imagens dos projetos não passam por varredura de vulnerabilidades. O Trivy roda no CI só sobre a imagem do próprio painel. As imagens node:*-alpine são oficiais e fixadas por digest, mas as dependências do seu projeto são suas: mantenha-as atualizadas (por exemplo, rodando trivy image você mesmo). A varredura das imagens dos projetos está no roadmap.
  • O Dockerfile gerado não vai para o seu repositório e o segredo do build é um arquivo temporário (0600) apagado logo depois do build.
  • O build roda o seu código (scripts de instalação, next.config), como qualquer build Docker: só publique repositórios em que você confia.

SintomaO que fazer
“O build ficou sem memória”Crie swap, libere memória ou use mais RAM (veja acima)
“O lockfile não bate com o package.json”Atualize e commite o lockfile
“pede output standalone, mas o build não gerou .next/standalone/server.js”Monorepo ou output vindo de expressão: escreva o valor direto ou use um Dockerfile seu
O site abre com a página de erro 502 logo após o deployO servidor demora a subir: confira a aba Logs do projeto
Mudei uma NEXT_PUBLIC_ e o site mostra o valor antigoFaça um novo deploy (reiniciar não basta)
O tipo aparece como “dockerfile” mas você queria o automáticoHá um Dockerfile na raiz: remova-o (ou renomeie) e rode a detecção de novo