# 部署指南 ## 生产部署 仓库提供完整的 `docker/Dockerfile` 与 `docker/docker-compose.prod.yml`。生产编排包含 API、Worker、PostgreSQL 和 Redis;数据库与 Redis 不发布宿主机端口,API 和 Worker 使用同一上传卷,并以非 root、只读根文件系统运行。 ### 环境要求 - Linux x86_64 - Docker Engine 26+ 与 Docker Compose 2.20+ - 最低 2 核 CPU、4GB 内存;启用 AVIF 和独立 Worker 时建议 4 核、8GB 内存 - 首次构建可访问 Docker Hub 与 crates.io Debian 13、4 核 CPU、8GB 内存的起始值建议为 `WORKER_TASK_CONCURRENCY=2`、`WORKER_CONCURRENCY=2` 和 `IMAGE_PROCESSING_CONCURRENCY=2`;8 核应用服务器可从 `4/2/4` 开始。三者分别表示同时处理的任务数、单任务内文件数和单进程 CPU 图片处理上限。最后一项是全局 CPU 闸门,因此不要把它设置为 CPU 核数的数倍。任务并发提高后,数据库连接池建议至少为 `WORKER_TASK_CONCURRENCY * WORKER_CONCURRENCY + 4`,生产示例使用 16。 8 核 16GB 应用服务器的 ZIP 起始值为 `ZIP_BUILD_CONCURRENCY=2`、`ZIP_MAX_ENTRIES=200`、`ZIP_MAX_UNCOMPRESSED_BYTES=2147483648`。ZIP 使用 stored 模式,构建时同时存在下载源和归档文件,按两个 2 GiB 构建估算应至少保留约 8 GiB 临时磁盘余量;磁盘较小时应先降低总字节或并发,而不是提高 HTTP 超时。 生产 Compose 默认按 8 核 16GB 主机设置可覆盖的资源上限:API 3GB、Worker 8GB、PostgreSQL 2GB、Redis 1GB;对应变量为 `API_MEMORY_LIMIT`、`WORKER_MEMORY_LIMIT`、`POSTGRES_MEMORY_LIMIT` 和 `REDIS_MEMORY_LIMIT`。Redis 的 `REDIS_MAXMEMORY` 默认 768MB,达到上限后返回写入错误而不是继续挤占宿主机内存。 ### 首次启动 ```bash cp docker/.env.production.example .env.production chmod 600 .env.production # 分别生成 POSTGRES_PASSWORD、JWT_SECRET、API_KEY_PEPPER 和管理员密码。 # hex 不包含数据库 URL 与 .env 需要转义的保留字符。 openssl rand -hex 32 # 编辑公开地址和全部 replace-with-* 值后检查配置。 docker compose \ --env-file .env.production \ -f docker/docker-compose.prod.yml \ config --quiet docker compose \ --env-file .env.production \ -f docker/docker-compose.prod.yml \ up -d --build ``` API 健康后 Worker 才会启动,避免两个进程在首次部署时同时执行迁移。 管理员可用邮箱或用户名登录。首次启动时设置 `ADMIN_USERNAME` 和 `ADMIN_PASSWORD` 即可创建管理员;若未设置 `ADMIN_EMAIL`,系统会生成仅用于满足内部数据约束的 `用户名@local.invalid` 占位邮箱。确认账号创建后应从生产环境文件中移除 `ADMIN_PASSWORD`,避免每次重启都重置密码。 ```bash docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps curl --fail http://127.0.0.1:8080/health curl --fail http://127.0.0.1:8080/metrics ``` 预期健康响应: ```json {"status":"healthy","database":"connected","redis":"connected"} ``` ### 更新与回滚 更新代码后保留 `.env.production` 和命名卷。包含迁移 `017` 至 `022` 的版本不能让旧、新 API 或 Worker 并行滚动:旧 API 不理解 ZIP 构建租约,旧 Worker 不理解任务 attempt fencing。先备份数据库并停止旧 API/Worker,再构建新镜像。 迁移 `017` 会在发现重复 Customer 或同用户多条未取消 Stripe 订阅时主动失败,迁移 `019` 会在发现同一 Stripe 发票对应多行时主动失败。部署前先检查并人工对账,三个查询都必须返回 0 行: ```sql SELECT billing_customer_id, COUNT(*) FROM users WHERE billing_customer_id IS NOT NULL AND billing_customer_id <> '' GROUP BY billing_customer_id HAVING COUNT(*) > 1; SELECT user_id, COUNT(*) FROM subscriptions WHERE provider = 'stripe' AND status <> 'canceled' GROUP BY user_id HAVING COUNT(*) > 1; SELECT provider, provider_invoice_id, COUNT(*) FROM invoices WHERE provider_invoice_id IS NOT NULL GROUP BY provider, provider_invoice_id HAVING COUNT(*) > 1; ``` 推荐顺序: ```bash git pull --ff-only docker compose --env-file .env.production -f docker/docker-compose.prod.yml stop api worker docker compose --env-file .env.production -f docker/docker-compose.prod.yml build api docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d postgres redis api docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d worker ``` 新 API 启动后会消费迁移 `018` 创建的订阅对账队列。迁移 `020` 为历史发票写入待对账哨兵,发票不主动批量拉取,而是在首个后续事件到达时取 Stripe 快照。启动 Worker 前应确认 API 健康、`STRIPE_SECRET_KEY` 可用且服务器能访问 `STRIPE_API_BASE_URL`;订阅对账可以后台继续,但必须监控失败项: ```sql SELECT status, COUNT(*) FROM stripe_subscription_reconciliations GROUP BY status; SELECT object_type, requires_reconciliation, COUNT(*) FROM provider_object_event_watermarks WHERE provider = 'stripe' GROUP BY object_type, requires_reconciliation ORDER BY object_type, requires_reconciliation; ``` `failed` 会指数退避重试;持续失败通常表示 Stripe 凭据、网络、Customer/Price 映射不完整。上线验收要求订阅队列的 `pending/processing/failed` 最终归零,且 subscription 水位不再待对账;invoice 水位在对应发票首个后续事件到达前保持 `requires_reconciliation=true` 属于预期状态。生产镜像应使用不可变的 `IMAGEFORGE_TAG`。数据库迁移已应用后,不能只回滚旧二进制;应保留新 schema,并使用兼容该 schema 的修复镜像。 ### 反向代理 生产 Compose 默认把 API 端口绑定到 `127.0.0.1`。若确实需要绕过反向代理直接通过服务器地址访问,显式设置 `IMAGEFORGE_BIND_ADDRESS=0.0.0.0`,同时保持 `TRUST_PROXY_HEADERS=false` 并配置主机防火墙。只有当 API 端口不对客户端开放、所有请求都经过可信反向代理时,才设置 `TRUST_PROXY_HEADERS=true`,并由代理覆盖 `X-Forwarded-For` 与 `X-Forwarded-Proto`。 代理至少需要: ```nginx location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 100m; proxy_read_timeout 300s; } ``` `/metrics` 包含运行状态与队列数据,不应通过公开域名暴露。生产 Nginx 应单独拒绝该路径,Prometheus 直接抓取只绑定回环地址的 API 端口: ```nginx location = /metrics { allow 127.0.0.1; allow ::1; deny all; proxy_pass http://127.0.0.1:8080; } ``` 应用会为所有响应设置 CSP、`X-Content-Type-Options`、`X-Frame-Options`、`Referrer-Policy` 和 `Permissions-Policy`。TLS 网关还应设置 `Strict-Transport-Security`。 ### 日志与备份 ```bash docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs -f api worker docker compose --env-file .env.production -f docker/docker-compose.prod.yml \ exec -T postgres pg_dump -U imageforge imageforge | gzip > imageforge.sql.gz ``` 除数据库外,还要备份 Compose 的 `uploads` 命名卷。恢复前停止 API 与 Worker,避免数据库记录与文件卷产生时间差。 ## 本地开发 ```bash cp .env.example .env docker compose -f docker/docker-compose.dev.yml up -d # 终端 1 IMAGEFORGE_ROLE=api cargo run # 终端 2 IMAGEFORGE_ROLE=worker cargo run # 终端 3 cd frontend npm ci npm run dev ``` API 或 Worker 启动时会通过 SQLx 顺序执行尚未应用的迁移。迁移失败时进程退出,不会在不完整的数据库结构上继续提供服务。 ## 压缩质量回归 基准工具会下载固定 Picsum 照片,分别生成 JPEG、PNG、WebP、AVIF 输入,再通过真实 HTTP API 测量目标达标率、格式签名、SSIM、PSNR、分辨率和耗时。 ```bash python3 -m venv .venv-benchmark . .venv-benchmark/bin/activate pip install -r scripts/requirements-benchmark.txt python scripts/benchmark_compression.py generate --output-dir .bench/corpus IMAGEFORGE_BENCH_EMAIL=admin@example.com \ IMAGEFORGE_BENCH_PASSWORD='replace-me' \ python scripts/benchmark_compression.py run \ --base-url http://127.0.0.1:8080 \ --input-dir .bench/corpus \ --output-dir .bench/results \ --formats jpeg,webp,avif \ --rates 30,50,70 \ --strict ``` `--strict` 会在请求失败、输出格式不匹配或目标体积未达标时返回非零退出码。测试素材与结果位于 `.bench/`,不会提交到 Git。 ## 故障排查 ```bash # 容器与健康状态 docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps # 最近日志 docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs --tail=200 api worker # 数据库迁移状态 docker compose --env-file .env.production -f docker/docker-compose.prod.yml \ exec -T postgres psql -U imageforge -d imageforge \ -c 'SELECT version, success FROM _sqlx_migrations ORDER BY version;' # 主机资源 docker stats df -h ``` 若图片压缩长时间排队,先检查 CPU,再调整 `IMAGE_PROCESSING_CONCURRENCY`;大任务阻塞小任务时提高 `WORKER_TASK_CONCURRENCY`,单个批量任务推进过慢时再评估 `WORKER_CONCURRENCY`。若 API 健康但批量任务不推进,检查 Worker 日志、Redis pending 数和数据库连接池等待情况。