Sammo Core2026 Docker deployment
이 저장소는 새 호스트에서 .env 하나로 Core2026 Gateway, PostgreSQL, Redis,
Caddy와 Git/PM2 release-controller를 기동하기 위한 공개 배포 골격입니다. 게임
profile은 관리자 화면에서 각각 다른 branch 또는 commit을 선택해 DB 유지 배포,
DB 초기화 배포와 rollback을 수행합니다.
현재 도메인 배치
상위 sam_rebuild 작업공간에서 어느 Docker 환경을 확인할지는
docs/docker-environment-routing.md를 먼저 사용합니다. 이 저장소의 Compose는
로컬 E2E 또는 새 호스트 배포 골격이며, 실행 중인 dev-sam2026.hided.net과
sam.hided.net 운영 stack의 호스트 전용 overlay를 직접 소유하지 않습니다.
dev-sam2026.hided.net은 실제 외부 Core2026 서비스입니다. 이 저장소로 띄운
로컬 E2E Docker stack은 dev-sam-e2e.hided.net을 사용하며, 외부 Caddy가 TLS를
종료한 뒤 172.30.1.54:14999의 HTTP listener로 호스트 전체를 전달합니다.
Docker Caddy가 /gateway/, /che/, /hwe/ 및 /image/*를 내부 서비스와
자산으로 분기합니다.
sam.hided.net의 /gateway/와 일곱 profile prefix는 ssh serv의 별도
core2026-sam-production project가 소유합니다. 로컬 E2E 성공이나
dev-sam2026.hided.net의 release 상태를 sam.hided.net 반영 근거로 사용하지
않습니다.
E2E .env의 비밀이 아닌 ingress 값은 다음과 같습니다.
DOMAIN=dev-sam-e2e.hided.net
PUBLIC_SCHEME=https
CADDY_SITE_ADDRESS=http://dev-sam-e2e.hided.net
HTTP_PORT=14999
외부 Caddy는 원래 Host header와 path prefix를 보존해야 합니다. 외부 확인은
https://dev-sam-e2e.hided.net/gateway/api/healthz를 사용합니다. HTTPS_PORT는
외부 proxy가 사용하지 않지만 Compose port 충돌을 피할 별도 값으로 둡니다.
Compose는 DOMAIN과 PUBLIC_SCHEME으로 GATEWAY_PUBLIC_URL과
KAKAO_REDIRECT_URI를 만듭니다. 외부 route만 바꾸면 이미 실행 중인 runtime의
process 환경은 바뀌지 않으므로, 도메인을 변경한 뒤 runtime과 Caddy container를
재생성하고 OAuth 시작 응답이
https://dev-sam-e2e.hided.net/gateway/oauth/callback을 사용하는지 확인합니다.
빠른 시작
Docker Engine과 Compose plugin이 설치된 Linux 호스트에서 다음을 실행합니다.
cp .env.example .env
# .env의 domain, Core2026 URL, Kakao key와 모든 비밀값을 교체합니다.
./scripts/check.sh
docker compose up -d --build --wait
docker compose ps
DNS의 DOMAIN A/AAAA record가 호스트를 가리키고 80/443 TCP 및 443 UDP가
열려 있으면 Caddy가 인증서를 자동 발급합니다. Kakao Developers에는
https://DOMAIN/gateway/oauth/callback을 redirect URI로 등록합니다. 첫 기동은
Core2026 clone, frozen-lockfile install, build와 Gateway migration 때문에 수 분이
걸릴 수 있습니다.
외부 reverse proxy가 TLS를 종료하고 이 stack의 HTTP port로 전달한다면
PUBLIC_SCHEME=https, CADDY_SITE_ADDRESS=http://DOMAIN을 함께 설정합니다.
예를 들어 host 14999로 전달할 때는 HTTP_PORT=14999로 두고, 사용하지 않는
HTTPS_PORT는 충돌하지 않는 별도 host port로 지정합니다. 외부 proxy는 원래
Host header를 보존해야 합니다.
runtime은 기본적으로 memory/swap 각각 4 GiB, CPU 4개, PID 512개 상한을
가집니다. 이 상한은 Gateway와 여러 profile의 PM2 backend process에만 적용되며
release build는 별도 builder service의 상한을 사용합니다. 호스트 용량에 맞춰
RUNTIME_*_LIMIT을 조정할 수 있지만 0 또는 무제한으로 두지 않습니다. PM2
process/restart 수가 예상보다 증가하면 로그 수집보다 runtime 중지가 우선입니다.
runtime Node process는 기본 heap 1536 MiB, RAYON_NUM_THREADS=1 안에서
실행됩니다. RUNTIME_NODE_OPTIONS의 heap 상한은
5122048 MiB, 4만 허용됩니다. 턴 데몬만 더 큰
heap이 필요하면 RUNTIME_RAYON_NUM_THREADS는 1TURN_DAEMON_NODE_OPTIONS를 512~4096 MiB 범위에서 지정합니다.
이 값은 Gateway orchestrator가 생성하는 turn-daemon PM2 process에만
NODE_OPTIONS로 적용되며 API·worker는 계속 RUNTIME_NODE_OPTIONS를
사용합니다. 큰 값을 쓰기 전에 RUNTIME_MEMORY_LIMIT과
동일한 swap 상한을 함께 늘리고 실제 container 총사용량을 확인합니다.
scripts/check.sh는 example placeholder, 짧은 비밀값, 잘못된 domain/email,
repository credential 조합을 실제 값 출력 없이 거부합니다. 최소 길이는 DB·Redis
password 24자, game/bootstrap token 32자, 최초 관리자 password 16자입니다.
배포 전 무작위 값은 예를 들어 openssl rand -base64 36으로 생성합니다.
INITIAL_ADMIN_*은 사용자 table이 비어 있을 때만 superuser를 한 번 생성합니다.
그 뒤 /gateway/admin/server-operations에서 che, kwe, pwe, twe, nya,
pya, hwe마다 source branch/commit을 독립 선택합니다. DB 유지 배포는 현
시즌을 보존하고, DB 초기화 배포는 scenario를 다시 seed합니다. Gateway 자체의
배포와 이전 commit rollback은 같은 화면의 별도 release 영역에서 처리됩니다.
초기 profile의 <profile>:default에서 default는 현재 scenario가 아니라 변경되지
않는 인스턴스 키입니다. 현재 scenario는 DB 초기화 배포가 성공한 뒤 Gateway DB의
별도 필드에 기록됩니다.
이미지 서비스 연동에는 서버 간 접근 URL IMAGE_SERVICE_URL, 브라우저 공개 URL
IMAGE_PUBLIC_URL과 서로 다른 두 secret 파일이 필요합니다. 업로드 secret은
Gateway 전용 아이콘과 game-api 편집기 첨부에만 사용하고, sync secret은 Gitea
webhook 누락 시 docker compose exec runtime pnpm sync:image로 현재 이미지
branch의 fast-forward를 요청할 때만 사용합니다. 두 파일은 runtime에 read-only로
mount되며 원문은 환경 변수나 브라우저 bundle에 들어가지 않습니다.
IMAGE_PUBLIC_URL의 기본값은 https://sam-image.hided.net입니다. runtime은
이를 Gateway·game frontend의 공개 이미지 origin과 사용자 아이콘 URL에 함께
주입합니다. 정적 게임 이미지는 /game, 공용 아이콘은 /icons, Core2026
사용자 아이콘은 서버 발급 경로 users/core2026/<파일>을 /icons base 아래에
붙여 읽습니다. 업로드 권한값은
URL query나 VITE_*가 아니라 IMAGE_UPLOAD_CORE2026_SECRET_FILE이 가리키는
mode 0600 secret 파일에만 둡니다.
Web Push 활성화 경계
Gateway의 Android, iPhone과 Windows 브라우저 알림 기반은 포함하지만 기본값은
WEB_PUSH_ENABLED=false입니다. 이 상태에서는 구독 버튼과 전송 worker가
비활성이고, 알림 이벤트를 나중에 소급 전송할 backlog도 만들지 않습니다.
활성화할 때만 VAPID key pair를 생성하여 공개키는
WEB_PUSH_VAPID_PUBLIC_KEY, private key는 Git에서 제외한
WEB_PUSH_VAPID_PRIVATE_KEY_FILE에 각각 넣고, 운영 연락처를
WEB_PUSH_VAPID_SUBJECT의 mailto: 또는 HTTPS URL로 설정합니다. private key는
runtime의 /run/secrets/web_push_vapid_private_key에 Compose secret으로만
mount됩니다. 설정 후 ./scripts/check.sh가 키 파일의 존재와 activation 필드를
검증한 다음 runtime을 재생성해야 하며, 실제 활성화와 배포는 별도 운영 작업입니다.
데이터와 복구 경계
- PostgreSQL, Redis, Core clone/worktree, PM2 상태와 Caddy 인증서는 named volume에 보존됩니다. 새 user icon은 외부 이미지 서비스가 보존하며 runtime의 user icon volume은 기존 설치 호환용입니다.
- Gateway와 게임 공통 메뉴는 runtime volume의
/srv/data/navigation.json을 읽습니다. 최초 기동 때만 Core의resources/navigation.json을 복사하며, 이후 배포와 container 재생성은 운영자가 편집한 파일을 덮어쓰지 않습니다. JSON을 유효하게 저장한 뒤 브라우저를 새로 열거나 새로고침하면 재빌드 없이 반영됩니다. 잘못된 스키마나javascript:URL은 API가 거부하므로 변경 전에 Core 문서의 메뉴 설정 검증 명령을 실행합니다. /image/*는 앱 artifact가 아니며data/image/의 별도 운영 자산을 제공합니다.- 일반
docker compose down은 volume을 보존합니다.down --volumes는 DB와 release 상태를 삭제하는 파괴적 명령이므로 backup 없이 실행하지 않습니다. - 앱 rollback은 Prisma migration을 되돌리지 않습니다. 이전 앱과 새 schema의 호환성을 배포 전에 확인합니다.
- Core2026 저장소가 비공개라면 credential을 Git에 추가하지 않습니다. HTTPS는
.env의CORE_REPOSITORY_USERNAME과 read-onlyCORE_REPOSITORY_TOKEN을 함께 설정합니다. URL에 credential을 포함하지 않으며 runtime은 0600 파일로 옮긴 뒤 원래 env를 제거하고GIT_ASKPASS로만 전달합니다. - SSH deploy key를 사용할 때는
CORE_SSH_PRIVATE_KEY_BASE64와CORE_SSH_KNOWN_HOSTS_BASE64를 함께 설정합니다. 각각base64 -w0로 인코딩한 read-only key와 검증한 known_hosts 내용입니다. HTTPS와 SSH mode를 동시에 설정할 수 없습니다. 원래 base64 env는 파일 생성 직후 제거됩니다. 생성된 credential/key 파일은 runtime volume에서 0600으로 관리되고 해당 mode를 해제한 다음 기동하면 제거됩니다.
상태 확인과 로그:
docker compose ps
docker compose logs --tail=200 builder runtime caddy
docker compose exec runtime pnpm --filter @sammo-ts/release-controller status
Caddy의 runtime logger는 reverse proxy 오류에도 요청 정보를 남길 수 있으므로
X-Session-Token 값을 encoder 단계에서 삭제합니다. 장애 로그를 수집할 때도
인증 헤더, cookie, token과 secret을 보고서나 채팅에 복사하지 않습니다.
프런트엔드 캐시 정책
Gateway frontend의 Vite build는 /gateway/assets/, profile 공용 Vite build는
/gateway/profile-assets/<commit-digest>/assets/ 아래 파일명에 8자리 content
hash를 붙입니다. 내부 Caddy는 이 형식을 만족하는 JS, CSS, source map과 기타 build asset에
Cache-Control: public, max-age=31536000, immutable을 적용합니다. 새 build는
내용이 달라지면 URL도 달라지므로 배포와 rollback 뒤에도 기존 URL의 장기 cache를
재사용할 수 있습니다. 같은 commit을 쓰는 일곱 profile은 같은 공용 URL을 사용하므로
브라우저가 profile을 옮겨도 이미 받은 JS, CSS와 source map을 다시 전송하지 않습니다.
index.html, router fallback, terms.*.html처럼 URL이 고정된 HTML은 이 정책에
포함하지 않습니다. Caddy는 이 응답에 Cache-Control: no-cache를 적용하여
저장은 허용하되 사용할 때마다 변경 여부를 재검증합니다. content hash가
없는 /assets/ 파일과 별도 운영 자산인 /image/*도 immutable로 취급하지
않습니다. 따라서 public directory에 장기 cache할 파일을 추가할 때는 먼저
content-hashed URL로 옮겨야 합니다.
정적 frontend와 격리 builder
운영 frontend는 vite preview로 제공하지 않습니다. Gateway build와 profile 공용
build는 frontend-artifacts named volume의 commit/digest 기반 불변 디렉터리에 복사되고,
runtime은 검증된 release의 current 심볼릭 링크만 원자적으로 교체합니다. Caddy는
이 volume을 read-only로 mount하여 직접 제공합니다. 첫 전환과 명시적 개발 모드를
위한 preview reverse-proxy fallback은 남아 있지만 운영 PM2에는 Vite process가
없습니다.
Profile Vite bundle은 commit과 공개 환경이 같으면 한 번만 생성되어
game-assets/releases/<commit-digest>에 게시됩니다. 각 profile의 current에는
profile base path, API/SSE URL과 Gateway URL을 담은 JSON <script>가 삽입된 작은
index.html, deployment-version.json과 manifest만 둡니다. 따라서 profile별 설정은
runtime에 결정하면서도 content hash asset과 sourcemap URL은 모두 공유합니다. 이전
profile 전용 정적 artifact도 전환·rollback 호환을 위해 계속 읽을 수 있습니다.
builder service는 Core source/worktree volume만 공유하고 release build를 한 번에
하나씩 실행합니다. DB, Redis, game token, OAuth 비밀값과 Docker socket은 받지
않으며, runtime은 공개 VITE_*와 heap/thread/cache 설정만 build 요청에
전달합니다. Migration, profile seed, Redis mutation, PM2 전환, readiness와 release
상태 게시는 계속 runtime이 소유합니다.
release 순서는 builder build → 불변 frontend stage → migration/seed → backend PM2
전환 → current 활성화 → API/Caddy readiness → 상태 게시입니다. 실패하면 이전
backend와 artifact를 함께 복구합니다. DEPLOY는 DB와 현 시즌을 보존하며,
RESET은 명시적으로 시나리오를 초기화한 뒤 같은 선택 commit의 artifact를
게시합니다. STOPPED와 가오픈 전 RESERVED는 current를 제거하여 오래된 SPA가
계속 노출되지 않게 합니다.
성공한 Gateway release는 Core clone의 refs/sammo/active-gateway도 compare-and-swap으로
갱신합니다. Runtime container가 재생성되면 tracked 변경이 없는지 확인한 뒤 이 ref를
detached checkout하여, release worktree에서 성공한 UPDATE가 예전 bootstrap HEAD로
되돌아가지 않게 합니다. Ref가 아직 없는 최초 설치만 CORE_BOOTSTRAP_REF checkout을
사용합니다. DB 상태 게시가 실패하면 ref도 이전 commit으로 복구합니다.
기존 preview stack을 처음 전환할 때는 Core main 반영 뒤 현재 runtime 안에서 release-controller를 먼저 새 commit으로 self-upgrade하고, 그 controller로 Gateway UPDATE를 완료하여 위 persistent ref가 생성된 것을 확인한 다음 Docker stack을 재생성합니다. 이 순서를 건너뛰고 새 Compose부터 적용하면 기존 Core bootstrap checkout에는 artifact publisher가 없을 수 있습니다. 이후 일반 Gateway UPDATE와 rollback은 controller가 ref를 함께 관리하므로 같은 사전 절차를 반복하지 않습니다.
docker compose exec \
-e GATEWAY_ACTIVE_RELEASE_GIT_REF=refs/sammo/active-gateway \
runtime pnpm --filter @sammo-ts/release-controller self-upgrade BRANCH main
이 명령 뒤 관리자 Gateway UPDATE가 성공하고 ref와 active commit이 같은지 확인합니다.
현재 RUNNING/PREOPEN profile은 Docker 재생성 전에 같은 commit으로 DB 보존 DEPLOY하여
.release-dist를 준비합니다. RESET은 DB 폐기 의도가 있는 dev 대상에서 별도로
검증하며, 정적 제공 전환 자체를 위해 운영 DB를 RESET하지 않습니다.
frontend-artifacts와 builder-cache, Core clone 안의 active release ref도 일반
down에서는 보존됩니다.
down --volumes는 DB뿐 아니라 이 release 상태도 삭제하므로 별도 폐기 권한과
backup 없이는 실행하지 않습니다.
개발 bind 모드
로컬 Core2026 checkout을 container에 bind하고 DB/Redis/Caddy는 같은 구성으로
사용할 수 있습니다. .env에 CORE_DEV_PATH=/absolute/path/to/core2026,
host 사용자의 DEV_UID/DEV_GID, PUBLIC_SCHEME=http, DOMAIN=localhost를
추가한 뒤 실행합니다. 기본 UID/GID는 1000:1000입니다.
docker compose -f compose.yaml -f compose.dev.yaml up -d --build
docker compose exec runtime pnpm --filter @sammo-ts/gateway-api dev
docker compose exec runtime pnpm --filter @sammo-ts/gateway-frontend dev --host 0.0.0.0
개발 override의 runtime은 의존성과 Prisma client를 준비한 뒤 대기합니다. 필요한 watch process를 별도 shell에서 실행합니다. 운영 release-controller를 시험하려면 override 없이 production mode를 사용해야 하며, bind checkout의 Git metadata에 container worktree 경로를 등록하지 않도록 주의합니다.
compose.dev.yaml은 RUNTIME_MODE=development와 모든 service의
restart: no를 literal로 고정합니다. .env의 RUNTIME_MODE로 이를 덮어쓸 수
없습니다. production PM2 검증에 개발 override를 섞지 않습니다. 개발 runtime에도
기본 4 GiB/4 CPU/256 PID 상한이 있으며 DEV_RUNTIME_*_LIMIT으로 더 낮출 수
있습니다.
운영 entrypoint와 PM2를 검증할 때는 개발 override 대신 전용 smoke override를
사용합니다. 이 override는 clone mode를 유지하면서 모든 service를 restart: no로
고정하고 runtime 상한을 최대 4 GiB/4 CPU/256 PID로 제한합니다.
docker compose -f compose.yaml -f compose.smoke.yaml up -d --build --wait
검증 shell에는 EXIT/INT/TERM trap과 전체 timeout을 두고, 정상·실패 어느 경우든
docker compose stop으로 끝냅니다. down --volumes는 사용하지 않습니다.
설정 검증
.env를 채운 뒤 실제 값을 출력하지 않는 검사를 실행합니다.
./scripts/check.sh
검사는 production/development Compose model과 Caddyfile 구문을 확인합니다.
다른 env 파일은 ENV_FILE=/path/to/file ./scripts/check.sh로 검사합니다.
docker compose config 전체 출력에는 펼쳐진 비밀값이 포함될 수 있으므로 CI
artifact나 이슈에 그대로 첨부하지 않습니다.
Caddy cache matcher의 적용·제외 경계는 다음처럼 비밀값 없이 별도로 검사할 수 있습니다.
node --test test/caddy-cache-policy.test.mjs