Alternar tema

Saindo da Vercel: guia completo para hospedar Next.js com Docker

Easton editorial illustration: performance inspection lens

No fim do mês passado, abri como sempre a página de cobrança da Vercel. US$ 47,32.

Fiz a conta rapidamente: o tráfego do blog aumentou só 20% naquele mês. Como a fatura conseguiu dobrar? Ao abrir os detalhes, descobri que o problema estava no número de chamadas de serverless functions: uma rota de API sem cache adequado era executada três vezes a cada atualização da página.

A experiência de desenvolvimento da Vercel é realmente fluida: basta um git push para implantar automaticamente, há aceleração global pela rede de borda e vários recursos prontos para uso. Mas, quando o tráfego começa a crescer, a conta sobe como um foguete. O plano Pro de US$ 20 é apenas o preço de entrada; o custo de verdade está nos itens cobrados por uso.

Naquele momento, percebi que era hora de tirar o projeto de lá.

Este artigo registra todo o processo de migração do meu projeto Next.js da Vercel para uma hospedagem própria com Docker. Reuni aqui as armadilhas que encontrei, a documentação que consultei e as configurações que testei. Se você também está pensando em hospedar por conta própria ou já tentou e esbarrou em problemas estranhos, como recursos estáticos retornando 404 ou streaming que não funciona, espero que este guia ajude.

US$ 35–50
Custo mensal na Vercel
Varia conforme o tráfego
US$ 12
Custo mensal da hospedagem própria
Custo fixo
US$ 300–500
Economia anual
Permite executar vários projetos
200 MB
Tamanho da imagem Docker
Após otimização em três etapas
Source: Dados reais

Por que sair da Vercel?

Antes de tudo, não quero atacar a Vercel. Em muitos cenários, ela continua sendo a melhor solução — principalmente para projetos corporativos, aplicações que precisam de uma rede de borda global ou equipes sem capacidade de operação. Mas, para projetos pessoais e equipes pequenas, o custo é realmente um ponto fraco.

Como funciona a cobrança da Vercel

O plano gratuito parece generoso: 100 GB de largura de banda e 1 milhão de Edge Requests. O problema é que um projeto com algum tráfego já consegue ultrapassar essas cotas. Ao migrar para o Pro, por US$ 20 por mês, você descobre que isso é apenas o ingresso:

  • Chamadas de Serverless Functions: cobrança por uso após 1 milhão de chamadas
  • Tempo de execução de funções de borda: custo adicional após 1 milhão de GB-s
  • Otimização de imagens: cobrança por operação após 5.000 usos
  • Largura de banda: cobrança por GB acima de 1 TB

O pior é que esse consumo é difícil de prever. Uma rota de API sem cache adequado ou uma página rastreada agressivamente por bots pode fazer a conta disparar.

Quanto é possível economizar com hospedagem própria?

Fiz as contas. Meu projeto custava cerca de US$ 35–50 por mês na Vercel, variando conforme o tráfego. Depois da migração para um servidor de US$ 12 por mês na DigitalOcean:

  • Servidor: US$ 12 por mês, com 2 núcleos e 4 GB, suficiente para dois ou três aplicativos Next.js
  • CDN da Cloudflare: grátis, pois eu já a utilizava
  • Armazenamento adicional: US$ 0, porque o disco local é suficiente

A economia mensal fica entre US$ 25 e US$ 40, ou US$ 300–500 por ano. Mais importante: esse custo é fixo e não aumenta de repente com um pico de tráfego.

Quando vale a pena hospedar por conta própria?

Hospedagem própria não é para todo mundo. Na minha opinião, vale considerar se você atende a estes critérios:

  • ✅ Já tem alguma experiência com Linux e Docker
  • ✅ O tráfego do projeto é relativamente estável e não exige uma rede de borda global
  • ✅ Aceita um processo de implantação manual de 5 a 10 minutos
  • ✅ Tem orçamento limitado, como em projetos pessoais ou no início de uma startup

Por outro lado, é melhor continuar na Vercel nestes casos:

  • ❌ A equipe não sabe operar servidores e não quer aprender
  • ❌ O tráfego oscila muito e exige escalabilidade automática
  • ❌ Você precisa de recursos exclusivos da Vercel, como Analytics e Edge Config
  • ❌ Há orçamento suficiente e a produtividade de desenvolvimento é mais importante

