Next.js tem dois caminhos pra deploy: o managed (Vercel) e o self-hosted (Docker). O managed funciona bem se você não se importa com vendor lock-in e custo em dólar. O self-hosted te dá controle total sobre o runtime, a infraestrutura e a conta no fim do mês.
O problema é que a maioria dos tutoriais de Next.js com Docker ignora o output standalone. Copiam a pasta .next inteira pra imagem e geram um container de 1.2GB que demora pra subir. Não precisa ser assim. Com o modo standalone e um Dockerfile multi-stage, a imagem fica abaixo de 150MB e o app roda sem o node_modules completo.
Vou mostrar o caminho inteiro, do next.config.js até o serviço respondendo com HTTPS na Guara Cloud. Sem pressa, sem enrolação.
Resposta rápida
Para publicar um app Next.js com Docker no Brasil, ative output: 'standalone' no next.config.js, use um Dockerfile multi-stage que copia apenas o diretório standalone e os assets estáticos, exponha a porta 3000 e publique o container na Guara Cloud. A plataforma cuida de HTTPS, domínio público, logs e cobra em Real.
Principais pontos
- Use
output: 'standalone'no next.config.js. Isso gera um servidor minimalista que não depende donode_modulesinteiro. - O Dockerfile multi-stage separa install, build e produção. A imagem final carrega cerca de 130MB.
- O
server.jsgerado pelo standalone escuta na porta 3000 por padrão. UseHOSTNAME=0.0.0.0para aceitar conexões externas. - Assets estáticos (
public/e.next/static) precisam ser copiados manualmente para o diretório do standalone na imagem final. - Configure variáveis de ambiente pelo painel da Guara Cloud, não no código.
Quando este tutorial se aplica
Use este fluxo para aplicações Next.js com App Router ou Pages Router que precisam rodar self-hosted. Funciona com rotas de API, Server Actions, SSR, ISR e rotas estáticas. Se o projeto usa next/image com otimização local, o container suporta via sharp (incluído no standalone).
Quando não usar este fluxo
Se o projeto é 100% estático (next export ou output: 'export'), você não precisa de um container rodando Node.js. Qualquer CDN serve os arquivos HTML. Se o app precisa de Edge Runtime (middleware com Edge, geolocation no edge), o self-hosted perde essa capacidade, já que o Edge Runtime é específico de plataformas serverless. Para esses casos, considere export estático pra CDN e um container separado só pras rotas que precisam de SSR.
Antes de começar
- Um projeto Next.js 14+ criado com create-next-app ou existente
- Node.js 20+ instalado localmente para testes
- Docker instalado para validar a imagem
- Uma conta na Guara Cloud (app.guaracloud.com)
1. Ative o output standalone
Abra next.config.js (ou next.config.mjs) e adicione a opção:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
export default nextConfig; Quando você roda next build, o Next.js gera o diretório .next/standalone com um server.js mínimo que carrega apenas as dependências necessárias. Sem o node_modules de 500MB.
Se o projeto usa @next/font ou next/image com sharp, eles são incluídos automaticamente no bundle standalone.
2. Dockerfile multi-stage para Next.js
Esse é o Dockerfile que eu uso nos meus projetos Next.js na Guara Cloud. Quatro estágios, cada um com uma função clara.
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"] Alguns pontos sobre esse Dockerfile:
O npm ci no estágio deps é separado do build. Isso significa que quando você muda código mas não muda dependências, o Docker reusa a layer de deps e o build é mais rápido.
O usuário nextjs não-root é criado no estágio final. Sei que muita gente ignora isso, mas rodar container como root é pedir problema, mesmo com a plataforma sandboxizando o processo.
Já o HOSTNAME=0.0.0.0 faz o servidor aceitar conexões de fora do container. Sem isso, o Next.js escuta só em 127.0.0.1 e o health check da plataforma falha. Esse é um erro que vejo muita gente cometer na primeira vez.
3. Adicione o .dockerignore
Sem isso, o Docker copia node_modules e .next pro contexto de build, o que torna cada build 30 segundos mais lento.
node_modules
.next
.git
.gitignore
docker-compose*.yml
Dockerfile
*.md
.env*.local 4. Configure variáveis de ambiente
No painel da Guara Cloud, adicione as variáveis que o app Next.js consome:
Variáveis comuns para Next.js
| Nome | Valor |
|---|---|
NODE_ENV | production |
DATABASE_URL | postgres://user:***@host:5432/mydb |
NEXT_PUBLIC_APP_URL | https://meu-app.guaracloud.com |
JWT_SECRET | string-aleatoria-longa |
Variáveis com prefixo NEXT_PUBLIC_ são embutidas no bundle JavaScript durante o build. Se você mudar essas variáveis, precisa fazer um novo deploy. Variáveis sem o prefixo (como DATABASE_URL) são lidas em runtime no servidor e podem ser alteradas sem rebuild.
5. Crie uma rota de health check
Guara Cloud usa probes HTTP para verificar se o container está respondendo. Crie uma rota simples:
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
export async function GET() {
return NextResponse.json({ status: 'ok' });
} Se usa Pages Router, crie pages/api/health.ts:
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ status: 'ok' });
} O force-dynamic garante que a rota não seja cacheada pelo ISR. O probe precisa de uma resposta fresca.
6. Deploy na Guara Cloud
Com o repositório no GitHub e o Dockerfile na raiz, o processo é:
Passo a passo no painel
- Crie um novo serviço e conecte o repositório do GitHub
- A plataforma detecta o Dockerfile automaticamente
- Confirme a porta HTTP (3000, que é o padrão do Next.js)
- Adicione as variáveis de ambiente pelo painel
- Inicie o deploy e acompanhe os logs de build em tempo real
O primeiro build demora entre 3 e 5 minutos, incluindo npm ci, next build e a geração da imagem. Os deploys seguintes são mais rápidos por causa do cache de layers do Docker. A layer de deps só é refeita quando o package-lock.json muda.
7. Valide o serviço em produção
Depois que o deploy concluir, verifique:
- A URL pública responde com 200.
- A rota
/healthretorna{ status: 'ok' }. - As páginas SSR renderizam corretamente (não retorna HTML vazio).
- A rota de API funciona se o projeto tem endpoints em
app/api/. - Os logs mostram o servidor Next.js escutando na porta 3000.
Se algo falha, os logs de build e runtime ficam disponíveis no painel. A maioria dos problemas aparece nos primeiros segundos.
Imagem final e performance
Com o Dockerfile acima, a imagem final fica em torno de 130MB. Isso inclui o Alpine, o Node.js 20, o server.js standalone e os assets estáticos. Para referência, um Dockerfile ingênuo que copia node_modules inteiro passa de 900MB.
O tempo de cold start do container é cerca de 1 a 2 segundos na Guara Cloud. O Next.js standalone já vem com as rotas pré-compiladas, então a primeira requisição não tem overhead adicional de compilação.
Problemas comuns
- Problema O container inicia mas o health check falha
- Solução Verifique se HOSTNAME está definido como "0.0.0.0". Sem isso, o Next.js escuta apenas em loopback. Confirme também que a porta no painel está configurada como 3000.
- Problema Imagens quebradas ou CSS não carrega
- Solução Os arquivos de public/ e .next/static não foram copiados para o diretório correto. No Dockerfile, confirme as duas linhas COPY que movem esses diretórios para dentro do standalone.
- Problema Rotas de API retornam 500 com "Module not found"
- Solução Alguma dependência usada em API routes não foi incluída no bundle standalone. Verifique se está importada corretamente no código. O standalone rastreia imports automaticamente, mas re-exports de pacotes com resolução dinâmica podem falhar.
- Problema O build do Next.js falha com erro de memória
- Solução O next build precisa de mais RAM durante a compilação. Adicione NODE_OPTIONS="--max-old-space-size=4096" como variável de ambiente de build na Guara Cloud ou no estágio builder do Dockerfile.
- Problema Variáveis NEXT_PUBLIC_ não aparecem no cliente
- Solução Essas variáveis são embutidas no bundle JavaScript durante o build. Se você mudou o valor depois do deploy, precisa fazer um novo build. Elas não são lidas em runtime.
- Problema next/image retorna 500 no container
- Solução O sharp pode não ter sido incluído. Confirme que está usando next/image padrão (não um loader customizado) e que o package-lock.json inclui sharp nas dependências.
Diferenças entre self-hosted e Vercel
Algumas coisas mudam quando você roda Next.js em container ao invés da Vercel:
Edge Runtime não funciona. Middleware definido com runtime: 'edge' precisa de uma plataforma que suporte o runtime do Edge (Vercel, Cloudflare Workers). Em container, use o Node.js runtime para middleware.
ISR funciona, mas a invalidação é local. No Vercel, o revalidate é global e distribuído pela edge network. Self-hosted, cada container mantém seu próprio cache. Se você tem múltiplas réplicas, a revalidation não propaga automaticamente entre elas.
Preview deploys precisam de configuração manual. O Vercel cria um deploy por PR automaticamente. Na Guara Cloud, você pode criar ambientes separados para staging, mas o fluxo de preview deploy por branch é diferente.
Custo. Aqui é onde o self-hosted ganha de verdade. Um app Next.js na Guara Cloud com 512MB de RAM custa uma fração do que a Vercel cobra no plano Pro em dólar, com IOF de 6.38% por cima. E a cobrança vem em Real, com nota fiscal.
Posso usar App Router e Pages Router juntos?
Sim. O Next.js suporta ambos no mesmo projeto, e o output standalone funciona com os dois. O Dockerfile não muda.
O modo standalone suporta Server Actions?
Sim. Server Actions funcionam normalmente no runtime Node.js. Elas só não funcionam no Edge Runtime, que não existe em container.
Como faço para escalar horizontalmente?
Na Guara Cloud, aumente o número de réplicas do serviço. Cada réplica roda uma instância independente do server.js. Para compartilhar cache de ISR entre réplicas, considere usar o Redis como cache backend.
A cobrança é em Real?
Sim. A Guara Cloud cobra em BRL via Stripe. Sem conversão em dólar, sem IOF no cartão.
Preciso de Nginx na frente do Next.js?
Não. A Guara Cloud fornece HTTPS, domínio público e roteamento sem que você precise configurar Nginx ou certificados TLS manualmente.
Publique seu Next.js na Guara Cloud
Deploy com Docker, HTTPS gerenciado, logs em tempo real e cobrança em Real. Infraestrutura em São Paulo.