Notificações — Telegram e webhook
O Runner envia notificações para cada evento relevante de deploy. Esta página cobre como configurar Telegram e webhook, quais eventos são disparados e como sobrescrever a config global por app.
Pré-requisitos
- Runner v2.30.10+
- Acesso a
/opt/runner/config.ymlno servidor - Para Telegram: conta Telegram e acesso ao @BotFather
- Para webhook: URL HTTPS acessível pelo servidor Runner
Onde fica a configuração
A config de notificações fica na seção notify: do /opt/runner/config.yml. Esta é a config global — vale para todas as apps do servidor.
# /opt/runner/config.yml
notify:
channel: telegram # telegram | webhook | both
telegram_bot_token: "123456:ABC-DEF..."
telegram_chat_id: "-100123456789"
webhook_url: "https://hooks.exemplo.com/runner"
# Classe deploy (iniciado/concluído/falha) — gate 2. default true.
# false = nenhuma notificação de chat do ciclo de deploy (audit log continua).
enabled: true
# Extras (gate 3) — todos opt-in, default false:
extras:
batch_summary: false # resumo "Runner deploy batch — N ok, M failed"
verbose_errors: false # 2ª msg com o erro completo numa falha
health_status: false # status do health check pós-deploy
init_log: false # log do boot do container (⚠️ pode vazar segredos)
# Alertas dos watchers do cron (v2.118.0+) — classe própria, default true.
# false = tls-watch, doctor-watch e auto-pause não mandam chat (audit log segue).
watch_alerts: true
# Fora de notify: intervalo mínimo entre re-alertas do MESMO problema (horas).
watch_realert_hours: 6Migração (v2.42.x → modelo de classes): as chaves antigas top-level
verbose_errors/notify_successforam substituídas. Configs antigos não quebram (chaves desconhecidas são ignoradas), mas não migram sozinhos —enabledcai no defaulttruee osextrasemfalse. Reescreva no formato novo para reativar verbose errors etc.
Modelo de classes e os 3 gates
As notificações são organizadas em classes de evento, e cada classe passa por até 3 gates antes de chegar ao chat. Os gates valem para todos os canais de chat configurados (Telegram, Discord, webhook) de forma uniforme — não há tratativa por canal.
| Gate | O que é | Controla |
|---|---|---|
| 1 | Canal configurado | Existe telegram_* / discord_webhook / webhook_url? |
| 2 | enabled |
Liga/desliga a classe deploy (iniciado/concluído/falha) |
| 3 | extras.<x> |
Liga cada evento extra (opt-in) |
Policy por classe:
| Classe | Vai pro chat quando |
|---|---|
| Deploy (iniciado/concluído/falha) | enabled |
| Batch summary | extras.batch_summary (independe do enabled) |
| Verbose error (2ª msg) | enabled e extras.verbose_errors |
| Health status | enabled e extras.health_status |
| Init log | enabled e extras.init_log |
| Alertas dos watchers (v2.118.0+) | watch_alerts (independe do enabled) |
Journal sempre-on: o audit log
deploy-events.jsonlregistra todos os eventos sempre, independentemente dos gates. Os gates só decidem o que vai pro chat — o histórico (runner audit/runner logs) nunca perde evento.
Exemplos:
- Padrão (
enabled: true, extrasfalse): chat recebe iniciado/concluído/falha; sem batch, sem 2ª msg de erro, sem health/init log. - Silenciar deploy no chat (
enabled: false): nenhum evento de deploy no chat (nem falha) — só o journal registra. Útil quando o batch summary já basta. - Só o resumo de batch:
enabled: false+extras.batch_summary: true.
Setup Telegram
1. Criar o bot
- Abra o Telegram e busque
@BotFather - Envie
/newbote siga as instruções - Copie o token (formato:
123456789:ABCdefGHI...)
2. Obter o chat_id
Para DM ou grupo — use o setup interativo:
runner notify setup --channel telegramO comando valida o token via API do Telegram, detecta o chat_id automaticamente e salva no config.yml.
Para canal — o bot não recebe getUpdates de canais. Obtenha o chat_id manualmente:
curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates"Envie uma mensagem no grupo ou canal primeiro, depois rode o comando. O chat_id fica em .result[].message.chat.id — negativo para grupos e canais.
3. Configurar manualmente
notify:
channel: telegram
telegram_bot_token: "123456789:ABCdefGHI..."
telegram_chat_id: "-100123456789"Para canal: adicione o bot como administrador com permissão "Post Messages" antes de usar.
4. Testar
runner notify testO comando envia uma mensagem de teste ao canal configurado. Se falhar, rode runner notify status para ver o diagnóstico.
Setup webhook
O Runner faz POST JSON para a URL configurada a cada evento.
O webhook é um canal de notificação como Telegram/Discord — passa pelos
mesmos gates (enabled / extras.*) de forma uniforme. Ou seja, com
enabled: false o webhook não recebe os eventos do ciclo de deploy
(iniciado/concluído/falha); os extras (batch_summary, etc.) só chegam se o
extras.<x> correspondente estiver ligado. Para consumir todos os eventos
programaticamente sem depender dos gates, leia o audit log deploy-events.jsonl
(sempre-on) em vez do webhook.
Configurar
notify:
channel: webhook
webhook_url: "https://hooks.exemplo.com/runner"Payload recebido
{
"event_type": "deploy_success",
"project": "meu-app",
"instance": "production",
"version": "v1.2.0",
"previous_version": "v1.1.9",
"deploy_id": "a3f7c2d1",
"timestamp": "2026-05-27T14:30:00Z",
"branch": "main",
"pr_number": 41,
"expires_at": "2026-05-29T14:30:00Z",
"last_commit": {
"author": "alice",
"hash": "ee8a4bf",
"subject": "CONFORMITY front: tokenized footer (#41)"
},
"commit_authors": ["alice", "bob"],
"commits_count": 3,
"commits": [
{ "author": "alice", "hash": "ee8a4bf", "subject": "fix(api): import nextTick" },
{ "author": "bob", "hash": "9afcdbe", "subject": "fix(api): restore KPIs" }
]
}Campos opcionais (v2.30.12+):
branch— branch que originou o deploypr_number— PR detectado (squash trailer(#N), merge commit, ourunner add --pr N)expires_at— TTL do PR preview (ISO 8601), quando aplicávellast_commit— autor, hash e subject do commit real mais novo do deploycommit_authors— autores únicos do range (multi-commit)commits_count— total de commits reais reconhecidos no deploycommits— lista dos commits reais do range (newest-first, capada), com os auto-commitschore: update encrypted secretsfiltradosrunner_config(v2.175.3+) — linhas "configuração atualizada pelo runner: …" dos auto-commits do range, quando o deploy também tem commits reais. Ausente quando vazio.
Para eventos de falha, o payload inclui também "error": "<mensagem>".
Usar Telegram e webhook simultaneamente
notify:
channel: both
telegram_bot_token: "123456789:ABCdefGHI..."
telegram_chat_id: "-100123456789"
webhook_url: "https://hooks.exemplo.com/runner"Eventos disparados
event_type |
Quando | O que inclui |
|---|---|---|
deploy_started |
Início do deploy (antes do fetch) | projeto, instância, versão, branch |
deploy_success |
Deploy termina com healthcheck OK | projeto, instância, versão, duração — chat conforme enabled (audit log sempre) |
deploy_failure |
Deploy falha ou healthcheck não passa | projeto, instância, versão, erro extraído |
rollback_started |
Rollback automático iniciado | projeto, instância, versão alvo |
rollback_success |
Rollback automático concluído | projeto, instância, versão revertida |
health_check_failed |
Healthcheck falhou após N retries | projeto, instância, versão, modo |
health_skipped |
healthcheck.mode: skip_check ativo (warn) |
projeto, instância |
canary_weight_changed |
Peso ajustado no scheduler | projeto, instância, peso anterior, peso novo |
canary_promoted |
Canary chegou a 100% | projeto, instância, versão promovida |
instance_destroyed |
runner destroy <instance> |
projeto, instância |
pr_expired |
runner cleanup --expired destruiu PR vencido (v2.30.12+) |
projeto, instância, pr_number |
manifest_repo_divergiu |
O repositório mudou o manifesto de uma app com manifesto fixado no servidor (2.178.0+) | projeto, os nomes dos campos em details, manifest |
manifesto_fixado_adulterado |
A raiz <app>/.deploy.yml não é o manifesto fixado: o cron pulou a app (2.178.0+) |
projeto, error, manifest |
manifesto_fixado_ilegivel |
O pin.json da app não pôde ser lido: o cron pulou a app (2.178.0+) |
projeto, error (código e caminho do arquivo), manifest |
Os três eventos do manifesto fixado saem do cron e só na transição: na
primeira vez que a situação aparece, não a cada ciclo. São alertas de
watcher: vão para o chat conforme watch_alerts, não conforme enabled. O
audit log registra sempre.
Eles trazem o campo estruturado manifest, além do texto em details/error:
"manifest": {"codigo": "manifest_repo_divergiu", "commit": "d4e5f6", "campos": ["healthcheck"]}codigo é o event_type. commit e campos (os nomes das chaves de
topo que mudaram, nunca os valores) só existem no manifest_repo_divergiu.
Ver `runner manifest`.
Commits reais na notificação de deploy
A lista de commits aparece na mensagem de início (🚀 Deploy iniciado) — pra
você ver o que está sendo deployado e diagnosticar um possível erro antes de a
conclusão chegar. Quando o deploy reconhece vários commits, lista os commits
reais do range (📝 Commits:, até 5, com trailer … +N mais). Os auto-commits
internos chore: update encrypted secrets (que o Runner cria ao persistir
.runner/secrets.enc) são filtrados — você vê o que de fato foi deployado, não o
auto-commit. Os mesmos campos vão pro payload do webhook (commits,
commits_count, commit_authors) e pro audit log, em todos os eventos.
Deploy por `dist`: a lista do builder (v2.175.3+)
Na dist só existe o commit de snapshot deploy: vX.Y.Z. A partir do builder
0.5.3, o corpo desse commit traz a lista do que entrou na release, antes do
trailer de carimbo:
deploy: v2.385.0
Commits:
- a1b2c3d feat(painel): filtro por instância (alice)
- d4e5f6a fix(auth): corrige refresh token expirado (bob)
- ... e mais 12 commits
Builder-Carimbo: v=...O Runner lê essa lista e ela vira o conteúdo da notificação: a manchete, os
itens (📝 Commits:) e os autores (👥 Por:). O "e mais N" entra na contagem.
👥 Por: `alice, bob` (14 commits)
📝 Commits:
• `a1b2c3d` — `feat(painel): filtro por instância`
• `d4e5f6a` — `fix(auth): corrige refresh token expirado`
… +12 maisA lista só é lida quando o commit tem o carimbo do builder. Um commit de dist
de release antigo, sem a lista, aparece como sempre: 📝 Commit: <hash> — deploy: v2.384.0.
Configuração alterada pelo próprio Runner (v2.175.3+)
Quando o Runner commita no repo da app (o bundle de secrets depois de um
runner env set, runner secrets push ou deploy, e o .deploy.yml no
bootstrap, no manifest via CCS ou no migrate-source), o subject continua o
mesmo de sempre (chore: update encrypted secrets). O corpo do commit
passa a dizer o que mudou, só pelo NOME:
chore: update encrypted secrets
adicionada: SMTP_HOST
alterada: SMTP_PASSWORD
removida: OLD_TOKEN
.deploy.yml alterado: healthcheck, port- Só nomes de chave e campos de topo do
.deploy.yml. O valor nunca vai para o commit, o log ou a notificação. Nada é decifrado: a comparação é feita contra a versão que já está no repo, pelo texto cifrado de cada chave. - O detalhe por chave existe só no esquema v2 de secrets, em que as chaves
são explícitas no arquivo. Num bundle legado, o corpo traz só a linha
secrets atualizados (bundle legado: o detalhe por chave aparece depois de migrar para v2 com runner secrets migrate), e a notificação mostrasecrets atualizados (bundle legado). - O nome só aparece se seguir
[A-Za-z_][A-Za-z0-9_]*e tiver até 64 caracteres. - Ficam sem corpo o re-key da ckey (
chore: reset encrypted secrets (ckey re-key)), porque a ckey nova não abre o bundle anterior, e a remoção do bundle do repo.
Na notificação, esse corpo vira uma linha:
configuração atualizada pelo runner: secrets (adicionada: SMTP_HOST; alterada: SMTP_PASSWORD), .deploy.yml (healthcheck, port)- Deploy só com o auto-commit (por exemplo,
env setseguido de deploy): a linha aparece no lugar do commit, em📝 Commit:. - Deploy com commits reais e auto-commit: o auto-commit não entra na
contagem nem em
Por:. A linha aparece separada, com⚙️. - A linha mostra até 10 nomes e depois "e mais N chaves". Sai uma linha de
configuração por mensagem; as demais viram
(+N).
Tamanho da mensagem (v2.175.3+)
👥 Por: mostra até 5 autores e depois "e mais N". A mensagem montada tem
orçamento de 4000 caracteres (o Telegram corta em 4096; o Discord usa 2000). Se
passar, o corte é feito numa quebra de linha, nunca no meio de um trecho entre
crases, e a última linha vira ….
- Auto-commit de runner anterior à 2.175.3 (sem corpo) continua filtrado.
Template Telegram/Discord (v2.43.2+)
O header lidera com a ação; a identidade é textual (sem badge de cor de
ambiente). Projeto: só aparece quando project_group está setado no .deploy.yml.
Início (traz domínio, branch e os commits):
🚀 Deploy iniciado
Deploy: loja-admin-front
Projeto: Loja
🌐 Instance: production
🔗 admin.exemplo.com.br
🏷️ fc4086f ← ca73d54
🌿 Branch: feature/prod-deploy · PR #41
🆔 6bc09611
👥 Por: equipe-dev (2 commits)
📝 Commits:
• fc4086f — merge: refund type frontend + label mapping wallet.
• d832c1d — fix(auth): pre-flight JWT expiry + abort-all 401 cascadeConclusão / Falha (enxutas e auto-contidas, com duração; falha traz o erro):
✅ Deploy concluído
Deploy: loja-admin-front
Projeto: Loja
🌐 Instance: production
🏷️ fc4086f
🆔 dca185c6
⏱️ 22sLinha 🌿 Branch:: mostra sempre a branch do deploy; anexa · PR #N quando um PR
é detectado (campo pr_number ou trailer (#N) no subject do commit). Não há
mais o ⚠️ direct push — quando não há PR, mostra só a branch.
Para PR deploys com TTL ativo, aparece uma linha ⏰ Auto-destroy em Xh no início.
Verbose errors (falha de deploy)
Com extras.verbose_errors: true (opt-in, default false), numa falha o Runner
envia uma segunda mensagem com o log completo do erro:
- Resumo com o erro extraído (até 800 chars)
- Log completo do erro (até 3800 chars)
As duas mensagens são correlacionadas pelo mesmo deploy_id. Exige a classe
deploy ligada (enabled: true).
notify:
enabled: true
extras:
verbose_errors: trueAlertas dos watchers — tls-watch, doctor-watch e auto-pause (v2.118.0+)
Além do ciclo de deploy, o cron (runner fetch --deploy) roda watchers que
olham a saúde do próprio servidor e avisam pelo mesmo canal do deploy. Não
são eventos de deploy — por isso têm classe própria e não seguem enabled.
| Watcher | O que observa | Quando avisa |
|---|---|---|
| tls-watch (v2.117.0+) | Todo route file do Traefik com certResolver: o domínio serve o TRAEFIK DEFAULT CERT? |
Sem registro DNS (NXDOMAIN): alerta e não retenta — o Traefik nunca vai emitir. DNS ok mas cert padrão: reescreve o route file (# tls-retry: <ts>) pra forçar o reload e o Traefik retentar o ACME; avisa a cada tentativa (mínimo 60 min entre elas). Quando o certificado sai, manda "✅ Certificado ACME emitido" e encerra a pendência |
| doctor-watch | Checks do runner doctor sobre o state (runner doctor --watch) |
Check que passou a falhar; "state OK novamente" na recuperação |
| auto-pause do fetch (#128) | Apps cujo git fetch falha em sequência |
Ao pausar (⏸️ "App auto-pausada") e ao retomar sozinho |
Exemplo de mensagem (tls-watch, DNS ausente):
🩺 Alerta de saúde do runner
Deploy: runner
🌐 Instance: tls-watch
🆔 e5623a98
app.exemplo.com.br: sem certificado ACME (Traefik no certificado padrão) porque o DNS
não resolve (NXDOMAIN). O Traefik NÃO retenta sozinho — crie o registro A; o runner
força a retentativa no próximo ciclo assim que o DNS existir.
Arquivo: /opt/traefik/dynamic/usuario_app-front__production.ymlO que fazer com ele: o texto já diz — criar o registro DNS, ou, se a app foi
descontinuada, runner unregister <app> (o route file some e o alerta para).
Ligar e desligar
Uma chave, default ligado:
# /opt/runner/config.yml
notify:
watch_alerts: true # false = silencia os três watchers no chatrunner settings set notify.watch_alerts false # desliga
runner settings set notify.watch_alerts true # liga de volta
runner settings get notify.watch_alertsDesligado, o alerta continua no audit log (deploy-events.jsonl) e no log do
cron (WARN [tls-watch] … em /var/log/runner-cron.log) — só não vai pro chat.
enabled: false (classe deploy) não silencia os watchers; é preciso desligar
watch_alerts explicitamente.
Com que frequência repete
O mesmo problema não repete a cada ciclo de 5 min. O runner guarda o estado
(<state_dir>/.tls-watch-state.json, .doctor-watch-state.json) e só re-alerta
depois de watch_realert_hours (top-level do config.yml, default 6):
runner settings set watch_realert_hours 12 # re-alerta a cada 12 h
runner settings set watch_realert_hours 0 # alerta em todo ciclo (barulhento)Vale pro tls-watch e pro doctor-watch. Domínio que sai dos route files (app desregistrada, router removido) sai do estado sozinho.
Confirmar que está funcionando
grep -n "tls-watch" /var/log/runner-cron.log | tail # o que o cron viu
cat /opt/runner/state/.tls-watch-state.json # pendências e alerted_at
curl -k -sS -o /dev/null -v --max-time 4 \
--resolve <dominio>:443:127.0.0.1 https://<dominio>/ 2>&1 | grep issuer # cert atualSe o alerta chegou no chat só com o cabeçalho (sem o parágrafo do domínio), o
binário é anterior à 2.117.1 — runner self-update.
Canais nomeados (config.yml)
Além da config global de deploy (seção anterior), o config.yml pode declarar canais nomeados em notify.channels:. Cada app referencia esses canais pelo nome — as credenciais ficam só aqui, nunca no .deploy.yml do repo.
# /opt/runner/config.yml
notify:
# ... config global (channel/enabled/extras) ...
channels:
time_telegram:
type: telegram
bot_token: "123456:ABC-DEF..."
chat_id: "-100123456789"
ops_email:
type: smtp
host: smtp.exemplo.com
port: 587
username: [email protected]
password: "{SMTP_PASSWORD}"
from: "[email protected]"
to: "[email protected]"
meu_webhook:
type: webhook
base_headers:
Authorization: "Basic {BASE64_CREDS}"Tipos de canal suportados: telegram, webhook, smtp (e-mail).
Notificações por app (.deploy.yml)
Cada app pode declarar, no próprio .deploy.yml, uma lista de subscrições que referenciam canais nomeados do config.yml. É aditivo à config global — não substitui as notificações de classe deploy do servidor.
# .deploy.yml da app
project: meu-app
image: caddy:2-alpine
port: 80
notify:
- on: [deploy_success, deploy_failure]
when: "{{::instance_name}} == production"
channel: time_telegram
template: "{{::repository_name}} v{{::deploy_version}} — {{::deploy_result_status}}"
- on: [deploy_rollback]
channel: meu_webhook
request:
method: POST
url: "https://hooks.exemplo.com/{{::TOKEN}}"
body: '{"msg":"{{::repository_name}} rollback"}'Campos:
| Campo | Descrição |
|---|---|
on: |
Eventos: deploy_started, deploy_success, deploy_failure, deploy_rollback |
when: |
Condição opcional {{::var}} == valor / != valor. Sem when, dispara sempre |
channel: |
Nome de um canal em notify.channels: do config.yml |
template: |
Corpo da mensagem (canais telegram/smtp) |
subject: |
Assunto do e-mail (opcional, smtp) |
request: |
method/url/headers/body (canal webhook) |
template, subject: e os campos de request: usam a mesma sintaxe {{::var}} de secrets:. Além do contexto do deploy (repository_name, deploy_result_status, deploy_version, deploy_error, instance_name, instance_domain, event, deploy_id, project e, em v2.135.0+, container_id, container_name, previous_container_id, previous_container_name — #177), dá pra referenciar variáveis do environment:/secrets: do próprio app — resolvidas do bundle criptografado no momento do deploy. Valores reais de secret nunca ficam gravados no manifesto.
Uma subscrição com canal inexistente, mal configurado ou que falhe no envio é apenas logada — nunca bloqueia o deploy (fire-and-forget).
Isso é útil quando apps de projetos diferentes precisam notificar chats/e-mails distintos no mesmo servidor.
Comandos disponíveis
# Setup interativo
runner notify setup --channel telegram
runner notify setup --channel webhook
runner notify setup --channel both
# Enviar mensagem de teste
runner notify test
# Ver status dos canais configurados
runner notify statusO que pode dar errado
Telegram não envia após configuração manual
O chat_id de grupos e canais é negativo (começa com -100...). Valores positivos são de DMs. Verifique o sinal.
Teste direto:
curl -s "https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<CHAT_ID>&text=teste"A resposta inclui "ok": true se funcionou, ou "description": "..." com o erro.
Bot em grupo não recebe mensagens
Por padrão, bots em grupos só recebem mensagens diretas (/comando). Para receber todas as mensagens e detectar o chat_id:
- No @BotFather:
/mybots> seu bot > Bot Settings > Group Privacy > Turn off - Remova e readicione o bot ao grupo
Webhook retorna erro de SSL
O Runner exige HTTPS com certificado válido. Certificados auto-assinados não são aceitos. Use um domínio com certificado Let's Encrypt ou similar.
App pausada não notifica
O evento app_paused é disparado apenas no momento da pausa. Se a app já estava pausada quando o Runner iniciou, não há notificação retroativa.
Links relacionados
- Referência config.yml — todos os campos da seção
notify: - GitOps mode — como o cron fetch --deploy funciona
- Pause e unpause — evento
app_pausede auto-pause - Referência config.yml — `watch_alerts` e `watch_realert_hours`