Files
danyang-match/infrastructure/docker-compose.yml
T

213 lines
9.9 KiB
YAML
Raw Normal View History

# 丹阳公益婚恋服务平台 - 本地完整环境
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: http://seaweedfs: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: