services: postgres: image: postgres:18-alpine container_name: b2bcall-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} ports: - "127.0.0.1:5432:5432" volumes: - postgres_data:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine container_name: b2bcall-redis restart: unless-stopped command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] ports: - "127.0.0.1:6379:6379" volumes: - redis_data:/data healthcheck: test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep -q PONG"] interval: 5s timeout: 5s retries: 10 fs-config: build: context: . dockerfile: apps/freeswitch-config/Dockerfile container_name: b2bcall-fs-config restart: unless-stopped depends_on: - postgres - redis environment: # Hostname interno do compose (postgres/redis/freeswitch), nao # localhost — ver docs/NETWORK_ARCHITECTURE.md. APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public ENCRYPTION_KEY: ${ENCRYPTION_KEY} FS_CONFIG_USER: ${FS_CONFIG_USER} FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 ESL_HOST: freeswitch ESL_PORT: "8021" ESL_PASSWORD: ${ESL_PASSWORD} SOFIA_EXTERNAL_GATEWAYS_DIR: /gateways CALLCENTER_QUEUES_DIR: /callcenter-queues volumes: # Compartilhado com o FreeSWITCH (agente.md secao 41-42): fs-config # escreve os XML de gateway aqui, o profile "external" ja inclui # sip_profiles/external/*.xml automaticamente (config vanilla). - freeswitch_external_gateways:/gateways # Compartilhado com o FreeSWITCH (agente.md secao 50-51): fs-config # escreve um XML de fila por arquivo aqui, incluido via # X-PRE-PROCESS no nosso callcenter.conf.xml (mesmo padrao acima). - freeswitch_callcenter_queues:/callcenter-queues # Sem porta publicada: so o FreeSWITCH (mesma rede do compose) chama isto. healthcheck: test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"] interval: 10s timeout: 5s retries: 10 start_period: 10s freeswitch: build: context: ./infrastructure/freeswitch secrets: - freeswitch_pat container_name: b2bcall-freeswitch restart: unless-stopped depends_on: - fs-config environment: ESL_PASSWORD: ${ESL_PASSWORD} FS_CONFIG_USER: ${FS_CONFIG_USER} FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD} volumes: - freeswitch_external_gateways:/etc/freeswitch/sip_profiles/external - freeswitch_callcenter_queues:/etc/freeswitch/autoload_configs/callcenter_queues.conf.d # Bind mount (não volume nomeado do Docker) porque apps/api roda no # host e precisa enxergar os mesmos arquivos que o FreeSWITCH grava # (agente.md secao 90-91) — ver docs/RECORDING.md. - ./data/recordings-spool:/recordings # Prompts de áudio do IVR (PHASE 59) — mesmo raciocínio do bind # mount acima, mas na direção contrária: apps/api (host) ESCREVE o # arquivo que o usuário sobe, e o FreeSWITCH precisa LER esse mesmo # arquivo ao vivo durante `play_and_get_digits`. Só funciona como # bind mount de disco local — um fetch em rede (S3/HTTP) durante # uma chamada ativa seria latência/confiabilidade desnecessárias # pra um prompt de poucos segundos. - ./data/ivr-prompts:/ivr-prompts # Event Socket (8021) NUNCA publicado — só alcançável por outros # containers na rede interna do compose (agente.md secao 22). # SIP (5060) e RTP (16384-16584, range fixo no Dockerfile) publicados # a partir da PHASE 54 — necessário pra registrar um softphone/telefone # de fora da rede Docker (achado testando registro de verdade). ports: - "5060:5060/udp" - "5060:5060/tcp" - "16384-16584:16384-16584/udp" healthcheck: test: ["CMD-SHELL", "fs_cli -p \"$$ESL_PASSWORD\" -x status | grep -q 'is ready'"] interval: 10s timeout: 5s retries: 10 start_period: 20s fs-events: build: context: . dockerfile: apps/freeswitch-events/Dockerfile container_name: b2bcall-fs-events restart: unless-stopped depends_on: - freeswitch - redis - postgres environment: ESL_HOST: freeswitch ESL_PORT: "8021" ESL_PASSWORD: ${ESL_PASSWORD} APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public # Hostnames internos do compose (freeswitch/redis), diferente do # REDIS_URL do .env que aponta pra localhost (uso pelo apps/api, que # ainda roda no host) — ver docs/NETWORK_ARCHITECTURE.md. REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 # Gravações (agente.md secao 90-93) — mesmo padrão do REDIS_URL # acima: path Docker-interno aqui, path do host no .env pro # apps/api enxergar os mesmos arquivos (ver docs/RECORDING.md). RECORDINGS_SPOOL_DIR: /recordings STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local} LOCAL_STORAGE_ROOT: /data/object-storage S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-} S3_ENDPOINT: ${S3_ENDPOINT:-} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-} volumes: - ./data/recordings-spool:/recordings - ./data/object-storage-local:/data/object-storage predictive-dialer: build: context: . dockerfile: apps/predictive-dialer/Dockerfile container_name: b2bcall-predictive-dialer restart: unless-stopped depends_on: - freeswitch - redis - postgres environment: ESL_HOST: freeswitch ESL_PORT: "8021" ESL_PASSWORD: ${ESL_PASSWORD} APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 # Real Outbound Safety (agente.md secao 186): as DUAS precisam estar # explicitamente ligadas pra originar PSTN de verdade — nunca ativado # por default. Ver docs/PREDICTIVE_DIALER.md. DIALER_SIMULATION: ${DIALER_SIMULATION:-true} ALLOW_REAL_OUTBOUND_CALLS: ${ALLOW_REAL_OUTBOUND_CALLS:-false} # Só usado pra montar o path do record_session (agente.md secao 91) # — quem de fato grava e' o processo FreeSWITCH, não este worker; # não precisa do volume montado aqui, só saber o path Docker-interno. RECORDINGS_SPOOL_DIR: /recordings ai-worker: build: context: . dockerfile: apps/ai-worker/Dockerfile container_name: b2bcall-ai-worker restart: unless-stopped depends_on: - postgres environment: APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public ENCRYPTION_KEY: ${ENCRYPTION_KEY} # Precisa ler as gravações que fs-events já subiu (agente.md secao # 104-108) — mesmo par de env vars/bind mount de fs-events, ver # docs/RECORDING.md e docs/AI_PIPELINE.md. STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local} LOCAL_STORAGE_ROOT: /data/object-storage S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-} S3_ENDPOINT: ${S3_ENDPOINT:-} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-} volumes: - ./data/object-storage-local:/data/object-storage secrets: freeswitch_pat: environment: FREESWITCH_PAT volumes: postgres_data: redis_data: freeswitch_external_gateways: freeswitch_callcenter_queues: