CI/CD Pipeline / Backend Build & Test (push) Waiting to run
CI/CD Pipeline / Admin Web Build (push) Waiting to run
CI/CD Pipeline / Mini Program Compile Check (push) Waiting to run
CI/CD Pipeline / Migration Validation (push) Blocked by required conditions
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Waiting to run
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Waiting to run
CI/CD Pipeline / E2E (live stack) (push) Blocked by required conditions
240 lines
11 KiB
YAML
240 lines
11 KiB
YAML
# 丹阳公益婚恋服务平台 - 本地完整环境
|
||
|
||
services:
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
container_name: danyang-match-postgres
|
||
environment:
|
||
POSTGRES_DB: danyang_match
|
||
POSTGRES_USER: postgres
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD:?请在本地 .env 设置 DB_PASSWORD}
|
||
ports:
|
||
- "5434:5432"
|
||
volumes:
|
||
- postgres_data:/var/lib/postgresql/data
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U postgres -d danyang_match"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
container_name: danyang-match-redis
|
||
ports:
|
||
- "6381:6379"
|
||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:?请在本地 .env 设置 REDIS_PASSWORD}
|
||
volumes:
|
||
- redis_data:/data
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?请在本地 .env 设置 REDIS_PASSWORD}", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
seaweedfs:
|
||
# 2026-09-22:对象存储从 MinIO 换成 SeaweedFS。
|
||
#
|
||
# 动机不是「拉不到镜像」。MinIO 的 GitHub 仓库已于 2026-02 归档,**不再产出安全补丁**,
|
||
# 已累积 CVE-2025-62506 / CVE-2026-39414 / CVE-2026-40344 / CVE-2026-42600 且不修。
|
||
# 本项目存身份证、实名材料与用户照片 —— 无人修 CVE 才是真正的驱动因素。
|
||
# (Docker Hub 上 minio/minio 仓库自 2026-09-12 起返回 404 只是表象;quay.io 仍在提供,
|
||
# 2026-09-22 核实时发现 quay 上仍有 2026-04 的 hotfix tag,故「换源」可以再拖,
|
||
# 「无人修 CVE」不能拖。)
|
||
#
|
||
# 选型依据来自本机实测而非文档推断:本项目只用到极窄的 S3 操作面
|
||
# —— presigned PUT/GET + putObject + 两个 bucket,无 versioning / lifecycle / policy。
|
||
# SeaweedFS 4.47 在 path-style 下这三项全部通过且**字节一致**。
|
||
#
|
||
# 必须显式启用鉴权:SeaweedFS 的 S3 网关**默认完全不鉴权** —— 实测匿名 GET 私有对象
|
||
# 200、匿名 PUT 200、匿名列举桶 200,AK/SK 形同虚设。对本项目的敏感数据不可接受。
|
||
# 故用 -s3.config 注入 identity;配好后匿名访问回到 403(已实测)。
|
||
#
|
||
# 版本钉 4.47(实测 `weed version` = 4.47 c50733600),不用 latest:
|
||
# 滚动 tag 会让 CI 拉到与本地验证时不同的镜像,属不可复现的偶发失败来源。
|
||
image: chrislusf/seaweedfs:4.47
|
||
container_name: danyang-match-seaweedfs
|
||
# 凭据不入库:compose 无法把变量插值进挂载文件的内容,故在容器内把环境变量
|
||
# 渲染成 s3cfg.json 再交给 weed。(未加引号的 heredoc 是故意的:需要展开变量。)
|
||
# 必须覆盖 entrypoint:该镜像的 ENTRYPOINT 是 /usr/bin/weed,
|
||
# 只写 command 会被追加成 `weed sh -c ...` → `weed: unknown subcommand "sh"`(实测踩到)。
|
||
entrypoint: ["sh", "-c"]
|
||
command:
|
||
- |
|
||
cat > /tmp/s3cfg.json <<CFG
|
||
{"identities":[{"name":"app","credentials":[{"accessKey":"$${MINIO_ACCESS_KEY}","secretKey":"$${MINIO_SECRET_KEY}"}],"actions":["Admin","Read","Write","List","Tagging"]}]}
|
||
CFG
|
||
# -volume.max=100 是必需的,不是调优:
|
||
# SeaweedFS 把 **bucket 名当作 collection**,每个 collection 至少要分到一个 volume。
|
||
# 而 volume server 默认上限只有 8 个(实测 /dir/status:Volumes 8、Max 8、Free 0),
|
||
# 于是第二个 bucket 写入时 master 报
|
||
# failed to find writable volumes for collection:public … topo failed to pick 1 from 0 node candidates
|
||
# 表现为 put_object 返回 InternalError —— 错误信息完全不提 volume 配额,
|
||
# 不知道这条就只会反复重试。volume 是按需增长的(/data 下空卷仅 8 字节),
|
||
# 故调大上限不占实际空间。
|
||
exec weed server -s3 -s3.port=8333 -s3.config=/tmp/s3cfg.json -dir=/data -volume.max=100
|
||
environment:
|
||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
|
||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
|
||
ports:
|
||
- "8333:8333" # S3 网关:后端签名、客户端直传都走它
|
||
- "9333:9333" # master:集群状态 / 排查用
|
||
- "8888:8888" # filer:storage-init 建桶用
|
||
volumes:
|
||
- seaweedfs_data:/data
|
||
healthcheck:
|
||
# 不能用 curl -f:鉴权开启后匿名列桶返回 403,而 403 恰恰说明网关正在服务。
|
||
# 因此显式接受 200(未鉴权时)与 403(已鉴权时)两种状态码。
|
||
test: ["CMD-SHELL", "code=$$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8333/); [ \"$$code\" = 200 ] || [ \"$$code\" = 403 ]"]
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 24
|
||
|
||
storage-init:
|
||
# SeaweedFS 与 MinIO 的一个关键差异:**PutObject 到不存在的 bucket 会直接失败**
|
||
# (实测:匿名下 AccessDenied、正确凭据下 404),不会自动建桶。
|
||
# 而本项目全仓库没有任何 createBucket 代码或初始化脚本 —— 桶必须显式预建,
|
||
# 否则换完存储照片上传依然是断的(这一点在 MinIO 时代就已是既成缺陷,
|
||
# 见 tests/e2e/storage_link_probe.py 的说明)。
|
||
#
|
||
# 建桶走 filer 的目录接口(POST /buckets/<name>/),无需 SigV4 签名,
|
||
# 因此可复用 seaweedfs 镜像自带的 curl,**不引入新镜像依赖**。
|
||
# 实测该方式建的目录会被 S3 网关识别为 bucket:list_buckets 可见、
|
||
# head_bucket 200、put_object 成功,且鉴权照常生效(匿名 GET 403)。
|
||
#
|
||
# 幂等语义(实测):首次 POST=201,已存在 POST=409,GET 到目录=200。
|
||
# 三者都视为「桶已就位」,故重跑 up 不会失败。
|
||
image: chrislusf/seaweedfs:4.47
|
||
container_name: danyang-match-storage-init
|
||
depends_on:
|
||
seaweedfs:
|
||
condition: service_healthy
|
||
entrypoint:
|
||
- sh
|
||
- -c
|
||
- |
|
||
for b in public private; do
|
||
code=$$(curl -s -o /dev/null -w '%{http_code}' "http://seaweedfs:8888/buckets/$$b/" || echo 000)
|
||
if [ "$$code" = "200" ]; then echo "bucket $$b: already exists"; continue; fi
|
||
i=0
|
||
while [ $$i -lt 30 ]; do
|
||
code=$$(curl -s -o /dev/null -w '%{http_code}' -X POST "http://seaweedfs:8888/buckets/$$b/" || echo 000)
|
||
case "$$code" in 200|201|409) break ;; esac
|
||
i=$$((i+1)); sleep 2
|
||
done
|
||
echo "bucket $$b: create -> $$code"
|
||
done
|
||
restart: "no"
|
||
|
||
backend:
|
||
build:
|
||
context: ../backend
|
||
dockerfile: Dockerfile
|
||
container_name: danyang-match-backend
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
seaweedfs:
|
||
condition: service_healthy
|
||
storage-init:
|
||
# 一次性容器:桶没建好时后端任何 putObject / presigned 直传都会失败。
|
||
condition: service_completed_successfully
|
||
environment:
|
||
SPRING_PROFILES_ACTIVE: docker
|
||
DB_HOST: danyang-match-postgres
|
||
DB_PORT: 5432
|
||
DB_NAME: danyang_match
|
||
DB_USER: postgres
|
||
DB_PASSWORD: ${DB_PASSWORD:?请在本地 .env 设置 DB_PASSWORD}
|
||
REDIS_HOST: danyang-match-redis
|
||
REDIS_PORT: 6379
|
||
REDIS_PASSWORD: ${REDIS_PASSWORD:?请在本地 .env 设置 REDIS_PASSWORD}
|
||
# 变量名仍沿用 MINIO_* 前缀(服务端 Bean 与 application-*.yml 都在读它),
|
||
# 但它现在指向 SeaweedFS 的 S3 网关 —— 属历史命名,待下一轮统一改名。
|
||
# 「presigned URL 的 host 必须是对外可达地址」缺陷已修复:endpoint 拆成
|
||
# 内网签发地址(MINIO_ENDPOINT=danyang-match-seaweedfs:8333,后端连存储)+
|
||
# 外网访问地址(MINIO_PUBLIC_ENDPOINT=localhost:8333,客户端直传)。
|
||
MINIO_ENDPOINT: http://danyang-match-seaweedfs:8333
|
||
# 对内网 endpoint 拆出「对外可达」地址:客户端直传 presigned URL 用。
|
||
# 本地 S3 网关已 publish 到宿主 8333,故客户端填 localhost:8333 即可连通。
|
||
MINIO_PUBLIC_ENDPOINT: http://localhost:8333
|
||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
|
||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
|
||
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?请在本地 .env 设置 JWT_ACCESS_SECRET}
|
||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?请在本地 .env 设置 JWT_REFRESH_SECRET}
|
||
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?请在本地 .env 设置 ENCRYPTION_KEY}
|
||
WECHAT_APP_ID: dev-wechat-app-id
|
||
WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:-}
|
||
# 仅本地演示固定到秀英预置用户;正式环境留空并使用真实微信 Provider。
|
||
# 留空时按每个 mock code 生成独立用户;演示预置用户使用 demo_openid_* code 显式登录。
|
||
WECHAT_MOCK_OPENID: ${WECHAT_MOCK_OPENID-}
|
||
ports:
|
||
- "8085:8080"
|
||
healthcheck:
|
||
# 与 nginx 同理,用 127.0.0.1 而非 localhost:backend 当前恰好双栈监听(:::8080),
|
||
# 故 localhost 解析成 ::1 也能通;但探针不应依赖「服务恰好绑了 IPv6」这一偶然事实。
|
||
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1:8080/api/v1/health/live || exit 1"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
|
||
admin-web:
|
||
build:
|
||
context: ../admin-web
|
||
dockerfile: Dockerfile
|
||
container_name: danyang-match-admin
|
||
depends_on:
|
||
backend:
|
||
condition: service_healthy
|
||
|
||
h5-web:
|
||
# 小程序用户端 + 红娘端的 H5 演示端(Vue3 + Vant)。与管理端 admin-web 同构,
|
||
# 共用同一 backend,故演示端与管理端数据天然一致、可联动。
|
||
# 产物挂在 /h5/ 子路径下(vite build base=/h5/,见 h5-demo/nginx-h5.conf)。
|
||
#
|
||
# 单独发布宿主端口 6689(与 admin 的 6688 分源):红娘端调 /auth/operator/login
|
||
# 会下发同款 HttpOnly Cookie,若与 admin 同源会把 /admin/ 也登成同一红娘账号。
|
||
# 独立 origin/Cookie jar 规避串号(见 h5-demo 实施计划 R3)。
|
||
build:
|
||
context: ../h5-demo
|
||
dockerfile: Dockerfile
|
||
container_name: danyang-match-h5-web
|
||
depends_on:
|
||
backend:
|
||
condition: service_healthy
|
||
ports:
|
||
- "6689:80"
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
container_name: danyang-match-nginx
|
||
depends_on:
|
||
backend:
|
||
condition: service_healthy
|
||
admin-web:
|
||
condition: service_started
|
||
h5-web:
|
||
condition: service_started
|
||
ports:
|
||
- "6688:80"
|
||
volumes:
|
||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||
healthcheck:
|
||
# 探针地址必须写 127.0.0.1,不能写 localhost。
|
||
# 实测:nginx 的 `listen 80` 只绑 IPv4(容器内 netstat = 0.0.0.0:80),
|
||
# 而 /etc/hosts 中 `::1 localhost` 同样存在,busybox wget 解析 localhost 时优先取 ::1
|
||
# → 探针连 IPv6 回环被拒(Connection refused)→ 容器被标 unhealthy,
|
||
# 但服务其实完全正常(宿主 curl http://localhost:6688/nginx-health = 200)。
|
||
# 这是一类「探针与被探对象脱钩」的假告警,还会连带拖垮 depends_on: service_healthy。
|
||
test: ["CMD-SHELL", "wget -qO /dev/null http://127.0.0.1/nginx-health || exit 1"]
|
||
interval: 30s
|
||
timeout: 10s
|
||
retries: 3
|
||
|
||
volumes:
|
||
postgres_data:
|
||
redis_data:
|
||
seaweedfs_data:
|