Files
danyang-match/infrastructure/docker-compose.yml
T
WorkBuddy dc0a62d4c5 fix(storage): presigned URL 改用对外可达 host,修复客户端直传断链
SeaweedFS 换存储后 MINIO_ENDPOINT 变为 seaweedfs:8333(容器名),
PhotoService 生成的 presigned URL 把该 host 直接下发给客户端,
客户端不在 docker 网络内无法解析 → 直传/直链全断。

- S3Config:新增 amazonS3Signer Bean,指向 object-storage.public-endpoint
  (缺省回退内网 endpoint,向后兼容)。独立签名端是因为 SigV4 签名覆盖
  host 头,事后改 URL 字符串会让签名失效返 403;独立客户端让签名 host
  与客户端实际命中的 host 天然一致。
- PhotoService:去掉 @RequiredArgsConstructor,改显式 @Qualifier 构造器;
  getUploadToken / getSignedUrl / getSignedUrlForAuthorizedReview 三处
  presigned 生成改用 amazonS3Signer;putObject 服务端上传仍走内网 amazonS3。
- application-docker.yml / application-dev.yml:public-endpoint 读
  MINIO_PUBLIC_ENDPOINT(缺省回退 MINIO_ENDPOINT)。
- 三套 compose:注入 MINIO_PUBLIC_ENDPOINT
  (dev localhost:8333 / staging localhost:8334 / prod 留作单一旋钮)。
- 测试:PhotoServiceTest 新增嵌套 PresignedUrlPublicHostTest,用真实
  AmazonS3 客户端(仅本地签名不联网)断言下发 URL host 为对外地址、
  不含 seaweedfs 容器名;同步补齐两处 5→6 参构造调用。

验证:本机 65 E2E 全绿 + storage_link_probe path-style 全通 +
PhotoServiceTest/PhotoUploadValidationTest/PhotoLimitGuardTest 19 例单测全绿。

顺带:backend/maven-settings.xml 的 Central 兜底由投毒的 repo1.maven.org
改为可达的 repo.maven.apache.org,使本机镜像构建可走 aliyun 主源。
2026-09-22 17:10:40 +08:00

217 lines
10 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: postgres
DB_PORT: 5432
DB_NAME: danyang_match
DB_USER: postgres
DB_PASSWORD: ${DB_PASSWORD:?请在本地 .env 设置 DB_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD:?请在本地 .env 设置 REDIS_PASSWORD}
# 变量名仍沿用 MINIO_* 前缀(服务端 Bean 与 application-*.yml 都在读它),
# 但它现在指向 SeaweedFS 的 S3 网关 —— 属历史命名,待下一轮统一改名。
# 「presigned URL 的 host 必须是对外可达地址」缺陷已修复:endpoint 拆成
# 内网签发地址(MINIO_ENDPOINT=seaweedfs:8333,后端连存储)+
# 外网访问地址(MINIO_PUBLIC_ENDPOINT=localhost:8333,客户端直传)。
MINIO_ENDPOINT: http://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:-}
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
nginx:
image: nginx:alpine
container_name: danyang-match-nginx
depends_on:
backend:
condition: service_healthy
admin-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: