• LINKS 1.0.2 — sh update.sh 한 명령 배포·coordinator Compose service

    LINKS 1.0.2

    이 patch 릴리스는 이전 릴리스 1.0.1(9cdd56ca4e)부터 최종 소스 4c4a5d3715까지를 묶었습니다. 루트 패키지는 1.0.2이고 ARCA pin은 exact 0.1.18로 그대로입니다. NAS 갱신은 이제 sh update.sh 한 명령입니다. 이 명령이 최신 release를 찾고, 배포 파일을 받아 대조하고, image 버전을 확인합니다. package coordinator의 정지·기동과 readiness 확인도 함께 합니다. package coordinator는 Compose service가 되어 재부팅 뒤 자동으로 기동합니다. schema는 바뀌지 않았으므로 DB 초기화와 migration이 필요 없습니다.

    업그레이드 주의: 1.0.1 → 1.0.2는 한 번만 수동으로 전환합니다(초기화 없음)

    1. 1회 수동 전환: 1.0.1의 update.sh는 자동 갱신을 모르므로 이번에만 아래 순서를 따릅니다.
      1. 실행 중인 host coordinator(sh ops/runtime-package-coordinator.sh …)를 종료합니다. foreground면 그 터미널에서 Ctrl+C를 누르고, background면 ps로 PID를 찾아 kill <PID>를 실행합니다.
      2. .runtime-package-coordinator.lock 파일은 지우지 않습니다. lock은 프로세스가 끝나면 풀립니다. coordinator가 아직 lock을 잡고 있으면 새 update.sh가 아무것도 바꾸지 않고 A host package coordinator is running; ...으로 멈춥니다.
      3. Task Scheduler 작업을 만들었다면 중지하고 삭제합니다. 작업을 삭제해도 이미 실행 중인 프로세스는 끝나지 않습니다.
      4. 새 update.sh를 받아 실행합니다(아래 명령). 이후 갱신은 sh update.sh 한 줄입니다.
    2. coordinator Compose service: coordinator는 Compose service runtime-package-coordinator로 실행됩니다. 구성은 docker:24.0.9-cli, network none, 재시작 정책 unless-stopped이며 배포 디렉터리를 read-only로 mount합니다. Docker socket을 mount하므로 이 service는 host root와 같은 권한을 가집니다. NAS 재부팅 뒤에는 Docker가 자동으로 기동하므로 Task Scheduler 작업이 필요 없습니다. 로그는 docker compose --env-file .env.production logs runtime-package-coordinator로 봅니다.
    3. shell override 거부: update.sh와 reset.sh는 Compose 값(compose.yaml이 interpolation하는 변수와 COMPOSE_PROJECT_NAME·COMPOSE_FILE·COMPOSE_PROFILES·COMPOSE_ENV_FILES)이 shell에 설정돼 있으면 빈 값이어도 아무것도 바꾸지 않고 Shell override is not supported: <NAME>. Set it in .env.production.으로 멈춥니다. 모든 값은 .env.production에 literal 값으로 둡니다. ${HOME} 같은 host shell 참조는 쓰지 않습니다. host와 coordinator container의 환경이 달라 서로 다른 값으로 풀리기 때문입니다.
    4. LINKS_REGISTRY_TAG: 평소에는 비우거나 latest로 둡니다. rollback 때만 <tag>@sha256:<digest> pin을 넣습니다. digest 없는 버전 tag는 update.sh가 거부합니다.
    5. HTTPS와 manifest: update.sh는 최초 URL과 redirect 모두 HTTPS만 사용합니다. release asset deploy-files.sha256과 7개 배포 파일의 SHA-256을 자동으로 대조하고, 하나라도 다르면 아무것도 바꾸지 않습니다. 이 대조는 무결성 확인이며 서명 인증이 아닙니다.
    6. NAS host Compose 2.20.1: NAS의 host Compose는 v2.20.1(Synology build)입니다. 릴리스 전에 공식 Docker Compose v2.20.1 binary(SHA-256 대조)로 바꾼 격리 docker:24.0.2-dind에서 이 릴리스의 배포 파일을 검증했습니다. reset → update, 재-update, container 재생성 update 세 시나리오 모두 성공했습니다. 12개 service의 network 연결은 선언과 모두 일치했고 package·runtime readiness는 종료 코드 0이었습니다. Synology build의 자체 패치와 공식 binary는 다를 수 있으므로 실제 NAS 결과는 확인 전입니다.
    7. DB 초기화 불필요: 기준 schema checksum은 14481d17c5947342be029bf6f850d2f862372b988dcbc1809360cdd7b333c918(infra/migrations/base.sha256)로 1.0.1과 같고 migration은 0개입니다. reset.sh를 실행하지 않습니다.

    NAS 절차(1.0.1에서 한 번):

    cd /volume1/docker/links
    # host coordinator를 Ctrl+C 또는 kill로 종료한 뒤(lock 파일 삭제 금지, Task Scheduler 작업이 있으면 삭제)
    curl -fL --proto '=https' https://git.2ex.dev/hanyul/LINKS/raw/tag/1.0.2/ops/deploy-registry.sh -o update.sh
    sh update.sh
    

    성공하면 단계 표시 6줄 뒤에 Updated to 1.0.2 (catalog f1c3cf7014ad), runtime.package ready.가 출력됩니다. 실패하면 원인 한 줄을 출력하고 nonzero로 끝납니다. 이후 갱신은 cd /volume1/docker/links && sh update.sh입니다.

    원본 game library는 읽기 전용으로 유지합니다. 이번 게시 작업에서 개발·운영 DB 초기화나 NAS 배포는 실행하지 않았습니다.

    sh update.sh 한 명령 배포

    • 1단계 release 파일: Gitea releases/latest에서 tag를 읽고 asset deploy-files.sha256을 받습니다. manifest는 정확히 7개 경로를 한 번씩 담아야 하며, 각 줄은 64자리 hex와 두 칸 구분 형식이어야 합니다. 그다음 7개 파일을 <이름>.tmp로 받아 대조합니다. asset이 없는 release(1.0.1 이하)에서는 Release <tag> does not support automatic updates; ...로 멈춥니다.
    • 자기 교체: 받은 update.sh가 실행 중인 script와 다르면 대조를 마친 새 script를 한 번 실행하고, 이후 단계는 새 release의 규칙으로 진행합니다. 받은 파일은 5단계 직전에 설치합니다. 그 전의 실패와 처리 가능한 중단 signal(HUP·INT·TERM)은 받은 파일을 지우고 끝냅니다. 이전 실행이 남긴 .tmp는 시작할 때 지웁니다.
    • image 버전 확인: pull한 image의 root version이 release tag와 같아야 진행합니다. 다르면 The registry image is version <v>, not release <tag>; try again later.로 멈춥니다.
    • coordinator: pull 전에 coordinator를 정지합니다. 다른 service를 up --wait로 갱신하고 catalog의 package 기능을 확인한 뒤 coordinator를 단독으로 기동합니다. 이번 기동의 Package coordinator ready (...) 줄을 최대 5분 기다립니다. 같은 container·StartedAt·RestartCount=0인지 확인해 ready 뒤 재시작이나 종료를 감지하고, 마지막으로 전체 readiness를 확인합니다.
    • 최초 설치: compose.yaml이 없는 디렉터리에서는 파일만 설치하고 sh reset.sh → sh update.sh를 안내합니다.
    • 거부: 인자, digest 없는 LINKS_REGISTRY_TAG, 실행 중인 host coordinator(lock), HTTPS가 아닌 저장소 URL, shell override는 아무것도 바꾸기 전에 거부합니다.

    package coordinator Compose service

    • compose.yaml에 runtime-package-coordinator service를 추가했습니다. 구성은 docker:24.0.9-cli, init, read-only rootfs, cap_drop: ALL + DAC_READ_SEARCH, no-new-privileges, network none, /tmp tmpfs 256 MB, stop_grace_period 60초, restart: unless-stopped입니다.
    • coordinator는 자기 mount에서 배포 디렉터리의 host 경로를 읽어 Compose의 project directory로 씁니다. 그래서 상대 bind 경로와 project 이름이 host Compose와 같게 해석됩니다.
    • 시작 준비가 끝나면 Package coordinator ready (ARCA <version>, builder <image ID 앞 12자>), 대기에 들어가면 Waiting for package jobs.를 출력합니다. 정지 요청을 받으면 실행 중인 Builder container를 바로 정지합니다.
    • Docker socket mount는 보안 정책에 예외로 기록했습니다. 개발 Windows에서는 이 service를 쓰지 않고 기존 host coordinator를 유지합니다.

    shell override 거부와 정리

    • update.sh와 reset.sh가 같은 규칙으로 Compose 값의 shell override를 거부합니다. 값은 출력하지 않습니다.
    • update.sh는 이전 실행의 .tmp를 설치하지 않으며, 중단 signal을 받으면 받은 파일을 지우고 129·130·143으로 끝납니다.

    문서

    • 운영 가이드: NAS 배포 두 줄, update.sh 단계·실패 메시지·되돌리기, 최초 설치와 1.0.1에서의 전환, release asset 첨부 절차, coordinator service 운영.
    • 보안 정책(Docker socket 예외), 아키텍처, 런타임 계약, 완료 기준선, 개발 계획, tests/README, README도 갱신했습니다.

    알려진 제한(후속 과제)

    • Docker 24 graphdriver에서는 Builder config digest를 pin과 직접 대조하지 않습니다. 이를 강화하려면 ARCA가 config digest를 공개 API로 제공해야 합니다(ARCA 후속).
    • 3단계 이후 실패하면 coordinator는 정지된 채 남습니다. 원인을 고친 뒤 다음 sh update.sh가 다시 기동합니다.
    • 1.0과 같은 제한:
      • Player 세이브 관리 listener(3005)는 catalog caller token으로만 보호되며 edge network에서 TCP로 닿을 수 있습니다.
      • catalog·Player의 일반 egress를 network가 제한하지 않습니다.
      • update.sh는 opt-in profile container(translation-import 등)를 갱신하지 않습니다. 갱신 뒤 직접 다시 기동합니다.

    검증과 남은 범위

    • PASS: one-command-update 브랜치(병합 ede4c1b)에서 통과한 검사
      • update.sh 기록기 98/98, reset.sh 기록기 30/30, coordinator unit 31/31
      • 전체 unit 149 files / 1,135 tests, static, 문서 검사, Compose config 3개 조합, BusyBox sh -n, 격리 foundation
      • HTTPS dind e2e(Docker 24.0.2): 최초 설치·자기 교체, reset, 배포, HTTPS만 허용, ready 뒤 종료 감지, 재부팅
      • 리뷰 최종 판정: 병합 가능
    • PASS: 릴리스 gate(Compose v2.20.1)
      • 환경: 격리 docker:24.0.2-dind(overlay2, BusyBox ash). host Compose는 공식 v2.20.1 linux-x86_64 binary로 바꿨습니다(SHA-256 804aa825c58b375901ba9dc9d0aa1648ce305d903a58f2a5c42bb5c6873b02bf, release의 .sha256·checksums.txt와 일치).
      • 입력: 이 릴리스와 byte가 같은 7개 배포 파일을 합성 HTTPS release로 제공하고, 실제 registry image를 익명으로 pull했습니다.
      • 최초 설치(자기 교체) → reset.sh → update.sh: 118초, exit 0
      • 재-update: exit 0
      • container 재생성 update(catalog·player 포함 8개 재생성): exit 0
      • 매 시나리오 뒤 12개 service의 network가 선언과 일치(누락 0)했고, unhealthy는 0이었으며 package·runtime readiness는 exit 0이었습니다.
    • PASS: 최종 1.0.2 소스. 릴리스 커밋 두 개(root version, 완료 기준선의 version 문구) 뒤 다음을 다시 통과했습니다.
      • 문서 검사(756개 파일 / Markdown 32개)
      • static(Prettier·ESLint·typecheck·docs-static)
      • git diff --check
    • 게시 이미지: 같은 최종 소스에서 build했습니다. image 안에서 root version 1.0.2, ARCA pin·설치본 0.1.18, base.sha256을 확인했습니다.
    • PARTIAL: 전체 UI/제품 acceptance. 화면·fixture·package/build 성공을 실제 browser play PASS로 기록하지 않습니다.
    • NOT RUN:
      • Synology NAS 실제 환경(Compose v2.20.1 Synology build, Docker 24 btrfs)에서의 1.0.1 → 1.0.2 전환과 sh update.sh
      • Docker 24에서 Builder package 전체 실행
      • 실제 library/private game과 실제 scan
      • 입력·오디오·save/reload의 실제 게임 acceptance
      • live provider
      • 개발 rebuild

    배포 이미지와 파일

    • Git annotated tag는 1.0.2이고 최종 소스는 4c4a5d3715입니다.
    • 단일 linux/amd64 이미지: git.2ex.dev/hanyul/links:1.0.2 = git.2ex.dev/hanyul/links:latest
    • 원격 digest: sha256:f1c3cf7014ad65aa00dd3895bc1d3c964c852ecbf4995420bb973b05fcebdcfb
    • release asset deploy-files.sha256(아래 7줄)을 첨부했습니다. update.sh가 이 asset으로 배포 파일을 자동 대조합니다.
    • 아래 SHA-256은 checkout의 CRLF 변환본이 아니라 1.0.2 태그의 Git blob 원본 byte로 계산했습니다.
    • NAS 배포 목록은 7개입니다(update.sh=ops/deploy-registry.sh, reset.sh=ops/reset-registry.sh, backup.sh=ops/backup-registry.sh).
    SHA-256 파일 1.0.1 대비
    c7b480fab3238bbefc008501bf2e65ab50b95c985dba2b8896f85af6e262caec compose.yaml 변경 (runtime-package-coordinator service)
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3 infra/docker/Caddyfile 동일
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8 ops/backup-verify.compose.yaml 동일
    9671a00c1a13723cf16b0eced336241f25299f45bbed5ca4044af23f9d81ac72 ops/deploy-registry.sh 변경 (한 명령 자동 갱신)
    cf6169a8ffa577d40b70ea476e10b662fc898a68ebfc28b43c513cf9c1b7bf33 ops/reset-registry.sh 변경 (shell override 거부)
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d ops/backup-registry.sh 동일
    6ba741a66e9744c0cd01ce8e3df81b94a55ae3f7d41b6f1d05200eedc5dccdb9 ops/runtime-package-coordinator.sh 변경 (Compose service 모드·상태 줄·Builder 즉시 정지)
    c7b480fab3238bbefc008501bf2e65ab50b95c985dba2b8896f85af6e262caec  compose.yaml
    6e1fd3f2f73faee52abca28c8552cde417f496d86fc3d78c7001dec480e171a3  infra/docker/Caddyfile
    39a4c55050e76043c8ca70f11f59dbdf08fff7ef38c1f1ec878f96194988d9d8  ops/backup-verify.compose.yaml
    9671a00c1a13723cf16b0eced336241f25299f45bbed5ca4044af23f9d81ac72  ops/deploy-registry.sh
    cf6169a8ffa577d40b70ea476e10b662fc898a68ebfc28b43c513cf9c1b7bf33  ops/reset-registry.sh
    89375a986b3cb64cf74145541701b32c6a0e983a72ca46be21a8bcfad7beaa3d  ops/backup-registry.sh
    6ba741a66e9744c0cd01ce8e3df81b94a55ae3f7d41b6f1d05200eedc5dccdb9  ops/runtime-package-coordinator.sh
    
    Downloads 3