Deploy por imagem de registry
Desde a v2.173.0, uma app pode ser implantada a partir de uma imagem
publicada num registry, sem repositório git. O CI constrói e publica a
imagem. O runner resolve a tag em um digest e sobe o container por
repo@sha256:….
O manifesto (.deploy.yml) mora só em <app>/.deploy.yml. O image: dele é
o repositório sem tag. A tag vem de cada deploy.
1. Registrar a app
# deploy.yml
project: minha-api
system: sys
image: ghcr.io/minha-org/minha-api
image_mode: prebuilt
port: 8080
healthcheck:
path: /health
instances:
production:
domain: api.exemplo.com
source:
type: registry
tags: "v[0-9]*"runner add --image ghcr.io/minha-org/minha-api --manifest-file deploy.yml --ckey v2O --image é o mesmo repositório do image: do manifesto. A app não tem
--repo nem --branch.
O image_mode: prebuilt é obrigatório: a imagem já tem o código, sem build.
O source com type: registry aceita:
| Campo | O que faz |
|---|---|
tags |
Padrão glob das tags que o cron considera. Escolhe a maior semver que casa. Pré-release (v1.5.0-rc.1, -beta) fica fora, salvo se o padrão tiver - (ex.: v*-rc*). |
track |
Tag mutável (main, latest). O cron segue o digest dela. |
poll |
false desliga a consulta do cron. O deploy fica só por webhook ou manual. |
tags e track são excludentes.
Com tags: "v*", um v2.0.0-rc.1 publicado não é implantado: o padrão não
pede pré-release. Para seguir os release candidates, escreva o - no padrão
(tags: "v*-rc*"). O - dentro de uma classe (v[0-9]*) é intervalo e não
conta.
O pull_policy não vale em app registry (registry_pull_policy): o runner
sempre puxa a imagem por digest, com a credencial da app, antes do
docker run.
2. Dar acesso ao registry
Imagem pública não precisa de credencial. Para repositório privado, grave um token de leitura:
printf %s "$TOKEN" | runner registry login minha-api --password-stdinO runner valida o acesso antes de gravar. Se falhar, a credencial antiga
fica. A credencial é cifrada por app, e o pull roda com um DOCKER_CONFIG
temporário: o login do host não é tocado.
runner registry test minha-api # confere o acesso
runner registry logout minha-api # volta ao pull anônimoDetalhes em runner registry.
3. Escolher como o deploy dispara
Há dois modos. Eles podem coexistir.
Webhook (imediato)
O CI avisa o runner assim que publica a imagem. Precisa do serviço HTTP do
runner ligado (runner service init --enable-api, depois start).
Gere o segredo da app:
runner hook secret minha-apiO segredo vai para um arquivo com modo 600. A saída mostra o caminho e a impressão digital, nunca o segredo.
Leve o segredo ao CI, sem a quebra de linha final:
sudo tr -d '\n' < /caminho/mostrado | gh secret set RUNNER_HOOK_SECRET --repo minha-org/minha-apiSe o CI está fora da VPN, abra o hook (opt-in):
runner service expose --domain runner.exemplo.com --vpn-only --hooks-publicosA API segue na VPN; só
/api/v1/hooks/fica pública. Sem--hooks-publicos, os hooks ficam na VPN como o resto da API.Use a Action no workflow:
name: release on: push: tags: ['v*'] jobs: deploy: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: build uses: docker/build-push-action@v6 with: push: true tags: ghcr.io/minha-org/minha-api:${{ github.ref_name }} - uses: devborlot/runner-deploy-action@v1 with: url: https://runner.exemplo.com app: minha-api secret: ${{ secrets.RUNNER_HOOK_SECRET }} image-tag: ${{ github.ref_name }} digest: ${{ steps.build.outputs.digest }}A Action assina o pedido, repete em falhas transitórias e acompanha o deploy até
sucessooufalhou(inputwait, padrãotrue;falsesó envia o aviso). Odigesté opcional: o runner o confere contra o registry.
Contrato do webhook
POST /api/v1/hooks/deploy/<app>, corpo {"image_tag":"v1.4.2"}
(digest e actor são opcionais). Cabeçalhos:
X-Runner-Timestamp: segundos Unix, aceito em ±300 s;X-Runner-Signature:sha256=<hex>, comHMAC-SHA256(segredo, timestamp + "." + corpo).
O segredo é usado sem a quebra de linha final. A API key global não abre esta rota, e o segredo da app não abre nenhuma outra.
| Status | Resposta | Quando |
|---|---|---|
| 202 | status: "rodando", deploy_id |
Aceito. Acompanhe pelo GET abaixo. |
| 200 | status: "ja_implantado" |
A mesma tag e o mesmo digest já estão no ar: a revisão gravada é img:<tag>@<digest> e o container da versão está rodando. |
| 400 | corpo_invalido, image_tag_invalida, digest_invalido |
Corpo ou campo fora do formato. |
| 401 | assinatura_invalida, timestamp_fora_da_janela |
Assinatura ausente ou errada; relógio fora da janela. |
| 403 | origem_nao_permitida |
Conexão de fora do host ou do Traefik. |
| 404 | nao_encontrado |
App inexistente, sem segredo de hook ou com segredo ilegível. É a mesma resposta nos três casos. |
| 409 | replay com deploy_id |
O mesmo pedido já foi aceito. Acompanhe esse deploy_id. |
| 409 | replay com status: "em_andamento" |
O mesmo pedido ainda está em voo. Repita com backoff. |
| 409 | digest_divergente |
O digest do corpo não é o do registry (tag repostada). |
| 409 | lock_contended |
Já há um deploy da app em andamento. |
| 409 | hook_so_registry |
A instância da app deixou de ser registry. |
| 409 | tag_repostada |
A versão desta tag está no ar com outro digest: a tag foi repostada. Nada é implantado. Publique uma tag nova, ou runner deploy <app> --force. |
| 409 | app_pausada |
A app está pausada (runner pause ou auto-pause). O corpo traz o motivo. Nada é implantado. |
| 411 / 413 | content_length_ausente, corpo_grande_demais |
Sem Content-Length, ou corpo acima de 64 KiB. |
| 500 | manifesto_ilegivel |
O .deploy.yml da app não carrega. |
| 422 | registry_auth_falhou, registry_tag_inexistente, registry_repositorio_inexistente, com message |
A resolução falhou por algo que repetir não resolve: credencial recusada, tag ou repositório que não existe. |
| 502 | código do registry (registry_inacessivel) |
Rede ou registry fora. Só o código; o detalhe fica no log do runner. |
| 503 | registry_limite, desligando |
O registry limitou os pedidos (429), ou o serviço está desligando. |
Repita com backoff em 409 lock_contended, 502 e 503. Os demais (422, 409
tag_repostada/app_pausada/digest_divergente) falham na hora: repetir
não muda nada. A Action faz isso.
Para acompanhar, use GET /api/v1/hooks/deploy/<app>/<deploy_id> com os
mesmos cabeçalhos. A assinatura é
HMAC-SHA256(segredo, timestamp + "." + deploy_id). A resposta é
{"deploy_id", "status": {"app", "estado", "versao", "erro"}}, e estado é
rodando, sucesso ou falhou. Um 404 aqui é estado perdido: o registro
dos deploys fica em memória e some quando o serviço reinicia. O resultado
real fica em runner logs history <app>.
Detalhes em runner hook.
Cron (sem servidor HTTP)
Sem o serviço HTTP, publique a imagem e deixe o cron trabalhar. A cada 5
minutos, o runner fetch --deploy consulta o registry e implanta o que
mudou:
- com
tags, a maior semver que casa com o padrão; - com
track, o digest novo da tag mutável.
O limite de pulls do registry (429) adia a consulta sem contar como erro, e
o cron respeita o Retry-After: não consulta aquela app antes do prazo
(mínimo 5 min, máximo 6 h).
Também são aviso, e não erro (a app fica up to date, nada conta para o
auto-pause): nenhuma tag casa o padrão, a tag ou o repositório não existe
(ainda), e tag repostada — a versão no ar teve a tag publicada de novo
com outro digest. O cron não implanta a imagem repostada: tag de versão é
imutável. O runner preflight <app> mostra o aviso. Credencial recusada
(registry_auth_falhou) continua sendo erro de fetch.
Veja a prontidão com runner preflight <app>: o componente registry
mostra a credencial, a última tag que casa e o limite restante.
Deploy manual
runner deploy minha-api --image-tag v1.4.2A tag é resolvida em digest antes do deploy e vira a versão da pasta.
--version não escolhe imagem numa app registry.
4. Rollback
runner rollback minha-api --version v1.4.1O rollback roda a imagem gravada em src/<instância>/<versão>/imagem.json
(tag e digest). Ele nunca usa o image: atual do manifesto. O runner
baixa a imagem antes de parar o container no ar: um pull que falha não
derruba a app.
O rollback vale até o registry publicar uma tag/digest novo que o
tags/track pegue. Ele não mexe na revisão gravada no state: o cron
compara o registry com ela e, sem novidade, não reimplanta. Quando sair uma
tag nova (ou o digest do track mudar), o cron implanta normalmente.
O que o status mostra
runner status --json e o snapshot trazem, na app registry,
source_type: "registry", revision (img:<tag>@<digest>), image_tag e
image_digest. O status traz também image, o repositório. image_tag,
image_digest e image são os do que está no ar (o imagem.json da versão
current): depois de um rollback, mostram a versão de destino, enquanto
revision segue a última resolvida. Fora de app
registry, esses campos não aparecem.
Limites
- Docker Hub anônimo tem limite baixo de pulls. Grave uma credencial
com
runner registry login. Com o limite estourado, o cron adia e tenta no ciclo seguinte. - ECR (AWS) fica para a fase seguinte.
- O registro dos deploys do webhook é em memória. Um restart do serviço durante o acompanhamento vira "estado perdido".
- Apps
registrynão usamrunner secrets push: não há repositório para osecrets.enc.