# Author: fruit.com.cn # # 办公室单机部署(docs/04-部署架构.md §三 精简方案 + §五 第一阶段用 Compose): # app-api(8081) + admin-api(8080,只绑本机) + transcode-worker + PostgreSQL 18 + Redis # 转码 worker 与在线服务**同机但独立进程/容器**(§8.5 隔离要求:不抢在线服务资源)。 # # 用法(目标主机需要 Docker + Compose): # cp .env.example .env # 填密钥与数据库密码 # docker compose up -d --build # tools/smoke.sh # # 本文件在本机**未验证**(本机没有 Docker),见 DEPLOY.md「未在本机验证的部分」。 services: # ─────────── 基础设施 ─────────── db: image: postgres:18-alpine container_name: fruitshorts-db restart: unless-stopped environment: POSTGRES_DB: ${DB_NAME:-fruitshorts} POSTGRES_USER: ${DB_USER:-fruitshorts} POSTGRES_PASSWORD: ${DB_PASSWORD:?数据库密码必填,见 .env.example} # 中文与印尼语内容需要 UTF-8(镜像默认已是 UTF8,这里显式一次) POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" volumes: # PostgreSQL 18 起镜像把数据放在 /var/lib/postgresql//,卷必须挂到父目录 - db-data:/var/lib/postgresql # 首次启动自动建表 + 造种子数据;顺序即文件名顺序(脚本本身幂等,可重复执行) - ./sql/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro - ./sql/schema-phase1.sql:/docker-entrypoint-initdb.d/02-schema-phase1.sql:ro - ./sql/partition-maintenance.sql:/docker-entrypoint-initdb.d/03-partition-maintenance.sql:ro - ./sql/data.sql:/docker-entrypoint-initdb.d/04-data.sql:ro - ./sql/data-phase1.sql:/docker-entrypoint-initdb.d/05-data-phase1.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-fruitshorts} -d ${DB_NAME:-fruitshorts}"] interval: 10s timeout: 5s retries: 10 start_period: 20s # 不映射端口:只有同网络的容器需要访问;本机要连库时再放开 5432 expose: - "5432" redis: # 第一阶段还没有代码依赖 Redis(C2 才接缓存与限流),先把基础设施就位, # 免得上线时再改编排。要持久化就留 volume,纯缓存场景可去掉。 image: redis:7-alpine container_name: fruitshorts-redis restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 10 expose: - "6379" # ─────────── 应用 ─────────── app-api: build: context: . args: MODULE: fruitshorts-app-api image: fruitshorts/app-api:latest container_name: fruitshorts-app-api restart: unless-stopped env_file: .env environment: SERVER_PORT: "8081" # 默认连编排里的 db;要连外部库就在 .env 里覆盖 DB_URL DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}} # 限流/缓存用 Redis;容器内 127.0.0.1 是容器自身,必须指向编排里的 redis REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} ports: - "8081:8081" volumes: # 媒资落盘:三个服务共享同一个卷 - ./data/uploads:/data/uploads depends_on: db: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8081/healthz || exit 1"] interval: 30s timeout: 5s start_period: 40s retries: 3 admin-api: build: context: . args: MODULE: fruitshorts-admin-api image: fruitshorts/admin-api:latest container_name: fruitshorts-admin-api restart: unless-stopped env_file: .env environment: SERVER_PORT: "8080" SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE: "2GB" SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE: "2GB" CORS_ALLOWED_ORIGINS: "http://localhost:3005,http://127.0.0.1:3005,http://39.162.50.99:3005,http://192.168.31.250:3005" DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}} # 管理端仅内网,页面上直接播放/下载裸媒资地址(不签播放签名), # 因此这里不启用 /files/episode/** 的签名校验;公开的 app-api 仍保持开启。 FRUITSHORTS_MEDIA_PROTECTED_PREFIXES: "" REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} ports: # docs/12-上线检查清单「管理端仅内网」:只对局域网开放;不在路由器做端口转发,公网访问不到 - "8080:8080" volumes: - ./data/uploads:/data/uploads depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/healthz || exit 1"] interval: 30s timeout: 5s start_period: 40s retries: 3 transcode-worker: build: context: . args: MODULE: fruitshorts-transcode-worker # worker 镜像里装 ffmpeg,两个 API 镜像不装(省 80MB) INSTALL_FFMPEG: "true" image: fruitshorts/transcode-worker:latest container_name: fruitshorts-transcode-worker restart: unless-stopped env_file: .env environment: DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}} REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} volumes: # 与两个入口同一个媒资根,否则找不到源文件 - ./data/uploads:/data/uploads depends_on: db: condition: service_healthy redis: condition: service_healthy healthcheck: # worker 不监听端口(server.port=0),用进程存活判断 test: ["CMD-SHELL", "pgrep -f app.jar >/dev/null || exit 1"] interval: 60s timeout: 5s start_period: 40s retries: 3 # ─────────── 监控 ─────────── # 编排统一在这里:prometheus(应用指标 + 容器指标 + 黑盒探活)+ grafana(自动加载看板) prometheus: image: prom/prometheus:latest container_name: fruitshorts-prometheus restart: unless-stopped ports: - "9090:9090" volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus command: - "--config.file=/etc/prometheus/prometheus.yml" # 15 天保留期,避免磁盘无限增长 - "--storage.tsdb.retention.time=15d" - "--web.enable-lifecycle" healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9090/-/healthy >/dev/null || exit 1"] interval: 30s timeout: 5s start_period: 20s retries: 3 grafana: image: grafana/grafana:latest container_name: fruitshorts-grafana restart: unless-stopped ports: - "3000:3000" volumes: - grafana-data:/var/lib/grafana # 数据源与看板全部来自仓库,换机器不需要手工配 - ./grafana/provisioning:/etc/grafana/provisioning:ro - ./grafana/dashboards:/var/lib/grafana/dashboards:ro environment: GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin} GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?Grafana 管理员密码必填,见 .env.example} GF_USERS_ALLOW_SIGN_UP: "false" TZ: Asia/Shanghai depends_on: - prometheus # 黑盒探活:从外部探测接口可用性与耗时 blackbox: image: prom/blackbox-exporter:latest container_name: fruitshorts-blackbox restart: unless-stopped volumes: - ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro # 容器资源指标(CPU / 内存 / 网络 / 重启),供 Prometheus 抓取 cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: fruitshorts-cadvisor restart: unless-stopped privileged: true volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro volumes: # 命名卷:跟着 compose 项目走,`docker compose down -v` 才会删 db-data: redis-data: prometheus-data: grafana-data: