[{"data":1,"prerenderedAt":3409},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial":3,"blog-surround-\u002Fblog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial":3394,"blog-en-alt-\u002Fblog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial":3407},{"id":4,"title":5,"author":6,"body":7,"category":3378,"cover":3379,"date":3380,"description":3381,"draft":3382,"extension":3383,"lastReviewed":3379,"meta":3384,"navigation":410,"path":3385,"readingTime":3386,"seo":3387,"sitemap":3388,"stem":3389,"tags":3390,"__hash__":3393},"blog_pt\u002Fblog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial.md","Deploy zero-downtime sem Kubernetes: tutorial prático em 2026","Equipe HeroCtl",{"type":8,"value":9,"toc":3353},"minimark",[10,14,17,22,38,41,55,58,62,65,87,90,94,101,104,107,110,114,117,204,207,210,212,216,219,222,274,285,288,313,316,338,345,349,356,366,371,915,919,999,1003,1079,1093,1096,1100,1103,1139,1142,1207,1210,1235,1238,1242,1245,1248,1251,1265,1272,1362,1368,1371,1433,1439,1456,1459,1463,1466,2345,2356,2382,2385,2443,2446,2450,2453,2456,2505,2508,2519,2524,2532,2535,2537,2541,2544,2606,2610,2617,2623,2637,2709,2712,2726,2730,2733,2754,2757,2761,2764,2782,2785,2789,2792,2940,2943,2946,2967,2970,2974,3167,3170,3174,3222,3226,3231,3238,3243,3246,3251,3254,3259,3265,3270,3273,3278,3284,3289,3292,3297,3304,3306,3310,3313,3316,3332,3346,3349],[11,12,13],"p",{},"Tem um mito persistente que zero-downtime deploy é exclusividade de quem rodou Kubernetes em produção. Não é. A técnica existe desde antes do colosso ter nome — qualquer time que rodou um par de servidores físicos atrás de um balanceador na década passada já fazia isso, com scripts de cinquenta linhas e nenhum CRD na vida. O que mudou foi o marketing em volta da prática, não a prática em si.",[11,15,16],{},"Esse post é um tutorial passo a passo pra montar deploy sem downtime do zero, em duas máquinas Linux, sem orquestrador pesado, sem painel mágico. No fim você vai ter um script de bash que troca uma instância por vez, espera a nova ficar saudável, e rola pra próxima — exatamente o algoritmo que orquestradores grandes implementam, só que sem o boilerplate.",[18,19,21],"h2",{"id":20},"tldr","TL;DR",[11,23,24,25,29,30,33,34,37],{},"Zero-downtime deploy depende de três ingredientes, não de uma ferramenta específica. Primeiro: ",[26,27,28],"strong",{},"duas ou mais instâncias da aplicação rodando em paralelo",", atrás de um proxy básico. Segundo: ",[26,31,32],{},"um endpoint de health check confiável"," que valida dependências reais (banco, cache, fila), não só responde 200 instantaneamente. Terceiro: ",[26,35,36],{},"um script ou orquestrador que substitua um contêiner por vez",", esperando o novo ficar saudável antes de prosseguir pro próximo.",[11,39,40],{},"Esse tutorial monta o setup completo em duas VPS Linux com Docker, Caddy na frente como proxy + balanceador, e um script bash de cinquenta linhas que faz rolling update com health check ativo, tempo mínimo saudável, e rollback automático se falhar. Resultado: deploy sem 5xx visível pro usuário, em menos de um minuto, sem janela de manutenção.",[11,42,43,46,47,50,51,54],{},[26,44,45],{},"Pré-requisitos:"," duas VPS Linux com Docker (Hetzner CPX11 a R$30 cada), domínio com DNS controlável, app com health check decente. ",[26,48,49],{},"Tempo de setup:"," duas a três horas. ",[26,52,53],{},"Custo mensal:"," R$60 (R$75 se você quiser uma terceira VPS dedicada ao proxy). No fim mostramos a versão \"robusta\" via HeroCtl pra quem quer parar de scriptar.",[56,57],"hr",{},[18,59,61],{"id":60},"os-tres-ingredientes-sem-isso-nao-e-zero-downtime","Os três ingredientes (sem isso, não é zero-downtime)",[11,63,64],{},"Antes de qualquer comando, vale fixar a teoria — porque toda configuração mais elaborada que você vai ver na internet é variação dessas três peças.",[66,67,68,75,81],"ol",{},[69,70,71,74],"li",{},[26,72,73],{},"Múltiplas instâncias da app rodando em paralelo."," Mínimo dois. Se você só tem uma, qualquer reinício é janela de erro. Não tem como contornar isso com truque de configuração.",[69,76,77,80],{},[26,78,79],{},"Um proxy\u002Fbalanceador na frente, fazendo health check."," O proxy decide pra qual instância mandar tráfego. Se uma cai (ou foi tirada deliberadamente pro deploy), o proxy só manda pras restantes.",[69,82,83,86],{},[26,84,85],{},"Um script que troca instâncias uma por vez."," Nunca todas juntas. Espera a nova ficar saudável antes de mexer na próxima. Se a nova falha, para o deploy e mantém as antigas servindo.",[11,88,89],{},"É só isso. O resto — Kubernetes, painéis modernos, orquestradores leves — é embalagem em volta desses três pontos.",[18,91,93],{"id":92},"por-que-single-server-nunca-e-zero-downtime-mesmo-se-for-rapido","Por que single-server NUNCA é zero-downtime (mesmo se for rápido)",[11,95,96,97,100],{},"Vejo essa pergunta toda semana no Discord da comunidade: \"consigo zero-downtime com um servidor só, se o deploy for rápido o suficiente?\". Resposta curta: ",[26,98,99],{},"não",".",[11,102,103],{},"Numa máquina única, o ciclo de deploy é: para o contêiner antigo, sobe o novo. Mesmo que tudo aconteça em três segundos, esses três segundos existem. Conexões TCP em curso são cortadas. Requisições que chegam nesse intervalo levam connection refused ou 502. Se você tem cinco requests por segundo, são quinze usuários vendo erro a cada deploy.",[11,105,106],{},"Tem variação esperta — subir o novo numa porta diferente, mudar o proxy local, derrubar o antigo. Isso melhora, mas não elimina. Se a app demora pra fechar conexões em curso, o cutover ainda gera erros. Se health check é fraco, o proxy aponta tráfego pra app que ainda não terminou de subir. Sempre tem uma janela.",[11,108,109],{},"A única forma confiável de eliminar a janela é ter pelo menos uma instância sempre disponível durante todo o deploy. Isso exige duas máquinas. Ponto.",[18,111,113],{"id":112},"o-setup-minimo-duas-vps-um-proxy","O setup mínimo (duas VPS + um proxy)",[11,115,116],{},"A topologia mais barata que entrega zero-downtime real:",[118,119,120,139],"table",{},[121,122,123],"thead",{},[124,125,126,130,133,136],"tr",{},[127,128,129],"th",{},"Componente",[127,131,132],{},"Tamanho",[127,134,135],{},"Custo",[127,137,138],{},"Função",[140,141,142,157,169,188],"tbody",{},[124,143,144,148,151,154],{},[145,146,147],"td",{},"VPS A",[145,149,150],{},"2 vCPU \u002F 2 GB RAM",[145,152,153],{},"R$30\u002Fmês",[145,155,156],{},"App instância 1",[124,158,159,162,164,166],{},[145,160,161],{},"VPS B",[145,163,150],{},[145,165,153],{},[145,167,168],{},"App instância 2",[124,170,171,174,182,185],{},[145,172,173],{},"Proxy",[145,175,176,177,181],{},"rodando em VPS A ",[178,179,180],"em",{},"ou"," terceira VPS",[145,183,184],{},"R$0 (compartilhado) ou R$15\u002Fmês",[145,186,187],{},"Caddy\u002Fnginx fazendo balance",[124,189,190,193,198,201],{},[145,191,192],{},"Banco",[145,194,195,196,181],{},"Postgres gerenciado ",[178,197,180],{},[145,199,200],{},"varia",[145,202,203],{},"Estado compartilhado entre A e B",[11,205,206],{},"Ter o proxy compartilhado em uma das próprias VPS economiza, mas tem trade-off: se a VPS que hospeda o proxy cair inteira, o site cai junto (mesmo com a outra VPS rodando). Pra time pequeno isso é aceitável. Quando crescer, o proxy migra pra VPS dedicada ou vira par redundante.",[11,208,209],{},"DNS A record do seu domínio aponta pro IP do proxy. Apps em A e B se conectam ao mesmo banco — sem essa parte compartilhada, as duas instâncias divergem e o usuário vê resultado diferente dependendo de qual respondeu.",[56,211],{},[18,213,215],{"id":214},"passo-1-provisionar-duas-vps-15-min","Passo 1 — Provisionar duas VPS (15 min)",[11,217,218],{},"Eu uso Hetzner CPX11 (€4,75 ≈ R$30) como referência. DigitalOcean Droplet de US$6, Vultr Cloud Compute de US$6 ou Linode Nanode de US$5 entregam algo parecido. O importante é Linux moderno (Ubuntu 24.04 LTS ou Debian 12) com Docker.",[11,220,221],{},"Provisione as duas máquinas com a mesma chave SSH:",[223,224,229],"pre",{"className":225,"code":226,"language":227,"meta":228,"style":228},"language-bash shiki shiki-themes github-dark-default","# do seu laptop\nssh-keygen -t ed25519 -f ~\u002F.ssh\u002Fdeploy_key -C \"deploy@meudominio.com\"\n# adicione ~\u002F.ssh\u002Fdeploy_key.pub na console do provedor antes de criar a VPS\n","bash","",[230,231,232,241,268],"code",{"__ignoreMap":228},[233,234,237],"span",{"class":235,"line":236},"line",1,[233,238,240],{"class":239},"sH3jZ","# do seu laptop\n",[233,242,244,248,252,256,259,262,265],{"class":235,"line":243},2,[233,245,247],{"class":246},"sQhOw","ssh-keygen",[233,249,251],{"class":250},"sFSAA"," -t",[233,253,255],{"class":254},"s9uIt"," ed25519",[233,257,258],{"class":250}," -f",[233,260,261],{"class":254}," ~\u002F.ssh\u002Fdeploy_key",[233,263,264],{"class":250}," -C",[233,266,267],{"class":254}," \"deploy@meudominio.com\"\n",[233,269,271],{"class":235,"line":270},3,[233,272,273],{"class":239},"# adicione ~\u002F.ssh\u002Fdeploy_key.pub na console do provedor antes de criar a VPS\n",[11,275,276,277,280,281,284],{},"Crie cada VPS, anote os IPs. Vou usar ",[230,278,279],{},"203.0.113.10"," (VPS A) e ",[230,282,283],{},"203.0.113.20"," (VPS B) como placeholders no resto do post.",[11,286,287],{},"Instale Docker em cada uma:",[223,289,291],{"className":225,"code":290,"language":227,"meta":228,"style":228},"ssh root@203.0.113.10 \"curl -fsSL https:\u002F\u002Fget.docker.com | sh\"\nssh root@203.0.113.20 \"curl -fsSL https:\u002F\u002Fget.docker.com | sh\"\n",[230,292,293,304],{"__ignoreMap":228},[233,294,295,298,301],{"class":235,"line":236},[233,296,297],{"class":246},"ssh",[233,299,300],{"class":254}," root@203.0.113.10",[233,302,303],{"class":254}," \"curl -fsSL https:\u002F\u002Fget.docker.com | sh\"\n",[233,305,306,308,311],{"class":235,"line":243},[233,307,297],{"class":246},[233,309,310],{"class":254}," root@203.0.113.20",[233,312,303],{"class":254},[11,314,315],{},"Configure firewall pra permitir só 22 (SSH) e 8080 (porta interna onde a app vai escutar). Tráfego HTTP\u002FHTTPS chega só no proxy:",[223,317,319],{"className":225,"code":318,"language":227,"meta":228,"style":228},"ssh root@203.0.113.10 \"ufw allow 22 && ufw allow 8080\u002Ftcp && ufw --force enable\"\nssh root@203.0.113.20 \"ufw allow 22 && ufw allow 8080\u002Ftcp && ufw --force enable\"\n",[230,320,321,330],{"__ignoreMap":228},[233,322,323,325,327],{"class":235,"line":236},[233,324,297],{"class":246},[233,326,300],{"class":254},[233,328,329],{"class":254}," \"ufw allow 22 && ufw allow 8080\u002Ftcp && ufw --force enable\"\n",[233,331,332,334,336],{"class":235,"line":243},[233,333,297],{"class":246},[233,335,310],{"class":254},[233,337,329],{"class":254},[11,339,340,341,344],{},"Validação: ",[230,342,343],{},"docker run --rm hello-world"," em cada máquina deve completar sem erro.",[18,346,348],{"id":347},"passo-2-app-com-health-check-decente-30-min","Passo 2 — App com health check decente (30 min)",[11,350,351,352,355],{},"O endpoint ",[230,353,354],{},"\u002Fhealthz"," é o coração do esquema. Se ele retorna 200 quando a app não está realmente pronta, o proxy manda tráfego pra instância quebrada e o usuário vê erro. Se ele retorna 500 quando a app está saudável, o proxy tira a instância boa do balanceamento. Ou seja: o health check é a fonte de verdade do sistema inteiro.",[11,357,358,359,361,362,365],{},"Regra de ouro: o ",[230,360,354],{}," valida ",[26,363,364],{},"dependências reais que a app precisa pra responder",". Mínimo: conexão com o banco. Se você tem cache (Redis), inclui. Se você tem fila (SQS, RabbitMQ), inclui. NÃO retorne 200 logo no boot — espere assets compilarem, cache aquecer, conexões abrirem.",[367,368,370],"h3",{"id":369},"nodejs-express","Node.js (Express)",[223,372,376],{"className":373,"code":374,"language":375,"meta":228,"style":228},"language-js shiki shiki-themes github-dark-default","import express from \"express\"\nimport { Pool } from \"pg\"\n\nconst app = express()\nconst pool = new Pool({ connectionString: process.env.DATABASE_URL })\n\nlet ready = false\n\n\u002F\u002F warm-up assíncrono — só fica ready quando dependencies validam\n;(async () => {\n  await pool.query(\"SELECT 1\")\n  \u002F\u002F outras inicializações: cache prime, etc.\n  ready = true\n})()\n\napp.get(\"\u002Fhealthz\", async (_req, res) => {\n  if (!ready) return res.status(503).send(\"warming up\")\n  try {\n    await pool.query(\"SELECT 1\")\n    res.status(200).send(\"ok\")\n  } catch (e) {\n    res.status(503).send(\"db down\")\n  }\n})\n\napp.get(\"\u002F\", (_req, res) => res.send(\"Hello v1\"))\n\nconst server = app.listen(8080, () => console.log(\"listening 8080\"))\n\n\u002F\u002F graceful shutdown — drena conexões antes de morrer\nprocess.on(\"SIGTERM\", () => {\n  ready = false  \u002F\u002F health check passa a falhar imediatamente\n  setTimeout(() => {\n    server.close(() => process.exit(0))\n  }, 5000)  \u002F\u002F 5s pro proxy notar e parar de mandar tráfego novo\n})\n","js",[230,377,378,394,406,412,431,456,461,476,481,487,505,526,532,543,549,554,591,632,640,656,680,692,714,720,726,731,768,773,812,817,823,843,856,869,895,910],{"__ignoreMap":228},[233,379,380,384,388,391],{"class":235,"line":236},[233,381,383],{"class":382},"suJrU","import",[233,385,387],{"class":386},"sZEs4"," express ",[233,389,390],{"class":382},"from",[233,392,393],{"class":254}," \"express\"\n",[233,395,396,398,401,403],{"class":235,"line":243},[233,397,383],{"class":382},[233,399,400],{"class":386}," { Pool } ",[233,402,390],{"class":382},[233,404,405],{"class":254}," \"pg\"\n",[233,407,408],{"class":235,"line":270},[233,409,411],{"emptyLinePlaceholder":410},true,"\n",[233,413,415,418,421,424,428],{"class":235,"line":414},4,[233,416,417],{"class":382},"const",[233,419,420],{"class":250}," app",[233,422,423],{"class":382}," =",[233,425,427],{"class":426},"sc3cj"," express",[233,429,430],{"class":386},"()\n",[233,432,434,436,439,441,444,447,450,453],{"class":235,"line":433},5,[233,435,417],{"class":382},[233,437,438],{"class":250}," pool",[233,440,423],{"class":382},[233,442,443],{"class":382}," new",[233,445,446],{"class":426}," Pool",[233,448,449],{"class":386},"({ connectionString: process.env.",[233,451,452],{"class":250},"DATABASE_URL",[233,454,455],{"class":386}," })\n",[233,457,459],{"class":235,"line":458},6,[233,460,411],{"emptyLinePlaceholder":410},[233,462,464,467,470,473],{"class":235,"line":463},7,[233,465,466],{"class":382},"let",[233,468,469],{"class":386}," ready ",[233,471,472],{"class":382},"=",[233,474,475],{"class":250}," false\n",[233,477,479],{"class":235,"line":478},8,[233,480,411],{"emptyLinePlaceholder":410},[233,482,484],{"class":235,"line":483},9,[233,485,486],{"class":239},"\u002F\u002F warm-up assíncrono — só fica ready quando dependencies validam\n",[233,488,490,493,496,499,502],{"class":235,"line":489},10,[233,491,492],{"class":386},";(",[233,494,495],{"class":382},"async",[233,497,498],{"class":386}," () ",[233,500,501],{"class":382},"=>",[233,503,504],{"class":386}," {\n",[233,506,508,511,514,517,520,523],{"class":235,"line":507},11,[233,509,510],{"class":382},"  await",[233,512,513],{"class":386}," pool.",[233,515,516],{"class":426},"query",[233,518,519],{"class":386},"(",[233,521,522],{"class":254},"\"SELECT 1\"",[233,524,525],{"class":386},")\n",[233,527,529],{"class":235,"line":528},12,[233,530,531],{"class":239},"  \u002F\u002F outras inicializações: cache prime, etc.\n",[233,533,535,538,540],{"class":235,"line":534},13,[233,536,537],{"class":386},"  ready ",[233,539,472],{"class":382},[233,541,542],{"class":250}," true\n",[233,544,546],{"class":235,"line":545},14,[233,547,548],{"class":386},"})()\n",[233,550,552],{"class":235,"line":551},15,[233,553,411],{"emptyLinePlaceholder":410},[233,555,557,560,563,565,568,571,573,576,579,581,584,587,589],{"class":235,"line":556},16,[233,558,559],{"class":386},"app.",[233,561,562],{"class":426},"get",[233,564,519],{"class":386},[233,566,567],{"class":254},"\"\u002Fhealthz\"",[233,569,570],{"class":386},", ",[233,572,495],{"class":382},[233,574,575],{"class":386}," (",[233,577,578],{"class":246},"_req",[233,580,570],{"class":386},[233,582,583],{"class":246},"res",[233,585,586],{"class":386},") ",[233,588,501],{"class":382},[233,590,504],{"class":386},[233,592,594,597,599,602,605,608,611,614,616,619,622,625,627,630],{"class":235,"line":593},17,[233,595,596],{"class":382},"  if",[233,598,575],{"class":386},[233,600,601],{"class":382},"!",[233,603,604],{"class":386},"ready) ",[233,606,607],{"class":382},"return",[233,609,610],{"class":386}," res.",[233,612,613],{"class":426},"status",[233,615,519],{"class":386},[233,617,618],{"class":250},"503",[233,620,621],{"class":386},").",[233,623,624],{"class":426},"send",[233,626,519],{"class":386},[233,628,629],{"class":254},"\"warming up\"",[233,631,525],{"class":386},[233,633,635,638],{"class":235,"line":634},18,[233,636,637],{"class":382},"  try",[233,639,504],{"class":386},[233,641,643,646,648,650,652,654],{"class":235,"line":642},19,[233,644,645],{"class":382},"    await",[233,647,513],{"class":386},[233,649,516],{"class":426},[233,651,519],{"class":386},[233,653,522],{"class":254},[233,655,525],{"class":386},[233,657,659,662,664,666,669,671,673,675,678],{"class":235,"line":658},20,[233,660,661],{"class":386},"    res.",[233,663,613],{"class":426},[233,665,519],{"class":386},[233,667,668],{"class":250},"200",[233,670,621],{"class":386},[233,672,624],{"class":426},[233,674,519],{"class":386},[233,676,677],{"class":254},"\"ok\"",[233,679,525],{"class":386},[233,681,683,686,689],{"class":235,"line":682},21,[233,684,685],{"class":386},"  } ",[233,687,688],{"class":382},"catch",[233,690,691],{"class":386}," (e) {\n",[233,693,695,697,699,701,703,705,707,709,712],{"class":235,"line":694},22,[233,696,661],{"class":386},[233,698,613],{"class":426},[233,700,519],{"class":386},[233,702,618],{"class":250},[233,704,621],{"class":386},[233,706,624],{"class":426},[233,708,519],{"class":386},[233,710,711],{"class":254},"\"db down\"",[233,713,525],{"class":386},[233,715,717],{"class":235,"line":716},23,[233,718,719],{"class":386},"  }\n",[233,721,723],{"class":235,"line":722},24,[233,724,725],{"class":386},"})\n",[233,727,729],{"class":235,"line":728},25,[233,730,411],{"emptyLinePlaceholder":410},[233,732,734,736,738,740,743,746,748,750,752,754,756,758,760,762,765],{"class":235,"line":733},26,[233,735,559],{"class":386},[233,737,562],{"class":426},[233,739,519],{"class":386},[233,741,742],{"class":254},"\"\u002F\"",[233,744,745],{"class":386},", (",[233,747,578],{"class":246},[233,749,570],{"class":386},[233,751,583],{"class":246},[233,753,586],{"class":386},[233,755,501],{"class":382},[233,757,610],{"class":386},[233,759,624],{"class":426},[233,761,519],{"class":386},[233,763,764],{"class":254},"\"Hello v1\"",[233,766,767],{"class":386},"))\n",[233,769,771],{"class":235,"line":770},27,[233,772,411],{"emptyLinePlaceholder":410},[233,774,776,778,781,783,786,789,791,794,797,799,802,805,807,810],{"class":235,"line":775},28,[233,777,417],{"class":382},[233,779,780],{"class":250}," server",[233,782,423],{"class":382},[233,784,785],{"class":386}," app.",[233,787,788],{"class":426},"listen",[233,790,519],{"class":386},[233,792,793],{"class":250},"8080",[233,795,796],{"class":386},", () ",[233,798,501],{"class":382},[233,800,801],{"class":386}," console.",[233,803,804],{"class":426},"log",[233,806,519],{"class":386},[233,808,809],{"class":254},"\"listening 8080\"",[233,811,767],{"class":386},[233,813,815],{"class":235,"line":814},29,[233,816,411],{"emptyLinePlaceholder":410},[233,818,820],{"class":235,"line":819},30,[233,821,822],{"class":239},"\u002F\u002F graceful shutdown — drena conexões antes de morrer\n",[233,824,826,829,832,834,837,839,841],{"class":235,"line":825},31,[233,827,828],{"class":386},"process.",[233,830,831],{"class":426},"on",[233,833,519],{"class":386},[233,835,836],{"class":254},"\"SIGTERM\"",[233,838,796],{"class":386},[233,840,501],{"class":382},[233,842,504],{"class":386},[233,844,846,848,850,853],{"class":235,"line":845},32,[233,847,537],{"class":386},[233,849,472],{"class":382},[233,851,852],{"class":250}," false",[233,854,855],{"class":239},"  \u002F\u002F health check passa a falhar imediatamente\n",[233,857,859,862,865,867],{"class":235,"line":858},33,[233,860,861],{"class":426},"  setTimeout",[233,863,864],{"class":386},"(() ",[233,866,501],{"class":382},[233,868,504],{"class":386},[233,870,872,875,878,880,882,885,888,890,893],{"class":235,"line":871},34,[233,873,874],{"class":386},"    server.",[233,876,877],{"class":426},"close",[233,879,864],{"class":386},[233,881,501],{"class":382},[233,883,884],{"class":386}," process.",[233,886,887],{"class":426},"exit",[233,889,519],{"class":386},[233,891,892],{"class":250},"0",[233,894,767],{"class":386},[233,896,898,901,904,907],{"class":235,"line":897},35,[233,899,900],{"class":386},"  }, ",[233,902,903],{"class":250},"5000",[233,905,906],{"class":386},")  ",[233,908,909],{"class":239},"\u002F\u002F 5s pro proxy notar e parar de mandar tráfego novo\n",[233,911,913],{"class":235,"line":912},36,[233,914,725],{"class":386},[367,916,918],{"id":917},"python-django-gunicorn","Python (Django + gunicorn)",[223,920,924],{"className":921,"code":922,"language":923,"meta":228,"style":228},"language-python shiki shiki-themes github-dark-default","# health\u002Fviews.py\nfrom django.db import connection\nfrom django.http import JsonResponse, HttpResponse\nimport redis, os\n\n_r = redis.from_url(os.environ[\"REDIS_URL\"])\n\ndef healthz(request):\n    try:\n        with connection.cursor() as c:\n            c.execute(\"SELECT 1\")\n        _r.ping()\n        return HttpResponse(\"ok\", status=200)\n    except Exception as e:\n        return HttpResponse(f\"unhealthy: {e}\", status=503)\n","python",[230,925,926,931,936,941,946,950,955,959,964,969,974,979,984,989,994],{"__ignoreMap":228},[233,927,928],{"class":235,"line":236},[233,929,930],{},"# health\u002Fviews.py\n",[233,932,933],{"class":235,"line":243},[233,934,935],{},"from django.db import connection\n",[233,937,938],{"class":235,"line":270},[233,939,940],{},"from django.http import JsonResponse, HttpResponse\n",[233,942,943],{"class":235,"line":414},[233,944,945],{},"import redis, os\n",[233,947,948],{"class":235,"line":433},[233,949,411],{"emptyLinePlaceholder":410},[233,951,952],{"class":235,"line":458},[233,953,954],{},"_r = redis.from_url(os.environ[\"REDIS_URL\"])\n",[233,956,957],{"class":235,"line":463},[233,958,411],{"emptyLinePlaceholder":410},[233,960,961],{"class":235,"line":478},[233,962,963],{},"def healthz(request):\n",[233,965,966],{"class":235,"line":483},[233,967,968],{},"    try:\n",[233,970,971],{"class":235,"line":489},[233,972,973],{},"        with connection.cursor() as c:\n",[233,975,976],{"class":235,"line":507},[233,977,978],{},"            c.execute(\"SELECT 1\")\n",[233,980,981],{"class":235,"line":528},[233,982,983],{},"        _r.ping()\n",[233,985,986],{"class":235,"line":534},[233,987,988],{},"        return HttpResponse(\"ok\", status=200)\n",[233,990,991],{"class":235,"line":545},[233,992,993],{},"    except Exception as e:\n",[233,995,996],{"class":235,"line":551},[233,997,998],{},"        return HttpResponse(f\"unhealthy: {e}\", status=503)\n",[367,1000,1002],{"id":1001},"ruby-rails","Ruby (Rails)",[223,1004,1008],{"className":1005,"code":1006,"language":1007,"meta":228,"style":228},"language-ruby shiki shiki-themes github-dark-default","# config\u002Froutes.rb\nget \"\u002Fhealthz\", to: \"health#show\"\n\n# app\u002Fcontrollers\u002Fhealth_controller.rb\nclass HealthController \u003C ApplicationController\n  def show\n    ActiveRecord::Base.connection.execute(\"SELECT 1\")\n    Rails.cache.read(\"__healthcheck__\")\n    head :ok\n  rescue => e\n    Rails.logger.warn(\"healthcheck failed: #{e.message}\")\n    head :service_unavailable\n  end\nend\n","ruby",[230,1009,1010,1015,1020,1024,1029,1034,1039,1044,1049,1054,1059,1064,1069,1074],{"__ignoreMap":228},[233,1011,1012],{"class":235,"line":236},[233,1013,1014],{},"# config\u002Froutes.rb\n",[233,1016,1017],{"class":235,"line":243},[233,1018,1019],{},"get \"\u002Fhealthz\", to: \"health#show\"\n",[233,1021,1022],{"class":235,"line":270},[233,1023,411],{"emptyLinePlaceholder":410},[233,1025,1026],{"class":235,"line":414},[233,1027,1028],{},"# app\u002Fcontrollers\u002Fhealth_controller.rb\n",[233,1030,1031],{"class":235,"line":433},[233,1032,1033],{},"class HealthController \u003C ApplicationController\n",[233,1035,1036],{"class":235,"line":458},[233,1037,1038],{},"  def show\n",[233,1040,1041],{"class":235,"line":463},[233,1042,1043],{},"    ActiveRecord::Base.connection.execute(\"SELECT 1\")\n",[233,1045,1046],{"class":235,"line":478},[233,1047,1048],{},"    Rails.cache.read(\"__healthcheck__\")\n",[233,1050,1051],{"class":235,"line":483},[233,1052,1053],{},"    head :ok\n",[233,1055,1056],{"class":235,"line":489},[233,1057,1058],{},"  rescue => e\n",[233,1060,1061],{"class":235,"line":507},[233,1062,1063],{},"    Rails.logger.warn(\"healthcheck failed: #{e.message}\")\n",[233,1065,1066],{"class":235,"line":528},[233,1067,1068],{},"    head :service_unavailable\n",[233,1070,1071],{"class":235,"line":534},[233,1072,1073],{},"  end\n",[233,1075,1076],{"class":235,"line":545},[233,1077,1078],{},"end\n",[11,1080,1081,1082,1085,1086,1089,1090,1092],{},"O detalhe que diferencia health check amador de profissional é o ",[26,1083,1084],{},"graceful shutdown",": ao receber ",[230,1087,1088],{},"SIGTERM",", a app passa a retornar 503 no ",[230,1091,354],{}," imediatamente, mas continua aceitando conexões em curso por mais alguns segundos. O proxy nota o 503, para de mandar tráfego novo, e quando o app finalmente fecha não tem mais ninguém esperando resposta.",[11,1094,1095],{},"Sem isso, o cutover sempre vaza alguns erros mesmo com tudo o resto certo.",[18,1097,1099],{"id":1098},"passo-3-subir-duas-instancias-docker-15-min","Passo 3 — Subir duas instâncias Docker (15 min)",[11,1101,1102],{},"Faça build da sua app em imagem Docker. Pro tutorial vou usar uma imagem genérica que você substitui:",[223,1104,1106],{"className":225,"code":1105,"language":227,"meta":228,"style":228},"# no seu laptop, push pra registry (Docker Hub, ECR, GHCR)\ndocker build -t meuusuario\u002Fmyapp:v1 .\ndocker push meuusuario\u002Fmyapp:v1\n",[230,1107,1108,1113,1129],{"__ignoreMap":228},[233,1109,1110],{"class":235,"line":236},[233,1111,1112],{"class":239},"# no seu laptop, push pra registry (Docker Hub, ECR, GHCR)\n",[233,1114,1115,1118,1121,1123,1126],{"class":235,"line":243},[233,1116,1117],{"class":246},"docker",[233,1119,1120],{"class":254}," build",[233,1122,251],{"class":250},[233,1124,1125],{"class":254}," meuusuario\u002Fmyapp:v1",[233,1127,1128],{"class":254}," .\n",[233,1130,1131,1133,1136],{"class":235,"line":270},[233,1132,1117],{"class":246},[233,1134,1135],{"class":254}," push",[233,1137,1138],{"class":254}," meuusuario\u002Fmyapp:v1\n",[11,1140,1141],{},"Sobe instância em VPS A:",[223,1143,1145],{"className":225,"code":1144,"language":227,"meta":228,"style":228},"ssh root@203.0.113.10 \"\n  docker pull meuusuario\u002Fmyapp:v1 &&\n  docker run -d --name app --restart=unless-stopped \\\n    -p 8080:8080 \\\n    -e DATABASE_URL='postgres:\u002F\u002Fuser:pass@db.example.com:5432\u002Fapp' \\\n    --health-cmd='curl -f http:\u002F\u002Flocalhost:8080\u002Fhealthz || exit 1' \\\n    --health-interval=5s --health-timeout=2s --health-retries=3 \\\n    meuusuario\u002Fmyapp:v1\n\"\n",[230,1146,1147,1156,1161,1169,1176,1183,1190,1197,1202],{"__ignoreMap":228},[233,1148,1149,1151,1153],{"class":235,"line":236},[233,1150,297],{"class":246},[233,1152,300],{"class":254},[233,1154,1155],{"class":254}," \"\n",[233,1157,1158],{"class":235,"line":243},[233,1159,1160],{"class":254},"  docker pull meuusuario\u002Fmyapp:v1 &&\n",[233,1162,1163,1166],{"class":235,"line":270},[233,1164,1165],{"class":254},"  docker run -d --name app --restart=unless-stopped ",[233,1167,1168],{"class":382},"\\\n",[233,1170,1171,1174],{"class":235,"line":414},[233,1172,1173],{"class":254},"    -p 8080:8080 ",[233,1175,1168],{"class":382},[233,1177,1178,1181],{"class":235,"line":433},[233,1179,1180],{"class":254},"    -e DATABASE_URL='postgres:\u002F\u002Fuser:pass@db.example.com:5432\u002Fapp' ",[233,1182,1168],{"class":382},[233,1184,1185,1188],{"class":235,"line":458},[233,1186,1187],{"class":254},"    --health-cmd='curl -f http:\u002F\u002Flocalhost:8080\u002Fhealthz || exit 1' ",[233,1189,1168],{"class":382},[233,1191,1192,1195],{"class":235,"line":463},[233,1193,1194],{"class":254},"    --health-interval=5s --health-timeout=2s --health-retries=3 ",[233,1196,1168],{"class":382},[233,1198,1199],{"class":235,"line":478},[233,1200,1201],{"class":254},"    meuusuario\u002Fmyapp:v1\n",[233,1203,1204],{"class":235,"line":483},[233,1205,1206],{"class":254},"\"\n",[11,1208,1209],{},"Repete pra VPS B trocando o IP. Valide:",[223,1211,1213],{"className":225,"code":1212,"language":227,"meta":228,"style":228},"curl http:\u002F\u002F203.0.113.10:8080\u002Fhealthz   # deve retornar \"ok\"\ncurl http:\u002F\u002F203.0.113.20:8080\u002Fhealthz   # deve retornar \"ok\"\n",[230,1214,1215,1226],{"__ignoreMap":228},[233,1216,1217,1220,1223],{"class":235,"line":236},[233,1218,1219],{"class":246},"curl",[233,1221,1222],{"class":254}," http:\u002F\u002F203.0.113.10:8080\u002Fhealthz",[233,1224,1225],{"class":239},"   # deve retornar \"ok\"\n",[233,1227,1228,1230,1233],{"class":235,"line":243},[233,1229,1219],{"class":246},[233,1231,1232],{"class":254}," http:\u002F\u002F203.0.113.20:8080\u002Fhealthz",[233,1234,1225],{"class":239},[11,1236,1237],{},"Se as duas voltam 200, a base está pronta.",[18,1239,1241],{"id":1240},"passo-4-caddy-como-reverse-proxy-balanceador-30-min","Passo 4 — Caddy como reverse proxy + balanceador (30 min)",[11,1243,1244],{},"Caddy é mais fácil de começar que nginx por causa do TLS automático embutido — Let's Encrypt funciona out of the box, sem configurar bot externo. nginx é mais flexível e tem ecossistema maior; Caddy é mais simples pra esse caso. Pro tutorial vou de Caddy.",[11,1246,1247],{},"Vou rodar o Caddy na VPS A, compartilhando a máquina com uma das instâncias da app. Se preferir uma terceira VPS dedicada, troca o IP onde for relevante.",[11,1249,1250],{},"Primeiro, libere portas 80 e 443 na VPS A:",[223,1252,1254],{"className":225,"code":1253,"language":227,"meta":228,"style":228},"ssh root@203.0.113.10 \"ufw allow 80 && ufw allow 443\"\n",[230,1255,1256],{"__ignoreMap":228},[233,1257,1258,1260,1262],{"class":235,"line":236},[233,1259,297],{"class":246},[233,1261,300],{"class":254},[233,1263,1264],{"class":254}," \"ufw allow 80 && ufw allow 443\"\n",[11,1266,1267,1268,1271],{},"Crie o ",[230,1269,1270],{},"Caddyfile",":",[223,1273,1277],{"className":1274,"code":1275,"language":1276,"meta":228,"style":228},"language-caddyfile shiki shiki-themes github-dark-default","meudominio.com {\n    reverse_proxy 203.0.113.10:8080 203.0.113.20:8080 {\n        lb_policy round_robin\n        health_uri \u002Fhealthz\n        health_interval 5s\n        health_timeout 2s\n        health_status 200\n\n        fail_duration 30s\n        max_fails 2\n        unhealthy_status 5xx\n\n        transport http {\n            dial_timeout 2s\n        }\n    }\n}\n","caddyfile",[230,1278,1279,1284,1289,1294,1299,1304,1309,1314,1318,1323,1328,1333,1337,1342,1347,1352,1357],{"__ignoreMap":228},[233,1280,1281],{"class":235,"line":236},[233,1282,1283],{},"meudominio.com {\n",[233,1285,1286],{"class":235,"line":243},[233,1287,1288],{},"    reverse_proxy 203.0.113.10:8080 203.0.113.20:8080 {\n",[233,1290,1291],{"class":235,"line":270},[233,1292,1293],{},"        lb_policy round_robin\n",[233,1295,1296],{"class":235,"line":414},[233,1297,1298],{},"        health_uri \u002Fhealthz\n",[233,1300,1301],{"class":235,"line":433},[233,1302,1303],{},"        health_interval 5s\n",[233,1305,1306],{"class":235,"line":458},[233,1307,1308],{},"        health_timeout 2s\n",[233,1310,1311],{"class":235,"line":463},[233,1312,1313],{},"        health_status 200\n",[233,1315,1316],{"class":235,"line":478},[233,1317,411],{"emptyLinePlaceholder":410},[233,1319,1320],{"class":235,"line":483},[233,1321,1322],{},"        fail_duration 30s\n",[233,1324,1325],{"class":235,"line":489},[233,1326,1327],{},"        max_fails 2\n",[233,1329,1330],{"class":235,"line":507},[233,1331,1332],{},"        unhealthy_status 5xx\n",[233,1334,1335],{"class":235,"line":528},[233,1336,411],{"emptyLinePlaceholder":410},[233,1338,1339],{"class":235,"line":534},[233,1340,1341],{},"        transport http {\n",[233,1343,1344],{"class":235,"line":545},[233,1345,1346],{},"            dial_timeout 2s\n",[233,1348,1349],{"class":235,"line":551},[233,1350,1351],{},"        }\n",[233,1353,1354],{"class":235,"line":556},[233,1355,1356],{},"    }\n",[233,1358,1359],{"class":235,"line":593},[233,1360,1361],{},"}\n",[11,1363,1364,1365,1367],{},"Quinze linhas. Tudo o que importa está aí: round-robin entre os dois IPs, health check ativo a cada cinco segundos no ",[230,1366,354],{},", marca como unhealthy depois de duas falhas seguidas em 30s, timeout de dois segundos pra abrir conexão.",[11,1369,1370],{},"Sobe Caddy:",[223,1372,1374],{"className":225,"code":1373,"language":227,"meta":228,"style":228},"ssh root@203.0.113.10 \"\n  mkdir -p \u002Fetc\u002Fcaddy &&\n  docker run -d --name caddy --restart=unless-stopped \\\n    --network host \\\n    -v \u002Fetc\u002Fcaddy\u002FCaddyfile:\u002Fetc\u002Fcaddy\u002FCaddyfile \\\n    -v caddy_data:\u002Fdata \\\n    -v caddy_config:\u002Fconfig \\\n    caddy:2-alpine\n\"\n",[230,1375,1376,1384,1389,1396,1403,1410,1417,1424,1429],{"__ignoreMap":228},[233,1377,1378,1380,1382],{"class":235,"line":236},[233,1379,297],{"class":246},[233,1381,300],{"class":254},[233,1383,1155],{"class":254},[233,1385,1386],{"class":235,"line":243},[233,1387,1388],{"class":254},"  mkdir -p \u002Fetc\u002Fcaddy &&\n",[233,1390,1391,1394],{"class":235,"line":270},[233,1392,1393],{"class":254},"  docker run -d --name caddy --restart=unless-stopped ",[233,1395,1168],{"class":382},[233,1397,1398,1401],{"class":235,"line":414},[233,1399,1400],{"class":254},"    --network host ",[233,1402,1168],{"class":382},[233,1404,1405,1408],{"class":235,"line":433},[233,1406,1407],{"class":254},"    -v \u002Fetc\u002Fcaddy\u002FCaddyfile:\u002Fetc\u002Fcaddy\u002FCaddyfile ",[233,1409,1168],{"class":382},[233,1411,1412,1415],{"class":235,"line":458},[233,1413,1414],{"class":254},"    -v caddy_data:\u002Fdata ",[233,1416,1168],{"class":382},[233,1418,1419,1422],{"class":235,"line":463},[233,1420,1421],{"class":254},"    -v caddy_config:\u002Fconfig ",[233,1423,1168],{"class":382},[233,1425,1426],{"class":235,"line":478},[233,1427,1428],{"class":254},"    caddy:2-alpine\n",[233,1430,1431],{"class":235,"line":483},[233,1432,1206],{"class":254},[11,1434,1435,1436,1438],{},"Aponte o DNS A do seu domínio pra ",[230,1437,279],{},". Em alguns minutos:",[223,1440,1442],{"className":225,"code":1441,"language":227,"meta":228,"style":228},"curl https:\u002F\u002Fmeudominio.com\u002F\n# deve retornar \"Hello v1\" (alternando entre as duas instâncias)\n",[230,1443,1444,1451],{"__ignoreMap":228},[233,1445,1446,1448],{"class":235,"line":236},[233,1447,1219],{"class":246},[233,1449,1450],{"class":254}," https:\u002F\u002Fmeudominio.com\u002F\n",[233,1452,1453],{"class":235,"line":243},[233,1454,1455],{"class":239},"# deve retornar \"Hello v1\" (alternando entre as duas instâncias)\n",[11,1457,1458],{},"Caddy emitiu certificado Let's Encrypt automaticamente. Isso funciona porque o domínio resolve pro IP onde Caddy está escutando na porta 80 (challenge HTTP-01).",[18,1460,1462],{"id":1461},"passo-5-script-bash-de-deploy-60-min","Passo 5 — Script bash de deploy (60 min)",[11,1464,1465],{},"Esse é o coração do tutorial. Um script que orquestra rolling update entre as duas VPS:",[223,1467,1469],{"className":225,"code":1468,"language":227,"meta":228,"style":228},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# deploy.sh — rolling deploy zero-downtime entre duas VPS\nset -euo pipefail\n\nIMAGE=\"${1:?Uso: .\u002Fdeploy.sh meuusuario\u002Fmyapp:v2}\"\nHOSTS=(\"203.0.113.10\" \"203.0.113.20\")\nHEALTH_DEADLINE=300   # max segundos esperando health check\nMIN_HEALTHY_TIME=10   # segundos saudável sustentado antes de prosseguir\nSSH_OPTS=\"-o StrictHostKeyChecking=no -o ConnectTimeout=5\"\n\ndeploy_host() {\n  local host=$1\n  local image=$2\n  echo \"==> [${host}] pulling ${image}\"\n  ssh ${SSH_OPTS} \"root@${host}\" \"docker pull ${image}\"\n\n  # guarda imagem antiga pro caso de rollback\n  local old_image\n  old_image=$(ssh ${SSH_OPTS} \"root@${host}\" \"docker inspect app --format '{{.Config.Image}}' 2>\u002Fdev\u002Fnull || echo none\")\n  echo \"==> [${host}] versão atual: ${old_image}\"\n\n  echo \"==> [${host}] substituindo contêiner\"\n  ssh ${SSH_OPTS} \"root@${host}\" \"\n    docker stop app 2>\u002Fdev\u002Fnull || true\n    docker rm app 2>\u002Fdev\u002Fnull || true\n    docker run -d --name app --restart=unless-stopped \\\n      -p 8080:8080 \\\n      -e DATABASE_URL='${DATABASE_URL}' \\\n      --health-cmd='curl -f http:\u002F\u002Flocalhost:8080\u002Fhealthz || exit 1' \\\n      --health-interval=5s --health-timeout=2s --health-retries=3 \\\n      ${image}\n  \"\n\n  echo \"==> [${host}] esperando health check (max ${HEALTH_DEADLINE}s)\"\n  local start=$(date +%s)\n  local healthy_since=0\n  while true; do\n    local now=$(date +%s)\n    if (( now - start > HEALTH_DEADLINE )); then\n      echo \"!!  [${host}] healthy_deadline excedido — fazendo rollback pra ${old_image}\"\n      ssh ${SSH_OPTS} \"root@${host}\" \"\n        docker stop app && docker rm app &&\n        docker run -d --name app --restart=unless-stopped \\\n          -p 8080:8080 -e DATABASE_URL='${DATABASE_URL}' \\\n          ${old_image}\n      \"\n      return 1\n    fi\n\n    if curl -sf --max-time 2 \"http:\u002F\u002F${host}:8080\u002Fhealthz\" > \u002Fdev\u002Fnull; then\n      if (( healthy_since == 0 )); then\n        healthy_since=${now}\n        echo \"    [${host}] saudável — confirmando por ${MIN_HEALTHY_TIME}s\"\n      elif (( now - healthy_since >= MIN_HEALTHY_TIME )); then\n        echo \"==> [${host}] saudável sustentado — promovendo\"\n        return 0\n      fi\n    else\n      healthy_since=0\n    fi\n    sleep 2\n  done\n}\n\necho \"### Deploy ${IMAGE} em ${#HOSTS[@]} hosts (rolling, max_parallel=1)\"\nfor host in \"${HOSTS[@]}\"; do\n  if ! deploy_host \"${host}\" \"${IMAGE}\"; then\n    echo \"### Deploy abortado em ${host}. Hosts anteriores mantidos como estavam.\"\n    exit 1\n  fi\ndone\necho \"### Deploy completo: todos os hosts em ${IMAGE}\"\n",[230,1470,1471,1476,1481,1492,1496,1549,1566,1579,1592,1602,1606,1614,1627,1639,1659,1682,1686,1691,1698,1723,1739,1743,1754,1768,1773,1778,1785,1792,1804,1811,1818,1827,1832,1836,1852,1871,1883,1898,1917,1941,1959,1975,1981,1989,2001,2011,2017,2026,2032,2037,2072,2092,2103,2122,2143,2155,2164,2170,2176,2186,2191,2200,2206,2211,2216,2244,2272,2299,2313,2321,2327,2333],{"__ignoreMap":228},[233,1472,1473],{"class":235,"line":236},[233,1474,1475],{"class":239},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[233,1477,1478],{"class":235,"line":243},[233,1479,1480],{"class":239},"# deploy.sh — rolling deploy zero-downtime entre duas VPS\n",[233,1482,1483,1486,1489],{"class":235,"line":270},[233,1484,1485],{"class":250},"set",[233,1487,1488],{"class":250}," -euo",[233,1490,1491],{"class":254}," pipefail\n",[233,1493,1494],{"class":235,"line":414},[233,1495,411],{"emptyLinePlaceholder":410},[233,1497,1498,1501,1503,1506,1509,1512,1515,1517,1520,1523,1526,1528,1531,1534,1536,1539,1541,1544,1547],{"class":235,"line":433},[233,1499,1500],{"class":386},"IMAGE",[233,1502,472],{"class":382},[233,1504,1505],{"class":254},"\"",[233,1507,1508],{"class":250},"${1",[233,1510,1511],{"class":382},":?",[233,1513,1514],{"class":386},"Uso",[233,1516,1271],{"class":382},[233,1518,1519],{"class":254}," .",[233,1521,1522],{"class":382},"\u002F",[233,1524,1525],{"class":386},"deploy",[233,1527,100],{"class":254},[233,1529,1530],{"class":386},"sh",[233,1532,1533],{"class":386}," meuusuario",[233,1535,1522],{"class":382},[233,1537,1538],{"class":386},"myapp",[233,1540,1271],{"class":382},[233,1542,1543],{"class":386},"v2",[233,1545,1546],{"class":250},"}",[233,1548,1206],{"class":254},[233,1550,1551,1554,1556,1558,1561,1564],{"class":235,"line":458},[233,1552,1553],{"class":386},"HOSTS",[233,1555,472],{"class":382},[233,1557,519],{"class":386},[233,1559,1560],{"class":254},"\"203.0.113.10\"",[233,1562,1563],{"class":254}," \"203.0.113.20\"",[233,1565,525],{"class":386},[233,1567,1568,1571,1573,1576],{"class":235,"line":463},[233,1569,1570],{"class":386},"HEALTH_DEADLINE",[233,1572,472],{"class":382},[233,1574,1575],{"class":254},"300",[233,1577,1578],{"class":239},"   # max segundos esperando health check\n",[233,1580,1581,1584,1586,1589],{"class":235,"line":478},[233,1582,1583],{"class":386},"MIN_HEALTHY_TIME",[233,1585,472],{"class":382},[233,1587,1588],{"class":254},"10",[233,1590,1591],{"class":239},"   # segundos saudável sustentado antes de prosseguir\n",[233,1593,1594,1597,1599],{"class":235,"line":483},[233,1595,1596],{"class":386},"SSH_OPTS",[233,1598,472],{"class":382},[233,1600,1601],{"class":254},"\"-o StrictHostKeyChecking=no -o ConnectTimeout=5\"\n",[233,1603,1604],{"class":235,"line":489},[233,1605,411],{"emptyLinePlaceholder":410},[233,1607,1608,1611],{"class":235,"line":507},[233,1609,1610],{"class":426},"deploy_host",[233,1612,1613],{"class":386},"() {\n",[233,1615,1616,1619,1622,1624],{"class":235,"line":528},[233,1617,1618],{"class":382},"  local",[233,1620,1621],{"class":386}," host",[233,1623,472],{"class":382},[233,1625,1626],{"class":246},"$1\n",[233,1628,1629,1631,1634,1636],{"class":235,"line":534},[233,1630,1618],{"class":382},[233,1632,1633],{"class":386}," image",[233,1635,472],{"class":382},[233,1637,1638],{"class":246},"$2\n",[233,1640,1641,1644,1647,1650,1653,1656],{"class":235,"line":545},[233,1642,1643],{"class":250},"  echo",[233,1645,1646],{"class":254}," \"==> [${",[233,1648,1649],{"class":386},"host",[233,1651,1652],{"class":254},"}] pulling ${",[233,1654,1655],{"class":386},"image",[233,1657,1658],{"class":254},"}\"\n",[233,1660,1661,1664,1667,1670,1672,1675,1678,1680],{"class":235,"line":551},[233,1662,1663],{"class":246},"  ssh",[233,1665,1666],{"class":386}," ${SSH_OPTS} ",[233,1668,1669],{"class":254},"\"root@${",[233,1671,1649],{"class":386},[233,1673,1674],{"class":254},"}\"",[233,1676,1677],{"class":254}," \"docker pull ${",[233,1679,1655],{"class":386},[233,1681,1658],{"class":254},[233,1683,1684],{"class":235,"line":556},[233,1685,411],{"emptyLinePlaceholder":410},[233,1687,1688],{"class":235,"line":593},[233,1689,1690],{"class":239},"  # guarda imagem antiga pro caso de rollback\n",[233,1692,1693,1695],{"class":235,"line":634},[233,1694,1618],{"class":382},[233,1696,1697],{"class":386}," old_image\n",[233,1699,1700,1703,1705,1708,1710,1712,1714,1716,1718,1721],{"class":235,"line":642},[233,1701,1702],{"class":386},"  old_image",[233,1704,472],{"class":382},[233,1706,1707],{"class":386},"$(",[233,1709,297],{"class":246},[233,1711,1666],{"class":386},[233,1713,1669],{"class":254},[233,1715,1649],{"class":386},[233,1717,1674],{"class":254},[233,1719,1720],{"class":254}," \"docker inspect app --format '{{.Config.Image}}' 2>\u002Fdev\u002Fnull || echo none\"",[233,1722,525],{"class":386},[233,1724,1725,1727,1729,1731,1734,1737],{"class":235,"line":658},[233,1726,1643],{"class":250},[233,1728,1646],{"class":254},[233,1730,1649],{"class":386},[233,1732,1733],{"class":254},"}] versão atual: ${",[233,1735,1736],{"class":386},"old_image",[233,1738,1658],{"class":254},[233,1740,1741],{"class":235,"line":682},[233,1742,411],{"emptyLinePlaceholder":410},[233,1744,1745,1747,1749,1751],{"class":235,"line":694},[233,1746,1643],{"class":250},[233,1748,1646],{"class":254},[233,1750,1649],{"class":386},[233,1752,1753],{"class":254},"}] substituindo contêiner\"\n",[233,1755,1756,1758,1760,1762,1764,1766],{"class":235,"line":716},[233,1757,1663],{"class":246},[233,1759,1666],{"class":386},[233,1761,1669],{"class":254},[233,1763,1649],{"class":386},[233,1765,1674],{"class":254},[233,1767,1155],{"class":254},[233,1769,1770],{"class":235,"line":722},[233,1771,1772],{"class":254},"    docker stop app 2>\u002Fdev\u002Fnull || true\n",[233,1774,1775],{"class":235,"line":728},[233,1776,1777],{"class":254},"    docker rm app 2>\u002Fdev\u002Fnull || true\n",[233,1779,1780,1783],{"class":235,"line":733},[233,1781,1782],{"class":254},"    docker run -d --name app --restart=unless-stopped ",[233,1784,1168],{"class":382},[233,1786,1787,1790],{"class":235,"line":770},[233,1788,1789],{"class":254},"      -p 8080:8080 ",[233,1791,1168],{"class":382},[233,1793,1794,1797,1799,1802],{"class":235,"line":775},[233,1795,1796],{"class":254},"      -e DATABASE_URL='${",[233,1798,452],{"class":386},[233,1800,1801],{"class":254},"}' ",[233,1803,1168],{"class":382},[233,1805,1806,1809],{"class":235,"line":814},[233,1807,1808],{"class":254},"      --health-cmd='curl -f http:\u002F\u002Flocalhost:8080\u002Fhealthz || exit 1' ",[233,1810,1168],{"class":382},[233,1812,1813,1816],{"class":235,"line":819},[233,1814,1815],{"class":254},"      --health-interval=5s --health-timeout=2s --health-retries=3 ",[233,1817,1168],{"class":382},[233,1819,1820,1823,1825],{"class":235,"line":825},[233,1821,1822],{"class":254},"      ${",[233,1824,1655],{"class":386},[233,1826,1361],{"class":254},[233,1828,1829],{"class":235,"line":845},[233,1830,1831],{"class":254},"  \"\n",[233,1833,1834],{"class":235,"line":858},[233,1835,411],{"emptyLinePlaceholder":410},[233,1837,1838,1840,1842,1844,1847,1849],{"class":235,"line":871},[233,1839,1643],{"class":250},[233,1841,1646],{"class":254},[233,1843,1649],{"class":386},[233,1845,1846],{"class":254},"}] esperando health check (max ${",[233,1848,1570],{"class":386},[233,1850,1851],{"class":254},"}s)\"\n",[233,1853,1854,1856,1859,1861,1863,1866,1869],{"class":235,"line":897},[233,1855,1618],{"class":382},[233,1857,1858],{"class":386}," start",[233,1860,472],{"class":382},[233,1862,1707],{"class":386},[233,1864,1865],{"class":246},"date",[233,1867,1868],{"class":254}," +%s",[233,1870,525],{"class":386},[233,1872,1873,1875,1878,1880],{"class":235,"line":912},[233,1874,1618],{"class":382},[233,1876,1877],{"class":386}," healthy_since",[233,1879,472],{"class":382},[233,1881,1882],{"class":250},"0\n",[233,1884,1886,1889,1892,1895],{"class":235,"line":1885},37,[233,1887,1888],{"class":382},"  while",[233,1890,1891],{"class":250}," true",[233,1893,1894],{"class":386},"; ",[233,1896,1897],{"class":382},"do\n",[233,1899,1901,1904,1907,1909,1911,1913,1915],{"class":235,"line":1900},38,[233,1902,1903],{"class":382},"    local",[233,1905,1906],{"class":386}," now",[233,1908,472],{"class":382},[233,1910,1707],{"class":386},[233,1912,1865],{"class":246},[233,1914,1868],{"class":254},[233,1916,525],{"class":386},[233,1918,1920,1923,1926,1929,1932,1935,1938],{"class":235,"line":1919},39,[233,1921,1922],{"class":382},"    if",[233,1924,1925],{"class":386}," (( now ",[233,1927,1928],{"class":382},"-",[233,1930,1931],{"class":386}," start ",[233,1933,1934],{"class":382},">",[233,1936,1937],{"class":386}," HEALTH_DEADLINE )); ",[233,1939,1940],{"class":382},"then\n",[233,1942,1944,1947,1950,1952,1955,1957],{"class":235,"line":1943},40,[233,1945,1946],{"class":250},"      echo",[233,1948,1949],{"class":254}," \"!!  [${",[233,1951,1649],{"class":386},[233,1953,1954],{"class":254},"}] healthy_deadline excedido — fazendo rollback pra ${",[233,1956,1736],{"class":386},[233,1958,1658],{"class":254},[233,1960,1962,1965,1967,1969,1971,1973],{"class":235,"line":1961},41,[233,1963,1964],{"class":246},"      ssh",[233,1966,1666],{"class":386},[233,1968,1669],{"class":254},[233,1970,1649],{"class":386},[233,1972,1674],{"class":254},[233,1974,1155],{"class":254},[233,1976,1978],{"class":235,"line":1977},42,[233,1979,1980],{"class":254},"        docker stop app && docker rm app &&\n",[233,1982,1984,1987],{"class":235,"line":1983},43,[233,1985,1986],{"class":254},"        docker run -d --name app --restart=unless-stopped ",[233,1988,1168],{"class":382},[233,1990,1992,1995,1997,1999],{"class":235,"line":1991},44,[233,1993,1994],{"class":254},"          -p 8080:8080 -e DATABASE_URL='${",[233,1996,452],{"class":386},[233,1998,1801],{"class":254},[233,2000,1168],{"class":382},[233,2002,2004,2007,2009],{"class":235,"line":2003},45,[233,2005,2006],{"class":254},"          ${",[233,2008,1736],{"class":386},[233,2010,1361],{"class":254},[233,2012,2014],{"class":235,"line":2013},46,[233,2015,2016],{"class":254},"      \"\n",[233,2018,2020,2023],{"class":235,"line":2019},47,[233,2021,2022],{"class":382},"      return",[233,2024,2025],{"class":250}," 1\n",[233,2027,2029],{"class":235,"line":2028},48,[233,2030,2031],{"class":382},"    fi\n",[233,2033,2035],{"class":235,"line":2034},49,[233,2036,411],{"emptyLinePlaceholder":410},[233,2038,2040,2042,2045,2048,2051,2054,2057,2059,2062,2065,2068,2070],{"class":235,"line":2039},50,[233,2041,1922],{"class":382},[233,2043,2044],{"class":246}," curl",[233,2046,2047],{"class":250}," -sf",[233,2049,2050],{"class":250}," --max-time",[233,2052,2053],{"class":250}," 2",[233,2055,2056],{"class":254}," \"http:\u002F\u002F${",[233,2058,1649],{"class":386},[233,2060,2061],{"class":254},"}:8080\u002Fhealthz\"",[233,2063,2064],{"class":382}," >",[233,2066,2067],{"class":254}," \u002Fdev\u002Fnull",[233,2069,1894],{"class":386},[233,2071,1940],{"class":382},[233,2073,2075,2078,2081,2084,2087,2090],{"class":235,"line":2074},51,[233,2076,2077],{"class":382},"      if",[233,2079,2080],{"class":386}," (( healthy_since ",[233,2082,2083],{"class":382},"==",[233,2085,2086],{"class":250}," 0",[233,2088,2089],{"class":386}," )); ",[233,2091,1940],{"class":382},[233,2093,2095,2098,2100],{"class":235,"line":2094},52,[233,2096,2097],{"class":386},"        healthy_since",[233,2099,472],{"class":382},[233,2101,2102],{"class":386},"${now}\n",[233,2104,2106,2109,2112,2114,2117,2119],{"class":235,"line":2105},53,[233,2107,2108],{"class":250},"        echo",[233,2110,2111],{"class":254}," \"    [${",[233,2113,1649],{"class":386},[233,2115,2116],{"class":254},"}] saudável — confirmando por ${",[233,2118,1583],{"class":386},[233,2120,2121],{"class":254},"}s\"\n",[233,2123,2125,2128,2130,2132,2135,2138,2141],{"class":235,"line":2124},54,[233,2126,2127],{"class":382},"      elif",[233,2129,1925],{"class":386},[233,2131,1928],{"class":382},[233,2133,2134],{"class":386}," healthy_since ",[233,2136,2137],{"class":382},">=",[233,2139,2140],{"class":386}," MIN_HEALTHY_TIME )); ",[233,2142,1940],{"class":382},[233,2144,2146,2148,2150,2152],{"class":235,"line":2145},55,[233,2147,2108],{"class":250},[233,2149,1646],{"class":254},[233,2151,1649],{"class":386},[233,2153,2154],{"class":254},"}] saudável sustentado — promovendo\"\n",[233,2156,2158,2161],{"class":235,"line":2157},56,[233,2159,2160],{"class":382},"        return",[233,2162,2163],{"class":250}," 0\n",[233,2165,2167],{"class":235,"line":2166},57,[233,2168,2169],{"class":382},"      fi\n",[233,2171,2173],{"class":235,"line":2172},58,[233,2174,2175],{"class":382},"    else\n",[233,2177,2179,2182,2184],{"class":235,"line":2178},59,[233,2180,2181],{"class":386},"      healthy_since",[233,2183,472],{"class":382},[233,2185,1882],{"class":254},[233,2187,2189],{"class":235,"line":2188},60,[233,2190,2031],{"class":382},[233,2192,2194,2197],{"class":235,"line":2193},61,[233,2195,2196],{"class":246},"    sleep",[233,2198,2199],{"class":250}," 2\n",[233,2201,2203],{"class":235,"line":2202},62,[233,2204,2205],{"class":382},"  done\n",[233,2207,2209],{"class":235,"line":2208},63,[233,2210,1361],{"class":386},[233,2212,2214],{"class":235,"line":2213},64,[233,2215,411],{"emptyLinePlaceholder":410},[233,2217,2219,2222,2225,2227,2230,2233,2235,2238,2241],{"class":235,"line":2218},65,[233,2220,2221],{"class":250},"echo",[233,2223,2224],{"class":254}," \"### Deploy ${",[233,2226,1500],{"class":386},[233,2228,2229],{"class":254},"} em ${",[233,2231,2232],{"class":382},"#",[233,2234,1553],{"class":386},[233,2236,2237],{"class":254},"[",[233,2239,2240],{"class":382},"@",[233,2242,2243],{"class":254},"]} hosts (rolling, max_parallel=1)\"\n",[233,2245,2247,2250,2253,2256,2259,2261,2263,2265,2268,2270],{"class":235,"line":2246},66,[233,2248,2249],{"class":382},"for",[233,2251,2252],{"class":386}," host ",[233,2254,2255],{"class":382},"in",[233,2257,2258],{"class":254}," \"${",[233,2260,1553],{"class":386},[233,2262,2237],{"class":254},[233,2264,2240],{"class":382},[233,2266,2267],{"class":254},"]}\"",[233,2269,1894],{"class":386},[233,2271,1897],{"class":382},[233,2273,2275,2277,2280,2283,2285,2287,2289,2291,2293,2295,2297],{"class":235,"line":2274},67,[233,2276,596],{"class":382},[233,2278,2279],{"class":382}," !",[233,2281,2282],{"class":246}," deploy_host",[233,2284,2258],{"class":254},[233,2286,1649],{"class":386},[233,2288,1674],{"class":254},[233,2290,2258],{"class":254},[233,2292,1500],{"class":386},[233,2294,1674],{"class":254},[233,2296,1894],{"class":386},[233,2298,1940],{"class":382},[233,2300,2302,2305,2308,2310],{"class":235,"line":2301},68,[233,2303,2304],{"class":250},"    echo",[233,2306,2307],{"class":254}," \"### Deploy abortado em ${",[233,2309,1649],{"class":386},[233,2311,2312],{"class":254},"}. Hosts anteriores mantidos como estavam.\"\n",[233,2314,2316,2319],{"class":235,"line":2315},69,[233,2317,2318],{"class":250},"    exit",[233,2320,2025],{"class":250},[233,2322,2324],{"class":235,"line":2323},70,[233,2325,2326],{"class":382},"  fi\n",[233,2328,2330],{"class":235,"line":2329},71,[233,2331,2332],{"class":382},"done\n",[233,2334,2336,2338,2341,2343],{"class":235,"line":2335},72,[233,2337,2221],{"class":250},[233,2339,2340],{"class":254}," \"### Deploy completo: todos os hosts em ${",[233,2342,1500],{"class":386},[233,2344,1658],{"class":254},[11,2346,2347,2348,2351,2352,2355],{},"Salva como ",[230,2349,2350],{},"deploy.sh",", dá ",[230,2353,2354],{},"chmod +x",", e:",[223,2357,2359],{"className":225,"code":2358,"language":227,"meta":228,"style":228},"export DATABASE_URL='postgres:\u002F\u002Fuser:pass@db.example.com:5432\u002Fapp'\n.\u002Fdeploy.sh meuusuario\u002Fmyapp:v2\n",[230,2360,2361,2374],{"__ignoreMap":228},[233,2362,2363,2366,2369,2371],{"class":235,"line":236},[233,2364,2365],{"class":382},"export",[233,2367,2368],{"class":386}," DATABASE_URL",[233,2370,472],{"class":382},[233,2372,2373],{"class":254},"'postgres:\u002F\u002Fuser:pass@db.example.com:5432\u002Fapp'\n",[233,2375,2376,2379],{"class":235,"line":243},[233,2377,2378],{"class":246},".\u002Fdeploy.sh",[233,2380,2381],{"class":254}," meuusuario\u002Fmyapp:v2\n",[11,2383,2384],{},"O algoritmo é literalmente o que orquestradores grandes fazem internamente:",[66,2386,2387,2393,2407,2413,2418,2424,2437],{},[69,2388,2389,2392],{},[26,2390,2391],{},"Para cada host, sequencialmente"," (max_parallel = 1)",[69,2394,2395,2398,2399,2402,2403,2406],{},[26,2396,2397],{},"Pull da nova imagem"," antes de mexer no contêiner — assim o downtime entre ",[230,2400,2401],{},"docker stop"," e ",[230,2404,2405],{},"docker run"," é mínimo",[69,2408,2409,2412],{},[26,2410,2411],{},"Guarda referência da imagem antiga"," pra rollback se algo der errado",[69,2414,2415],{},[26,2416,2417],{},"Substitui o contêiner",[69,2419,2420,2423],{},[26,2421,2422],{},"Loop esperando health check"," com deadline de cinco minutos",[69,2425,2426,2429,2430,2432,2433,2436],{},[26,2427,2428],{},"Min healthy time de dez segundos",": só avança quando o ",[230,2431,354],{}," retornou 200 ",[178,2434,2435],{},"sustentadamente"," por dez segundos (se cair no meio, reinicia a contagem)",[69,2438,2439,2442],{},[26,2440,2441],{},"Rollback automático"," se passar do deadline",[11,2444,2445],{},"Os números (max_parallel: 1, min_healthy_time: 10s, healthy_deadline: 300s) são exatamente os defaults que usamos no HeroCtl. Não é coincidência — são os valores que sobreviveram a anos de tentativa e erro. Min healthy time muito curto detecta sintomas transitórios como \"saudável\" e quebra; muito longo deixa o deploy lento sem ganho. Dez segundos é o ponto onde ruído some e o deploy ainda termina rápido.",[18,2447,2449],{"id":2448},"passo-6-validar-com-teste-de-carga-durante-deploy-15-min","Passo 6 — Validar com teste de carga durante deploy (15 min)",[11,2451,2452],{},"Esse é o teste de fogo: rodar carga sustentada e fazer deploy ao mesmo tempo. Se algum 5xx aparecer, alguma parte do esquema está quebrada.",[11,2454,2455],{},"Numa máquina externa (seu laptop ou outra VPS):",[223,2457,2459],{"className":225,"code":2458,"language":227,"meta":228,"style":228},"# instale hey\ngo install github.com\u002Frakyll\u002Fhey@latest\n\n# carga sustentada de 60s, 5 conexões concorrentes\nhey -z 60s -c 5 https:\u002F\u002Fmeudominio.com\u002F\n",[230,2460,2461,2466,2477,2481,2486],{"__ignoreMap":228},[233,2462,2463],{"class":235,"line":236},[233,2464,2465],{"class":239},"# instale hey\n",[233,2467,2468,2471,2474],{"class":235,"line":243},[233,2469,2470],{"class":246},"go",[233,2472,2473],{"class":254}," install",[233,2475,2476],{"class":254}," github.com\u002Frakyll\u002Fhey@latest\n",[233,2478,2479],{"class":235,"line":270},[233,2480,411],{"emptyLinePlaceholder":410},[233,2482,2483],{"class":235,"line":414},[233,2484,2485],{"class":239},"# carga sustentada de 60s, 5 conexões concorrentes\n",[233,2487,2488,2491,2494,2497,2500,2503],{"class":235,"line":433},[233,2489,2490],{"class":246},"hey",[233,2492,2493],{"class":250}," -z",[233,2495,2496],{"class":254}," 60s",[233,2498,2499],{"class":250}," -c",[233,2501,2502],{"class":250}," 5",[233,2504,1450],{"class":254},[11,2506,2507],{},"Em outra janela, simultaneamente:",[223,2509,2511],{"className":225,"code":2510,"language":227,"meta":228,"style":228},".\u002Fdeploy.sh meuusuario\u002Fmyapp:v2\n",[230,2512,2513],{"__ignoreMap":228},[233,2514,2515,2517],{"class":235,"line":236},[233,2516,2378],{"class":246},[233,2518,2381],{"class":254},[11,2520,2521,2522,1271],{},"No final do ",[230,2523,2490],{},[223,2525,2530],{"className":2526,"code":2528,"language":2529},[2527],"language-text","Status code distribution:\n  [200] 1847 responses\n","text",[230,2531,2528],{"__ignoreMap":228},[11,2533,2534],{},"Só 200. Se aparecer um 502 ou 503, alguma das três peças tá fraca: health check retornando 200 cedo demais, graceful shutdown ausente, ou min healthy time curto. Investiga e corrige.",[56,2536],{},[18,2538,2540],{"id":2539},"os-seis-detalhes-que-separam-zero-downtime-real-de-aproximacao","Os seis detalhes que separam zero-downtime real de aproximação",[11,2542,2543],{},"Cobrimos boa parte deles ao longo do tutorial, mas vale consolidar — porque um único desses ausentes converte o esquema todo em \"mostly zero-downtime\", que é diferente.",[66,2545,2546,2555,2572,2588,2594,2600],{},[69,2547,2548,2551,2552,2554],{},[26,2549,2550],{},"Connection draining no SIGTERM."," Quando o contêiner recebe sinal de parada, a app marca ",[230,2553,354],{}," como falhando imediatamente, mas continua aceitando conexões em curso por alguns segundos. Sem isso, conexões abertas no momento do stop são cortadas.",[69,2556,2557,2560,2561,2564,2565,2568,2569,100],{},[26,2558,2559],{},"Pre-stop hook se você tem worker assíncrono."," Filas que processam jobs em background precisam de pausa explícita antes de matar o processo, ou o job em execução fica órfão. Em Sidekiq, é o ",[230,2562,2563],{},":quiet"," antes de ",[230,2566,2567],{},":term",". Em Celery, é ",[230,2570,2571],{},"--soft-time-limit",[69,2573,2574,2577,2578,2581,2582,2584,2585,2587],{},[26,2575,2576],{},"Health check ANTES de promover, não \"container running\"."," ",[230,2579,2580],{},"docker ps"," mostra \"running\" milisegundos depois do ",[230,2583,2405],{},". Não significa nada. Promover só depois de ",[230,2586,354],{}," retornar 200 sustentadamente.",[69,2589,2590,2593],{},[26,2591,2592],{},"Min healthy time de dez segundos sustentados."," Não vale ver um único 200 e seguir em frente — apps com warm-up irregular passam um momento e voltam a falhar.",[69,2595,2596,2599],{},[26,2597,2598],{},"Versão anterior pré-puxada pra rollback rápido."," Se você confiou em \"manter a imagem antiga em cache do Docker\", em algum momento ela é apagada por garbage collection e o rollback fica lento. Mantenha as últimas três imagens explicitamente.",[69,2601,2602,2605],{},[26,2603,2604],{},"Auto-revert ao passar do healthy deadline."," Sem isso, o deploy trava num estado parcial — metade dos hosts em v2, metade em v1, sem ninguém pra decidir o que fazer.",[18,2607,2609],{"id":2608},"database-migrations-zero-downtime-a-parte-que-quebra-deploy-de-gente-experiente","Database migrations + zero-downtime (a parte que quebra deploy de gente experiente)",[11,2611,2612,2613,2616],{},"Esse é o tópico que eu mais vejo desenvolvedor sênior errar. Rolling deploy assume que ",[26,2614,2615],{},"as duas versões da app rodam simultaneamente em produção por algum período",". Se a v2 espera schema incompatível com o que a v1 entende, alguma das duas quebra durante a janela de transição.",[11,2618,2619,2620,100],{},"Regra de ouro inegociável: ",[26,2621,2622],{},"migrations são sempre backward-compatible",[11,2624,2625,2626,2629,2630,2633,2634,2636],{},"Caso clássico: você quer renomear coluna ",[230,2627,2628],{},"email"," pra ",[230,2631,2632],{},"email_address",". Solução errada: faz a migration que renomeia direto antes do deploy. Resultado: durante o rolling, instâncias v1 ainda escrevem em ",[230,2635,2628],{}," (que não existe mais) e quebram. Solução certa, em três deploys:",[118,2638,2639,2652],{},[121,2640,2641],{},[124,2642,2643,2646,2649],{},[127,2644,2645],{},"Deploy",[127,2647,2648],{},"Migration",[127,2650,2651],{},"Código v*",[140,2653,2654,2676,2694],{},[124,2655,2656,2659,2665],{},[145,2657,2658],{},"1",[145,2660,2661,2662,2664],{},"Adiciona ",[230,2663,2632],{}," (nullable). Nenhuma remoção.",[145,2666,2667,2668,2670,2671,2673,2674,100],{},"App escreve em ",[230,2669,2628],{}," E em ",[230,2672,2632],{},"; lê de ",[230,2675,2628],{},[124,2677,2678,2681,2688],{},[145,2679,2680],{},"2",[145,2682,2683,2684,2687],{},"Backfill: ",[230,2685,2686],{},"UPDATE users SET email_address = email WHERE email_address IS NULL",". NOT NULL constraint.",[145,2689,2690,2691,2693],{},"App lê de ",[230,2692,2632],{},"; ainda escreve nas duas.",[124,2695,2696,2699,2704],{},[145,2697,2698],{},"3",[145,2700,2701,2702,100],{},"Drop ",[230,2703,2628],{},[145,2705,2706,2707,100],{},"App só usa ",[230,2708,2632],{},[11,2710,2711],{},"Três deploys, semanas de espaçamento. É chato, é o jeito. Drop de coluna direto sempre quebra. Mudança de tipo direto sempre quebra. Adicionar NOT NULL sem default direto sempre quebra.",[11,2713,2714,2715,2402,2718,2721,2722,2725],{},"Ferramentas que ajudam: ",[230,2716,2717],{},"pg-osc",[230,2719,2720],{},"pgroll"," (Postgres), ",[230,2723,2724],{},"gh-ost"," (MySQL) — fazem schema change online, sem lock longo. Pra migrations leves, o jeito manual em três passos resolve.",[18,2727,2729],{"id":2728},"padroes-alem-de-rolling","Padrões além de rolling",[11,2731,2732],{},"Rolling é o padrão default e mais econômico. Tem outros que valem conhecer:",[2734,2735,2736,2742,2748],"ul",{},[69,2737,2738,2741],{},[26,2739,2740],{},"Blue-green."," Dois ambientes paralelos completos — \"blue\" rodando v1, \"green\" provisionado com v2 vazio. Você sobe v2 inteiro em green, valida, troca DNS (ou cutover do balanceador). Vantagem: rollback instantâneo (volta DNS pra blue). Desvantagem: custa o dobro de recursos durante a janela de deploy.",[69,2743,2744,2747],{},[26,2745,2746],{},"Canary."," Manda 5% do tráfego pra v2, observa métricas (erros, latência, taxa de conversão), decide se promove pra 100% ou aborta. Detecta bugs sutis que health check não pega — tipo regression em conversão de checkout. Exige proxy com weighted routing e observabilidade decente.",[69,2749,2750,2753],{},[26,2751,2752],{},"Rainbow \u002F N+1."," Generalização do blue-green com N versões coexistindo. Útil quando você quer A\u002FB test de longa duração entre versões inteiras.",[11,2755,2756],{},"Pro tutorial, rolling é o que faz sentido. Os outros valem quando o tamanho do tráfego justifica investimento extra.",[18,2758,2760],{"id":2759},"versao-facil-coolify-ou-dokploy","Versão \"fácil\" — Coolify ou Dokploy",[11,2762,2763],{},"Se você não quer scriptar, dois painéis modernos fazem rolling deploy automaticamente:",[2734,2765,2766,2772],{},[69,2767,2768,2771],{},[26,2769,2770],{},"Coolify"," em modo multi-server faz rolling com health check configurável. Multi-server foi adicionado nas versões mais recentes — antes era single-server only. Vale checar a versão.",[69,2773,2774,2777,2778,2781],{},[26,2775,2776],{},"Dokploy"," em cima de Docker Swarm faz rolling com ",[230,2779,2780],{},"--update-parallelism 1 --update-delay",". Aproveita o que o Swarm já oferece.",[11,2783,2784],{},"Trade-off: você troca o script de cinquenta linhas (que entende tudo o que está acontecendo) por um painel (que é mais rápido pra subir, mas vira caixa-preta quando algo dá errado). Pra time pequeno onde uma pessoa cuida de operação parcialmente, o painel ganha. Pra time onde você precisa entender exatamente o que rolou na 3h da manhã, o script ganha.",[18,2786,2788],{"id":2787},"versao-robusta-heroctl","Versão \"robusta\" — HeroCtl",[11,2790,2791],{},"Pra quem quer parar de scriptar mas não quer caixa-preta, o HeroCtl combina rolling deploy automático com plano de controle replicado. Você descreve o serviço em arquivo de configuração e o orquestrador faz o resto:",[223,2793,2797],{"className":2794,"code":2795,"language":2796,"meta":228,"style":228},"language-hcl shiki shiki-themes github-dark-default","job \"minhaapp\" {\n  group \"web\" {\n    count = 2\n\n    task \"app\" {\n      driver = \"docker\"\n      config {\n        image = \"meuusuario\u002Fmyapp:v2\"\n        ports = [\"http\"]\n      }\n\n      service {\n        port = \"http\"\n        check {\n          type     = \"http\"\n          path     = \"\u002Fhealthz\"\n          interval = \"5s\"\n          timeout  = \"2s\"\n        }\n      }\n    }\n\n    update {\n      max_parallel      = 1\n      min_healthy_time  = \"10s\"\n      healthy_deadline  = \"5m\"\n      auto_revert       = true\n    }\n  }\n}\n","hcl",[230,2798,2799,2804,2809,2814,2818,2823,2828,2833,2838,2843,2848,2852,2857,2862,2867,2872,2877,2882,2887,2891,2895,2899,2903,2908,2913,2918,2923,2928,2932,2936],{"__ignoreMap":228},[233,2800,2801],{"class":235,"line":236},[233,2802,2803],{},"job \"minhaapp\" {\n",[233,2805,2806],{"class":235,"line":243},[233,2807,2808],{},"  group \"web\" {\n",[233,2810,2811],{"class":235,"line":270},[233,2812,2813],{},"    count = 2\n",[233,2815,2816],{"class":235,"line":414},[233,2817,411],{"emptyLinePlaceholder":410},[233,2819,2820],{"class":235,"line":433},[233,2821,2822],{},"    task \"app\" {\n",[233,2824,2825],{"class":235,"line":458},[233,2826,2827],{},"      driver = \"docker\"\n",[233,2829,2830],{"class":235,"line":463},[233,2831,2832],{},"      config {\n",[233,2834,2835],{"class":235,"line":478},[233,2836,2837],{},"        image = \"meuusuario\u002Fmyapp:v2\"\n",[233,2839,2840],{"class":235,"line":483},[233,2841,2842],{},"        ports = [\"http\"]\n",[233,2844,2845],{"class":235,"line":489},[233,2846,2847],{},"      }\n",[233,2849,2850],{"class":235,"line":507},[233,2851,411],{"emptyLinePlaceholder":410},[233,2853,2854],{"class":235,"line":528},[233,2855,2856],{},"      service {\n",[233,2858,2859],{"class":235,"line":534},[233,2860,2861],{},"        port = \"http\"\n",[233,2863,2864],{"class":235,"line":545},[233,2865,2866],{},"        check {\n",[233,2868,2869],{"class":235,"line":551},[233,2870,2871],{},"          type     = \"http\"\n",[233,2873,2874],{"class":235,"line":556},[233,2875,2876],{},"          path     = \"\u002Fhealthz\"\n",[233,2878,2879],{"class":235,"line":593},[233,2880,2881],{},"          interval = \"5s\"\n",[233,2883,2884],{"class":235,"line":634},[233,2885,2886],{},"          timeout  = \"2s\"\n",[233,2888,2889],{"class":235,"line":642},[233,2890,1351],{},[233,2892,2893],{"class":235,"line":658},[233,2894,2847],{},[233,2896,2897],{"class":235,"line":682},[233,2898,1356],{},[233,2900,2901],{"class":235,"line":694},[233,2902,411],{"emptyLinePlaceholder":410},[233,2904,2905],{"class":235,"line":716},[233,2906,2907],{},"    update {\n",[233,2909,2910],{"class":235,"line":722},[233,2911,2912],{},"      max_parallel      = 1\n",[233,2914,2915],{"class":235,"line":728},[233,2916,2917],{},"      min_healthy_time  = \"10s\"\n",[233,2919,2920],{"class":235,"line":733},[233,2921,2922],{},"      healthy_deadline  = \"5m\"\n",[233,2924,2925],{"class":235,"line":770},[233,2926,2927],{},"      auto_revert       = true\n",[233,2929,2930],{"class":235,"line":775},[233,2931,1356],{},[233,2933,2934],{"class":235,"line":814},[233,2935,719],{},[233,2937,2938],{"class":235,"line":819},[233,2939,1361],{},[11,2941,2942],{},"Os mesmos parâmetros do script bash, declarativos. A diferença é que o orquestrador coordena rolling entre N servidores (não só dois), faz eleição automática de coordenador em torno de sete segundos se o nó atual cair, e mantém o plano de controle distribuído entre os primeiros três servidores. Cluster sobrevive a perda de qualquer servidor único sem intervenção humana.",[11,2944,2945],{},"Instalação:",[223,2947,2949],{"className":225,"code":2948,"language":227,"meta":228,"style":228},"curl -sSL https:\u002F\u002Fget.heroctl.com\u002Finstall.sh | sh\n",[230,2950,2951],{"__ignoreMap":228},[233,2952,2953,2955,2958,2961,2964],{"class":235,"line":236},[233,2954,1219],{"class":246},[233,2956,2957],{"class":250}," -sSL",[233,2959,2960],{"class":254}," https:\u002F\u002Fget.heroctl.com\u002Finstall.sh",[233,2962,2963],{"class":382}," |",[233,2965,2966],{"class":246}," sh\n",[11,2968,2969],{},"Plano Community é gratuito permanente — sem limite de servidores ou jobs, com todas as features de orquestração descritas no tutorial. Plano Business adiciona SSO\u002FSAML, RBAC granular, auditoria detalhada e suporte com SLA, pra times que têm requisitos formais de plataforma. Plano Enterprise adiciona escrow de código-fonte, contrato de continuidade e suporte 24×7. Os preços de Business e Enterprise estão publicados na página de planos — sem \"fale com vendas\" obrigatório.",[18,2971,2973],{"id":2972},"comparativo-cinco-caminhos-lado-a-lado","Comparativo: cinco caminhos lado a lado",[118,2975,2976,3001],{},[121,2977,2978],{},[124,2979,2980,2983,2986,2989,2992,2995,2998],{},[127,2981,2982],{},"Critério",[127,2984,2985],{},"Script bash (2 servers)",[127,2987,2988],{},"Coolify multi-server",[127,2990,2991],{},"Dokploy + Swarm",[127,2993,2994],{},"HeroCtl",[127,2996,2997],{},"Kamal",[127,2999,3000],{},"Kubernetes",[140,3002,3003,3025,3048,3070,3088,3105,3127,3147],{},[124,3004,3005,3008,3011,3014,3017,3020,3022],{},[145,3006,3007],{},"Tempo de setup",[145,3009,3010],{},"2-3h",[145,3012,3013],{},"30 min",[145,3015,3016],{},"1h",[145,3018,3019],{},"5 min",[145,3021,3016],{},[145,3023,3024],{},"4h-4 dias",[124,3026,3027,3030,3033,3036,3039,3042,3045],{},[145,3028,3029],{},"Linhas de config",[145,3031,3032],{},"~50 (script)",[145,3034,3035],{},"UI",[145,3037,3038],{},"~20",[145,3040,3041],{},"~50",[145,3043,3044],{},"~40",[145,3046,3047],{},"300+",[124,3049,3050,3053,3056,3059,3062,3065,3067],{},[145,3051,3052],{},"HA do plano de controle",[145,3054,3055],{},"N\u002FA",[145,3057,3058],{},"Não",[145,3060,3061],{},"Limitado",[145,3063,3064],{},"Sim",[145,3066,3055],{},[145,3068,3069],{},"Sim (5+ componentes)",[124,3071,3072,3075,3078,3080,3082,3084,3086],{},[145,3073,3074],{},"Health check declarativo",[145,3076,3077],{},"Manual",[145,3079,3064],{},[145,3081,3064],{},[145,3083,3064],{},[145,3085,3064],{},[145,3087,3064],{},[124,3089,3090,3092,3095,3097,3099,3101,3103],{},[145,3091,2441],{},[145,3093,3094],{},"Manual no script",[145,3096,3064],{},[145,3098,3064],{},[145,3100,3064],{},[145,3102,3064],{},[145,3104,3064],{},[124,3106,3107,3110,3113,3116,3119,3122,3124],{},[145,3108,3109],{},"Escala alvo",[145,3111,3112],{},"1-3 servers",[145,3114,3115],{},"1-10 servers",[145,3117,3118],{},"1-20 servers",[145,3120,3121],{},"1-500 servers",[145,3123,3115],{},[145,3125,3126],{},"50+ servers",[124,3128,3129,3132,3135,3137,3140,3143,3145],{},[145,3130,3131],{},"Caixa-preta?",[145,3133,3134],{},"Não (você escreveu)",[145,3136,3064],{},[145,3138,3139],{},"Parcial",[145,3141,3142],{},"Não (declarativo curto)",[145,3144,3058],{},[145,3146,3064],{},[124,3148,3149,3152,3155,3157,3160,3162,3164],{},[145,3150,3151],{},"Curva de aprendizado",[145,3153,3154],{},"Baixa",[145,3156,3154],{},[145,3158,3159],{},"Média",[145,3161,3154],{},[145,3163,3154],{},[145,3165,3166],{},"Alta",[11,3168,3169],{},"Cada coluna tem seu nicho. Script bash é insuperável quando você quer entender cada linha. Coolify ganha quando você só quer um painel. HeroCtl ganha quando você precisa de HA real sem montar plano de controle externo. Kubernetes ganha em escala planetária — onde a complexidade compensa.",[18,3171,3173],{"id":3172},"os-cinco-erros-mais-comuns","Os cinco erros mais comuns",[66,3175,3176,3188,3194,3204,3216],{},[69,3177,3178,3184,3185,3187],{},[26,3179,3180,3181,3183],{},"Health check em ",[230,3182,1522],{}," retornando 200 sem validar dependências."," A app retorna 200 antes de conectar no banco, o proxy promove, e o usuário vê erro 500 nas primeiras requests. Solução: ",[230,3186,354],{}," valida banco, cache, fila — qualquer coisa que a app precise pra responder de verdade.",[69,3189,3190,3193],{},[26,3191,3192],{},"Min healthy time de 1 segundo."," Apps com warm-up irregular podem retornar 200 num momento e 503 logo depois (cache populando, classe sendo lazy-loaded). O orquestrador promove na primeira janela boa, e a próxima request bate em estado ruim. Dez segundos sustentados eliminam noventa por cento desses casos.",[69,3195,3196,3199,3200,3203],{},[26,3197,3198],{},"Sem max_parallel (ou max_parallel = N)."," Se você troca todas as instâncias juntas, durante a janela do cutover não tem ninguém saudável servindo. É single-server downtime disfarçado. Sempre ",[230,3201,3202],{},"max_parallel = 1"," pra começar.",[69,3205,3206,3209,3210,3212,3213,3215],{},[26,3207,3208],{},"Mix de versões em produção sem schema compat."," v1 escreve em ",[230,3211,2628],{},", v2 lê de ",[230,3214,2632],{},", e durante o rolling de cinco minutos as duas convivem — usuários que pegam v2 não veem dados que v1 acabou de gravar. Migration backward-compatible em três passos resolve.",[69,3217,3218,3221],{},[26,3219,3220],{},"Cache stale no cliente (CDN, browser, service worker)."," Backend já é v2 mas o usuário tem o JS de v1 em cache, e o JS antigo chama API que não existe mais. Solução: mantém endpoints antigos por uma janela; versionamento de API; cache-busting forte em assets críticos.",[18,3223,3225],{"id":3224},"faq","FAQ",[11,3227,3228],{},[26,3229,3230],{},"Posso fazer zero-downtime com um servidor só?",[11,3232,3233,3234,3237],{},"Não. Toda variação que prometa isso tem janela de erro mensurável quando você mede com ",[230,3235,3236],{},"hey -c 20",". A única forma de ter zero-downtime real é manter pelo menos uma instância sempre saudável durante todo o deploy — o que exige duas máquinas no mínimo.",[11,3239,3240],{},[26,3241,3242],{},"DNS round-robin funciona como balanceador?",[11,3244,3245],{},"Funciona como balanceador básico, mas não como mecanismo de health check. DNS não tira IP morto da rotação rapidamente — TTLs caching em ISPs e clientes mantêm o IP errado em uso por minutos ou horas. Pra zero-downtime você precisa de um proxy real (Caddy, nginx, HAProxy) que tira instância unhealthy do balanceamento em segundos.",[11,3247,3248],{},[26,3249,3250],{},"Caddy ou Traefik — qual é melhor pra esse setup?",[11,3252,3253],{},"Pra dois servidores e um setup estático, Caddy é mais simples — Caddyfile de quinze linhas resolve. Traefik brilha quando você tem descoberta dinâmica de serviços (tipo Docker labels ou Consul) e muitos backends mudando o tempo todo. nginx fica no meio: mais flexível, sem TLS automático embutido (precisa de certbot externo). Pra esse tutorial, Caddy.",[11,3255,3256],{},[26,3257,3258],{},"WebSocket connections sobrevivem durante rolling?",[11,3260,3261,3262,3264],{},"Conexões abertas em instância que está sendo derrubada são cortadas. O cliente tem que reconectar. Boa biblioteca de WebSocket (Socket.IO, Phoenix Channels) reconecta automaticamente — usuário vê uma piscada de meio segundo no estado. Connection draining ajuda: a instância marca ",[230,3263,354],{}," falhando, o proxy para de mandar conexões novas, mas as existentes continuam até o pre-stop timer. Trinta segundos de drain costumam ser suficientes pra que conexões longas se esvaziem naturalmente.",[11,3266,3267],{},[26,3268,3269],{},"Database migrations — qual a regra de ouro?",[11,3271,3272],{},"Toda migration tem que ser backward-compatible. Drop de coluna nunca direto. Rename nunca direto. Mudança de tipo nunca direto. Em vez disso, três deploys: adiciona estrutura nova, backfill, remove a antiga. Lento, sim. Mas o rolling deploy depende disso pra não quebrar.",[11,3274,3275],{},[26,3276,3277],{},"Rollback automático — como implementar?",[11,3279,3280,3281,100],{},"Duas peças: deadline (tempo máximo esperando health check) e referência da imagem anterior pré-puxada. Se passar do deadline sem ficar saudável, o script reinstala a versão anterior. O exemplo no Passo 5 faz exatamente isso. Em orquestradores declarativos, vira ",[230,3282,3283],{},"auto_revert = true",[11,3285,3286],{},[26,3287,3288],{},"Sticky sessions complicam zero-downtime?",[11,3290,3291],{},"Sim. Se a app guarda estado de sessão em memória do processo, derrubar a instância derruba as sessões dos usuários conectados a ela. Solução: tira sessão da memória — Redis, Postgres ou JWT signed. Aí qualquer instância serve qualquer usuário, e rolling não corta nenhuma sessão.",[11,3293,3294],{},[26,3295,3296],{},"Quanto tempo demora um deploy completo?",[11,3298,3299,3300,3303],{},"Dois servidores, app que sobe em quinze segundos: cerca de um minuto. Detalhamento: pull da imagem (5-15s, depende da rede e do tamanho), substituição do contêiner (1s), warm-up + health check (10-30s), min healthy time de 10s, total uns 30-50s por host, multiplicado por dois hosts em sequência = 1-2 min. Quatro servidores ficam em torno de 2-4 min. Com cinquenta servidores, deploy começa a tomar dez ou quinze minutos — momento de aumentar ",[230,3301,3302],{},"max_parallel"," pra dois ou três (mantendo health check rigoroso).",[56,3305],{},[18,3307,3309],{"id":3308},"fechamento","Fechamento",[11,3311,3312],{},"Zero-downtime deploy é arquitetura, não ferramenta. Os três ingredientes — múltiplas instâncias, proxy com health check, rolling controlado — funcionam com bash e Caddy tanto quanto com orquestrador grande. A diferença está em quanto da operação você quer escrever na mão e quanto delegar.",[11,3314,3315],{},"Pra um SaaS pequeno, três VPS e um script de cinquenta linhas resolvem indefinidamente. Quando o cluster cresce pra dezenas de servidores ou o time precisa de HA real do plano de controle, vale subir pro orquestrador declarativo:",[223,3317,3318],{"className":225,"code":2948,"language":227,"meta":228,"style":228},[230,3319,3320],{"__ignoreMap":228},[233,3321,3322,3324,3326,3328,3330],{"class":235,"line":236},[233,3323,1219],{"class":246},[233,3325,2957],{"class":250},[233,3327,2960],{"class":254},[233,3329,2963],{"class":382},[233,3331,2966],{"class":246},[11,3333,3334,3335,3340,3341,3345],{},"Mais sobre o algoritmo de rolling em ",[3336,3337,3339],"a",{"href":3338},"\u002Fblog\u002Frolling-deploy-seguro-por-que-o-seu-talvez-nao-seja","Rolling deploy seguro: por que o seu talvez não seja",". Pra quem está saindo de Compose pra setup multi-servidor, ",[3336,3342,3344],{"href":3343},"\u002Fblog\u002Fdeploy-docker-producao-do-compose-ao-cluster","Deploy Docker em produção: do compose ao cluster"," cobre o caminho intermediário.",[11,3347,3348],{},"Orquestração de contêineres, sem cerimônia.",[3350,3351,3352],"style",{},"html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}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 .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}",{"title":228,"searchDepth":243,"depth":243,"links":3354},[3355,3356,3357,3358,3359,3360,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377],{"id":20,"depth":243,"text":21},{"id":60,"depth":243,"text":61},{"id":92,"depth":243,"text":93},{"id":112,"depth":243,"text":113},{"id":214,"depth":243,"text":215},{"id":347,"depth":243,"text":348,"children":3361},[3362,3363,3364],{"id":369,"depth":270,"text":370},{"id":917,"depth":270,"text":918},{"id":1001,"depth":270,"text":1002},{"id":1098,"depth":243,"text":1099},{"id":1240,"depth":243,"text":1241},{"id":1461,"depth":243,"text":1462},{"id":2448,"depth":243,"text":2449},{"id":2539,"depth":243,"text":2540},{"id":2608,"depth":243,"text":2609},{"id":2728,"depth":243,"text":2729},{"id":2759,"depth":243,"text":2760},{"id":2787,"depth":243,"text":2788},{"id":2972,"depth":243,"text":2973},{"id":3172,"depth":243,"text":3173},{"id":3224,"depth":243,"text":3225},{"id":3308,"depth":243,"text":3309},"engenharia",null,"2026-06-09","Você não precisa de Kubernetes pra ter deploy sem downtime. Tutorial completo com 2 servidores, Caddy\u002FTraefik na frente, e rolling update via script ou orquestrador leve.",false,"md",{},"\u002Fblog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial","15 min",{"title":5,"description":3381},{"loc":3385},"blog\u002Fdeploy-zero-downtime-sem-kubernetes-tutorial",[1525,3391,3392,3378],"zero-downtime","tutorial","6-My071frsY_ilBftAnrX2BPg0OBLI-wOOVTkQBW3O0",[3395,3400],{"title":3396,"path":3343,"stem":3397,"description":3398,"date":3399,"category":3378,"children":-1},"Deploy de Docker em produção: do compose ao cluster com alta disponibilidade","blog\u002Fdeploy-docker-producao-do-compose-ao-cluster","Docker Compose resolve o dev. Em produção basta até um servidor sem SLA. Acima disso, você precisa de cluster real. Trajetória honesta dos quatro estágios de maturidade.","2026-04-21",{"title":3401,"path":3402,"stem":3403,"description":3404,"date":3405,"category":3406,"children":-1},"GitHub Actions vs GitLab CI vs Drone: qual CI\u002FCD escolher pra startup brasileira","\u002Fblog\u002Fgithub-actions-vs-gitlab-ci-vs-drone-self-hosted","blog\u002Fgithub-actions-vs-gitlab-ci-vs-drone-self-hosted","GitHub Actions venceu mindshare mas tem custos de minutos. GitLab CI é mais completo mas pesa mais. Drone (e Woodpecker) auto-hospedado roda em VPS pequeno. Comparação prática.","2026-05-15","comparativo",{"path":3408},"\u002Fen\u002Fblog\u002Fzero-downtime-deploy-without-kubernetes",1777362205658]