Hide_D 7af9c88c27 perf(caddy): 해시 빌드 자산에 장기 캐시를 적용
Vite content hash가 포함된 frontend asset만 1년 immutable로 제공한다. HTML, hash 없는 asset과 별도 image 경로는 기존 재검증 정책을 유지하고 matcher 회귀 테스트와 운영 문서를 추가한다.
2026-08-18 12:55:07 +00:00
2026-08-08 04:04:39 +00:00
2026-08-08 04:04:39 +00:00

Sammo Core2026 Docker deployment

이 저장소는 새 호스트에서 .env 하나로 Core2026 Gateway, PostgreSQL, Redis, Caddy와 Git/PM2 release-controller를 기동하기 위한 공개 배포 골격입니다. 게임 profile은 관리자 화면에서 각각 다른 branch 또는 commit을 선택해 DB 유지 배포, DB 초기화 배포와 rollback을 수행합니다.

현재 도메인 배치

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/*를 내부 서비스와 자산으로 분기합니다.

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는 DOMAINPUBLIC_SCHEME으로 GATEWAY_PUBLIC_URLKAKAO_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 process가 실행 중인 상태에서도 release build의 Node/Rolldown thread를 수용하기 위한 값입니다. 호스트 용량에 맞춰 RUNTIME_*_LIMIT을 조정할 수 있지만 0 또는 무제한으로 두지 않습니다. PM2 process/restart 수가 예상보다 증가하면 로그 수집보다 runtime 중지가 우선입니다.

초기 빌드와 profile worktree 빌드는 기본 Node heap 1536 MiB, RAYON_NUM_THREADS=1 안에서 실행됩니다. RUNTIME_NODE_OPTIONS의 heap 상한은 5122048 MiB, RUNTIME_RAYON_NUM_THREADS는 14만 허용됩니다. 턴 데몬만 더 큰 heap이 필요하면 TURN_DAEMON_NODE_OPTIONS를 512~4096 MiB 범위에서 지정합니다. 이 값은 Gateway orchestrator가 생성하는 turn-daemon PM2 process에만 NODE_OPTIONS로 적용되며 API·frontend·worker와 build는 계속 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 파일에만 둡니다.

데이터와 복구 경계

  • PostgreSQL, Redis, Core clone/worktree, PM2 상태와 Caddy 인증서는 named volume에 보존됩니다. 새 user icon은 외부 이미지 서비스가 보존하며 runtime의 user icon volume은 기존 설치 호환용입니다.
  • /image/*는 앱 artifact가 아니며 data/image/의 별도 운영 자산을 제공합니다.
  • 일반 docker compose down은 volume을 보존합니다. down --volumes는 DB와 release 상태를 삭제하는 파괴적 명령이므로 backup 없이 실행하지 않습니다.
  • 앱 rollback은 Prisma migration을 되돌리지 않습니다. 이전 앱과 새 schema의 호환성을 배포 전에 확인합니다.
  • Core2026 저장소가 비공개라면 credential을 Git에 추가하지 않습니다. HTTPS는 .envCORE_REPOSITORY_USERNAME과 read-only CORE_REPOSITORY_TOKEN을 함께 설정합니다. URL에 credential을 포함하지 않으며 runtime은 0600 파일로 옮긴 뒤 원래 env를 제거하고 GIT_ASKPASS로만 전달합니다.
  • SSH deploy key를 사용할 때는 CORE_SSH_PRIVATE_KEY_BASE64CORE_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 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와 profile frontend의 Vite build는 /gateway/assets/, /<profile>/assets/ 아래 파일명에 8자리 content hash를 붙입니다. 내부 Caddy는 이 형식을 만족하는 JS, CSS, source map과 기타 build asset에 Cache-Control: public, max-age=31536000, immutable을 적용합니다. 새 build는 내용이 달라지면 URL도 달라지므로 배포와 rollback 뒤에도 기존 URL의 장기 cache를 재사용할 수 있습니다.

index.html, router fallback, terms.*.html처럼 URL이 고정된 HTML은 이 정책에 포함하지 않습니다. 이 응답은 Vite preview의 Cache-Control: no-cache와 ETag를 유지하여 저장은 허용하되 사용할 때마다 변경 여부를 재검증합니다. content hash가 없는 /assets/ 파일과 별도 운영 자산인 /image/*immutable로 취급하지 않습니다. 따라서 public directory에 장기 cache할 파일을 추가할 때는 먼저 content-hashed URL로 옮겨야 합니다.

개발 bind 모드

로컬 Core2026 checkout을 container에 bind하고 DB/Redis/Caddy는 같은 구성으로 사용할 수 있습니다. .envCORE_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.yamlRUNTIME_MODE=development와 모든 service의 restart: no를 literal로 고정합니다. .envRUNTIME_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
S
Description
No description provided
Readme
241 KiB
Languages
JavaScript 80.9%
Shell 17.6%
Dockerfile 1.5%