• 5.4.0

    5.3.0 대비 변경사항

    배포·컨테이너

    • 운영 compose.yml이 backend·crawler·web을 user: "1000:1000"(이미지의 node)로 실행하고 루트 파일시스템 읽기 전용, 모든 capability 제거, no-new-privileges를 적용합니다. 쓸 수 있는 곳은 메모리 tmpfs /tmp(64MiB)와 web .next/cache(128MiB, uid=1000,gid=1000)뿐입니다. 세 앱은 볼륨을 마운트하지 않으므로 기존 data/ 소유권은 바뀌지 않습니다. log-reader는 Docker 소켓을 읽기 위해 user: "0:0"으로 고정합니다.
    • 모든 서비스에 json-file 로그 회전(20MB×5)을 적용하고, PostgreSQL stop_grace_period: 120s·oom_score_adj: -500, MinIO 60s, crawler init: true·cpu_shares: 512·oom_score_adj: 500을 추가했습니다.
    • healthcheck는 node 프로세스 대신 busybox wget으로 127.0.0.1을 묻습니다. nginx는 자기 자신을 거친 /api/health/live를 묻고, log-reader는 Docker 소켓 /_ping이 실패하면 503입니다.
    • backend·crawler가 선택적으로 앱별 최소 권한 MinIO 계정을 쓸 수 있습니다(BACKEND_MINIO_ACCESS_KEY·BACKEND_MINIO_SECRET_KEY, CRAWLER_MINIO_ACCESS_KEY·CRAWLER_MINIO_SECRET_KEY). 두 키를 모두 비우면 기존 MINIO_ACCESS_KEY·MINIO_SECRET_KEY를 그대로 쓰고, 한쪽만 채우면 그 앱은 키 이름을 담은 설정 오류로 시작하지 않습니다.
    • Nginx는 썸네일·보호 원본 응답을 NAS 볼륨 임시 파일로 spool하지 않습니다.
    • ops/deploy-production.sh는 실패하면 migration 때문에 멈춘 crawler의 실제 상태를 안내합니다. ops/publish-images.ps1에 -VerifyOnly·-PromoteOnly·게시 뒤 latest 재검증을 추가했고, 모든 Dockerfile의 FROM·# syntax를 digest로 고정했으며 ops/update-base-images.ps1로 최신 여부를 조회합니다. backend health의 version은 이미지 릴리스 버전을 보고합니다. ops/production-audit.sh는 이미지 label·health·invalid index·migration도 점검합니다.

    계정·보안

    • 계정 보안 화면에 로그인 session 목록(기기 요약·로그인·마지막 사용 시각)과 개별 종료를 추가했습니다(GET /api/auth/sessions, DELETE /api/auth/sessions/:sessionId).
    • cookie로 인증한 요청에서 token 남은 수명이 절반 이하이면 같은 session으로 다시 발급해 만료를 늘립니다. 처음 passkey로 로그인한 뒤 30일을 넘겨 늘리지 않습니다. 배포 전 session은 그대로 유지됩니다.
    • 요청 제한을 고정 창에서 sliding window counter로 바꿨습니다. 로그인 전역 제한은 계정 단위로 세고 주소별 상한을 더했습니다. Redis 명령 2초·연결 3초 timeout을 넘으면 fail-closed 503입니다.
    • 브라우저가 Push 구독을 스스로 교체하면 service worker가 새 구독을 한 번 다시 등록하고 이전 행을 교체합니다.

    홈·viewer·화면

    • 홈 검색 조건을 URL에 담아 검색어·태그를 조합해 검색하고 수집순 정렬을 추가했습니다. 뒤로·앞으로 가기는 history 항목별로 목록과 기준 카드 위치를 복원합니다. 다른 화면의 검색 링크와 예전 ?search= 링크는 저장된 정렬 없이 기본 정렬로 엽니다. 출처 선택은 없앴고 예전 링크·저장값의 source는 버립니다.
    • 첫 실행 빈 서재 안내, 접힌 이어 읽기 header의 바로 이어 읽기, 정렬 방향 화살표, 고급 검색 태그 분류의 번역 표기, 카드 완독 배지와 제목 상세 링크, 완독 작품의 다시 읽기, 운영 화면 첫 수집 안내, 빈 대시보드 안내와 선호 태그 번역 표기를 추가하고 대시보드·차단 관리·북마크 폭을 맞췄습니다.
    • viewer 한 페이지·두 페이지 모드에 마우스 환경의 이전·다음 가장자리 버튼을 추가했습니다.
    • 썸네일 재요청 상한, dialog·보조 화면 지연 로딩, 상세 미리 받기, 움직임 줄이기와 시스템 테마 추종을 개선했습니다.

    수집

    • 완료된 Hitomi 작품의 원본 metadata(제목·태그·파일 목록 변경)를 30초 간격으로 큰 ID부터 한 바퀴 다시 확인합니다. 설정을 저장한 적이 없으면 켜져 있고, 한 바퀴를 마치면 스스로 꺼지며 운영 화면의 다시 실행으로 새 바퀴를 시작합니다. 수집 중에는 쉬고 수집과 같은 보호 회로를 지킵니다.
    • E-Hentai에서 관리자 재개 뒤에도 같은 실패가 반복되는 작품을 격리하고 다음 작품으로 진행합니다. 원본 삭제는 detail 404 재확인으로만 확정합니다.
    • Hitomi 한국어 색인·최신 ID·gg.js의 보호 응답(403·429·503)을 공용 보호 회로에 기록하고 회로가 열려 있으면 요청하지 않습니다. 중지 신호와 요청 기한, Retry-After 상한(하루)을 적용했습니다.
    • 페이지 저장의 완료 재계산·카운터 갱신을 줄이고 이미지 색인 누계를 지문 변경과 같은 transaction에서 올립니다.

    중복 검사·DB

    • 합본·부분 수록을 찾는 포함 관계 중복 후보를 추가했습니다. 기본 비활성이며 중복 검토 화면의 검사 설정에서 켭니다. 해결은 큰 쪽을 보존합니다.
    • 쌍 판정 정책을 phash-pair-v2로 올렸습니다. 짧은 작품은 괄호 블록을 지운 제목 key로도 근거를 인정합니다. 정책 문자열이 바뀌어 다음 중복 검사에서 전체를 다시 비교합니다.
    • 작품의 probe를 8개 묶음 문장으로 나누고 pHash 블록 index를 INCLUDE (phash) covering index로 바꿨습니다. 공통 페이지 재검사는 문장 단위 trigger로 바뀌었습니다.
    • 목록 cursor를 row 비교로 바꾸고 읽기 상태 필터를 EXISTS·NOT EXISTS로 바꿨으며, 목록 cache는 개인 차단 revision과 같은 snapshot에서 계산합니다. 태그 자동완성은 일치 순위로 정렬합니다. 운영 로그는 컨테이너별 since 이후만 증분으로 받습니다.

    좁힌 입력 검증

    • offset 목록 조회는 page × limit이 100,000을 넘으면 400입니다(cursor 조회나 정렬 방향 변경 안내). 목록 source는 hitomi·e-hentai 외 값이 400입니다. 이미지 업로드 검색은 최대 5천만 픽셀입니다. 자사 web은 이 범위를 넘지 않습니다.

    5.3.0에 있던 공개 API는 제거되지 않았습니다(viewer 이 페이지와 비슷한 작품 API는 5.3.0 이후 추가됐다가 게시 전에 제거). 필수 환경 변수·수동 전환·데이터 재설정이 없고, 새 Compose와 5.3.0 이미지, 5.3.0 Compose와 새 이미지의 교차 조합도 동작하므로 minor 5.4.0을 적용합니다. 제품 버전은 Git 태그·OCI label·문서로 관리하며 private package와 lockfile의 기존 버전은 유지합니다.

    이미지 게시 범위

    production 빌드 입력 지문이 다섯 서비스 모두 기존 latest label과 달라 byedpi·nginx·backend·crawler·web을 모두 새로 빌드해 5.4.0·latest로 게시합니다. 공통으로 지문 recipe(ops/image-inputs.ps1)가 바뀌었고, byedpi·nginx는 기반 이미지 digest 고정, nginx는 media 응답 spool 금지 설정, backend·crawler·web은 Dockerfile·소스·shared가 바뀌었습니다. npm 의존성과 lockfile은 바뀌지 않았고, 기반 이미지 고정 digest는 게시 준비 시점의 upstream 최신 digest와 같습니다. 모든 자체 이미지에 5.4.0 태그가 생기므로 IMAGE_TAG=5.4.0을 지정할 수 있습니다.

    NAS 업데이트와 주의사항

    운영자는 운영 반영에 따라 5.4.0의 compose.yml·ops/deploy-production.sh·sql/init-db.sql·.env.example을 받고 sh ops/deploy-production.sh로 반영합니다.

    • Compose 갱신 필수: 실행 제한(non-root·읽기 전용·tmpfs)·로그 회전·종료 유예·healthcheck·앱별 MinIO 키 전달은 compose.yml이 소유합니다. Watchtower가 이미지만 바꾸면 앱은 이전처럼 root로 동작하므로 새 compose.yml로 재배포해야 적용됩니다. 파일을 받은 뒤 grep -n '1000:1000\|read_only\|x-logging\|BACKEND_MINIO_ACCESS_KEY\|CRAWLER_MINIO_ACCESS_KEY' compose.yml로 확인합니다.
    • .env.example 대조: 새 키는 선택 키 4개(BACKEND_MINIO_ACCESS_KEY·BACKEND_MINIO_SECRET_KEY·CRAWLER_MINIO_ACCESS_KEY·CRAWLER_MINIO_SECRET_KEY)뿐이며 빈 값이 기본입니다. 비워 두면 기존 MinIO 키를 그대로 씁니다. 앱별 계정으로 전환하려면 MinIO 앱별 계정 절차대로 앱마다 두 키를 함께 채웁니다. 한쪽만 채운 채 5.3.0 이하로 rollback하지 않습니다.
    • DB migration 6개(AddDuplicateScanGalleryIndexes1792030000000, DuplicateCommonPageStatementTrigger1792030100000, DuplicateFingerprintStatementTrigger1792030200000, InvalidateDuplicateSummaries1792030300000, CoveringImageSearchBlockIndexes1792070000000, DuplicateContainment1792080000000)가 backend 시작 때 자동 적용됩니다. 모두 데이터를 보존하지만 forward-only입니다. index 생성·교체는 CONCURRENTLY라 오래 열린 transaction이 있으면 기다리고, covering index 교체는 pHash 블록 index를 다시 만들므로 수집·백필이 적은 시간에 배포합니다. 배포 전 백업을 확인하세요.
    • 배포 순서: Watchtower식 개별 교체(crawler를 멈추지 않고 backend 먼저 교체) 리허설에서 DuplicateCommonPageStatementTrigger가 이전 crawler 쓰기와 deadlock을 내고 재시도 끝에 수렴했습니다. Watchtower 자동 업데이트와 겹치지 않는 시점에 ops/deploy-production.sh로 반영합니다. 이 스크립트는 backend 이미지가 바뀌면 migration 전에 crawler를 멈추고, byedpi → postgres → redis → minio → log-reader → backend → crawler → web → nginx 순서로 교체합니다.
    • rollback 제한: 5.3.0 이미지로 되돌릴 수는 있지만 보장 범위는 데이터 보존과 crawler 비교 SQL이 오류 없이 도는 것입니다. 이전 crawler·web은 포함 관계 후보를 다루지 못하며 자세한 범위는 운영 반영의 rollback 항목을 따릅니다. 5.2.0 이하 backend로는 5.3.0부터 계속 rollback할 수 없습니다.
    • 배포 뒤 자동으로 시작되는 작업: 완료 Hitomi 작품 metadata 재확인이 설정 행이 없으므로 켜진 상태로 시작합니다. 30초에 한 작품씩 확인하므로 운영 규모(2026-10-08 작품 약 6.9만 개)에서는 수집이 없는 시간 기준 한 바퀴에 약 24일이 걸립니다. 원하지 않으면 운영 화면 Hitomi 패널에서 끕니다. 중복 검사는 InvalidateDuplicateSummaries와 정책 phash-pair-v2 때문에 다음 검사에서 다시 비교하며, 그 전까지 영향받는 pHash 후보의 해결은 409입니다.
    • 로그인 cookie는 그대로 유효하며 다시 로그인할 필요가 없습니다.

    NAS 배포·운영 데이터 변경은 이번 게시에서 실행하지 않았습니다.

    5.3.0 대비 전체 변경

    검증

    • 최종 main 0a9ef0e4에서 run-unit.ps1 -Workspace all이 shared lint를 포함해 통과했습니다(backend 78 suites·580 tests, crawler 69·876, web 133·1,653).
    • 같은 main의 run-integration.ps1 첫 실행은 새 viewer 브라우저 검증(session-viewer-browser.mjs)의 페이지 모드 selector strict mode 위반으로 실패해 exact: true로 고쳤고(2dd06a15, 테스트만 변경·앱 코드 동일), 병합 main 4cd97204에서 전체 통과했습니다.
    • 개발 stack을 4cd97204로 재빌드해 전 서비스 healthy, 홈·/api/health/live·/api/health/ready 200을 확인했습니다.
    • 알려진 리스크(P2, 수용): 새 설치에서 상태 행이 없을 때 30초 이상 지연된 metadata 재확인 켜기가 한 바퀴 완료 저장과 교차하면 이번 바퀴 확인 수가 초기화되지 않습니다.
    • 병합 main 37f4449d(UI/UX 패키지 A~D와 통합 selector 수정)에서 web 단위 133 suites·1,657 tests, run-integration.ps1 전체, run-schema.ps1, 로컬 hard reset이 통과했습니다.
    • 기술 검토 후속 병합 main(f3d752d1)에서 run-unit.ps1 -Workspace all, run-schema.ps1, run-integration.ps1, run-crawler-recovery.ps1(전체·-EHentaiOnly), run-duplicate-benchmark.ps1, run-search-benchmark.ps1, run-operations.ps1 -DeploymentOnly, run-compose-compat.ps1(3조합), run-upgrade.ps1 -BuildNew(deploy-script PASS, watchtower WATCHTOWER RISK)가 통과했습니다.
    • ops/update-base-images.ps1(레지스트리 조회만)은 고정 digest 11개가 모두 현재 digest와 같다고 확인했습니다.
    • 각 변경은 병합 전에 읽기 전용 리뷰를 거쳤습니다. 최종 main에서 run-schema.ps1·run-crawler-recovery.ps1·benchmark·run-compose-compat.ps1·run-upgrade.ps1은 다시 실행하지 않았고(위 이전 병합 main 결과), 전체 WebAuthn runner(run-passkey.ps1), 실제 Hitomi·E-Hentai 통신 runner, 운영 DB 재측정, NAS 배포는 NOT RUN입니다.

    게시 확인

    • 릴리스 소스: c6c0fbb9a6469f31ca69e41d72ebdd6f8feee8cf (annotated Git 태그 5.4.0 대상).
    • backend 5.4.0·latest: sha256:3b7eb7cd03355cfe088bc67a3f207a300d9f4bd344b14344b395537bb4f479ab. linux/amd64 manifest: sha256:2f8a97c87d441478637cb00e5869148149c647139b5fe8815a2ff9aa06945bb7. OCI version 5.4.0·revision c6c0fbb9a6469f31ca69e41d72ebdd6f8feee8cf·입력 지문 9270deed8859d573b6f48f9c8020e66e57414ae55b11d585dc6a1d0e36739c52.
    • crawler 5.4.0·latest: sha256:29a53b4295a79846e3ecf9c258c19c9967837406b106dfb8fcf9714150429f60. linux/amd64 manifest: sha256:de909254beff8955e040b9a14b291e0f406ce5e948588b002a9cb959cf57ed54. 입력 지문 8229d5b02b55808e37f790544dd6f0bcee8e9efb81f7805bb435dc55a93d9be5.
    • web 5.4.0·latest: sha256:dc13bbde279b9ca2098fb8ad097311228356a02ae5de05f8e9afa86ce088cf0d. linux/amd64 manifest: sha256:be819ee26ae0a9bb5e72daf07e9534c70bdb30e0f0e3330ac3ca3a2c16b09e40. 입력 지문 3a18af3a80a82dc63245c310ffe8a90cc8fc24879790ece7194050bb22947673.
    • byedpi 5.4.0·latest: sha256:04cf2838499d67a033a3c3101913563bf983e551fee27b891dab0d2f75a4233a. linux/amd64 manifest: sha256:93ea65f6266dc78f928678edb9940720f62d218a7d93cf8e97f03ab3bdb4f670. 입력 지문 eb782ec044138f129f03f3574c429e270562cf0f3b47020518dcd64be9fb3584.
    • nginx 5.4.0·latest: sha256:fb3a63eb7be971660a5b8e4936d42d9ce6454e726a121383c060789b10583b40. linux/amd64 manifest: sha256:8a09d389b1a8396f2c4620b192694b63537c85b3ca1e31d06806af1461195f19. 입력 지문 63f29fb371128296c1c1226981bf745d2593b3e1ef1d57713ac0200e59003c67.
    • ops/publish-images.ps1 -Version 5.4.0 -Publish로 다섯 서비스를 모두 새로 빌드·게시했고(재사용·재시도 없음), 버전 태그 게시 뒤 같은 digest로 latest를 승격해 다섯 서비스의 5.4.0·latest 일치를 최종 검증했습니다(Registry tags verified for 5.4.0.).
    Downloads 2