[{"data":1,"prerenderedAt":1463},["ShallowReactive",2],{"blog-\u002Fblog\u002Fmigrar-do-heroku-guia-tecnico":3,"blog-surround-\u002Fblog\u002Fmigrar-do-heroku-guia-tecnico":1447,"blog-en-alt-\u002Fblog\u002Fmigrar-do-heroku-guia-tecnico":1461},{"id":4,"title":5,"author":6,"body":7,"category":1429,"cover":1430,"date":1431,"description":1432,"draft":1433,"extension":1434,"lastReviewed":1430,"meta":1435,"navigation":443,"path":1436,"readingTime":1437,"seo":1438,"sitemap":1439,"stem":1440,"tags":1441,"__hash__":1446},"blog_pt\u002Fblog\u002Fmigrar-do-heroku-guia-tecnico.md","Migrar do Heroku pra cluster próprio: guia técnico em 5 passos","Equipe HeroCtl",{"type":8,"value":9,"toc":1416},"minimark",[10,14,17,22,25,28,88,91,94,98,101,106,128,131,134,139,158,161,166,182,194,199,215,225,228,243,248,251,281,284,289,320,323,326,329,333,336,342,345,351,354,360,363,366,370,373,534,537,542,603,608,670,673,678,681,706,711,722,725,729,732,737,747,767,785,790,798,803,928,931,936,939,942,947,950,968,971,975,978,983,986,991,998,1003,1040,1043,1048,1055,1060,1063,1071,1074,1079,1082,1086,1089,1092,1146,1149,1152,1156,1159,1173,1190,1208,1216,1233,1239,1245,1251,1255,1258,1290,1293,1296,1299,1303,1309,1315,1331,1337,1343,1349,1362,1366,1369,1372,1393,1396,1399,1412],[11,12,13],"p",{},"Em 28 de novembro de 2022, a Salesforce desligou o plano gratuito do Heroku. Centenas de milhares de hobby projects foram apagados de uma vez, e o ciclo de notícias durou uns dois meses — gente migrando pra Render, pra Fly.io, pra Railway, pra um VPS qualquer. O que ninguém previu naquele momento é o que aconteceu depois: quatro anos passaram, estamos em 2026, e ainda existem milhares de SaaS brasileiros em produção pagando entre US$25 e US$100 por mês por dyno só porque \"migrar\" é o décimo terceiro item da backlog. Sempre tem uma feature mais urgente. Sempre tem um cliente perguntando quando sai o módulo X. Migrar dá zero receita nova — então fica.",[11,15,16],{},"Esse post é o plano pra fazer essa migração caber em uma semana de trabalho de um dev part-time, e o restante de um mês pra estabilizar. Não é um manifesto, não é uma comparação de fornecedores, não é \"venha pro HeroCtl\". É um runbook. No final tem uma seção sobre opções de destino, incluindo o nosso produto, mas se você terminar de ler e for pra Render ou pra Coolify ou pra Fly.io, o post fez o trabalho.",[18,19,21],"h2",{"id":20},"por-que-ainda-doi-migrar-a-verdade-nao-dita","Por que ainda dói migrar (a verdade não dita)",[11,23,24],{},"A primeira coisa que precisa ficar clara: não é o Dockerfile. Escrever Dockerfile pra um app Rails ou Node é meia tarde — tem template pronto pra cada framework, tem cinco posts no DEV explicando, tem o Copilot escrevendo. Se a sua resistência é \"ainda não dockerizamos\", essa parte é a menos importante.",[11,26,27],{},"A dor está no ecossistema:",[29,30,31,54,60,66,72,78],"ul",{},[32,33,34,38,39,43,44,43,47,43,50,53],"li",{},[35,36,37],"strong",{},"Postgres com extensions específicas"," que você esqueceu que ativou em 2019. ",[40,41,42],"code",{},"pg_stat_statements",", ",[40,45,46],{},"pgcrypto",[40,48,49],{},"hstore",[40,51,52],{},"postgis"," — cada uma é um motivo pra migração quebrar silenciosamente.",[32,55,56,59],{},[35,57,58],{},"Redis Premium com persistência"," que você usa pra fila do Sidekiq E pra cache E pra rate limit. Pra cache pode reiniciar do zero. Pra fila não pode.",[32,61,62,65],{},[35,63,64],{},"Sidekiq workers stateful"," com jobs agendados meses à frente. Migrar enquanto eles rodam é correr atrás do trem em movimento.",[32,67,68,71],{},[35,69,70],{},"Heroku Scheduler"," com aquele cron que ninguém olha desde 2020 mas que faz o relatório mensal do CEO.",[32,73,74,77],{},[35,75,76],{},"Papertrail"," integrado, NewRelic instrumentado, Bugsnag em todos os erros — três SaaS extras que você nem sabe se vão fazer sentido na nova arquitetura.",[32,79,80,83,84,87],{},[35,81,82],{},"Buildpack"," que rodou seis anos sem ninguém saber direito o que faz. Tem um ",[40,85,86],{},"bin\u002Fpost_compile"," que minifica algo, tem variável de ambiente que define qual versão do Ruby — em algum lugar, a sua aplicação depende de seis comportamentos do buildpack que nunca foram documentados.",[11,89,90],{},"E tem a parte humana: você e o seu time internalizaram primitivas Heroku ao longo de anos. Procfile, slug compilation, dynos, release phase, config vars. Tudo isso virou intuição. Quando a gente vai refazer fora do Heroku, refaz inconscientemente — e em geral mal, porque o Heroku tinha defaults que escondem decisões importantes que agora são suas.",[11,92,93],{},"A migração técnica leva uma semana. A migração mental leva um mês. Esse post tenta encurtar os dois.",[18,95,97],{"id":96},"pre-flight-check-uma-a-duas-horas-antes-de-qualquer-commit","Pré-flight check — uma a duas horas, antes de qualquer commit",[11,99,100],{},"Antes de abrir o editor, você precisa do inventário. A maior parte das migrações que dão errado é por uma surpresa que poderia ter sido descoberta na primeira hora.",[11,102,103],{},[35,104,105],{},"Inventário de apps:",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-bash shiki shiki-themes github-dark-default","heroku apps\n","bash","",[40,114,115],{"__ignoreMap":112},[116,117,120,124],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"sQhOw","heroku",[116,125,127],{"class":126},"s9uIt"," apps\n",[11,129,130],{},"Quantos apps existem na conta? Quais ainda estão em uso de verdade? Quais podem virar cron-job e morrer? Quais foram criados pra um cliente que saiu em 2021? Marque cada um numa planilha com três colunas: nome, status (vivo\u002Fzumbi\u002Fcron), prioridade de migração (alta\u002Fmédia\u002Fbaixa).",[11,132,133],{},"A maioria das contas tem 30% de apps zumbis. Migrar zumbi não tem ROI — destruir tem.",[11,135,136],{},[35,137,138],{},"Inventário de addons por app:",[107,140,142],{"className":109,"code":141,"language":111,"meta":112,"style":112},"heroku addons -a meu-app\n",[40,143,144],{"__ignoreMap":112},[116,145,146,148,151,155],{"class":118,"line":119},[116,147,123],{"class":122},[116,149,150],{"class":126}," addons",[116,152,154],{"class":153},"sFSAA"," -a",[116,156,157],{"class":126}," meu-app\n",[11,159,160],{},"Cada linha é uma decisão futura. Postgres? Redis? Papertrail? Heroku Scheduler? SendGrid? Mailgun? Pra cada um, escreva na planilha: vai migrar pra equivalente self-hosted, vai virar SaaS externo, ou vai descartar. Se você não sabe pra que serve, pesquise antes — não na hora do cutover.",[11,162,163],{},[35,164,165],{},"Inventário de buildpacks:",[107,167,169],{"className":109,"code":168,"language":111,"meta":112,"style":112},"heroku buildpacks -a meu-app\n",[40,170,171],{"__ignoreMap":112},[116,172,173,175,178,180],{"class":118,"line":119},[116,174,123],{"class":122},[116,176,177],{"class":126}," buildpacks",[116,179,154],{"class":153},[116,181,157],{"class":126},[11,183,184,185,43,188,43,191,193],{},"Multi-buildpack? Custom buildpack? Se a saída tem mais de uma linha, leia cada um. Buildpack customizado costuma ter hooks (",[40,186,187],{},"bin\u002Frelease",[40,189,190],{},"bin\u002Fcompile",[40,192,86],{},") que executam coisas específicas. Você vai precisar replicar esses passos no Dockerfile ou num release container.",[11,195,196],{},[35,197,198],{},"Inventário de env vars:",[107,200,202],{"className":109,"code":201,"language":111,"meta":112,"style":112},"heroku config -a meu-app\n",[40,203,204],{"__ignoreMap":112},[116,205,206,208,211,213],{"class":118,"line":119},[116,207,123],{"class":122},[116,209,210],{"class":126}," config",[116,212,154],{"class":153},[116,214,157],{"class":126},[11,216,217,218,43,221,224],{},"Exporte tudo pra arquivo seguro. NÃO commite. NÃO mande pelo Slack. NÃO cole no ChatGPT. Esse arquivo tem ",[40,219,220],{},"DATABASE_URL",[40,222,223],{},"SECRET_KEY_BASE",", chave de API de pagamento. Trate como senha, porque é exatamente isso.",[11,226,227],{},"Atenção a duas armadilhas:",[29,229,230,237],{},[32,231,232,233,236],{},"Variáveis com caractere ",[40,234,235],{},":"," no nome (algumas libs antigas usam) escapam diferente em containers.",[32,238,239,242],{},[40,240,241],{},"BUNDLE_WITHOUT=development:test"," gravado em produção é bomba relógio depois da migração.",[11,244,245],{},[35,246,247],{},"Inventário de Procfile:",[11,249,250],{},"Cada linha do Procfile é um serviço:",[29,252,253,259,265,271],{},[32,254,255,258],{},[40,256,257],{},"web"," vira o container principal.",[32,260,261,264],{},[40,262,263],{},"worker"," vira segundo container ou job separado.",[32,266,267,270],{},[40,268,269],{},"release"," vira step pré-deploy (typicamente migrations).",[32,272,273,276,277,280],{},[40,274,275],{},"clock"," ou ",[40,278,279],{},"scheduler"," vira cron job.",[11,282,283],{},"Se o seu Procfile tem cinco linhas, você vai ter cinco serviços no destino. Não são detalhes — são o desenho da topologia.",[11,285,286],{},[35,287,288],{},"Métricas atuais:",[107,290,292],{"className":109,"code":291,"language":111,"meta":112,"style":112},"heroku ps -a meu-app\nheroku logs --tail -a meu-app\n",[40,293,294,305],{"__ignoreMap":112},[116,295,296,298,301,303],{"class":118,"line":119},[116,297,123],{"class":122},[116,299,300],{"class":126}," ps",[116,302,154],{"class":153},[116,304,157],{"class":126},[116,306,308,310,313,316,318],{"class":118,"line":307},2,[116,309,123],{"class":122},[116,311,312],{"class":126}," logs",[116,314,315],{"class":153}," --tail",[116,317,154],{"class":153},[116,319,157],{"class":126},[11,321,322],{},"Quantos dynos rodando? Qual o tipo (Standard-1X, Performance-M)? Volume de logs por minuto? Latência média no NewRelic? Pico de CPU\u002Fmemória do mês passado?",[11,324,325],{},"Esses números servem pra dimensionar o destino. Migrar e descobrir depois que a memória é metade do necessário é o jeito mais rápido de quebrar a confiança no projeto inteiro.",[11,327,328],{},"No fim do pré-flight você tem uma planilha com tudo. Esse arquivo é o coração da migração. Toda decisão volta pra ele.",[18,330,332],{"id":331},"passo-1-escolha-de-stack-alvo-decisao-arquitetural-30-minutos","Passo 1 — Escolha de stack alvo (decisão arquitetural, 30 minutos)",[11,334,335],{},"Três caminhos possíveis. Vou ser honesto sobre cada um.",[11,337,338,341],{},[35,339,340],{},"Opção A — VPS único com painel self-hosted.","\nUm servidor na DigitalOcean ou Hetzner, instala Coolify ou Dokploy, deploya seu app pelo painel. Custo: R$30 a R$50 por mês pra começar, escala bem até uns 10 apps em servidor médio. Sem alta disponibilidade — se o servidor cai, tudo cai. SLA que você consegue prometer: best-effort.",[11,343,344],{},"Ideal pra: indie hacker, projeto pessoal, MVP, SaaS sem cliente que exige SLA por escrito.",[11,346,347,350],{},[35,348,349],{},"Opção B — Cluster com alta disponibilidade.","\nTrês ou mais servidores, orquestrador que coordena entre eles, sobrevive à queda de um servidor sem afetar tráfego. Custo: R$150 a R$300 por mês pra um cluster de três nós modestos. SLA possível: 99,9% sem desespero.",[11,352,353],{},"Ideal pra: SaaS B2B com clientes pagantes, qualquer aplicação onde meia hora de downtime gera ticket de suporte.",[11,355,356,359],{},[35,357,358],{},"Opção C — Plataforma gerenciada externa.","\nRender, Railway, Fly.io. Você paga mais, mas zero ops. Custo: R$200 a R$500 por mês pra workload comparável a 2-3 dynos Heroku, escala linear daí em diante.",[11,361,362],{},"Ideal pra: time que não tem absolutamente ninguém pra cuidar de servidor e prefere transferir o problema pra outra empresa.",[11,364,365],{},"Decisão honesta, em uma pergunta: tem cliente exigindo SLA? Se não, opção A. Se sim, B. Se o time não tem ninguém disposto a aprender mínima ops, C. Não existe resposta certa universal — existe resposta certa pra o seu contexto. Misturar os três também é válido: app principal em B, ferramenta interna em A, scheduler isolado em C.",[18,367,369],{"id":368},"passo-2-dockerizacao-meio-dia-a-dois-dias-por-app","Passo 2 — Dockerização (meio dia a dois dias por app)",[11,371,372],{},"Aqui o trabalho técnico começa. A lógica geral é a mesma pra qualquer stack:",[107,374,378],{"className":375,"code":376,"language":377,"meta":112,"style":112},"language-dockerfile shiki shiki-themes github-dark-default","FROM ruby:3.3-slim AS builder\nWORKDIR \u002Fapp\nCOPY Gemfile Gemfile.lock .\u002F\nRUN bundle install --without development test\nCOPY . .\nRUN bundle exec rake assets:precompile\n\nFROM ruby:3.3-slim\nWORKDIR \u002Fapp\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    libpq5 nodejs && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\nCOPY --from=builder \u002Fusr\u002Flocal\u002Fbundle \u002Fusr\u002Flocal\u002Fbundle\nCOPY --from=builder \u002Fapp \u002Fapp\nEXPOSE 3000\nCMD [\"bundle\", \"exec\", \"puma\", \"-C\", \"config\u002Fpuma.rb\"]\n","dockerfile",[40,379,380,396,404,413,422,430,438,445,453,460,468,474,482,490,499],{"__ignoreMap":112},[116,381,382,386,390,393],{"class":118,"line":119},[116,383,385],{"class":384},"suJrU","FROM",[116,387,389],{"class":388},"sZEs4"," ruby:3.3-slim ",[116,391,392],{"class":384},"AS",[116,394,395],{"class":388}," builder\n",[116,397,398,401],{"class":118,"line":307},[116,399,400],{"class":384},"WORKDIR",[116,402,403],{"class":388}," \u002Fapp\n",[116,405,407,410],{"class":118,"line":406},3,[116,408,409],{"class":384},"COPY",[116,411,412],{"class":388}," Gemfile Gemfile.lock .\u002F\n",[116,414,416,419],{"class":118,"line":415},4,[116,417,418],{"class":384},"RUN",[116,420,421],{"class":388}," bundle install --without development test\n",[116,423,425,427],{"class":118,"line":424},5,[116,426,409],{"class":384},[116,428,429],{"class":388}," . .\n",[116,431,433,435],{"class":118,"line":432},6,[116,434,418],{"class":384},[116,436,437],{"class":388}," bundle exec rake assets:precompile\n",[116,439,441],{"class":118,"line":440},7,[116,442,444],{"emptyLinePlaceholder":443},true,"\n",[116,446,448,450],{"class":118,"line":447},8,[116,449,385],{"class":384},[116,451,452],{"class":388}," ruby:3.3-slim\n",[116,454,456,458],{"class":118,"line":455},9,[116,457,400],{"class":384},[116,459,403],{"class":388},[116,461,463,465],{"class":118,"line":462},10,[116,464,418],{"class":384},[116,466,467],{"class":388}," apt-get update && apt-get install -y --no-install-recommends \\\n",[116,469,471],{"class":118,"line":470},11,[116,472,473],{"class":388},"    libpq5 nodejs && rm -rf \u002Fvar\u002Flib\u002Fapt\u002Flists\u002F*\n",[116,475,477,479],{"class":118,"line":476},12,[116,478,409],{"class":384},[116,480,481],{"class":388}," --from=builder \u002Fusr\u002Flocal\u002Fbundle \u002Fusr\u002Flocal\u002Fbundle\n",[116,483,485,487],{"class":118,"line":484},13,[116,486,409],{"class":384},[116,488,489],{"class":388}," --from=builder \u002Fapp \u002Fapp\n",[116,491,493,496],{"class":118,"line":492},14,[116,494,495],{"class":384},"EXPOSE",[116,497,498],{"class":388}," 3000\n",[116,500,502,505,508,511,513,516,518,521,523,526,528,531],{"class":118,"line":501},15,[116,503,504],{"class":384},"CMD",[116,506,507],{"class":388}," [",[116,509,510],{"class":126},"\"bundle\"",[116,512,43],{"class":388},[116,514,515],{"class":126},"\"exec\"",[116,517,43],{"class":388},[116,519,520],{"class":126},"\"puma\"",[116,522,43],{"class":388},[116,524,525],{"class":126},"\"-C\"",[116,527,43],{"class":388},[116,529,530],{"class":126},"\"config\u002Fpuma.rb\"",[116,532,533],{"class":388},"]\n",[11,535,536],{},"Multi-stage. Build pesado fica num estágio que é descartado. Imagem final tem só o necessário pra rodar.",[11,538,539],{},[35,540,541],{},"Por linguagem:",[29,543,544,561,574,590],{},[32,545,546,549,550,553,554,43,557,560],{},[35,547,548],{},"Ruby\u002FRails",": ",[40,551,552],{},"ruby:3.x-slim"," como base, multi-stage pra reduzir tamanho. O slug compilation do Heroku virou suas próprias linhas no Dockerfile — ",[40,555,556],{},"bundle install",[40,558,559],{},"assets:precompile",", copiar artefatos.",[32,562,563,549,566,569,570,573],{},[35,564,565],{},"Node",[40,567,568],{},"node:20-alpine"," resolve a maioria dos casos. Atenção a deps com binários nativos (sharp, bcrypt, sqlite3, canvas) — Alpine usa musl, e algumas libs precisam de glibc. Se quebrar, troque pra ",[40,571,572],{},"node:20-slim",".",[32,575,576,549,579,582,583,276,586,589],{},[35,577,578],{},"Python\u002FDjango",[40,580,581],{},"python:3.x-slim",", gunicorn ou uvicorn como server. ",[40,584,585],{},"requirements.txt",[40,587,588],{},"pyproject.toml"," no estágio de build.",[32,591,592,549,595,598,599,602],{},[35,593,594],{},"Elixir\u002FPhoenix",[40,596,597],{},"elixir:1.x-alpine",", release como artefato (",[40,600,601],{},"mix release","), runtime image só com erlang.",[11,604,605],{},[35,606,607],{},"Mapeamento Procfile → Docker:",[609,610,611,624],"table",{},[612,613,614],"thead",{},[615,616,617,621],"tr",{},[618,619,620],"th",{},"Procfile",[618,622,623],{},"Equivalente em destino",[625,626,627,640,650,660],"tbody",{},[615,628,629,635],{},[630,631,632],"td",{},[40,633,634],{},"web: bundle exec puma",[630,636,637,639],{},[40,638,504],{}," do container principal",[615,641,642,647],{},[630,643,644],{},[40,645,646],{},"worker: bundle exec sidekiq",[630,648,649],{},"Container separado, mesma imagem, comando diferente",[615,651,652,657],{},[630,653,654],{},[40,655,656],{},"release: bundle exec rake db:migrate",[630,658,659],{},"Job de release, executa antes do rolling deploy",[615,661,662,667],{},[630,663,664],{},[40,665,666],{},"clock: bundle exec clockwork",[630,668,669],{},"Cron job, ou container singleton",[11,671,672],{},"A maior parte dos orquestradores modernos (HeroCtl, Render, Railway, Coolify) entende esses quatro formatos diretamente.",[11,674,675],{},[35,676,677],{},"Assets:",[11,679,680],{},"Slug compilation do Heroku faz precompile automático. Em Docker você precisa pensar:",[29,682,683,689,695],{},[32,684,685,686,589],{},"Rails: ",[40,687,688],{},"RUN bundle exec rake assets:precompile",[32,690,691,692,589],{},"Node: ",[40,693,694],{},"RUN npm run build",[32,696,697,698,701,702,705],{},"Asset host (CDN): se você usa CloudFront ou S3 pra servir static, configurar ",[40,699,700],{},"RAILS_SERVE_STATIC_FILES"," e ",[40,703,704],{},"ASSET_HOST"," corretamente.",[11,707,708],{},[35,709,710],{},"Tempo médio realista:",[29,712,713,716,719],{},[32,714,715],{},"App Rails médio (CRUD com Sidekiq): 1 a 2 dias.",[32,717,718],{},"App Node simples (API, sem build pesado de frontend): 4 horas.",[32,720,721],{},"App com 5+ workers stateful e processamento de mídia: 3 a 5 dias.",[11,723,724],{},"A primeira app demora mais. A segunda demora metade. Da terceira em diante, é mecânico.",[18,726,728],{"id":727},"passo-3-migracao-de-banco-a-parte-mais-arriscada-2-a-8-horas","Passo 3 — Migração de banco (a parte mais arriscada, 2 a 8 horas)",[11,730,731],{},"Aqui mora o medo. Banco é o único lugar onde \"voltar atrás\" é caro. Tudo o mais é redeploy.",[11,733,734],{},[35,735,736],{},"Postgres:",[11,738,739,740,743,744,746],{},"Heroku Postgres expõe acesso direto via ",[40,741,742],{},"pg_dump"," se você tiver as credenciais (estão em ",[40,745,220],{},"). Antes de qualquer coisa, descubra suas extensions:",[107,748,752],{"className":749,"code":750,"language":751,"meta":112,"style":112},"language-sql shiki shiki-themes github-dark-default","SELECT extname, extversion FROM pg_extension;\n","sql",[40,753,754],{"__ignoreMap":112},[116,755,756,759,762,764],{"class":118,"line":119},[116,757,758],{"class":384},"SELECT",[116,760,761],{"class":388}," extname, extversion ",[116,763,385],{"class":384},[116,765,766],{"class":388}," pg_extension;\n",[11,768,769,770,43,772,43,774,43,776,43,778,43,781,784],{},"Comuns: ",[40,771,46],{},[40,773,49],{},[40,775,52],{},[40,777,42],{},[40,779,780],{},"uuid-ossp",[40,782,783],{},"unaccent",". Se o destino não tem todas, ou tem em versão diferente, você descobre antes — não no meio do restore às 3 da manhã.",[11,786,787],{},[35,788,789],{},"Destino possível pra Postgres:",[29,791,792,795],{},[32,793,794],{},"Postgres rodando como job no próprio cluster (RPO\u002FRTO menor, controle total, você cuida do backup).",[32,796,797],{},"Postgres gerenciado regional — RDS São Paulo, Neon, Supabase, Aiven. Mais caro, menos ops.",[11,799,800],{},[35,801,802],{},"Migração com mínimo downtime — opção A (com janela):",[107,804,806],{"className":109,"code":805,"language":111,"meta":112,"style":112},"# Drena tráfego: coloca app em manutenção, espera Sidekiq esvaziar\nheroku maintenance:on -a meu-app\n\n# Dump\npg_dump $HEROKU_DATABASE_URL --no-owner --no-privileges --format=custom --file=dump.sql\n\n# Restore no destino\npg_restore --no-owner --no-privileges --dbname=$DEST_DATABASE_URL dump.sql\n\n# Smoke test no destino\npsql $DEST_DATABASE_URL -c 'SELECT count(*) FROM users;'\n\n# Cutover de DNS, app no destino aponta pro novo banco\nheroku maintenance:off -a meu-app  # opcional, só pra Heroku continuar atendendo \u002Fhealthz\n",[40,807,808,814,825,829,834,853,857,862,881,885,890,904,908,913],{"__ignoreMap":112},[116,809,810],{"class":118,"line":119},[116,811,813],{"class":812},"sH3jZ","# Drena tráfego: coloca app em manutenção, espera Sidekiq esvaziar\n",[116,815,816,818,821,823],{"class":118,"line":307},[116,817,123],{"class":122},[116,819,820],{"class":126}," maintenance:on",[116,822,154],{"class":153},[116,824,157],{"class":126},[116,826,827],{"class":118,"line":406},[116,828,444],{"emptyLinePlaceholder":443},[116,830,831],{"class":118,"line":415},[116,832,833],{"class":812},"# Dump\n",[116,835,836,838,841,844,847,850],{"class":118,"line":424},[116,837,742],{"class":122},[116,839,840],{"class":388}," $HEROKU_DATABASE_URL ",[116,842,843],{"class":153},"--no-owner",[116,845,846],{"class":153}," --no-privileges",[116,848,849],{"class":153}," --format=custom",[116,851,852],{"class":153}," --file=dump.sql\n",[116,854,855],{"class":118,"line":432},[116,856,444],{"emptyLinePlaceholder":443},[116,858,859],{"class":118,"line":440},[116,860,861],{"class":812},"# Restore no destino\n",[116,863,864,867,870,872,875,878],{"class":118,"line":447},[116,865,866],{"class":122},"pg_restore",[116,868,869],{"class":153}," --no-owner",[116,871,846],{"class":153},[116,873,874],{"class":153}," --dbname=",[116,876,877],{"class":388},"$DEST_DATABASE_URL",[116,879,880],{"class":126}," dump.sql\n",[116,882,883],{"class":118,"line":455},[116,884,444],{"emptyLinePlaceholder":443},[116,886,887],{"class":118,"line":462},[116,888,889],{"class":812},"# Smoke test no destino\n",[116,891,892,895,898,901],{"class":118,"line":470},[116,893,894],{"class":122},"psql",[116,896,897],{"class":388}," $DEST_DATABASE_URL ",[116,899,900],{"class":153},"-c",[116,902,903],{"class":126}," 'SELECT count(*) FROM users;'\n",[116,905,906],{"class":118,"line":476},[116,907,444],{"emptyLinePlaceholder":443},[116,909,910],{"class":118,"line":484},[116,911,912],{"class":812},"# Cutover de DNS, app no destino aponta pro novo banco\n",[116,914,915,917,920,922,925],{"class":118,"line":492},[116,916,123],{"class":122},[116,918,919],{"class":126}," maintenance:off",[116,921,154],{"class":153},[116,923,924],{"class":126}," meu-app",[116,926,927],{"class":812},"  # opcional, só pra Heroku continuar atendendo \u002Fhealthz\n",[11,929,930],{},"Janela típica: 30 minutos a 2 horas, dependendo do tamanho do banco. Pra base abaixo de 5GB, 30 min é folgado.",[11,932,933],{},[35,934,935],{},"Migração com mínimo downtime — opção B (logical replication):",[11,937,938],{},"Replicação lógica do Postgres permite que você inicie a cópia enquanto o app continua escrevendo no Heroku. Quando a réplica chega no estado atual, faz o cutover de DNS e o destino vira o novo primário.",[11,940,941],{},"Funciona se o destino conseguir alcançar o Heroku via rede. Pra Heroku Postgres precisa abrir whitelist do IP do destino (Heroku tem mecanismo pra isso em planos pagos). Setup leva uma tarde, cutover dura segundos.",[11,943,944],{},[35,945,946],{},"Redis:",[11,948,949],{},"Duas naturezas distintas — trate diferente:",[29,951,952,958],{},[32,953,954,957],{},[35,955,956],{},"Redis como cache",": simplesmente reinicie do zero no destino. O cache reaquece sozinho. Não há nada a migrar.",[32,959,960,963,964,967],{},[35,961,962],{},"Redis como fila Sidekiq\u002FResque com persistência",": aqui dói. Snapshot via ",[40,965,966],{},"BGSAVE",", transfira o RDB, restore no destino. Ou: pause os workers no Heroku, processe a fila até o fim, faça cutover com fila vazia.",[11,969,970],{},"Redis Premium do Heroku tem persistência ligada por padrão; Redis simples no destino pode não ter — confira antes.",[18,972,974],{"id":973},"passo-4-dns-ssl-e-cutover-1-a-3-horas","Passo 4 — DNS, SSL e cutover (1 a 3 horas)",[11,976,977],{},"O cutover é a hora da verdade. Tudo o que veio antes era preparação.",[11,979,980],{},[35,981,982],{},"24 horas antes:",[11,984,985],{},"Reduz o TTL do registro DNS pra 60 segundos. Isso garante que, quando você apontar pro destino, propaga rápido. TTL alto é o que faz cutover virar pesadelo de 6 horas com metade dos clientes ainda hitando o servidor antigo.",[11,987,988],{},[35,989,990],{},"Setup paralelo:",[11,992,993,994,997],{},"App rodando em paralelo nos dois destinos. Heroku continua respondendo no domínio antigo. Destino responde num domínio temporário (ex: ",[40,995,996],{},"app-novo.heroctl.com",").",[11,999,1000],{},[35,1001,1002],{},"Smoke test no destino:",[107,1004,1006],{"className":109,"code":1005,"language":111,"meta":112,"style":112},"curl https:\u002F\u002Fapp-novo.heroctl.com\u002Fhealthz\ncurl https:\u002F\u002Fapp-novo.heroctl.com\u002Fapi\u002Fv1\u002Fusuarios -H \"Authorization: Bearer $TOKEN\"\n# Hit endpoints críticos manualmente, com olhos humanos\n",[40,1007,1008,1016,1035],{"__ignoreMap":112},[116,1009,1010,1013],{"class":118,"line":119},[116,1011,1012],{"class":122},"curl",[116,1014,1015],{"class":126}," https:\u002F\u002Fapp-novo.heroctl.com\u002Fhealthz\n",[116,1017,1018,1020,1023,1026,1029,1032],{"class":118,"line":307},[116,1019,1012],{"class":122},[116,1021,1022],{"class":126}," https:\u002F\u002Fapp-novo.heroctl.com\u002Fapi\u002Fv1\u002Fusuarios",[116,1024,1025],{"class":153}," -H",[116,1027,1028],{"class":126}," \"Authorization: Bearer ",[116,1030,1031],{"class":388},"$TOKEN",[116,1033,1034],{"class":126},"\"\n",[116,1036,1037],{"class":118,"line":406},[116,1038,1039],{"class":812},"# Hit endpoints críticos manualmente, com olhos humanos\n",[11,1041,1042],{},"Se algo estiver errado, descubra agora. Depois do cutover você vai estar lidando com tickets de suporte simultaneamente.",[11,1044,1045],{},[35,1046,1047],{},"Cutover:",[11,1049,1050,1051,1054],{},"Muda o CNAME (ou A record) do domínio de produção pro destino. Em até 60 segundos, novos requests vão pro destino novo. Heroku continua respondendo no domínio antigo (a URL ",[40,1052,1053],{},"*.herokuapp.com",") por 30 dias — isso é cinto de segurança importante.",[11,1056,1057],{},[35,1058,1059],{},"SSL\u002FTLS:",[11,1061,1062],{},"Heroku tinha certificado automático embutido. No destino, dependendo da escolha:",[29,1064,1065,1068],{},[32,1066,1067],{},"HeroCtl, Coolify, Render, Railway, Fly.io: certificado automático via Let's Encrypt, sem você pensar.",[32,1069,1070],{},"VPS único nu: você configura cert-manager-equivalente, ou Caddy com ACME, ou nginx + certbot.",[11,1072,1073],{},"Antes do cutover de DNS, valida que o destino emitiu o certificado pra o domínio. Let's Encrypt valida via HTTP-01 ou DNS-01 — o desafio HTTP-01 só funciona depois do DNS apontar, então tem ovo-galinha. Solução: emite via DNS-01 antes (não precisa do DNS apontar pra o destino), ou aceita 30 segundos de erro de TLS no momento do cutover.",[11,1075,1076],{},[35,1077,1078],{},"Sticky sessions:",[11,1080,1081],{},"Se o seu app usa WebSocket, ou tem sessão em memória (em vez de Redis ou banco), você precisa de sticky session no balanceador. Heroku não fazia isso por padrão, mas alguns apps acabam dependendo de roteamento estável sem perceber. No destino, configure cookie-based session affinity se necessário.",[18,1083,1085],{"id":1084},"passo-5-decommissao-heroku-1-hora-30-dias-depois","Passo 5 — Decommissão Heroku (1 hora, 30 dias depois)",[11,1087,1088],{},"Trinta dias é o cinto de segurança. Mantenha o app no Heroku ligado, sem tráfego (afinal o DNS já apontou pra outro lugar), só pra caso de emergência. Custo: o que você já estava pagando, divido proporcionalmente até a data de cancelamento.",[11,1090,1091],{},"Trinta dias depois, se nada quebrou:",[107,1093,1095],{"className":109,"code":1094,"language":111,"meta":112,"style":112},"heroku addons:destroy heroku-postgresql -a meu-app\nheroku addons:destroy heroku-redis -a meu-app\nheroku addons:destroy papertrail -a meu-app\nheroku apps:destroy meu-app\n",[40,1096,1097,1111,1124,1137],{"__ignoreMap":112},[116,1098,1099,1101,1104,1107,1109],{"class":118,"line":119},[116,1100,123],{"class":122},[116,1102,1103],{"class":126}," addons:destroy",[116,1105,1106],{"class":126}," heroku-postgresql",[116,1108,154],{"class":153},[116,1110,157],{"class":126},[116,1112,1113,1115,1117,1120,1122],{"class":118,"line":307},[116,1114,123],{"class":122},[116,1116,1103],{"class":126},[116,1118,1119],{"class":126}," heroku-redis",[116,1121,154],{"class":153},[116,1123,157],{"class":126},[116,1125,1126,1128,1130,1133,1135],{"class":118,"line":406},[116,1127,123],{"class":122},[116,1129,1103],{"class":126},[116,1131,1132],{"class":126}," papertrail",[116,1134,154],{"class":153},[116,1136,157],{"class":126},[116,1138,1139,1141,1144],{"class":118,"line":415},[116,1140,123],{"class":122},[116,1142,1143],{"class":126}," apps:destroy",[116,1145,157],{"class":126},[11,1147,1148],{},"Cada addon tem que ser cancelado separadamente — alguns têm billing próprio que continua mesmo com app destruído. Confere a fatura do mês seguinte com lupa.",[11,1150,1151],{},"Heroku faz reembolso pro-rata até o dia da cancelação. Não esqueça de cancelar a conta inteira se for o último app — senão você paga taxa de plataforma todo mês por nada.",[18,1153,1155],{"id":1154},"armadilhas-comuns","Armadilhas comuns",[11,1157,1158],{},"A maior parte das migrações trava nessas oito coisas. Lê tudo antes de começar.",[11,1160,1161,1164,1165,43,1167,43,1169,1172],{},[35,1162,1163],{},"Slug compilation hooks invisíveis."," Apps antigos têm ",[40,1166,187],{},[40,1168,86],{},[40,1170,1171],{},"bin\u002Fpre_compile",". Esses scripts rodam dentro do buildpack e fazem coisas como minificar JS, gerar arquivos derivados, ou rodar uma migração que ninguém lembra. Antes de Dockerizar, abre cada um e replica num step do Dockerfile ou em release container.",[11,1174,1175,1178,1179,1182,1183,1185,1186,1189],{},[35,1176,1177],{},"Config vars com formato quebrado."," Heroku aceita ",[40,1180,1181],{},"MY:VAR"," como nome de variável (com ",[40,1184,235],{},"). Containers em geral também, mas algumas ferramentas de orquestração escapam diferente. Renomeia pra ",[40,1187,1188],{},"MY_VAR"," antes de migrar.",[11,1191,1192,1195,1196,1199,1200,1203,1204,1207],{},[35,1193,1194],{},"Redis URL com formato variante."," Heroku usa ",[40,1197,1198],{},"redis:\u002F\u002Fh:senha@host:port",". Alguns clients (gems Ruby antigas, principalmente) esperam ",[40,1201,1202],{},"redis:\u002F\u002F:senha@host:port",". Se ver ",[40,1205,1206],{},"Redis::CommandError: WRONGPASS",", é provavelmente isso.",[11,1209,1210,1215],{},[35,1211,1212,1214],{},[40,1213,241],{}," gravado no env."," Quando você roda esse mesmo container fora do Heroku, ele continua sem instalar gems de desenvolvimento. Em produção, ok. Em staging onde você precisa rodar testes, quebra. Limpa essa variável antes de usar a config dump em outro ambiente.",[11,1217,1218,1221,1222,1225,1226,43,1229,1232],{},[35,1219,1220],{},"Gems específicas do Heroku."," ",[40,1223,1224],{},"rails_12factor"," (deprecado mas ainda em apps de 2014), ",[40,1227,1228],{},"heroku_san",[40,1230,1231],{},"taps",". Removeu, fim. Se algo depender, troca por equivalente padrão.",[11,1234,1235,1238],{},[35,1236,1237],{},"DNS com Heroku-DNS-Target."," Heroku recomenda usar ALIAS ou ANAME pra apontar pro app, em vez de CNAME, pra raízes de domínio. Quando migrar, troca pra A record direto pro IP do destino. ALIAS apontando pro Heroku é o que vai te ferrar em domínios apex.",[11,1240,1241,1244],{},[35,1242,1243],{},"Papertrail \u002F NewRelic \u002F Bugsnag desligados sem substituto."," Logs e observabilidade são fáceis de deixar pra depois e quebrar na primeira hora pós-migração. Antes do cutover, tem que ter: logs centralizados (HeroCtl tem escritor único embutido; Render expõe via UI; Coolify tem Loki opcional), métricas básicas (CPU, memória, requests), e alguma ferramenta de erros (Sentry self-hosted ou SaaS).",[11,1246,1247,1250],{},[35,1248,1249],{},"Sidekiq\u002FResque com jobs em voo durante cutover."," Durante o momento do cutover, alguns jobs vão pra fila do destino sem terem sido processados no origem. Se o seu job não é idempotente (pode rodar duas vezes sem efeito colateral), isso é problema. Solução: pause os workers no Heroku 5 minutos antes do cutover, espera fila esvaziar, faz cutover com fila vazia.",[18,1252,1254],{"id":1253},"cronograma-realista-pra-startup-media-5-a-10-apps-heroku","Cronograma realista pra startup média (5 a 10 apps Heroku)",[11,1256,1257],{},"Time pequeno, um dev part-time:",[29,1259,1260,1266,1272,1278,1284],{},[32,1261,1262,1265],{},[35,1263,1264],{},"Semana 1",": pré-flight completo + escolha de stack + setup do destino (cluster vazio rodando, painel acessível).",[32,1267,1268,1271],{},[35,1269,1270],{},"Semana 2",": Dockerização do primeiro app de baixo risco + migração de banco em ambiente de staging.",[32,1273,1274,1277],{},[35,1275,1276],{},"Semana 3",": cutover do primeiro app em produção + validação de 7 dias.",[32,1279,1280,1283],{},[35,1281,1282],{},"Semanas 4 a 6",": migração dos demais apps em paralelo, ritmo de 1 a 2 por semana.",[32,1285,1286,1289],{},[35,1287,1288],{},"Total",": 4 a 6 semanas de elapsed time, talvez 80 horas de trabalho efetivo distribuídas.",[11,1291,1292],{},"Time médio (3 devs, 20 apps): 8 semanas, 200 horas de trabalho efetivo.",[11,1294,1295],{},"Time grande (cluster de 50+ apps): trate como projeto formal, com gerente de projeto, e calcule trimestre.",[11,1297,1298],{},"A regra de bolso: nunca migre mais de 2 apps em paralelo se for o mesmo dev fazendo. O custo de contexto-switching engole o ganho de paralelismo.",[18,1300,1302],{"id":1301},"faq","FAQ",[11,1304,1305,1308],{},[35,1306,1307],{},"Quanto custa a migração em horas-homem?","\nPra um SaaS de 5 apps, dev part-time: ~80 horas. A R$200\u002Fh, R$16k. Comparado a R$2k\u002Fmês de fatura Heroku que você economiza, payback em 8 meses. Nos 4 anos seguintes, é só economia.",[11,1310,1311,1314],{},[35,1312,1313],{},"E se eu não tiver Docker setup?","\nNão precisa pré-instalar nada — as plataformas de destino constroem a imagem por você (Render, Railway, Fly.io aceitam Dockerfile direto do git). HeroCtl exige imagem em registry, então você sobe pra ECR, GCR, Docker Hub ou GHCR. Pra uso local, instala Docker Desktop e tá pronto.",[11,1316,1317,1320,1321,1323,1324,1326,1327,1330],{},[35,1318,1319],{},"Heroku Postgres tem export limit?","\nTem limite de IOPS durante ",[40,1322,742],{}," em planos baixos. Bancos acima de 5GB em plano Hobby podem precisar ",[40,1325,742],{}," em modo paralelo (",[40,1328,1329],{},"-j",") ou usar logical replication pra evitar carga grande. Pra Standard ou superior, sem problema relevante.",[11,1332,1333,1336],{},[35,1334,1335],{},"Sidekiq scheduled jobs sobrevivem?","\nSobrevivem se você migrar o Redis com snapshot (BGSAVE → restore). Se reiniciar Redis do zero no destino, perde scheduled jobs. Considera isso no cutover: ou faz a transferência de Redis junto, ou aceita reagendar manualmente alguns jobs.",[11,1338,1339,1342],{},[35,1340,1341],{},"Posso testar com 1 app antes?","\nEsse é o caminho recomendado. Pega o app menos crítico (interno, ou de baixíssimo tráfego), faz a migração inteira nele primeiro. Aprende com os tropeços ali. Depois migra os de produção com confiança. A primeira migração ensina mais que ler 10 posts como esse.",[11,1344,1345,1348],{},[35,1346,1347],{},"E se a migração falhar?","\nOs 30 dias de Heroku rodando em paralelo são a sua rede. Se o destino quebrar de forma irreversível na primeira hora, volta o DNS pro Heroku, leva 60 segundos, vida normal. O único caso onde rollback é caro é se você fez cutover de banco com escritas no destino — aí precisa replicar de volta. Por isso a recomendação é cutover de DNS e cutover de banco simultâneos, com janela curta.",[11,1350,1351,1354,1355,1358,1359,1361],{},[35,1352,1353],{},"Tem caminho de migração assistida do HeroCtl?","\nPra o HeroCtl, sim — temos um conversor experimental que lê ",[40,1356,1357],{},"app.json"," + ",[40,1360,620],{}," e gera um manifesto de job equivalente. Funciona pra apps simples (web + worker + release), e tropeça em casos exóticos (multi-buildpack pesado, hooks customizados). Se quiser testar, manda mensagem.",[18,1363,1365],{"id":1364},"fechamento","Fechamento",[11,1367,1368],{},"Migrar do Heroku quatro anos depois é constrangedor — tinha que ter saído em 2022. Mas quatro anos virando cinco é pior. O custo composto de não migrar (R$25k a R$100k por ano em fatura Heroku acumulada, mais a fragilidade de depender de um produto que a Salesforce já mostrou que não tem afeto por usuários pequenos) é maior que o custo de uma semana de trabalho focado.",[11,1370,1371],{},"Se você decidir testar o HeroCtl, instala em qualquer servidor Linux:",[107,1373,1375],{"className":109,"code":1374,"language":111,"meta":112,"style":112},"curl -sSL https:\u002F\u002Fget.heroctl.com\u002Finstall.sh | sh\n",[40,1376,1377],{"__ignoreMap":112},[116,1378,1379,1381,1384,1387,1390],{"class":118,"line":119},[116,1380,1012],{"class":122},[116,1382,1383],{"class":153}," -sSL",[116,1385,1386],{"class":126}," https:\u002F\u002Fget.heroctl.com\u002Finstall.sh",[116,1388,1389],{"class":384}," |",[116,1391,1392],{"class":122}," sh\n",[11,1394,1395],{},"Funciona em 1 servidor (modo simples) ou em 3+ (modo HA real). O plano Community é gratuito sem limite de servidores e sem limite de jobs — você não precisa decidir nada comercial pra fazer a migração inteira.",[11,1397,1398],{},"Se decidir pelo Render, Railway ou Coolify, ótimo também. O ponto desse post não é capturar você como cliente — é tirar você do Heroku. Quatro anos depois, é hora.",[11,1400,1401,1402,1407,1408,573],{},"Pra contexto adicional sobre auto-hospedagem em 2026, lê ",[1403,1404,1406],"a",{"href":1405},"\u002Fblog\u002Fheroku-auto-hospedado-2026","Heroku auto-hospedado: o estado da arte em 2026",". Pra entender por que construímos um orquestrador novo em vez de adotar um existente, lê ",[1403,1409,1411],{"href":1410},"\u002Fblog\u002Fpor-que-criamos-o-heroctl","Por que criamos o HeroCtl",[1413,1414,1415],"style",{},"html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}",{"title":112,"searchDepth":307,"depth":307,"links":1417},[1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428],{"id":20,"depth":307,"text":21},{"id":96,"depth":307,"text":97},{"id":331,"depth":307,"text":332},{"id":368,"depth":307,"text":369},{"id":727,"depth":307,"text":728},{"id":973,"depth":307,"text":974},{"id":1084,"depth":307,"text":1085},{"id":1154,"depth":307,"text":1155},{"id":1253,"depth":307,"text":1254},{"id":1301,"depth":307,"text":1302},{"id":1364,"depth":307,"text":1365},"caso-de-uso",null,"2026-03-11","O fim do plano gratuito do Heroku em novembro\u002F2022 transformou migração em prioridade pra centenas de times brasileiros. Plano detalhado com checklist, tempo estimado, e armadilhas comuns.",false,"md",{},"\u002Fblog\u002Fmigrar-do-heroku-guia-tecnico","16 min",{"title":5,"description":1432},{"loc":1436},"blog\u002Fmigrar-do-heroku-guia-tecnico",[123,1442,1443,1444,1445],"migracao","guia","tutorial","saida","F-DwMAVr8fAgCzUJD2TUGJ1fPKk1DrRptr3FLmwPkio",[1448,1454],{"title":1449,"path":1450,"stem":1451,"description":1452,"date":1453,"category":1429,"children":-1},"Migrar de Kubernetes pra stack mais simples: case real de redução de complexidade","\u002Fblog\u002Fmigrar-de-kubernetes-pra-stack-mais-simples-case","blog\u002Fmigrar-de-kubernetes-pra-stack-mais-simples-case","Quando a empresa adota K8s cedo demais, todo mundo paga. O caminho inverso — sair do K8s pra orquestração mais simples — é viável e mais comum do que parece. O que validar antes, durante e depois.","2026-03-18",{"title":1455,"path":1456,"stem":1457,"description":1458,"date":1459,"category":1460,"children":-1},"Monitoring stack completa em 2026: Prometheus + Grafana + Loki passo a passo","\u002Fblog\u002Fmonitoring-stack-completa-prometheus-grafana-loki-passo-a-passo","blog\u002Fmonitoring-stack-completa-prometheus-grafana-loki-passo-a-passo","Tutorial honesto pra subir métricas, logs e dashboards pro seu cluster — em 4 horas, sem Datadog. Stack open-source que cabe em 1 VPS de R$80\u002Fmês.","2026-05-12","engenharia",{"path":1462},"\u002Fen\u002Fblog\u002Fmigrating-from-heroku-technical-guide",1777362184742]