Meus deploys de produção levam 15 minutos. Eu não fico assistindo. Não prendo a respiração. Faço um chá, e quando ele fica pronto, a nova versão já está no ar.

Nem sempre foi assim.

Dois anos atrás, deploy era um ritual. Bloquear uma hora na agenda. Avisar o time. Rodar o script manualmente. Assistir os logs. Rezar. Corrigir o que quebrou. Rodar de novo. Rezar com mais força. O processo inteiro levava 2-3 horas e deixava todo mundo exausto. Fazíamos deploy na sexta porque nos odiávamos.

Se isso soa familiar, você está rodando deploys na adrenalina em vez de sistemas. Estamos em março de 2026, não existe mais motivo pra isso. Aqui está como transformei um festival de ansiedade de 3 horas em 15 minutos entediantes. ⚙️

O problema: onze passos na cabeça de alguém

Deploy — o processo de enviar código novo do seu computador para o servidor onde os usuários realmente o veem — significava 11 coisas diferentes ao mesmo tempo. Puxar o código. Instalar dependências (bibliotecas externas que seu app precisa). Rodar migrations (atualizar a estrutura do banco de dados para bater com o código novo). Compilar assets. Reiniciar o serviço. Limpar o cache. Atualizar o reverse proxy. Verificar o health endpoint.

Tudo isso vivia numa página de wiki que estava 40% desatualizada. Membros novos do time ficavam apavorados. Os seniores estavam apenas cansados.

A solução não foi uma ferramenta sofisticada. Foi disciplina.

Passo 1: Um script, um comando

Escrevi um único script bash — um pequeno programa que executa comandos em sequência, como uma receita que o computador segue linha por linha. Chamei de deploy.sh. Ele faz todas as 11 coisas em ordem. Se qualquer passo falha, tudo para e diz qual passo quebrou e por quê.

#!/bin/bash
set -euo pipefail

APP=$1
DEPLOY_LOG="/var/log/deploys/${APP}-$(date +%Y%m%d-%H%M%S).log"

echo "Deploying ${APP}..." | tee "$DEPLOY_LOG"

# Cada passo: nome, comando, rollback
deploy_step "Pull code"       "git -C /srv/${APP} pull origin main"
deploy_step "Install deps"    "cd /srv/${APP} && pip install -r requirements.txt"
deploy_step "Run migrations"   "cd /srv/${APP} && python manage.py migrate"
deploy_step "Build assets"    "cd /srv/${APP} && npm run build"
deploy_step "Restart service"  "systemctl restart ${APP}"
deploy_step "Health check"    "curl -sf http://localhost:8080/health"

echo "Deploy complete: ${APP}" | tee -a "$DEPLOY_LOG"

A função deploy_step envolve cada comando com logging, cronometragem e tratamento de erros. Se o health check falha, ela automaticamente reinicia a versão anterior — um rollback, ou seja, "desfaz e volta pro que estava funcionando."

Tempo total de setup: 45 minutos. Esses 45 minutos já economizaram centenas de horas desde então. A ideia central vem direto da metodologia Twelve-Factor App — um codebase, um build, um comando de deploy.

Passo 2: CI/CD — o robô que faz deploy por você

CI/CD significa Continuous Integration / Continuous Deployment. Em português direto: um sistema que testa automaticamente seu código quando você envia mudanças (essa é a parte CI), e se os testes passam, faz deploy automaticamente no servidor (essa é a parte CD). Você envia o código, o robô cuida do resto.

Para um time pequeno, você não precisa de Jenkins (um servidor de automação pesadão), ArgoCD (uma ferramenta de deploy para Kubernetes), ou um cluster Kubernetes (um sistema de orquestração de containers — se essas palavras não significam nada pra você, ótimo, você não precisa disso). Você precisa de GitHub Actions e um servidor com acesso SSH. O GitHub Actions foi lançado em 2019 e amadureceu como a escolha padrão de CI/CD para times que já usam GitHub — estável, bem documentado e gratuito para workloads pequenos.

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: pytest --tb=short

  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: success()
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SSH_KEY }}
          script: bash /srv/myapp/deploy.sh myapp

Esse é o pipeline inteiro — uma sequência de passos automatizados que o código percorre do seu laptop até produção. Push para main (o branch principal do seu código). Testes rodam nos servidores do GitHub. Se passam, o GitHub conecta no seu servidor via SSH (um protocolo de conexão remota segura) e roda o deploy.sh.

Sem containers. Sem orquestradores. Sem arquivos YAML de 200 linhas. O plano gratuito do GitHub Actions dá 2.000 minutos por mês — mais que suficiente para um time pequeno fazendo deploy 2-3 vezes por dia.

Passo 3: Um health check que realmente verifica saúde

A maioria dos health checks é uma URL /health retornando {"status": "ok"}. Isso diz que o servidor web está rodando. Não diz nada sobre se o app realmente funciona. É como perguntar pra alguém "você está vivo?" quando deveria perguntar "você consegue ver, ouvir e mexer os dedos?"

Meus health checks verificam três coisas:

@app.get("/health")
async def health():
    checks = {
        "database": await check_db_connection(),
        "cache": await check_redis_ping(),
        "disk": check_disk_space_above(20),  # percent
    }
    all_ok = all(checks.values())
    return JSONResponse(
        status_code=200 if all_ok else 503,
        content={"status": "healthy" if all_ok else "degraded", "checks": checks}
    )

Banco de dados caiu? Health check falha. Redis (um cache em memória — uma camada de armazenamento temporário e rápido) inacessível? Falha. Disco 95% cheio? Falha. O script de deploy chama esse endpoint após o restart. Se retorna 503 (o código HTTP para "serviço indisponível"), o deploy faz rollback automaticamente.

Esse único endpoint pegou mais problemas pós-deploy do que todos os nossos testes manuais juntos. ⚙️

Passo 4: Notificações, não dashboards

Eu não fico sentado olhando dashboards de monitoramento. O script de deploy manda exatamente duas mensagens:

  1. Deploy iniciado — "Deploying myapp às 14:32. Triggered by commit abc123."
  2. Deploy finalizado — "myapp deployed em 14m 22s" ou "myapp deploy FAILED no passo: health check. Rolled back."

Essas vão pra um canal no Telegram. Se vejo uma notificação de falha, investigo. Se vejo sucesso, tomo meu chá.

Sem Grafana (uma ferramenta popular de dashboards). Sem PagerDuty (um serviço de alertas que te acorda às 3 da manhã). Para um time pequeno rodando 2-3 serviços, uma mensagem no Telegram é o nível certo de complexidade. Complexidade deve ser proporcional ao tamanho do problema, não ao tamanho da sua ambição. 🫶

Passo 5: Deploy na segunda de manhã

Paramos de fazer deploy na sexta. Agora fazemos deploy na segunda e na quarta de manhã.

Por quê? Porque se algo quebra na segunda, você tem cinco dias pra resolver com o cérebro descansado. Deploy na sexta significa debugar no fim de semana. Debugar no fim de semana significa burnout. Burnout significa erros. Erros significam mais debug no fim de semana.

Isso é um ciclo de feedback negativo — um ciclo onde cada resultado ruim causa o próximo. Não é uma estratégia de deploy.

A agenda de deploy entediante: segunda e quarta de manhã. Nada depois das 15h. Nada na sexta. Nada durante o almoço. Se não está pronto pra janela de segunda, espera. Esperar é barato. Burnout é caro.

O que você está abrindo mão

Hora da honestidade. Esse setup tem tradeoffs.

Sem deploy zero-downtime. Quando o systemctl restart roda, existe um gap de 2-5 segundos onde o app fica indisponível. Para um serviço com milhões de requisições, isso importa. Para a maioria dos times pequenos, ninguém percebe. Se você superar essa fase, pesquise blue-green deployments (rodar duas cópias do seu app e alternar o tráfego entre elas) — mas não comece por aí.

Servidor único, ponto único de falha. Se o servidor morre, tudo morre. Redundância custa dinheiro. Para a maioria dos projetos com faturamento abaixo de US$10K por mês, um servidor bem mantido com backups diários é a resposta certa. Problemas de escala são bons problemas de se ter.

Sem containerização. Docker (uma ferramenta que empacota seu app com todas as dependências em uma unidade isolada) é maravilhoso para ambientes complexos. Para um app Python com banco de dados e Redis em um servidor, é overhead. Você sempre pode adicionar depois.

A pesquisa DORA metrics do Google mostra que times de elite fazem deploy sob demanda com lead times abaixo de uma hora. Mas também mostra que você chega lá de forma incremental — não adotando todas as ferramentas de uma vez.

O resultado

Antes: deploys de 3 horas, 1-2 vezes por semana, time de 3 pessoas bloqueado. Depois: deploys de 15 minutos, 2-3 vezes por semana, zero pessoas assistindo.

Tempo economizado por mês: aproximadamente 24 horas de tempo coletivo do time. Custo anual do setup de CI/CD: R$0 (plano gratuito do GitHub Actions mais um script bash). Tempo de setup: um fim de semana.

Torne entediante

O objetivo de boas ops não é tornar deploys empolgantes. É torná-los tão entediantes que você esquece que acontecem.

Quando você faz deploy sem adrenalina, faz deploy com mais frequência. Quando faz deploy mais frequentemente, cada deploy é menor. Quando cada deploy é menor, menos coisas quebram. Quando menos coisas quebram, você dorme melhor.

Essa é toda a filosofia. Um script. Um pipeline. Um health check. Um canal de notificação. Uma manhã tranquila por semana.

Torne entediante. Torne pequeno. Torne automático. Vai fazer um chá. 🫶