Files
FruitServer/docker-compose.yml
T
Zhou Weidong a5c9357a24
backend-ci / test (push) Failing after 4m39s
fix: 消除 JWT/签名密钥源码硬编码,全部改为环境变量注入
- JwtProperties.adminSecret/appSecret 默认值从 dev 密钥改为空字符串
- MediaProperties.signatureSecret 默认值从 dev 密钥改为空字符串
- app-api/admin-api application.yml 三处密钥回退值均改为空
- docker-compose.yml CORS_ALLOWED_ORIGINS 追加 ymb-site 3007 端口白名单
- .gitignore 追加 *.secrets.env / SSL 证书 / nginx 部署目录忽略规则
2026-11-13 16:53:53 +08:00

268 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<major>/,卷必须挂到父目录
- 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。
# reWriteBatchedInserts=true:pgjdbc 把 JDBC 批内多条 INSERT 重写成多值
# INSERT,观看进度缓冲落库(watch:pg:dirty 消费任务)依赖它跑满吞吐。
DB_URL: ${DB_URL:-jdbc:postgresql://db:5432/${DB_NAME:-fruitshorts}?reWriteBatchedInserts=true}
# 限流/缓存用 Redis;容器内 127.0.0.1 是容器自身,必须指向编排里的 redis
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
# CORS:公网入口 nginx 把 SSL 跑在 80 端口上(腾讯云安全组只放 80),
# 浏览器 fetch 带 Origin 时端口不匹配会被 Spring CORS 拒(必须精确字符串匹配)。
CORS_ALLOWED_ORIGINS: "https://api.frostgiant.vip:80,https://api.frostgiant.vip,http://api.frostgiant.vip,https://39.162.50.99:8081,http://localhost:3005,http://127.0.0.1:3005,http://39.162.50.99:3007,http://192.168.31.250:3007"
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"
# 浏览后台时 Origin 是实际访问地址,必须精确列全:本机、局域网、公网 IP、域名。
CORS_ALLOWED_ORIGINS: "http://localhost:3005,http://127.0.0.1:3005,http://39.162.50.99:3005,http://192.168.31.250:3005,https://api.frostgiant.vip,https://admin.frostgiant.vip"
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"
# 新用户默认界面语言:简体中文
GF_USERS_LANGUAGE: "zh-Hans"
TZ: Asia/Shanghai
depends_on:
- prometheus
- loki
# ─────────── 日志(Loki + Promtail)───────────
# Loki:日志存储与查询引擎;Promtail:采集容器 stdout/stderr 推给 Loki。
# Grafana 里通过 Loki 数据源可直接搜索/过滤各容器的请求日志。
loki:
image: grafana/loki:latest
container_name: fruitshorts-loki
restart: unless-stopped
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml:ro
- loki-data:/loki
expose:
- "3100"
# 镜像为 distroless(无 shell/wget),无法在容器内做 HTTP 探活;
# 靠 restart: unless-stopped 自动拉起,Grafana 数据源状态可反映可用性。
promtail:
image: grafana/promtail:latest
container_name: fruitshorts-promtail
restart: unless-stopped
command: -config.file=/etc/promtail/config.yml
volumes:
- ./promtail-config.yml:/etc/promtail/config.yml:ro
# 读取容器日志需要 Docker socket
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- loki
# 黑盒探活:从外部探测接口可用性与耗时
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:
loki-data: