cloudflarepagesnextjsstatic-exportdevopssunset

Site estático no Cloudflare Pages: a migração que desligou containers

Douglas Haruo 11 min 19/09/2026

Hospedar um site estático no Cloudflare Pages é isto: o build do seu framework gera uma pasta de HTML/CSS/JS. Um npx wrangler pages deploy sobe a pasta para a CDN, a rede que entrega os arquivos. O domínio próprio entra por CNAME, um registro de DNS. E servir os arquivos não custa nada: nos planos gratuito e pago, requisições a assets estáticos são gratuitas e ilimitadas (documentação de preços, conferida em 25/08/2026). Sem servidor, sem container, sem porta aberta, sem atualização de imagem Docker no sábado. A parte difícil não é o deploy. É admitir que o seu app “dinâmico” talvez nunca tenha precisado ser dinâmico.

Este post documenta uma migração real da casa: o Tradutor Regional (tradutor.haruo.dev), um tradutor de expressões regionais brasileiras, com 696 expressões ativas em 6 dialetos. Ele rodava como dois containers, processos isolados: um com o app Next.js, outro com o Postgres. Os dois ficavam na mesma VPS que serve tudo por Cloudflare Tunnel sem porta aberta.

Em agosto de 2026 ele virou export estático, uma pasta de arquivos prontos servida pelo Cloudflare Pages. Os containers foram desligados, e o site continua no ar: mesma cara, mesma tradução, agora rodando 100% no browser. O que segue é o critério de decisão, o caminho da migração, o checklist de desligamento e os limites honestos.


Quando um app “dinâmico” é na verdade estático?

O app tinha a fantasia completa de app dinâmico: Postgres com Prisma, rotas de API, painel admin com login (next-auth), middleware de rate-limit (que barra excesso de requisições). Tudo isso existia. E tudo isso era infraestrutura para responder uma pergunta que ninguém fez: e se os dados mudarem agora?

O critério que decidiu a migração cabe numa pergunta: escreve alguém além do dono?

  • Quem escrevia no banco era só o dono. O dicionário de expressões mudava quando o dono o editava, o que era raro. Visitantes só liam. Um humano escreve de vez em quando; o resto do mundo só lê. Isso não é um banco de dados: é um arquivo de conteúdo fantasiado de banco, cobrando aluguel de container para manter a fantasia.
  • A única escrita de visitante era, na prática, uma mensagem. O formulário de sugestão fazia POST numa rota de API. Mas toda sugestão passava pelo dono antes de entrar no dicionário. Escrita moderada por uma pessoa é uma caixa de entrada, não uma transação. Virou um mailto: para o dono, e nada de valor se perdeu.
  • Os dados cabem no browser. As 696 expressões serializadas dão um JSON de ~270 KB, menos que muita foto de hero section. Se o seu dataset inteiro cabe em algumas centenas de KB, o browser processa sem esforço. Se não cabe, ainda dá para dividir por rota no build. O limite real aparece bem depois do que a intuição sugere.

A segunda pergunta, corolário da primeira: o que sobra do servidor quando ninguém escreve? No caso, sobrava um admin sem nada para administrar. Sobrava um rate-limit protegendo uma API que podia deixar de existir. E um healthcheck, a checagem automática de saúde, vigiando um processo que só repetia o conteúdo de um arquivo. A resposta honesta era “nada”.

Como foi a migração: de Next.js com banco para export estático?

O Next.js tem um modo para exatamente isso. output: 'export' no next.config faz o next build gerar uma pasta de HTML/CSS/JS pura, hospedável em qualquer servidor de estáticos (guia oficial de static exports). O modo é também um contrato do que você perde. Deixam de existir: rotas de API que dependem da requisição, headers()/redirects()/rewrites() do config e middleware. Somem também ISR (a regeneração estática incremental), cookies, Server Actions e a otimização de imagem padrão. A migração inteira é decidir, peça por peça, o que cada recurso de servidor vira:

Era no servidorVirou
Rotas de API consultando o PrismaUm módulo no cliente reproduz as mesmas respostas sobre /data/expressoes.json (~270 KB), gerado no build a partir do dump final do banco
A função de tradução rodando no servidorA mesma função, sem alteração, rodando no browser. Paridade verificada frase a frase (12 de 12) contra a API ainda no ar, antes de desligar qualquer coisa
Admin, login, next-auth, middleware de rate-limitApagados: não há mais o que administrar nem API para limitar
Formulário de sugestão (POST na API)mailto: para o dono. A escrita moderada já era uma mensagem
headers() no next.configpublic/_headers, a convenção do Pages para headers de segurança
Imagem OG dinâmica da página de compartilharPerdida. O preview do link no WhatsApp ficou sem imagem, só título. Custo aceito, e dito em voz alta
Dashboard mostrando uptime do processoMostra a data da última atualização do dicionário. Site estático não deve fingir que está vivo

Dois detalhes que pouparam dor:

  • trailingSlash: true faz /rota virar /rota/index.html, o formato que qualquer host estático serve sem regra especial de rewrite.
  • O artefato final é versionado. A pasta exportada (~2 MB, 79 arquivos) entrou no git: é o site inteiro, republicável de qualquer máquina, sem Node instalado. Um site estático versionado é o próprio backup de si mesmo. Isso conversa com o desenho 3-2-1 de backup da casa: menos um banco na rotação de dumps.

E se o seu caso precisa manter um admin? A casa resolve isso sem tela de login própria, com identidade na borda. O desenho está no post sobre Cloudflare Access. Mas note a ordem das perguntas: primeiro “este admin precisa existir?”, só depois “como protegê-lo”.

Como publicar no Cloudflare Pages?

O caminho manual, do zero ao ar:

npx wrangler login
npx wrangler pages project create meu-site --production-branch main
npx wrangler pages deploy   # lê pages_build_output_dir do wrangler.toml