Pense bem antes de começar. Não vale criar um grande problema só para economizar dinheiro.

Configurações essenciais para implantar Next.js com Docker

Vamos ao que interessa. Uma implantação de Next.js com Docker tem três pontos essenciais de configuração. Ao acertar os três, você evita a maioria dos problemas.

1. Modo de saída Standalone

Esta é a etapa mais importante. Por padrão, next build gera vários arquivos, incluindo o node_modules completo. Isso deixa a imagem Docker enorme e torna a inicialização mais lenta.

Adicione esta linha ao next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

Depois de executar npm run build, você verá o diretório .next/standalone. Ele contém:

  • server.js: script de inicialização
  • Um node_modules reduzido, apenas com os pacotes necessários em runtime
  • O código da aplicação

Ponto importante: o modo standalone não copia public e .next/static automaticamente. Você precisa copiá-los manualmente para o diretório standalone; caso contrário, todos os recursos estáticos retornarão 404. Levei dois dias para descobrir essa armadilha.

2. Dockerfile multiestágio

Este é o Dockerfile que uso. Os comentários explicam cada parte:

# ============ Etapa 1: instalação de dependências ============
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Copia apenas os manifestos de dependências para aproveitar o cache do Docker
COPY package.json package-lock.json ./
RUN npm ci

# ============ Etapa 2: build da aplicação ============
FROM node:20-alpine AS builder
WORKDIR /app

# Copia as dependências e o código-fonte
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Variáveis de ambiente de build, se necessárias
ENV NEXT_TELEMETRY_DISABLED=1

# Build
RUN npm run build

# ============ Etapa 3: execução em produção ============
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Cria um usuário não root como prática de segurança
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copia a pasta public com os recursos estáticos
COPY --from=builder /app/public ./public

# Copia a saída standalone
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# Copia os arquivos static gerados, como CSS e JS
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# Comando de inicialização
CMD ["node", "server.js"]

Pontos principais:

  1. Build em três etapas: instalação de dependências, build e execução ficam separados. A imagem final contém apenas o necessário em runtime, reduzindo o tamanho de 1,5 GB para 200 MB
  2. COPY --from=builder /app/public: não esqueça essa linha, ou favicon e robots.txt ficarão inacessíveis
  3. COPY ./.next/static: ainda mais importante; sem ela, todos os arquivos JS e CSS retornarão 404
  4. Usuário não root: é uma prática de segurança; não execute a aplicação como root em produção

3. Armadilhas das variáveis de ambiente

Também tive problemas aqui. O Next.js divide as variáveis de ambiente em dois tipos:

  • Variáveis de build: começam com NEXT_PUBLIC_ e são incorporadas ao código durante a compilação
  • Variáveis de runtime: usadas no servidor, como o endereço do banco de dados

No modo Standalone, runtimeConfig não funciona. A recomendação oficial é seguir o padrão do App Router:

// app/api/example/route.ts
export async function GET() {
  // Lê diretamente de process.env
  const dbUrl = process.env.DATABASE_URL
  // ...
}

Passe as variáveis de ambiente ao iniciar o Docker:

docker run -p 3000:3000 \
  -e DATABASE_URL="postgres://..." \
  -e API_KEY="xxx" \
  your-image-name

Ou use um docker-compose.yml:

version: '3.8'
services:
  nextjs:
    image: your-image-name
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: "postgres://..."
      API_KEY: "xxx"
    restart: unless-stopped

Atenção: as variáveis com prefixo NEXT_PUBLIC_ precisam ser definidas durante o build e não podem ser alteradas em runtime. Se você precisa de configuração dinâmica em runtime, use variáveis de ambiente do servidor.

Pontos importantes na configuração do proxy reverso

É possível expor o contêiner Next.js diretamente à internet, mas não faça isso. Uma aplicação Node.js sem proteção não resiste por muito tempo a solicitações maliciosas e ataques lentos. O proxy reverso não é opcional; é necessário.

Por que usar um proxy reverso?

  1. Proteção de segurança: bloqueio de solicitações maliciosas, limitação de taxa e mitigação de DDoS
  2. Suporte a HTTPS: gerenciamento centralizado de certificados SSL
  3. Implantação de vários aplicativos: execução de vários projetos no mesmo servidor, separados por domínio ou caminho
  4. Cache de recursos estáticos: redução da carga no servidor da aplicação

Eu uso o Nginx, que é estável e confiável. Se você prefere uma configuração mais simples, o Caddy também é uma boa opção, com HTTPS automático e um arquivo de configuração mais amigável.

Exemplo de configuração do Nginx

server {
    listen 80;
    server_name yourdomain.com;
    
    # Força o redirecionamento para HTTPS, se o SSL estiver configurado
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;
    
    # Configuração do certificado SSL com Let's Encrypt
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    # Proxy reverso para o contêiner Next.js
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        
        # Cabeçalhos obrigatórios
        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;
        
        # Essencial: desativa o buffering para permitir streaming
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header X-Accel-Buffering no;
        
        # Suporte a WebSocket, se necessário
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
    
    # Cache de recursos estáticos, opcional mas recomendado
    location /_next/static/ {
        proxy_pass http://localhost:3000;
        proxy_cache_valid 200 60m;
        add_header Cache-Control "public, max-age=3600, immutable";
    }
}

Três configurações essenciais:

  1. proxy_buffering off: desativa o buffering; sem isso, a saída em streaming fica retida
  2. X-Accel-Buffering: no: informa explicitamente ao Nginx que ele não deve armazenar o corpo da resposta em buffer
  3. Suporte a WebSocket: se você usa Socket.io ou recursos em tempo real, precisa adicionar o cabeçalho Upgrade

Configuração simplificada com Caddy

Se a configuração do Nginx parece complicada demais, experimente o Caddy:

yourdomain.com {
    reverse_proxy localhost:3000 {
        # Por padrão, o Caddy não usa buffering; não é necessária configuração especial
    }
}

Só isso. O Caddy solicita e renova automaticamente o certificado do Let’s Encrypt, e o arquivo de configuração é tão simples quanto esse.

Integração com Docker Compose

Você também pode colocar o Nginx em um contêiner para facilitar o gerenciamento:

version: '3.8'
services:
  nextjs:
    build: .
    restart: unless-stopped
    environment:
      DATABASE_URL: "postgres://..."
    # Não expõe ao host; apenas o nginx pode acessar
    expose:
      - "3000"
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certs:/etc/letsencrypt
    depends_on:
      - nextjs
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Observe que o serviço nextjs usa expose, e não ports. Assim, apenas os contêineres na mesma rede podem acessá-lo, o que é mais seguro.

Como corrigir falhas na renderização por streaming

Esse problema me tomou um dia inteiro. O chat com IA funcionava perfeitamente no desenvolvimento local, mas, depois da implantação com Docker, o streaming deixou de funcionar: ou tudo aparecia de uma vez após uma longa espera, ou a resposta simplesmente travava.

Sintomas

Os sintomas típicos são:

  • As respostas em streaming das APIs da OpenAI ou Anthropic não funcionam
  • Server-Sent Events (SSE) não são enviados em tempo real
  • A página demora para atualizar e depois mostra tudo de uma vez, sem o efeito de saída gradual

Localmente, com npm run dev, tudo funciona. O problema surge apenas no ambiente de produção.

Causa raiz

Dois pontos podem provocar esse problema:

  1. Buffering do proxy reverso: por padrão, o Nginx armazena o corpo da resposta e só o envia ao cliente depois de receber todo o conteúdo
  2. Runtime do Next.js: em algumas situações, rotas de API fora do Edge Runtime não oferecem suporte adequado à saída em streaming

Solução 1: configurar o Nginx

Estas são as três linhas mencionadas na seção sobre proxy reverso:

proxy_buffering off;
proxy_cache off;
proxy_set_header X-Accel-Buffering no;

É obrigatório adicioná-las ao bloco location /. Depois da alteração, reinicie o Nginx:

nginx -t  # Testar a sintaxe da configuração
nginx -s reload  # Recarregar a configuração

Solução 2: usar o Edge Runtime

Se a sua rota de API produz uma saída em streaming, como em um chat com IA, adicione esta linha no início do arquivo:

// app/api/chat/route.ts
export const runtime = 'edge'

export async function POST(req: Request) {
  const stream = new ReadableStream({
    async start(controller) {
      // Sua lógica de streaming
      const response = await openai.chat.completions.create({
        model: 'gpt-4',
        messages: [...],
        stream: true,
      })

      for await (const chunk of response) {
        controller.enqueue(chunk.choices[0]?.delta?.content || '')
      }
      
      controller.close()
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  })
}

O Edge Runtime é um runtime leve, otimizado para respostas em streaming, e costuma se comportar de forma mais estável em ambientes Docker.

Como validar a correção

Teste com curl. Se a saída aparecer linha por linha, a correção funcionou:

curl -N http://yourdomain.com/api/chat \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'

O parâmetro -N desativa o buffering. O conteúdo deve aparecer aos poucos, em vez de surgir inteiro depois de uma longa espera.

Ainda não funciona? Verifique estes pontos

  1. Proxy da Cloudflare: se você usa a nuvem laranja da Cloudflare, ela também pode armazenar a resposta em buffer. Desative-a, deixando a nuvem cinza, ou faça upgrade para o plano Pro, que oferece suporte a Streaming
  2. Health check do Docker: algumas configurações de verificação de integridade podem interferir em conexões de streaming; confira a seção healthcheck do docker-compose.yml
  3. Load balancer: se houver um balanceador de carga antes da aplicação, ele também pode armazenar a resposta em buffer e precisa de configuração própria

Diagnóstico e correção de problemas comuns

Reuni algumas armadilhas que encontrei e dúvidas frequentes da comunidade. Elas cobrem cerca de 80% dos casos de falha na implantação.

Problema 1: recursos estáticos retornam 404

Sintoma: a página abre, mas todo o estilo está quebrado. O console mostra vários erros 404 em caminhos como /_next/static/....

Causa: a pasta .next/static não foi copiada corretamente no Dockerfile.

Correção: verifique se o Dockerfile contém estas duas linhas:

COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

Se elas já estiverem presentes e o erro 404 continuar, verifique as permissões dos arquivos:

COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

Problema 2: falha no build do Docker

Sintoma: docker build mostra o erro “Could not find a production build in the ‘.next’ directory”.

Causa: configuração incorreta do .dockerignore ou ordem inadequada das etapas de build.

Correção: crie um arquivo .dockerignore e exclua os diretórios desnecessários:

.next
node_modules
.git
.env*.local
out
.DS_Store
*.log

Atenção: .next deve ser ignorado porque o projeto será recompilado dentro do contêiner Docker.

Problema 3: variáveis de ambiente não funcionam

Sintoma: a leitura de process.env.DATABASE_URL no código retorna undefined.

Causa: as variáveis foram passadas de forma incorreta ou houve confusão entre build e runtime.

Correção:

  1. Para variáveis de runtime, como endereço do banco de dados e chaves de API, use docker run -e ou docker-compose.yml:

    docker run -e DATABASE_URL="..." your-image
  2. Para variáveis de build, que começam com NEXT_PUBLIC_, passe-as durante docker build:

    docker build --build-arg NEXT_PUBLIC_API_URL="https://api.example.com" .

    Também é preciso declará-las no Dockerfile:

    ARG NEXT_PUBLIC_API_URL
    ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

Problema 4: falta de memória durante o build

Sintoma: o build trava no meio ou apresenta o erro “JavaScript heap out of memory”.

Causa: o limite padrão de memória do Node.js é insuficiente. O build de projetos Next.js grandes consome bastante memória.

Correção: aumente a memória na etapa de build do Dockerfile:

# Na etapa builder
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run build

Ou limite os recursos com o Docker BuildKit:

docker build --memory=8g --memory-swap=8g -t your-image .

Problema 5: não é possível acessar o contêiner após iniciá-lo

Sintoma: o contêiner está em execução, mas o acesso a http://localhost:3000 é recusado.

Causa: o Next.js está ouvindo em 127.0.0.1 por padrão e, dentro do contêiner Docker, não pode ser acessado externamente.

Correção: defina estas variáveis no Dockerfile:

ENV HOSTNAME="0.0.0.0"
ENV PORT=3000

Ou passe-as ao iniciar o contêiner:

docker run -p 3000:3000 -e HOSTNAME="0.0.0.0" your-image

Comandos rápidos de diagnóstico

Quando surgir um problema, comece por estes comandos:

# 1. Verificar se o contêiner está em execução
docker ps

# 2. Consultar os logs do contêiner
docker logs <container-id>

# 3. Entrar no contêiner e verificar a estrutura de arquivos
docker exec -it <container-id> sh
ls -la .next/
ls -la public/

# 4. Testar o serviço dentro do contêiner
docker exec -it <container-id> wget -O- http://localhost:3000

# 5. Verificar o mapeamento de portas
docker port <container-id>

Conclusão

Migrar da Vercel para uma hospedagem própria com Docker não foi tão assustador quanto eu imaginava. A configuração inicial exige algum tempo, mas, depois que tudo funciona, o custo de manutenção é baixo. Hoje pago US$ 12 fixos por mês por um servidor que executa três projetos Next.js, sem me preocupar com aumentos repentinos na conta.

Estes são, novamente, os três pontos essenciais deste artigo:

  1. Modo Standalone — adicione uma linha ao next.config.js e lembre-se de copiar public e .next/static manualmente
  2. Dockerfile multiestágio — um build em três etapas reduz a imagem final para cerca de 200 MB e agiliza a inicialização
  3. Proxy reverso — é obrigatório desativar o buffering no Nginx com proxy_buffering off, ou o streaming deixará de funcionar

Se você está com problemas de streaming, em 99% dos casos a causa é o buffering do proxy reverso. Adicionar export const runtime = 'edge' também costuma resolver.

Vercel vs. hospedagem própria

CritérioVercelHospedagem própria com Docker
Velocidade de implantação⚡️ Implantação com git push🐢 Operação manual de 5 a 10 minutos
Experiência de desenvolvimento🌟 Ambientes de preview, logs e Analytics🔧 Você precisa configurar o monitoramento
Custo💸 A partir de US$ 20 por mês e mais caro com alto tráfego💰 US$ 12 fixos por mês, com vários projetos
Escalabilidade📈 Escalabilidade automática📊 Ajuste manual de recursos
Controle⚠️ Limitado pelas regras da plataforma✅ Controle total
Cenário idealProjetos corporativos e serviços globaisProjetos pessoais, equipes pequenas e orçamento limitado

Recomendação final:

  • Se você é desenvolvedor independente e mantém vários side projects, a hospedagem própria pode economizar bastante dinheiro
  • Se a equipe não tem capacidade de operação ou o tráfego do projeto oscila muito, é melhor continuar na Vercel
  • Não existe escolha técnica certa ou errada; existe a escolha adequada para cada contexto

Os arquivos completos de configuração e mais detalhes estão no repositório do GitHub (o repositório é um placeholder e deve ser substituído no uso real). Se tiver dúvidas, deixe um comentário. Quem vem depois não precisa cair nas mesmas armadilhas.

Processo completo para hospedar Next.js com Docker

Do modo standalone à implantação em produção, incluindo proxy reverso e correção do streaming

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Configurar o modo de saída Standalone

    Ative o modo standalone no next.config.js:

    1. Abra o arquivo next.config.js
    2. Adicione a configuração: output: 'standalone'
    3. Execute o build: npm run build
    4. Verifique a saída: confirme que o diretório .next/standalone foi criado

    Pontos importantes:
    • O modo standalone não copia public e .next/static automaticamente
    • Esses dois diretórios precisam ser copiados manualmente no Dockerfile
    • Caso contrário, todos os recursos estáticos retornarão 404

    Exemplo de configuração:
    ```javascript
    const nextConfig = {
    output: 'standalone',
    }
    module.exports = nextConfig
    ```
  2. 2

    Step 2: Criar um Dockerfile multiestágio

    Escreva um Dockerfile com três etapas de build:

    Etapa 1 — instalação de dependências:
    • Use node:20-alpine como imagem base
    • Copie apenas package.json e package-lock.json
    • Execute npm ci para instalar as dependências e aproveitar o cache do Docker

    Etapa 2 — build da aplicação:
    • Copie node_modules da etapa 1
    • Copie todo o código-fonte
    • Execute npm run build

    Etapa 3 — execução em produção:
    • Crie um usuário não root como prática de segurança
    • Copie a pasta public com os recursos estáticos
    • Copie a saída .next/standalone
    • Copie os arquivos .next/static, que contêm CSS e JS gerados
    • Defina HOSTNAME="0.0.0.0" e PORT=3000
    • Use o comando de inicialização: node server.js

    Pontos importantes:
    • O build em três etapas pode reduzir a imagem de 1,5 GB para 200 MB
    • É obrigatório copiar public e .next/static para evitar erros 404 nos recursos estáticos
    • Execute a aplicação com um usuário não root para aumentar a segurança
  3. 3

    Step 3: Configurar o proxy reverso Nginx

    Configure o proxy reverso Nginx e desative o buffering:

    1. Instale o Nginx ou use o Caddy
    2. Configure o certificado SSL com Let's Encrypt
    3. Crie o arquivo de configuração do Nginx

    Configurações essenciais:
    • proxy_buffering off; para desativar o buffering
    • proxy_cache off; para desativar o cache
    • proxy_set_header X-Accel-Buffering no; para informar explicitamente ao Nginx que ele não deve armazenar a resposta em buffer

    Cabeçalhos obrigatórios:
    • 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;

    Suporte a WebSocket, se necessário:
    • proxy_set_header Upgrade $http_upgrade;
    • proxy_set_header Connection "upgrade";

    Teste a configuração:
    ```bash
    nginx -t # Testar a sintaxe
    nginx -s reload # Recarregar a configuração
    ```

    Atenção: sem desativar o buffering, a renderização por streaming deixará de funcionar
  4. 4

    Step 4: Configurar as variáveis de ambiente

    Diferencie as variáveis de ambiente de build e de runtime:

    Variáveis de build, com prefixo NEXT_PUBLIC_:
    • Devem ser informadas durante docker build
    • Use o parâmetro --build-arg
    • Declare no Dockerfile: ARG NEXT_PUBLIC_API_URL
    • Defina a variável: ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

    Variáveis de runtime, usadas no servidor:
    • Informe-as com docker run -e
    • Ou configure-as no docker-compose.yml
    • Leia-as diretamente de process.env no código

    No modo Standalone:
    • runtimeConfig não funciona
    • É preciso ler as variáveis conforme o padrão do App Router
    • Código do servidor: const dbUrl = process.env.DATABASE_URL

    Exemplo:
    ```bash
    # Durante o build
    docker build --build-arg NEXT_PUBLIC_API_URL="https://api.example.com" .

    # Em runtime
    docker run -e DATABASE_URL="postgres://..." your-image
    ```
  5. 5

    Step 5: Corrigir problemas de renderização por streaming

    Resolva falhas no streaming, como em chats com IA e SSE:

    Sintomas:
    • A saída em streaming não funciona e todo o conteúdo aparece de uma vez após uma longa espera
    • Server-Sent Events não são enviados em tempo real

    Solução 1 — configuração do Nginx, obrigatória:
    • Confirme que proxy_buffering off foi adicionado
    • Confirme que X-Accel-Buffering: no foi adicionado
    • Reinicie o serviço Nginx

    Solução 2 — usar o Edge Runtime:
    • Adicione no início do arquivo da rota de API: export const runtime = 'edge'
    • O Edge Runtime é otimizado para respostas em streaming
    • Seu comportamento é mais estável em ambientes Docker

    Valide a correção:
    ```bash
    curl -N http://yourdomain.com/api/chat \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"message": "Hello"}'
    ```
    O parâmetro -N desativa o buffering; a saída deve aparecer linha por linha

    Outras verificações:
    • Proxy da Cloudflare: a nuvem laranja também pode armazenar a resposta em buffer; desative-a ou faça upgrade para o plano Pro
    • Health check do Docker: pode interferir em conexões de streaming
    • Load balancer: se houver um LB antes da aplicação, ele precisa de configuração própria
  6. 6

    Step 6: Implantar e validar

    Crie a imagem e faça a implantação:

    1. Crie a imagem Docker:
    ```bash
    docker build -t nextjs-app .
    ```

    2. Inicie o contêiner:
    ```bash
    docker run -d \
    -p 3000:3000 \
    -e DATABASE_URL="postgres://..." \
    -e API_KEY="xxx" \
    --name nextjs-app \
    nextjs-app
    ```

    3. Valide a implantação:
    • Verifique o estado do contêiner: docker ps
    • Consulte os logs: docker logs nextjs-app
    • Teste o acesso: curl http://localhost:3000
    • Verifique os recursos estáticos acessando /_next/static/

    4. Configure o Nginx e reinicie-o:
    • Confirme que o proxy reverso está configurado corretamente
    • Teste o acesso via HTTPS
    • Valide a renderização por streaming

    5. Monitore e faça a manutenção:
    • Configure o reinício automático do contêiner: --restart unless-stopped
    • Consulte os logs regularmente para investigar problemas
    • Monitore o uso de recursos do servidor

    Solução de problemas comuns:
    • Recursos estáticos retornam 404: verifique se o Dockerfile copia public e .next/static
    • Variáveis de ambiente não funcionam: diferencie variáveis de build e de runtime
    • Não é possível acessar o contêiner: verifique se HOSTNAME está definido como 0.0.0.0

FAQ

Quanto é possível economizar com hospedagem própria?
A Vercel custa cerca de US$ 35–50 por mês, dependendo do tráfego, enquanto a hospedagem própria com Docker tem custo fixo de US$ 12 por mês e pode executar vários projetos. A economia mensal é de US$ 25–40, ou US$ 300–500 por ano. Mais importante: o custo é fixo e não dispara com um pico de tráfego. É uma boa opção para projetos pessoais, equipes pequenas e cenários com orçamento limitado.
Por que os recursos estáticos retornam 404 e como corrigir?
O modo standalone não copia automaticamente os diretórios public e .next/static. Para corrigir, adicione estas duas linhas à etapa runner do Dockerfile:
• COPY --from=builder /app/public ./public
• COPY --from=builder /app/.next/static ./.next/static
Se o erro 404 continuar, verifique as permissões dos arquivos e use --chown=nextjs:nodejs para definir o proprietário correto.
O que fazer quando a renderização por streaming não funciona?
Em 99% dos casos, a causa é o buffering do proxy reverso. Para corrigir:
1) Adicione à configuração do Nginx: proxy_buffering off; proxy_set_header X-Accel-Buffering no;
2) Use o Edge Runtime na rota de API: export const runtime = 'edge'
3) Se estiver usando o proxy da Cloudflare, desative a nuvem laranja ou faça upgrade para o plano Pro
4) Valide com curl -N; a saída deve aparecer linha por linha
O que fazer quando as variáveis de ambiente não funcionam?
Diferencie variáveis de build e de runtime:
• Com prefixo NEXT_PUBLIC_: devem ser passadas durante docker build com --build-arg, e o Dockerfile precisa declarar ARG e ENV
• Variáveis de runtime: passe-as com docker run -e ou docker-compose.yml e leia-as de process.env no código
• No modo Standalone, runtimeConfig não funciona; use o padrão do App Router para ler as variáveis de ambiente
O que fazer quando a imagem Docker fica grande demais?
Use um build multiestágio:
• Etapa 1: instale apenas as dependências e aproveite o cache do Docker
• Etapa 2: faça o build da aplicação
• Etapa 3: copie apenas os arquivos necessários em runtime, como a saída standalone, public e static
• A imagem final pode cair de 1,5 GB para 200 MB
• Use a imagem base node:20-alpine para reduzir ainda mais o tamanho
Quando vale a pena hospedar por conta própria e quando é melhor usar a Vercel?
A hospedagem própria é indicada para quem conhece Linux e Docker, tem tráfego estável, orçamento limitado, aceita implantação manual e trabalha em um projeto pessoal ou equipe pequena. A Vercel é mais adequada para equipes sem capacidade de operação, projetos com grandes oscilações de tráfego que precisam de escalabilidade automática, serviços que exigem uma rede de borda global ou recursos exclusivos da Vercel, como Analytics e Edge Config, e situações em que o orçamento permite priorizar a produtividade.
O que fazer quando não é possível acessar o contêiner após iniciá-lo?
Verifique estes pontos:
1) HOSTNAME deve ser 0.0.0.0, e não 127.0.0.1; defina ENV HOSTNAME="0.0.0.0" no Dockerfile
2) Confira o mapeamento de portas: docker run -p 3000:3000
3) Verifique se o contêiner está em execução: docker ps
4) Consulte os logs: docker logs <container-id>
5) Teste dentro do contêiner: docker exec -it <container-id> wget -O- http://localhost:3000

15 min de leitura · Publicado em: 20 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog