fix: release QA hardening across processing, media, security, and CI gates (#649)

A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
This commit is contained in:
SnapOtter
2026-07-27 15:37:30 +08:00
committed by GitHub
parent bc32f86a07
commit d10d0f544f
855 changed files with 54564 additions and 13092 deletions
+24 -13
View File
@@ -1,8 +1,9 @@
---
description: "使用 Docker 将 SnapOtter 部署到生产环境。涵盖硬件要求、GPU 配置,以及 Nginx、Traefik 和 Cloudflare 的反向代理配置。"
i18n_output_hash: 63267650bd5f
i18n_source_hash: 98172965118b
i18n_source_hash: 2a722f86da75
i18n_provenance: human
i18n_output_hash: 94e82ef1fffe
i18n_hash_version: 2
---
# 部署 {#deployment}
@@ -47,7 +48,7 @@ services:
# - MAX_USERS=0 # Max user accounts
# --- Networking ---
# - TRUST_PROXY=true # Trust X-Forwarded-For headers (set false if not behind a proxy)
# - TRUST_PROXY=loopback,linklocal,uniquelocal # Which peers may set the client IP via X-Forwarded-For (default shown)
# --- Bind mount permissions ---
# - PUID=1000 # Match your host user's UID (run: id -u)
@@ -82,7 +83,7 @@ services:
- SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U snapotter"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
@@ -170,13 +171,13 @@ services:
container_name: SnapOtter-postgres
environment:
POSTGRES_USER: snapotter
POSTGRES_PASSWORD: snapotter
POSTGRES_PASSWORD: snapotter # 针对非本地部署更改此设置
POSTGRES_DB: snapotter
volumes:
- SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U snapotter"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12
@@ -207,13 +208,17 @@ volumes:
docker compose -f docker-compose-gpu.yml up -d
```
在日志中检查 CUDA 是否被检测到:
### 验证 GPU 加速 {#verify-gpu-acceleration}
检查日志中的 CUDA 检测:
```bash
docker logs SnapOtter 2>&1 | head -20
# Look for: [gpu] CUDA available via torch
```
如果 AI 工具在 CPU 上运行,即使 `--gpus all` 和 NVIDIA Container Toolkit 设置正确,请从 **设置 → AI 功能** 重新安装受影响的捆绑包(例如背景删除)。安装程序会恢复 ONNX 运行时的 GPU 版本,而由另一个捆绑包(例如转录)引入的仅 CPU 版本可能会在共享 AI 环境中隐藏。如果从 UI 重新安装无法恢复旧映像上的 GPU,请参阅 [问题 #490](https://github.com/snapotter-hq/SnapOtter/issues/490) 中的手动修复。
## 硬件要求 {#hardware-requirements}
这些数字来自一系列系统上的基准测试,从配备 NVIDIA RTX 4070 的现代 amd64 工作站到 Raspberry Pi,在每台设备上运行整个工具目录,并扫描 Docker 资源限制以找出真实的下限。
@@ -436,11 +441,11 @@ securityContext:
| `AUTH_ENABLED` | `true` | 启用/禁用登录要求 |
| `DEFAULT_USERNAME` | `admin` | 初始管理员用户名 |
| `DEFAULT_PASSWORD` | `admin` | 初始管理员密码(首次登录时强制更改) |
| `MAX_UPLOAD_SIZE_MB` | `100` | 单文件上传限制 |
| `MAX_BATCH_SIZE` | `100` | 每个批量请求的最大文件数 |
| `MAX_UPLOAD_SIZE_MB` | `0`(无限制) | 单文件上传限制MB)。镜像出厂即为 `0`;从源码构建则从 100 起 |
| `MAX_BATCH_SIZE` | `0`(无限制) | 每个批量请求的最大文件数。镜像出厂即为 `0`;从源码构建则从 100 起 |
| `RATE_LIMIT_PER_MIN` | `1000` | 每 IP 每分钟的 API 请求数(设为 0 可禁用) |
| `MAX_USERS` | `0`(无限制) | 最大用户账户数 |
| `TRUST_PROXY` | `true` | 信任来自反向代理的 X-Forwarded-For 头 |
| `TRUST_PROXY` | `loopback,linklocal,uniquelocal` | 允许哪些对端通过 `X-Forwarded-For` 设置客户端 IP。默认仅限私有网络 |
| `PUID` | `999` | 以此 UID 运行(用于绑定挂载权限) |
| `PGID` | `999` | 以此 GID 运行(用于绑定挂载权限) |
| `LOG_LEVEL` | `info` | 日志详细程度:fatal、error、warn、info、debug、trace |
@@ -483,7 +488,13 @@ curl http://localhost:1349/api/v1/health
## 反向代理 {#reverse-proxy}
SnapOtter 默认设置 `TRUST_PROXY=true`,因此速率限制和日志记录会使用来自 `X-Forwarded-For` 头的真实客户端 IP。
`TRUST_PROXY` 默认为 `loopback,linklocal,uniquelocal`,因此 SnapOtter 只相信来自私有网络对端的 `X-Forwarded-For`。同一主机、Docker 网络或局域网中的反向代理开箱即受信任,这意味着速率限制、登录暴力破解限制、审计日志以及 enterprise 版的 IP 允许列表,无需任何配置就能看到真实客户端 IP。
只有当前置代理从**公网**地址访问 SnapOtter 时才设为 `TRUST_PROXY=true`,例如位于另一网络的云负载均衡器。在直接暴露的实例上,该值会让 `request.ip` 落入攻击者手中,因为不断更换该头的调用方每次请求都能拿到一个全新的速率限制计数。
在动手测量客户端 IP 之前,有两件事要知道。macOS 和 Windows 上的 Docker Desktop 通过用户态代理提供已发布端口,会把所有源地址改写为虚拟机网关 `192.168.65.1`,因此在那里无论 `TRUST_PROXY` 取何值都拿不回真实客户端;面向互联网的部署请放在 Linux 上。而且在任何平台上,经 `localhost` 访问已发布端口都会被视为网桥网关而非你的客户端,所以 localhost 测试完全说明不了真实客户端是如何归属的。`TRUST_PROXY` 取值的完整表格和 Docker Desktop 注意事项见 [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md#client-ip-resolution-trust_proxy)。
对于下面的每个代理来说,有两件事很重要:允许大型请求正文(上传),并且不缓冲响应。响应缓冲代理会破坏 SSE 进度,更明显的是,使大文件下载“开始但永远不会完成”,因为代理在传递之前保存整个文件。 SnapOtter 在下载时发送 `X-Accel-Buffering: no`,因此即使在其他地方保留缓冲,nginx 也会对它们进行流式传输,但除 nginx 之外的代理需要显式禁用响应缓冲(如下面的每个配置所示)。如果下载中途停止,首先要检查前面的缓冲代理。
### Nginx {#nginx}
@@ -505,7 +516,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support (batch progress, feature install progress)
# 流响应而不是缓冲:SSE 进度(批量、AI、功能安装)和大文件下载需要。
proxy_buffering off;
proxy_read_timeout 300s;
}
@@ -549,7 +560,7 @@ images.example.com {
}
```
`flush_interval -1` 禁用响应缓冲,这对于 SSE 进度事件(批处理、AI 工具、功能安装)必需的。延长的超时时间允许大文件上传 Caddy 提前关闭连接的情况下完成
`flush_interval -1` 禁用响应缓冲,这 SSE 进度事件(批处理、AI 工具、功能安装)以及大文件下载流式传输(而不是停滞)所必需的。延长的超时允许完成大文件上传,而无需 Caddy 提前关闭连接。
### Cloudflare Tunnels {#cloudflare-tunnels}