Dois deploys em producao da mesma branch

Voce tem um repositorio (ex.: um miniSMTP) e precisa rodar dois deploys em producao com configuracoes diferentes — dominios, HELO, credenciais de relay. Mesmo codigo, mesma branch, configs distintas.

Feito do jeito errado, um deploy sobrescreve o outro. Este guia mostra o jeito que nao quebra.

O que quebra (e por que)

Se voce simplesmente registra a mesma branch duas vezes:

  • o .deploy.yml e um so (esta na branch compartilhada) — colocar os dois configs literais nele faz um sobrescrever o outro;
  • o bundle cifrado tem um caminho no repo — dois registros escrevendo no mesmo nome se clobberam;
  • runner add bloqueia o segundo registro (exige --force-duplicate);
  • e ha uma nuance de qual instancia o deploy escolhe — resolvida na v2.55.1 (veja Qual instancia o deploy escolhe).

O anti-pattern e por o valor especifico de cada deploy dentro do manifesto. A branch e compartilhada; o manifesto so pode carregar o formato.

O principio

.deploy.yml   →  SHAPE: nome da instancia, {{::REFERENCIAS}}, literais COMUNS
secret.enc    →  VALORES de cada deploy (namespaced por instancia)

Detalhes em Hierarquia env/secrets.

Saida recomendada: um manifesto por deploy

Cada deploy ganha seu proprio manifesto dentro do mesmo repo. Assim cada registro tem uma unica instancia — sem ambiguidade de "qual instancia deployar" — alem de app dir, ckey e bundle proprios.

1. Crie um manifesto por deploy no repo

seu-repo/
├── src/
├── deploy/
│   ├── cliente-a/.deploy.yml
│   └── cliente-b/.deploy.yml

deploy/cliente-a/.deploy.yml:

project: smtp-cliente-a
system: sys
type: sys
image: python:3.12-slim
port: 2525

github:
  repo: usuario/smtp-mini
  dist_branch: main

version:
  source: git-commit

environment:
  SMTP_MAX_SIZE: "26214400"        # literal COMUM aos dois
  SMTP_HELO: mx.clientea.com       # literal DESTE deploy
secrets:
  SMTP_RELAY_PASSWORD: "{{::SMTP_RELAY_PASSWORD}}"   # referencia; valor no bundle

healthcheck:
  tcp: 2525

instances:
  cliente-a:                        # UMA instancia por manifesto
    domain: mx.clientea.com
    source:
      type: dist
    keep_versions: 3

deploy/cliente-b/.deploy.yml e igual, trocando project, SMTP_HELO, o nome da instancia (cliente-b) e o dominio.

Como cada manifesto tem uma unica instancia, ela e o default — e tanto runner deploy quanto fetch --deploy acertam sozinhos.

2. Registre os dois (cada um com sua ckey)

runner add --repo usuario/smtp-mini --branch main \
  --token "$(gh auth token)" \
  --manifest-path deploy/cliente-a/.deploy.yml \
  --slug smtp-cliente-a \
  --instance cliente-a \
  --ckey "<ckey-do-cliente-a>"

runner add --repo usuario/smtp-mini --branch main \
  --token "$(gh auth token)" \
  --manifest-path deploy/cliente-b/.deploy.yml \
  --slug smtp-cliente-b \
  --instance cliente-b \
  --ckey "<ckey-do-cliente-b>"
Flag Papel
--manifest-path aponta o manifesto daquele deploy (implica --force-duplicate quando ja existe outro app do mesmo repo)
--slug nome/diretorio proprio do registro (/data/apps/smtp-cliente-a) — isola .env/.secrets/state
--instance fixa a instancia do registro e namespaceia o bundle (secrets.cliente-a.enc)
--ckey chave propria — o bundle de A nunca abre com a ckey de B

3. Preencha os valores sensiveis (por registro)

runner env set --secret smtp-cliente-a SMTP_RELAY_PASSWORD 'senha-do-A'
runner env set --secret smtp-cliente-b SMTP_RELAY_PASSWORD 'senha-do-B'

Valor com caractere especial vai sempre aqui (o store e verbatim), nunca no manifesto — as aspas sao do seu shell.

4. Deploy

runner deploy smtp-cliente-a
runner deploy smtp-cliente-b

Cada um sobe com seu dominio, seu env e seus secrets. No repo, os bundles ficam separados:

.runner/
├── secrets.cliente-a.enc     # so abre com a ckey de A
└── secrets.cliente-b.enc     # so abre com a ckey de B

Alternativa: manifesto unico com `instances:`

Se a config que diverge e apenas nao-sensivel (dominio, HELO, portas) e os secrets sao os mesmos, da pra usar um manifesto so com duas instancias:

instances:
  cliente-a:
    domain: mx.clientea.com
    source: { type: dist }
    environment_overrides:            # prioridade maxima
      SMTP_HELO: mx.clientea.com
  cliente-b:
    domain: mx.clienteb.com
    source: { type: dist }
    environment_overrides:
      SMTP_HELO: mx.clienteb.com

Limites desta alternativa:

  • Uma ckey por registro. state.ckey e um campo unico por app registrado — duas instancias no mesmo registro compartilham ckey e .env/.secrets. Se os secrets divergem, use a saida recomendada.
  • Rodar runner set-ckey com uma ckey diferente da ja registrada retorna erro loud (exit 1) sem escrever nada.

Qual instancia o deploy escolhe (v2.55.1+)

A partir da v2.55.1, runner deploy, runner deploy --all e runner fetch --deploy resolvem a instancia da mesma forma: state.instance (a instancia com que o app foi registrado via runner add --instance <N>) → default do manifesto → production.

Ou seja, num manifesto com duas instancias, cada registro deploya a sua instancia registrada — tanto no manual quanto no cron.

Desde a v2.151.2, a mesma ordem vale para os outros comandos de app que aceitam -i: rollback, health, restart, versions, container logs, config get/diff e env validate. Veja Um registro por instância no mesmo host.

runner deploy smtp-cliente-b         # honra state.instance = cliente-b
runner fetch --deploy smtp-cliente-b # idem

Em runners anteriores a v2.55.1, os deploys manuais (deploy/--all) ignoravam state.instance e miravam sempre a instancia default do manifesto — so o fetch --deploy respeitava o registro. Se voce esta numa versao antiga, atualize (runner self-update) ou registre um manifesto por deploy (a saida recomendada, que elimina a ambiguidade em qualquer versao).

Um registro por instância no mesmo host (v2.151.5+)

É a topologia de quem tem um manifesto com várias instâncias e põe dois ou mais registros do mesmo repo no mesmo servidor, um por instância. Exemplo: staging e staging-b de uma mesma API, cada um com sua branch e seu cofre.

runner add --repo usuario/minha-api --branch dev-a \
  --instance staging --slug minha-api --ckey v2

runner add --repo usuario/minha-api --branch dev-b \
  --instance staging-b --slug minha-api-b \
  --force-duplicate --ckey v2

O project: do manifesto é o mesmo nos dois registros. O que identifica cada um é o slug, que vira o nome do diretório (/data/apps/<slug>) e o label ccs.systems/uid dos containers dele.

Use runner v2.151.5 ou mais novo. A partir dessa versão:

Comando Comportamento com dois registros do mesmo repo
instances, list, status, snapshot, health, versions cada registro mostra só os próprios containers (casados pelo slug, não pelo project:)
unregister, destroy só tocam nos containers do registro pedido
health, restart, versions, container logs, config get/diff, env validate, rollback sem -i, agem na instância do registro, e não em production
restart, container logs acham o container pelos labels, inclusive com instância com hífen (staging-b)
pause vale até o unpause: nem um fetch manual nem o cron desarmam a pausa
edit -b valida a branch nova com a credencial do registro
env validate faz a mesma conta do deploy na instância do registro

Sintomas em versões anteriores. Se o servidor roda uma versão mais antiga, estes sintomas são esperados. A correção é runner self-update.

Sintoma Versão que corrige
instances/list mostram [OK] com o container do outro registro e [--] no próprio, ou [OK] ao lado de not deployed 2.151.1
unregister de um registro pode parar o container do outro 2.151.0
runner fetch <app> manual numa app pausada e o cron volta a deployar 2.151.0
edit -b <branch> responde "does not exist on remote" para uma branch que aparece listada logo abaixo 2.151.0
env validate acusa como ausentes chaves que estão no cofre da instância, e não aceita -i 2.151.0
rollback falha com Instance 'production' not found ou reverte a instância errada 2.151.0
health/versions/restart sem -i consultam production 2.151.2
restart responde ✓ Restarted sem reiniciar nada em instância com hífen 2.151.3
depois de um rollback, status/list mostram [WARN] e a versão abandonada 2.151.5

Verificação depois de atualizar:

runner -V                        # 2.151.5 ou mais novo
runner instances minha-api       # [OK] só em staging
runner instances minha-api-b     # [OK] só em staging-b
docker ps --format '{{.Names}}  {{.Label "ccs.systems/uid"}}' | grep minha-api
runner env validate minha-api-b  # valida a instância do registro

Cada registro deve mostrar [OK] só na própria instância, e o uid de cada container deve ser o slug do registro dele.

Antes de agir, confira a identidade (v2.152.0+). runner preflight <slug> mostra a instância e de onde ela veio, o cofre, o container e o router que o próximo deploy vai usar. Ver runner preflight.

Fixe a instância de cada registro. Registro sem --instance, num manifesto com várias instâncias, resolve pela ordem do bloco instances:. Desde a v2.152.0, list, status, preflight e doctor avisam nesse caso. Corrija com runner edit <slug> -i <instância>; numa app v2, o cofre da instância nova é criado na hora (v2.152.1+).

Copiar um secret entre os registros sem passá-lo pela tela:

runner secrets copy --from minha-api --to minha-api-b --keys APP_KEY

Verificacao

runner list                              # os dois registros, dominios distintos
runner manifest smtp-cliente-a           # manifesto resolvido daquele deploy
runner secrets show smtp-cliente-a       # abre so com a ckey de A
runner status smtp-cliente-b

Armadilhas

Nao faca Por que
Dois blocos de config literais no mesmo manifesto a branch e compartilhada — um sobrescreve o outro
Mesma ckey nos dois deploys vazou uma, abriu as duas (perde o blast-radius)
Mesmo --slug (ou sem slug) os registros colidem no mesmo app dir
Sem --instance os bundles colidem no mesmo nome no repo
Secret com caractere especial no manifesto o YAML reinterpreta $, {}, # — use env set --secret

Veja tambem

By Borlot.com.br on 09/10/2026