O deploy devolve um URL de preview (https://<hash>.<projeto>.pages.dev). Teste ali antes de mexer em DNS. O domínio próprio entra em Custom domains no projeto: a Cloudflare cria o CNAME e o certificado sozinha, ativo em minutos.

No caso da casa havia um passo a mais, porque o hostname pertencia a um Cloudflare Tunnel. A troca foi apagar o public hostname do túnel e adicionar o custom domain no Pages. A indisponibilidade durou segundos, e o site saiu da VPS sem tocar em nenhum outro registro.

Para quem prefere não fazer deploy manual: o Pages também constrói a partir do repositório (integração git: push na branch de produção publica). É assim que o próprio haruo.dev, onde este post está, é publicado.

Como desligar os containers sem quebrar nada?

A ordem importa mais que os comandos. O princípio que organiza o checklist é este: o hostname troca de dono antes de qualquer coisa morrer. Um subdomínio órfão servindo 5xx, os erros de servidor, contamina o relatório de indexação do domínio inteiro no Search Console. A casa aprendeu isso do jeito caro.

  1. Dump final do banco, com hash registrado. Um pg_dump datado, com o sha256 anotado na documentação do sunset, guardado fora da máquina que vai morrer. É ele que permite regenerar o JSON estático daqui a dois anos. O dado sobrevive ao banco.
  2. Publicar o estático e testar no URL de preview. Todas as rotas, o JSON, o 404. Tudo antes de qualquer troca de DNS.
  3. Trocar o hostname (túnel → custom domain do Pages) e conferir com curl: status 200, headers de segurança presentes, conteúdo certo.
  4. Só então derrubar os containers: docker compose down sem -v. O volume do banco fica intacto por um período de carência (7 dias, no caso). Rollback nesse período custa um up -d e a recriação da rota. São minutos, não uma restauração.
  5. Limpar o entorno: o cron de backup do banco morre junto. O monitor de uptime do hostname fica: o endereço continua respondendo 200 pelo Pages. Monitor é para o endereço, não para o container.
  6. Depois da carência: apagar volume, imagem e diretório. Só quando o estático já provou que se sustenta.

Repare no que o checklist não tem: nenhum momento em que tradutor.haruo.dev responde erro. O site velho serviu até o minuto da troca; o novo serviu a partir dela. O Googlebot nunca viu um 502. É exatamente isso que o passo a passo compra.

Quanto custa hospedar um site estático no Cloudflare Pages?

Números públicos, conferidos em 25/08/2026 na documentação de limites e de preços:

  • Servir assets estáticos: US$ 0. “Requisições a assets estáticos são gratuitas e ilimitadas”, nos planos gratuito e pago. Sem medidor de banda, sem susto de fatura por tráfego.
  • Plano gratuito: 500 builds/mês, até 20.000 arquivos por site (100.000 nos planos pagos), 25 MiB por arquivo, 100 projetos por conta, 100 domínios próprios por projeto.
  • O caso deste post: 79 arquivos, maior arquivo ~270 KB, um deploy ocasional. Ordens de grandeza de folga dentro do plano gratuito.

E uma honestidade sobre o que “custo zero” significa aqui: a fatura da casa não mudou. A VPS continua existindo para os outros serviços.

O que foi a zero foi o footprint deste site. Sumiram dois containers para atualizar, um banco para fazer dump toda noite, um volume para vigiar e um healthcheck a mais na lista. Cada container é um imposto de manutenção recorrente, e a migração cancelou a assinatura. Se o seu site é o único inquilino de um servidor, aí sim a conta de dinheiro também zera. Mas essa é a exceção, não a promessa.

Quando o Cloudflare Pages NÃO basta?

O critério da primeira seção corta nos dois sentidos. O Pages (como qualquer host estático) não resolve quando:

  • Alguém além do dono escreve de verdade. Contas de usuário, comentários, pedidos, qualquer transação: isso é backend. Existem Pages Functions e Workers para adicionar código de servidor, mas aí você saiu do “estático grátis”: requisições a Functions contam na cota de Workers.
  • O dado muda por requisição. Preço em tempo real, estoque, personalização, conteúdo atrás de login. Nada disso sobrevive a um build que roda de vez em quando.
  • O dataset não cabe no browser nem se divide no build. O JSON de ~270 KB funciona; um catálogo de centenas de MB pesquisável precisa de uma API na frente.
  • Você depende do que o export descarta. Imagem OG por página, redirects/headers do framework (os _headers/_redirects do Pages cobrem os casos simples, mas são outra ferramenta), middleware, ISR. A tabela de perdas da migração acima é o preço de tabela. Pague-o de olhos abertos.

Fica uma nota de rodapé do próprio produto, conferida em 25/08/2026. A documentação do Pages hoje recomenda Workers para projetos novos: “Workers supports most Pages use cases… Start new projects with Workers”. O Pages continua servindo os sites que já estão nele (este blog incluído), e static assets em Workers é o mesmo desenho com outro chassi. Para um site novo, vale avaliar começar direto por lá. Para uma migração como a deste post, o Pages segue sendo o caminho mais curto entre “tenho uma pasta de HTML” e “está no ar de graça”.

O resumo que você leva

  1. O critério: escreve alguém além do dono? Não → o banco é um arquivo de conteúdo fantasiado de banco, e o site é estático. Escrita moderada por uma pessoa é caixa de entrada (mailto: serve), não transação.
  2. O caminho: output: 'export' no Next.js, dados num JSON público gerado do dump final, a mesma função de servidor rodando no browser. A paridade foi testada contra a API viva antes de desligar qualquer coisa.
  3. As perdas têm nome: imagem OG dinâmica, headers()/redirects() do framework, middleware, qualquer coisa por requisição. Liste-as antes, aceite-as por escrito.
  4. Publicar: wrangler pages deploy, testar no URL de preview, custom domain por CNAME, headers de segurança em _headers.
  5. Desligar na ordem: dump com hash → publicar → trocar hostname → down sem -v → carência → limpeza. O hostname nunca fica órfão: 5xx de subdomínio morto envenena o Search Console do domínio inteiro.
  6. Custo (25/08/2026): assets estáticos gratuitos e ilimitados; 500 builds/mês e 20.000 arquivos por site no plano gratuito. O que zera com certeza é o imposto de manutenção; a fatura, só se o site era o único inquilino.
  7. Pages não basta quando há escrita real de usuário, dado por requisição ou dataset grande. E, para projeto novo, a própria Cloudflare hoje aponta Workers com static assets.

Infraestrutura que escala sem quebrar o orçamento

Conta de cloud fora de controle? Opero a minha própria numa VPS única, sem porta aberta, com deploy automático e rollback por healthcheck. O desenho inteiro está publicado aqui.

Ver os posts de infraestrutura →