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 v2

O --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-stdin

O 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ônimo

Detalhes 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).

  1. Gere o segredo da app:

    runner hook secret minha-api

    O segredo vai para um arquivo com modo 600. A saída mostra o caminho e a impressão digital, nunca o segredo.

  2. 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-api
  3. Se o CI está fora da VPN, abra o hook (opt-in):

    runner service expose --domain runner.exemplo.com --vpn-only --hooks-publicos

    A API segue na VPN; só /api/v1/hooks/ fica pública. Sem --hooks-publicos, os hooks ficam na VPN como o resto da API.

  4. 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é sucesso ou falhou (input wait, padrão true; false só envia o aviso). O digest é 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>, com HMAC-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.2

A 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.1

O 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 registry não usam runner secrets push: não há repositório para o secrets.enc.
By Borlot.com.br on 27/10/2026