Files
WorkBuddy 69ebf33e18
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
release: freeze demo v1.1.0 2026-10-19
2026-10-19 20:36:22 +08:00

240 lines
11 KiB
YAML
Raw Permalink 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.
# 丹阳公益婚恋服务平台 - 本地完整环境
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: