services: # ============================================================ # 中间件层:Postgres / Redis / MinIO # ============================================================ postgres: image: postgres:17 container_name: echochat-postgres environment: POSTGRES_DB: ${POSTGRES_DB:-echochat} POSTGRES_USER: ${POSTGRES_USER:-echochat} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-echochat_dev_2026} ports: - "7432:5432" volumes: - pgdata:/var/lib/postgresql/data - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-echochat}"] interval: 5s timeout: 5s retries: 5 redis: image: redis:7 container_name: echochat-redis ports: - "7379:6379" volumes: - redisdata:/data - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 5 minio: image: minio/minio:latest container_name: echochat-minio command: server /data --console-address ":9201" ports: - "9200:9000" - "9201:9001" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-echochat} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-echochat123456} volumes: - minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9200/minio/health/live"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped # ============================================================ # 应用层:Go backend # ============================================================ go-service: build: context: ../backend/go-service dockerfile: Dockerfile container_name: echochat-go-service environment: CONFIG_NAME: config.docker # 透传关键环境变量,config.docker.yaml 内部引用(可选覆盖) ECHOCHAT_JWT_SECRET: ${JWT_SECRET:-} ECHOCHAT_MEDIA_SERVER_INTERNAL_TOKEN: ${MEDIA_INTERNAL_TOKEN:-} # media-server 失败回调共享密钥:与 media-server 服务的 INTERNAL_WEBHOOK_SECRET 保持一致 # 未配置时 go-service 会拒绝所有 webhook(Forbidden),录制失败状态将无法及时落 failed ECHOCHAT_MEETING_INTERNAL_WEBHOOK_SECRET: ${INTERNAL_WEBHOOK_SECRET:-internal-webhook-12xqs} ECHOCHAT_MEETING_RECORDING_TRANSCODE_ENABLED: ${RECORDING_TRANSCODE_ENABLED:-true} ECHOCHAT_MEETING_RECORDING_TRANSCODE_FFMPEG_PATH: ${RECORDING_TRANSCODE_FFMPEG_PATH:-ffmpeg} ECHOCHAT_MEETING_RECORDING_TRANSCODE_TIMEOUT_SECONDS: ${RECORDING_TRANSCODE_TIMEOUT_SECONDS:-600} ports: - "${GO_SERVICE_PORT:-8085}:8085" depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy media-server: condition: service_healthy volumes: # 与 media-server 使用同一个 host 目录,确保容器部署 go-service 时也能读取录制产物并转码/上传 - ${RECORDING_OUTPUT_DIR:-/home/ykf/jenkins/yuyin/data}:${RECORDING_OUTPUT_DIR:-/home/ykf/jenkins/yuyin/data} restart: unless-stopped # ============================================================ # 媒体层:mediasoup Node SFU # ============================================================ media-server: build: context: ../media-server dockerfile: Dockerfile network_mode: host container_name: echochat-media-server environment: HTTP_HOST: 0.0.0.0 HTTP_PORT: 3300 LOG_LEVEL: info LOG_PRETTY: "false" MEDIA_INTERNAL_TOKEN: ${MEDIA_INTERNAL_TOKEN:-internal-token-22xqs} MEDIASOUP_LISTEN_IP: ${MEDIASOUP_LISTEN_IP:-0.0.0.0} # 关键:公网部署时必须填服务器公网 IP,本机留空 MEDIASOUP_ANNOUNCED_IP: ${MEDIASOUP_ANNOUNCED_IP:-123.6.102.114} MEDIASOUP_RTC_MIN_PORT: ${MEDIASOUP_RTC_MIN_PORT:-30000} MEDIASOUP_RTC_MAX_PORT: ${MEDIASOUP_RTC_MAX_PORT:-30199} MEDIASOUP_WORKER_LOG_LEVEL: warn MEDIASOUP_MAX_ROUTERS: "200" # 录制输出目录:与 host 路径保持一致,使 go-service(host 进程)可直接读取 ffmpeg 写出的 mp4 # 容器内路径 = host 路径,ffmpeg 写出的 outputPath 字符串可在两侧通用 RECORDING_OUTPUT_DIR: ${RECORDING_OUTPUT_DIR:-/home/ykf/jenkins/yuyin/data} # ffmpeg 异常退出回调地址:media-server 容器使用 network_mode=host,可直连 host 上 go-service 的 8085 端口 # 路径与 backend/go-service/app/meeting/router.go 注册的 /internal/meeting/recordings/failure 一致 RECORDING_FAILURE_WEBHOOK_URL: ${RECORDING_FAILURE_WEBHOOK_URL:-http://127.0.0.1:8085/internal/meeting/recordings/failure} # webhook 共享密钥:必须与 go-service 端 INTERNAL_WEBHOOK_SECRET 一致,否则 go-service 直接 403 INTERNAL_WEBHOOK_SECRET: ${INTERNAL_WEBHOOK_SECRET:-internal-webhook-12xqs} volumes: # 录制文件 host 共享卷:media-server 容器内 ffmpeg 写入此目录,go-service host 进程直接读 # 没有这条挂载时 go-service 会报 "stat ...: no such file or directory" - ${RECORDING_OUTPUT_DIR:-/home/ykf/jenkins/yuyin/data}:${RECORDING_OUTPUT_DIR:-/home/ykf/jenkins/yuyin/data} #ports: # - "3300:3300" # mediasoup RTC 流量端口范围(UDP + TCP 双栈,ICE-TCP fallback 用) # - "30000-30199:40000-40199/udp" # - "30000-30199:40000-40199/tcp" healthcheck: test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3300/healthz"] interval: 10s timeout: 3s start_period: 15s retries: 3 restart: unless-stopped # ============================================================ # TURN 层:coturn(仅公网部署,profiles=public 才启动) # ------------------------------------------------------------ # 本机 Demo 默认不拉起(coturn 在 profiles 里) # 公网部署:docker compose --profile public up -d # 或使用 scripts/deploy-public.sh 一键启动 # ============================================================ coturn: image: coturn/coturn:latest container_name: echochat-coturn profiles: ["public"] network_mode: host command: > -n --log-file=stdout --external-ip=123.6.102.114 --listening-ip=0.0.0.0 --listening-port=${TURN_LISTEN_PORT:-3478} --tls-listening-port=${TURN_TLS_PORT:-5349} --min-port=${TURN_MIN_PORT:-49160} --max-port=${TURN_MAX_PORT:-49200} --realm=${TURN_REALM:-echochat} --user=${TURN_USERNAME:-echochat}:${TURN_PASSWORD:-echochat_2026-1s32dswW@#} --fingerprint --lt-cred-mech --no-multicast-peers --cli=false restart: unless-stopped volumes: pgdata: redisdata: minio_data: