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: a82cc0e64487
i18n_source_hash: 98172965118b
i18n_source_hash: 2a722f86da75
i18n_provenance: human
i18n_output_hash: 7e2aa7fa98b6
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
```
`--gpus all` 및 NVIDIA 컨테이너 툴킷이 올바르게 설정된 경우에도 AI 도구가 CPU에서 실행되는 경우 **설정 → AI 기능**에서 영향을 받는 번들(예: 배경 제거)을 다시 설치하세요. 설치 프로그램은 다른 번들(예: 전사)에서 가져온 CPU 전용 빌드가 공유 AI 환경에서 섀도잉될 수 있는 ONNX 런타임의 GPU 빌드를 복원합니다. 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는 속도 제한과 로깅이 `X-Forwarded-For` 헤더에서 실제 클라이언트 IP를 사용하도록 기본적으로 `TRUST_PROXY=true`를 설정한다.
`TRUST_PROXY`의 기본값은 `loopback,linklocal,uniquelocal`이므로 SnapOtter는 사설 네트워크의 피어가 보낸 `X-Forwarded-For`만 믿습니다. 같은 호스트, Docker 네트워크, 또는 LAN에 있는 리버스 프록시는 기본적으로 신뢰되며, 그래서 속도 제한, 로그인 무차별 대입 차단, 감사 로그, enterprise 판의 IP 허용 목록이 모두 설정 없이 실제 클라이언트 IP를 봅니다.
앞단의 프록시가 **공개** 주소에서 SnapOtter에 접근할 때만, 예를 들어 다른 네트워크에 있는 클라우드 로드 밸런서일 때만 `TRUST_PROXY=true`로 설정하세요. 직접 노출된 인스턴스에서 이 값은 `request.ip`를 공격자가 좌우하게 만듭니다. 헤더를 계속 바꿔 보내는 호출자는 요청마다 새 속도 제한 버킷을 받기 때문입니다.
클라이언트 IP를 재보기 전에 알아 둘 것이 두 가지 있습니다. macOS와 Windows의 Docker Desktop은 게시된 포트를 사용자 공간 프록시로 제공하면서 모든 출발지 주소를 VM 게이트웨이 `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}