Files
DeskcommCRM/Dockerfile.scheduler
T
Rafael MelgaçoandClaude Opus 5 3823853159 feat(packaging): o worker e o scheduler viram imagem publicada — e o gate que impede a volta
O serviço `worker` do compose de produção não tinha `image:`, só `build:`. As
consequências, todas medidas, encadeiam:

1. `docker compose pull` PULA serviço build-only ("Skipped - No image to be
   pulled"), então o `dc pull` do install/update nunca o trazia;
2. `up -d` sem `--build` recria o contêiner sobre a imagem velha — provei num
   compose stub: Dockerfile alterado, `up -d`, e o contêiner segue imprimindo
   VERSAO-1;
3. logo o worker era construído na VPS de cada cliente (pnpm install de 82
   pacotes) e NUNCA mais atualizado.

E ele não é acessório: `app/api/v1/cron/agent-dispatcher` é no-op permanente, o
agent-worker é o único consumidor de `ai_agent.dispatch_requested`. O runtime do
agente de IA — a feature-título — era a única peça que não recebia correção. O
QA de instalação real até registrou o sintoma sem tirar a conclusão
("a VPS realmente compila o worker", user-journey-map.md:295).

A mudança é ADITIVA por construção: `build:` continua ao lado de `image:`, e
medi que o Compose constrói localmente quando a imagem não existe no registry —
com e sem pull_policy. Registry fora do ar, ou clone anterior à primeira
publicação, caem no comportamento de hoje em vez de quebrar.

Também:

- scheduler vira imagem e para de rodar `apk add curl tzdata` a cada start. Num
  serviço `restart: unless-stopped` isso amarrava a volta do cron à internet da
  VPS e ao mirror do Alpine, justamente quando a máquina se recupera de algo;
- WAHA pinado em `latest-2026.7.2` — MESMO digest de `latest` hoje
  (65e593e30bb7), então zero mudança de conteúdo; muda quem decide quando o
  WhatsApp do cliente troca de motor;
- `srh` pinado por DIGEST, não por tag: `latest` (5b0bb923) e `0.0.10`
  (65128347) são imagens DIFERENTES, então `:0.0.10` trocaria o binário do
  parque — o risco que a pendência T6 mandava validar antes. O digest congela o
  que já roda, sem incorrer nesse risco;
- `/api/v1/health` para de mentir. Lia `npm_package_version`, que é `undefined`
  sob `CMD ["node","server.js"]`: toda instalação do mundo reportava "0.1.0". O
  fallback agora é "desconhecido" — um campo que responde errado com confiança
  desliga a pergunta, e é pior que um campo ausente.

O gate novo tem guarda do próprio instrumento: a primeira versão do teste de
pull_policy passava sem avaliar um único serviço (só cobrava tag imutável, e os
três apontam para tag móvel). Agora ele cobra os dois sentidos e falha se
avaliar menos que os três.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
2026-08-13 10:57:46 -03:00

29 lines
1.2 KiB
Docker

# Scheduler dos crons — busybox crond batendo nas rotas /api/v1/cron/* pela rede
# interna do compose. Sem docker.sock: o contêiner não fala com o daemon, só HTTP.
#
# Existe como imagem (em vez de `alpine:3.20` + `apk add` no command do compose)
# porque instalar pacote em todo start amarra a recuperação do cron do cliente à
# internet da VPS e ao mirror do Alpine — dependência de rede num caminho que
# deveria ser offline. Doutrina: docs/doctrine/packaging.md, invariante 1.
FROM alpine:3.20
LABEL org.opencontainers.image.source="https://github.com/melgarafael/DeskcommCRM" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="DeskcommCRM scheduler"
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION \
TZ=UTC
RUN apk add --no-cache curl tzdata
COPY docker/scheduler/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Prova de vida: se o crond morrer, o contêiner cai e o `restart: unless-stopped`
# o traz de volta. Sem isto, um crond morto fica indistinguível de um cron ocioso.
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=3 \
CMD pgrep crond >/dev/null || exit 1
CMD ["/usr/local/bin/entrypoint.sh"]