Desembaraçando a dependência do Colima nos deploys do Kamal
Toda vez que eu precisava fazer um deploy de produção do blog-manager, tinha que lembrar de uma coisa: o Colima está rodando? Se não estivesse, o kamal deploy simplesmente travava. O daemon Docker local do meu Mac era uma peça estrutural do caminho de deploy de produção, e eu vivia esquecendo disso.
A causa raiz: o config/deploy.yml tinha builder.remote: ssh://kamal@blog-manager.internal. O Kamal fazia SSH no LXC de produção, mas precisava de um daemon Docker local para o contexto do buildx. Sem Colima, sem build, sem deploy.
O staging tinha o mesmo problema, só que mais escondido. O workflow de deploy do staging rodava em um runner self-hosted dentro do LXC de staging e chamava bin/kamal deploy -d staging, que usava builder.remote: ssh://kamal@blog-manager-staging.internal para construir de volta nele mesmo. Funcionava, mas cada merge gerava uma imagem recém-construída que nunca era testada em um runner do GitHub Actions, e seria construída de novo na máquina de desenvolvimento ao fazer deploy para produção. “Passou no staging” não significava nada como sinal de confiança, tecnicamente.
A correção é o padrão clássico do Kamal, build-once-deploy-many: o CI constrói e envia a imagem uma vez, e cada ambiente puxa e faz deploy dessa mesma imagem.
O plano
Três partes:
- Um novo workflow
build.ymlque constrói e enviagitea.example.net/vic/blog-manager:<sha>a cada push paramain. PRs recebem uma execução só de build (sem push) para pegar Dockerfiles quebrados cedo. - Um
deploy-staging.ymlreescrito, encadeado a partir dobuild.ymlviaworkflow_run, que faz o deploy com--skip-push --version=<sha>em vez de construir localmente. - Remover
builder.remotedas duas configurações de deploy.
Para produção: bin/kamal deploy --skip-push --version=<sha> de qualquer máquina. Sem Colima. Sem daemon Docker local. Só acesso SSH ao host.
Três coisas que a pesquisa errou
A descrição original do issue dizia para enviar para registry.internal/blog-manager; na verdade isso é um cache pull-through do Docker Hub, não um destino de push. O registro de verdade é a instância do Gitea (container 1009 no Proxmox).
A pesquisa também dizia que o Gitea era acessível publicamente via Cloudflare. Não é. dig +short gitea.example.net @1.1.1.1 retorna 192.168.x.x, um endereço RFC1918 interno. O runner ubuntu-latest hospedado pelo GitHub não consegue alcançá-lo. Descobri isso da pior forma, quando a primeira tentativa de build deu timeout tentando fazer login no registro. A correção: rodar o job de build no runner [self-hosted, blog-manager-staging], que fica dentro do homelab.
A terceira coisa: o kamal deploy --skip-push valida se a imagem tem um label service que bate com o nome do serviço definido em config/deploy.yml. O kamal build push adiciona isso automaticamente. O docker/build-push-action não. O deploy puxava a imagem sem problema, e então a rejeitava:
Image gitea.../blog-manager:<sha> is missing the 'service' label
Uma linha no workflow resolveu:
labels: service=blog_manager
A pegadinha do dotenv, de novo
Tem mais uma pegadinha com os secrets do Kamal no CI, a mesma que já tinha pegado a configuração do staging. O Kamal avalia .kamal/secrets-common usando Dotenv.parse, não um subprocesso bash. Isso significa que as variáveis de ambiente do CI não ficam disponíveis dentro da expansão de parâmetro ${VAR:-fallback}, só em subprocessos $(cmd). A solução: escrever config/master.key explicitamente a partir do secret do CI antes de rodar qualquer comando kamal.
A pegadinha do cache em camadas
O plano previa o cache type=registry,mode=max; é a resposta “certa” para cachear camadas Docker em um registro self-hosted. Só que está quebrado contra o Gitea (gitea#28973, o registro do Gitea retorna um erro em PATCH durante o push do cache). O type=gha funciona corretamente e não exige nenhum suporte do registro.
Como o pipeline está agora
push to main
└─ Build and push image (self-hosted, blog-manager-staging)
└─ Deploy staging (self-hosted, blog-manager-staging, --skip-push)
Deploy de produção: bin/kamal deploy --skip-push --version=<sha>. Staging e produção agora rodam exatamente a mesma imagem.
A parte que falta é o deploy automático para produção com um gate de aprovação do GitHub Environments. Isso exige um runner self-hosted no LXC de produção (1027), a mesma configuração do staging, só que no outro host. Planejado para uma próxima etapa.
Leitura relacionada
Deploy automático do Rails 8 para staging com Kamal e um runner self-hosted do GitHub Actions
Fazendo todo merge disparar deploy automático para staging: um runner self-hosted, quatro paredes seguidas, e a pegadinha dos secrets do Kamal que demorou mais para ser decifrada.
Movendo o CI para um runner self-hosted depois que o GitHub quebrou nosso billing
CI morto, deploys ao vivo: dobrando todo job no runner do homelab, apagando a maquinaria de compensação de runner hospedado, e o backlog de CVEs esperando atrás do gate.
Um playbook de falhas de CI para um projeto Rails de uma pessoa só
Escrevendo as regras do que fazer quando o CI fica vermelho num projeto Rails solo, e a limitação do GitHub que transformou o gate de merge num comentário estrutural.
Você também pode achar útil
Proton VPN
VPN comercial com filtragem NetShield e interruptor de desligamento automático.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maiseSIM Airalo
eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.
Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.
Saiba maisProton Drive
Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais