mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
435 lines
21 KiB
YAML
435 lines
21 KiB
YAML
# DeskcommCRM — stack de produção self-host (VPS HostGator).
|
|
# Sobe: caddy (HTTPS) → app (Next standalone) + waha + redis+srh + scheduler.
|
|
# Só o Caddy publica portas (80/443). Todo o resto vive na rede interna.
|
|
#
|
|
# docker compose -f docker-compose.prod.yml --env-file .env up -d
|
|
|
|
x-logging: &default-logging
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m" # rotação: evita que o log encha o disco do VPS
|
|
max-file: "3"
|
|
|
|
services:
|
|
app:
|
|
# Teto de memória — 335 MiB de pico MEDIDO nesta VPS.
|
|
#
|
|
# Sem ele o contêiner é ilimitado, e numa VPS de 3,8 GB isso não é teoria:
|
|
# um contêiner ALHEIO (agente da hospedagem, fora deste compose) registrou
|
|
# pico de 3.068 MiB no mesmo host. Quando a RAM acaba, o OOM killer escolhe
|
|
# a vítima por PONTUAÇÃO — quem cresceu mais rápido —, não por importância:
|
|
# o que morre pode ser o CRM.
|
|
#
|
|
# O teto não impede o pico legítimo; impede que UM serviço leve a máquina
|
|
# inteira. Estourá-lo mata só quem estourou, e o `restart: unless-stopped`
|
|
# o traz de volta — falha isolada em vez de queda geral.
|
|
mem_limit: 768m
|
|
# Imagem GENÉRICA pré-buildada: o leigo só puxa (sobe em ~2min, roda em 2GB).
|
|
# Os NEXT_PUBLIC_* reais vêm em RUNTIME (env_file abaixo) — o browser lê via
|
|
# <PublicEnvScript/> e o servidor via lib/env.ts. Não precisa buildar no VPS.
|
|
# Build local (avançado): docker compose -f docker-compose.prod.yml -f docker-compose.build.yml build
|
|
# `:stable` (última release), não `:latest` (topo da `main`). Quem cai neste
|
|
# default é quem NÃO tem APP_IMAGE no .env — avaliação, ou um .env que
|
|
# perdeu a chave. Nenhum desses casos quer código não lançado; quem quiser o
|
|
# topo da main pede `APP_IMAGE=…:latest` explicitamente.
|
|
image: ${APP_IMAGE:-ghcr.io/melgarafael/deskcommcrm:stable}
|
|
pull_policy: ${APP_PULL_POLICY:-always}
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
environment:
|
|
SUPABASE_DB_ADMIN_URL: ""
|
|
depends_on:
|
|
srh:
|
|
condition: service_started
|
|
waha:
|
|
condition: service_started
|
|
healthcheck:
|
|
# Probe TCP puro (node existe na imagem; curl/nc não). NÃO usar /api/v1/health:
|
|
# ele dá 503 se WAHA/Redis caem, o que deixaria o app "unhealthy" e derrubaria
|
|
# o Caddy junto (blackout total por uma dependência secundária).
|
|
test: ["CMD", "node", "-e", "require('net').connect(3000,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 40s
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
|
|
# Worker 24/7 do agent-engine (fusão Vendaval) — cérebro do agente SDR.
|
|
worker:
|
|
# Teto de memória — 230 MiB de pico MEDIDO nesta VPS.
|
|
#
|
|
# Sem ele o contêiner é ilimitado, e numa VPS de 3,8 GB isso não é teoria:
|
|
# um contêiner ALHEIO (agente da hospedagem, fora deste compose) registrou
|
|
# pico de 3.068 MiB no mesmo host. Quando a RAM acaba, o OOM killer escolhe
|
|
# a vítima por PONTUAÇÃO — quem cresceu mais rápido —, não por importância:
|
|
# o que morre pode ser o CRM.
|
|
#
|
|
# O teto não impede o pico legítimo; impede que UM serviço leve a máquina
|
|
# inteira. Estourá-lo mata só quem estourou, e o `restart: unless-stopped`
|
|
# o traz de volta — falha isolada em vez de queda geral.
|
|
mem_limit: 512m
|
|
# Imagem publicada — e não mais `build:` sozinho. O motivo é o modo de falha
|
|
# que isso escondia: serviço build-only é PULADO por `docker compose pull`
|
|
# ("Skipped - No image to be pulled") e imune a `up -d` sem `--build`. Ele era
|
|
# construído na VPS de todo cliente, no install, e NENHUM update.sh jamais o
|
|
# reconstruía — o runtime do agente de IA congelava no código do dia da
|
|
# instalação e atravessava todas as atualizações seguintes.
|
|
#
|
|
# O `build:` continua abaixo de propósito: com os dois presentes, o Compose
|
|
# usa a imagem quando ela existe e CONSTRÓI quando não existe (medido). É o
|
|
# que mantém a mudança aditiva — registry fora do ar, ou versão anterior à
|
|
# primeira publicação, caem no comportamento antigo em vez de quebrar.
|
|
#
|
|
# O default é `:stable` (a última RELEASE), não `:latest`, e a diferença é
|
|
# concreta para o parque instalado. Numa VPS que atualiza pela primeira vez
|
|
# depois desta mudança, quem executa é o `update.sh` VELHO — o que está no
|
|
# disco —, e ele só sabe gravar APP_IMAGE. O `git checkout` da tag já trocou
|
|
# ESTE arquivo, então worker e scheduler caem no default daqui. Com
|
|
# `:latest`, esse default seria o TOPO DA MAIN (o canal segue a branch
|
|
# default): o app pinado numa release e o runtime do agente de
|
|
# IA rodando código não lançado, sobre o banco da release. `:stable` é a
|
|
# última release publicada — pareia com o app em vez de correr na frente.
|
|
image: ${WORKER_IMAGE:-ghcr.io/melgarafael/deskcomm-worker:stable}
|
|
pull_policy: ${WORKER_PULL_POLICY:-always}
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.worker
|
|
restart: unless-stopped
|
|
env_file: .env
|
|
environment:
|
|
# dentro da rede do compose, o WAHA é o serviço 'waha' na porta interna 3000
|
|
WAHA_API_BASE_URL: http://waha:3000
|
|
SUPABASE_DB_ADMIN_URL: ""
|
|
networks:
|
|
- internal
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8787/healthz"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 20s
|
|
waha:
|
|
# Teto de memória — 893 MiB de pico MEDIDO nesta VPS.
|
|
#
|
|
# Sem ele o contêiner é ilimitado, e numa VPS de 3,8 GB isso não é teoria:
|
|
# um contêiner ALHEIO (agente da hospedagem, fora deste compose) registrou
|
|
# pico de 3.068 MiB no mesmo host. Quando a RAM acaba, o OOM killer escolhe
|
|
# a vítima por PONTUAÇÃO — quem cresceu mais rápido —, não por importância:
|
|
# o que morre pode ser o CRM.
|
|
#
|
|
# O teto não impede o pico legítimo; impede que UM serviço leve a máquina
|
|
# inteira. Estourá-lo mata só quem estourou, e o `restart: unless-stopped`
|
|
# o traz de volta — falha isolada em vez de queda geral.
|
|
mem_limit: 1280m
|
|
# Core grátis por padrão; troque para devlikeapro/waha-plus via WAHA_IMAGE se
|
|
# precisar de multi-número/retry/S3 (licença paga).
|
|
#
|
|
# Tag PINADA: `devlikeapro/waha` sem tag é `:latest`, e o `dc pull` do
|
|
# update.sh entregaria ao cliente qualquer versão que o upstream tivesse
|
|
# publicado. O instalador grava `latest-2026.7.2` em x86 e
|
|
# `noweb-arm-2026.7.2` em ARM64; as duas tags fixam a versão NOWEB.
|
|
# Bumpar faz parte do release.
|
|
image: ${WAHA_IMAGE:-devlikeapro/waha:latest-2026.7.2}
|
|
restart: unless-stopped
|
|
environment:
|
|
# Prefixo sha512: faz o WAHA HASHEAR o X-Api-Key recebido e comparar com este
|
|
# hash. Sem o prefixo ele compara literal → o app (que manda o plaintext) toma 401.
|
|
WAHA_API_KEY: "sha512:${WAHA_API_KEY_SHA512}"
|
|
WHATSAPP_HOOK_URL: ${WAHA_WEBHOOK_BASE_URL}/api/v1/webhooks/waha
|
|
# `message` NÃO entra, e `message.any` sozinho não perde nada — medido.
|
|
#
|
|
# O WAHA emite os DOIS para a mesma mensagem, e `lib/waha/ingest.ts` já
|
|
# chama isso de corrida e deduplica. Só que o arquivo do corpo cru guarda
|
|
# as duas cópias: numa instalação real, 22.939 linhas de `message` ao lado
|
|
# de 24.582 de `message.any` — 41% da tabela que era 86% do banco.
|
|
#
|
|
# A prova de que dá para tirar veio do banco, não da doc: cruzando os ids
|
|
# de mensagem dos dois eventos em 22.815 mensagens reais, os que chegaram
|
|
# SÓ por `message` foram ZERO, e 1.642 chegaram só por `message.any` (os
|
|
# que saíram de nós, que `message` não cobre). Superconjunto estrito.
|
|
WHATSAPP_HOOK_EVENTS: "message.any,message.ack,message.edited,message.revoked,session.status,state.change"
|
|
WHATSAPP_HOOK_HMAC: ${WAHA_HMAC_SECRET}
|
|
# WAHA lê WHATSAPP_DEFAULT_ENGINE (NÃO WAHA_DEFAULT_ENGINE — o nome errado
|
|
# fazia cair no default WEBJS, engine com browser que trava/corrompe a sessão
|
|
# quando o WhatsApp Web atualiza; NOWEB é websocket direto, estável e leve).
|
|
WHATSAPP_DEFAULT_ENGINE: ${WHATSAPP_DEFAULT_ENGINE:-NOWEB}
|
|
WAHA_DASHBOARD_ENABLED: "false" # nunca expor o dashboard num template público
|
|
# Sem isto o container volta e as sessões ficam STOPPED: o número continua
|
|
# pareado em /app/.sessions, mas nada entra nem sai até alguém clicar
|
|
# Reconectar. True retoma as que já existiam — não reloga, não pede QR.
|
|
# Default do WAHA é False.
|
|
WHATSAPP_RESTART_ALL_SESSIONS: "${WHATSAPP_RESTART_ALL_SESSIONS:-True}"
|
|
volumes:
|
|
- waha-data:/app/.sessions # sessões pareadas (QR) — perder = re-parear
|
|
- waha-media:/app/.media
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
command: ["redis-server", "--save", "", "--appendonly", "no"] # efêmero (rate-limit/debounce)
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 30s
|
|
timeout: 3s
|
|
retries: 3
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
srh:
|
|
# serverless-redis-http: fala o REST do Upstash sobre o redis normal, mantendo
|
|
# o SDK @upstash/redis sem tocar código.
|
|
#
|
|
# Pinado por DIGEST, não por tag, e o motivo é específico: `latest` e `0.0.10`
|
|
# NÃO são a mesma imagem (5b0bb923… vs 65128347…), então trocar `:latest` por
|
|
# `:0.0.10` mudaria o binário que roda no parque — que é exatamente o risco
|
|
# que a pendência T6 mandava validar antes (encoding base64 do rate limit).
|
|
# O digest congela o que já está em produção HOJE: zero mudança de conteúdo,
|
|
# e o `:latest` deixa de poder mudar sozinho debaixo de todo mundo.
|
|
# Quando a T6 for validada, isto vira `:0.0.10` e o comentário sai.
|
|
image: hiett/serverless-redis-http@sha256:5b0bb9239fce53abf87b2018a7a0deb9ec7bd900c5360738fe5fbeeb426f9150
|
|
restart: unless-stopped
|
|
environment:
|
|
SRH_MODE: env
|
|
SRH_TOKEN: ${SRH_TOKEN}
|
|
SRH_CONNECTION_STRING: "redis://redis:6379"
|
|
depends_on:
|
|
redis:
|
|
condition: service_healthy
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
scheduler:
|
|
# Cron sem docker.sock: crond interno batendo curl na rede interna.
|
|
# Resolução de 1 min — o menor passo que o crond tem, e o piso de latência
|
|
# de tudo que depende de cron (a lista tem rotas de minuto em minuto).
|
|
#
|
|
# A lista de crons mora em docker/scheduler/entrypoint.sh, não mais aqui. O
|
|
# `command:` inline instalava curl e tzdata (`apk add`) a CADA start — num
|
|
# serviço `restart: unless-stopped`, isso significa que o cron do cliente só
|
|
# voltava se a VPS tivesse internet e o mirror do Alpine estivesse de pé,
|
|
# justamente no momento em que a máquina está se recuperando de algo.
|
|
image: ${SCHEDULER_IMAGE:-ghcr.io/melgarafael/deskcomm-scheduler:stable}
|
|
pull_policy: ${SCHEDULER_PULL_POLICY:-always}
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.scheduler
|
|
restart: unless-stopped
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
environment:
|
|
TZ: UTC
|
|
INTERNAL_SECRET: ${INTERNAL_SECRET}
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
# ── Chamada de voz WhatsApp (spec 18) — DESLIGADA POR PADRÃO ──────────────
|
|
#
|
|
# ═══ POR QUE ELA VIVE NUM PROFILE ═══
|
|
#
|
|
# A chamada de voz vincula um SEGUNDO APARELHO ao mesmo número que já atende,
|
|
# por um caminho que não é o oficial: o risco é a CONTA ser bloqueada. Serviço
|
|
# que sobe em toda instalação transforma esse risco em padrão — e um contêiner
|
|
# a mais custa RAM numa VPS onde o WAHA já registrou 893 MiB de pico.
|
|
#
|
|
# `profiles:` é o mesmo mecanismo com que `docker-compose.traefik.yml` desliga
|
|
# o Caddy: sem o profile ativo, o compose NÃO cria o contêiner. Ligar é uma
|
|
# linha no .env — `COMPOSE_PROFILES=voz` — e nada mais.
|
|
#
|
|
# ⚠️ DUAS ARMADILHAS MEDIDAS, as duas do próprio Compose:
|
|
#
|
|
# 1. Nomear o serviço explicitamente (`docker compose up -d wacalls`) ATIVA
|
|
# o profile e sobe o contêiner mesmo com COMPOSE_PROFILES vazio. É por
|
|
# isso que o update.sh checa antes de recriar o caddy, e vale igual aqui.
|
|
# 2. `app` e `worker` NÃO podem ganhar `depends_on: wacalls`. Dependência
|
|
# em serviço de profile inativo não sobe — o CRM inteiro deixaria de
|
|
# subir em toda instalação que não usa a feature.
|
|
#
|
|
# ═══ POR QUE A IMAGEM É DO UPSTREAM, PINADA POR DIGEST ═══
|
|
#
|
|
# WaCalls (github.com/JotaDev66/WaCalls, MIT) é peça UPSTREAM, e a doutrina
|
|
# (docs/doctrine/packaging.md) é explícita: peça upstream se REFERENCIA, nunca
|
|
# se republica — "a regra vale para todas, não só a licenciada, porque a
|
|
# exceção é o que apaga a regra". Construir e publicar `deskcomm-wacalls`
|
|
# seria republicar.
|
|
#
|
|
# O digest é o pin mais forte que existe e o único disponível aqui: `:latest`
|
|
# e `:latest-pure` NÃO existem neste registry (404 medido em 2026-09-08); as
|
|
# tags publicadas são `develop` e `sha-*`. Ele resolve para
|
|
# `sha-d16a076`/`develop`, index OCI multi-arch (linux/amd64 + linux/arm64).
|
|
#
|
|
# `pull_policy: missing` porque o digest é imutável: com `always`, um GHCR
|
|
# fora do ar impediria o contêiner de subir mesmo com a imagem já no disco.
|
|
#
|
|
# ═══ SEM `ports:` — E ISSO É VIGIADO ═══
|
|
#
|
|
# A API do WaCalls fica só na rede interna, alcançável pelo app e pelo worker.
|
|
# `tests/unit/portas-do-compose.test.ts` reprova quem publicar porta aqui.
|
|
wacalls:
|
|
profiles: ["voz"]
|
|
mem_limit: 256m
|
|
image: ${WACALLS_IMAGE:-ghcr.io/jotadev66/wacalls@sha256:9ff52ef9455cf42125a911c38bb8c855f0cc205516cada2054e5c05546909022}
|
|
pull_policy: ${WACALLS_PULL_POLICY:-missing}
|
|
restart: unless-stopped
|
|
environment:
|
|
# O serviço NÃO SOBE sem estes dois — é o upstream que exige, e é o que
|
|
# substitui o build anterior, cujo README dizia "the API has no
|
|
# authentication ... run it only on a trusted LAN".
|
|
WACALLS_ADMIN_USER: ${WACALLS_ADMIN_USER:-}
|
|
WACALLS_ADMIN_PASSWORD: ${WACALLS_ADMIN_PASSWORD:-}
|
|
# Bearer da automação. Sem ele a API só aceita o cookie de login, que um
|
|
# processo server-to-server não tem: o app não conseguiria falar nada.
|
|
WACALLS_API_TOKEN: ${WACALLS_API_TOKEN:-}
|
|
# Vazio = same-origin. Nossa UI não fala com o WaCalls direto do browser —
|
|
# tudo passa pelas rotas /api/v1/voice/*, então não há origem a liberar.
|
|
WACALLS_CORS_ORIGINS: ${WACALLS_CORS_ORIGINS:-}
|
|
WACALLS_TRUSTED_PROXIES: ${WACALLS_TRUSTED_PROXIES:-}
|
|
WACALLS_PUBLIC_IP: ${WACALLS_PUBLIC_IP:-}
|
|
# A porta que o áudio usa, e ela precisa ser FIXA. Sem esta variável o
|
|
# upstream não monta o mux de UDP e cai em portas efêmeras aleatórias —
|
|
# impossíveis de publicar aqui, e o áudio nunca sai da máquina.
|
|
WACALLS_WEBRTC_UDP_PORT: ${WACALLS_WEBRTC_UDP_PORT:-7881}
|
|
ports:
|
|
# A ÚNICA porta publicada por um serviço nosso que não é o proxy, e ela é
|
|
# UDP. Não é exceção sem razão: o áudio do WebRTC não fala HTTP, então não
|
|
# há proxy que o carregue — o Caddy é L7 e não repassa UDP.
|
|
#
|
|
# host == contêiner de PROPÓSITO. O número entra no candidato SDP que o
|
|
# navegador recebe; um remap (7881:7882) faria o navegador mandar mídia
|
|
# para uma porta que o host não escuta, e a ligação ficaria muda com tudo
|
|
# "certo" nos logs.
|
|
#
|
|
# NUNCA TCP: a API HTTP do serviço continua só na rede interna, alcançável
|
|
# apenas pelo app e pelo worker. Publicar TCP aqui exporia a interface de
|
|
# administração dele à internet.
|
|
- "${WACALLS_WEBRTC_UDP_PORT:-7881}:${WACALLS_WEBRTC_UDP_PORT:-7881}/udp"
|
|
volumes:
|
|
# wacalls.db guarda a credencial de sessão do WhatsApp — mesmo padrão do
|
|
# waha-data. Apagar o volume desvincula todos os aparelhos.
|
|
- wacalls-data:/data
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
# ═══ TELEFONIA POR SIP (#677) — MÓDULO OPCIONAL, DESLIGADO POR PADRÃO ═══
|
|
#
|
|
# Decisão do dono (doc 27 da triagem): a telefonia por SIP entra como módulo
|
|
# opcional do compose, no molde do `wacalls`. Os dois serviços abaixo só
|
|
# existem com `telefonia` em COMPOSE_PROFILES no .env — sem isso o compose
|
|
# nem os cria, e nenhuma instalação que não pediu ganha porta aberta,
|
|
# contêiner ou consumo de memória.
|
|
#
|
|
# As mesmas duas regras do `wacalls` valem aqui: nomear o serviço no
|
|
# `up -d` ATIVA o profile; e `app`/`worker` não podem ganhar `depends_on`
|
|
# nestes serviços (dependência de profile inativo derruba o CRM inteiro).
|
|
#
|
|
# Para ligar: copiar asterisk/pjsip.conf.example e asterisk/ari.conf.example
|
|
# para os nomes sem `.example` (com as credenciais do tronco e do ARI),
|
|
# pôr `telefonia` em COMPOSE_PROFILES e subir.
|
|
#
|
|
# ═══ POR QUE O ASTERISK É PINADO POR DIGEST ═══
|
|
#
|
|
# Asterisk é peça UPSTREAM — se referencia, nunca se republica
|
|
# (docs/doctrine/packaging.md). E neste registry nenhuma tag é imutável: a
|
|
# `20-alpine` e até as tags de versão (`20.20.1-alpine`) são reconstruídas
|
|
# a cada build do mantenedor (medido em 18/09/2026: a mesma `20.20.1` com
|
|
# digests diferentes em 19/08, 26/08, 02/09, 09/09 e 16/09). O digest abaixo
|
|
# é o da `20-alpine` = `20.20.1_alpine-3.24-e9d725f` (16/09), multi-arch
|
|
# (amd64 + arm64). `pull_policy: missing` porque o digest é imutável.
|
|
asterisk:
|
|
profiles: ["telefonia"]
|
|
mem_limit: 256m
|
|
image: ${ASTERISK_IMAGE:-andrius/asterisk@sha256:927d4c98777149c3380c4f8d0e82b1b1ba9e7b38528f86c1e1593d5a25032f27}
|
|
pull_policy: ${ASTERISK_PULL_POLICY:-missing}
|
|
restart: unless-stopped
|
|
ports:
|
|
# Só UDP, e pelo mesmo motivo do `wacalls`: sinalização SIP e mídia RTP
|
|
# não falam HTTP, não há proxy L7 que as carregue, e o tronco do
|
|
# provedor precisa alcançá-las de fora.
|
|
#
|
|
# NUNCA TCP: a ARI (REST + WebSocket, 8088) é o controle total das
|
|
# chamadas e fica só na rede interna, alcançável apenas pelo
|
|
# `voice-agent`. Publicá-la entregaria à internet quem liga para quem.
|
|
- "5060:5060/udp"
|
|
# Faixa de RTP casada com asterisk/rtp.conf — mudar uma sem a outra deixa
|
|
# a ligação muda com tudo "conectado" nos logs.
|
|
- "10000-10200:10000-10200/udp"
|
|
volumes:
|
|
- ./asterisk/pjsip.conf:/etc/asterisk/pjsip.conf:ro
|
|
- ./asterisk/extensions.conf:/etc/asterisk/extensions.conf:ro
|
|
- ./asterisk/ari.conf:/etc/asterisk/ari.conf:ro
|
|
- ./asterisk/http.conf:/etc/asterisk/http.conf:ro
|
|
- ./asterisk/rtp.conf:/etc/asterisk/rtp.conf:ro
|
|
- asterisk-data:/var/lib/asterisk
|
|
- asterisk-logs:/var/log/asterisk
|
|
healthcheck:
|
|
test: ["CMD", "asterisk", "-rx", "core show uptime"]
|
|
interval: 15s
|
|
timeout: 5s
|
|
retries: 5
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
# O runtime de voz é NOSSO: imagem publicada pelo CI (publish-image.yml),
|
|
# nunca build-only (doutrina de packaging, invariante 1). `build:` fica ao
|
|
# lado só como escape.
|
|
voice-agent:
|
|
profiles: ["telefonia"]
|
|
mem_limit: 512m
|
|
image: ${VOICE_AGENT_IMAGE:-ghcr.io/melgarafael/deskcomm-voice-agent:stable}
|
|
pull_policy: ${VOICE_AGENT_PULL_POLICY:-always}
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.voice-agent
|
|
restart: unless-stopped
|
|
depends_on:
|
|
asterisk:
|
|
condition: service_healthy
|
|
# env_file completo, como o `worker`: lib/env.ts valida todas as variáveis
|
|
# da aplicação no import, não só as de voz.
|
|
env_file: .env
|
|
environment:
|
|
ARI_URL: "http://asterisk:8088"
|
|
ARI_WS_URL: "ws://asterisk:8088/ari/events"
|
|
ARI_APP: "voice-agent"
|
|
# Como no app e no worker: a conexão do dono do banco fica no .env só
|
|
# para o kit no host (update.sh do cron); nenhum código de workers/ a lê.
|
|
SUPABASE_DB_ADMIN_URL: ""
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
restart: unless-stopped
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
environment:
|
|
DOMAIN: ${DOMAIN}
|
|
ACME_EMAIL: ${ACME_EMAIL}
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddy-data:/data # certificados Let's Encrypt (persistir!)
|
|
- caddy-config:/config
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
logging: *default-logging
|
|
networks: [internal]
|
|
|
|
volumes:
|
|
waha-data:
|
|
waha-media:
|
|
wacalls-data:
|
|
asterisk-data:
|
|
asterisk-logs:
|
|
caddy-data:
|
|
caddy-config:
|
|
|
|
networks:
|
|
internal:
|
|
driver: bridge
|