Separa o runner da forge. O Forgejo e o Caddy ficam na VM, no repositório gibeira; o runner roda na máquina de quem desenvolve, porque o job é que come RAM — build do Nuxt mais Postgres de testcontainers passam de 3 GB por alguns minutos a cada push, contra uma VM de 2 GB que ficaria parada o resto do dia. O runner-config.example.yml é o arquivo pronto, não a colagem do generate-config: aquele exemplo já traz um bloco `runner:`, e acrescentar os rótulos num segundo bloco dá chave duplicada, que YAML recusa. O runner-config.yml de verdade fica fora do repositório — é ele que carrega o segredo do registro, e a configuração não expande ambiente, de modo que não há como manter o segredo em outro lugar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BmUugDy3EBa7XihKRaCMq |
||
|---|---|---|
| runner | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
O runner da forge
Este repositório é só o runner do Forgejo Actions. A forge — Forgejo e Caddy
— roda na VM e mora em gibeira, dentro de ci/. O runner mora à parte
porque ele roda em outro lugar: na sua máquina.
A separação é de recursos, não de arrumação. Na VM ficam Forgejo (~300 MB) e Caddy (~20 MB), e ela é uma BV1-2-40 de 2 GB. Quem come RAM é o job: build do Nuxt mais Postgres de testcontainers passam de 3 GB por alguns minutos a cada push. Botar isso na VM significa pagar o dobro por hardware que fica parado 23 horas por dia. Na sua máquina é CPU que já está ligada e não é cobrada por hora.
O preço não é dinheiro, é disponibilidade. CI só existe com a sua máquina
ligada. Quando isso incomodar — ou quando o tofu apply de produção passar a
rodar aqui, com as credenciais de produção aqui — é hora de subir a VM para
BV2-4-40 e mover o runner para lá. É o mesmo docker compose up deste
repositório, na VM maior. Nada aqui presume a sua máquina.
Como ele alcança a forge
O runner não abre porta e não recebe conexão. Ele pergunta à forge, por HTTPS
de saída, se há job — a cada 30 s. Por isso funciona atrás de NAT, sem IP
fixo e sem nada liberado no roteador. A única coisa que ele precisa é chegar
em https://git.souzapablo.dev.br.
O par docker-in-docker existe para o runner ter um daemon Docker onde criar
os containers de job. É um daemon separado de propósito: job é código com
acesso ao daemon que o cria, e o daemon da sua máquina é a sua máquina.
1. Registrar, na VM
O registro é feito na forge, com um segredo de 40 hex que você mesmo gera — sem clicar em nada, sem token de uso único:
# na VM, em /opt/forge
SECRET=$(openssl rand -hex 20)
docker compose exec -u git forgejo \
forgejo forgejo-cli actions register --name local --scope me --secret "$SECRET"
Guarde as duas coisas: o UUID que ele imprime e o $SECRET que você gerou.
Não é um par que se recupera depois. Se perder, registre de novo.
2. Configurar, na sua máquina
cp runner/runner-config.example.yml runner/runner-config.yml
chmod 600 runner/runner-config.yml
$EDITOR runner/runner-config.yml # url, uuid e token
O modelo já decide o resto — rótulo, capacidade, intervalo de busca — e comenta o porquê de cada um. Falta só a conexão.
O runner-config.yml fica fora do repositório pelo .gitignore: é ele que
carrega o segredo. O .example é o que se versiona. Mesma divisão do
.env.example e do .env lá no gibeira.
Confira o dono do diretório antes de subir:
id -u # se for 1000, não faça nada: já está certo
A imagem do runner roda como uid 1000, que é o do primeiro usuário comum na maioria dos Linux. Se o seu não for, e só nesse caso:
sudo chown -R 1000:1000 runner
3. Subir
docker compose up -d
docker compose logs -f runner
O que você quer ver, em ordem:
Starting runner daemon
runner: local, with version: v13.0.0, with labels: [docker], ephemeral: false, declared successfully
[poller] launched
4. Conferir
O runner aparece verde em https://git.souzapablo.dev.br/admin/actions/runners.
Um push em qualquer repositório com workflow dispara o job, e o estado da
última execução sai daqui sem abrir o navegador:
curl -s 'https://git.souzapablo.dev.br/api/v1/repos/me/<repo>/actions/tasks?limit=1' \
| python3 -c 'import json,sys; r=json.load(sys.stdin)["workflow_runs"][0]; print(r["name"], r["status"])'
Três armadilhas, todas já pagas
A configuração não expande ambiente. ${GIT_HOST} no YAML não vira
nada: o runner lê o arquivo cru. O que estiver entre chaves chega literal e
ele recusa a configuração inteira — malformed url "${GIT_HOST}". O segredo
vai no arquivo, e é por isso que o arquivo é ignorado pelo git.
generate-config não dá um arquivo pronto. Ele imprime o exemplo
comentado de 277 linhas, com um bloco runner: já dentro. Colar um segundo
runner: no fim para acrescentar os rótulos dá chave duplicada, que YAML não
aceita, e o runner morre em laço de reinício:
Error: invalid configuration: cannot parse config file "runner-config.yml":
line 77: mapping key "runner" already defined at line 21
O runner-config.example.yml deste repositório já é o arquivo pronto. Use o
generate-config só para consultar a lista completa de opções.
O uid da imagem é 1000, não 1001. Um chown -R 1001:1001 runner deixa o
runner sem conseguir abrir a própria configuração — open runner-config.yml: permission denied — e o erro só não aparece se o arquivo estiver legível por
todos, que é exatamente o que ele não pode estar.
O que ainda não está decidido
container.docker_host está em "-": nenhum socket do Docker é montado
dentro do container de job. O que não é montado não é escapada, e nenhum job
de hoje precisa. Quando o pipeline trouxer testcontainers, este é o campo que
vira automount — e a decisão de segurança que vem junto merece ser tomada
na hora, não herdada por descuido.