웹 게임 배포 파이프라인: 빌드부터 서버 재시작까지 혼자 굴리는 순서
Godot 웹 게임 두 개와 React 게임 하나를 Cloudflare Worker와 서울 VM에 올리는 절차를 정리했다. 파일 이름 해시, 버전 확인, 진행 중인 판을 기다리는 드레인, 백업까지 실제 스크립트 기준으로 적었다.

혼자 게임 세 개를 운영하면 배포가 하루에도 몇 번씩 있다. Salmonholic 저장소의 커밋 메시지만 봐도 “Deployed web”, “Deployed: DO …, KR …“이 줄줄이 나온다. 매번 손으로 하면 언젠가 한 단계를 빼먹는다. 그래서 배포를 스크립트 몇 개로 고정했다. 이 글은 그 순서를 웹 쪽과 서버 쪽으로 나눠 적은 것이다.
웹 쪽: 네 단계
Godot 게임(Salmonholic, AvoidNow)의 웹 배포는 네 단계다.
| 단계 | 하는 일 | 도구 |
|---|---|---|
| 1. 내보내기 | Godot 헤드리스 export로 build/web 생성 |
godot --headless --export-release "Web" |
| 2. 스테이징 | 배포 폴더로 복사, 압축, 버전 파일 작성 | stage.sh |
| 3. 올리기 | Worker와 정적 파일 업로드 | npx wrangler deploy |
| 4. 확인 | 헤드리스 브라우저로 첫 화면까지 띄워 보기 | 스모크 스크립트 |
핵심은 2단계다. 스테이징 스크립트가 하는 일은 세 가지다.
첫째, wasm과 pck를 brotli 최고 단계로 미리 압축한다. Workers 정적 에셋은 파일 하나가 25MiB를 넘으면 올라가지 않는다. Godot 기본 wasm은 39.5MB라서 압축본만 올리고, Worker가 Content-Encoding: br을 붙여 내보낸다. Salmonholic의 Worker는 브라우저가 br을 못 받는다고 하면 406을 돌려준다.
둘째, 파일 이름에 내용 해시를 붙인다. AvoidNow의 stage.sh는 pck, wasm, js를 이어 붙여 SHA-256을 구하고 앞 10자리를 이름에 넣는다. index.html만 매번 새로 받게 하고, 해시가 붙은 파일은 1년짜리 immutable 캐시로 둔다. 효과는 첫 로딩 7MB 글에 적었다.
셋째, version.json을 쓴다. 커밋 해시, 파일 해시, 빌드 시각이 들어간다. 지금 떠 있는 게 어느 빌드인지 브라우저에서 바로 확인할 수 있다.
스크립트는 실패를 조용히 넘기지 않게 짰다. index.html 안의 엔진 파일 이름을 해시 이름으로 바꾼 뒤, 바뀌었는지 grep으로 확인하고 아니면 멈춘다. 이름만 바뀌고 HTML이 옛 이름을 가리키면 검은 화면이 뜨기 때문이다.
타이틀 화면 구석의 버전 문자열. 배포할 때마다 이 숫자와 version.json을 먼저 맞춰 본다
옛 클라이언트를 돌려보내는 장치
웹 게임은 사용자가 탭을 열어 둔 채로 며칠씩 있을 수 있다. 서버 프로토콜이 바뀌었는데 옛 클라이언트가 붙으면 이상하게 동작한다. Salmonholic 서버는 /health 응답에 minClientVersion을 넣는다. 클라이언트는 자기 버전이 이보다 낮으면 페이지를 새로 고친다. 비교는 0.8.1 같은 숫자 세 자리만 하고 -r5r 같은 꼬리는 무시한다. 이 필드가 없는 옛 서버에는 검사를 건너뛴다.
서버 쪽: 두 가지 방식
서울 VM 재배포는 게임마다 조금 다르다.
- AvoidNow:
redeploy.sh가 서버 폴더를 VM에 rsync한 뒤, VM 안에서docker compose up -d --build를 돌린다. 방 서버와 Caddy(자동 TLS) 두 컨테이너다. - 랜선포차: 개발 맥에서 linux/amd64 이미지를 빌드해
docker save | ssh docker load로 넘긴다. VM 안에서 빌드하는 옵션(--vm-build)도 있지만 기본은 맥에서 빌드해 넘기는 쪽이다. 로컬에서 빌드한 이미지는 164MB(arm64 기준)였다.
해외 DO 서버는 wrangler deploy 한 줄이다. 대신 Salmonholic 커밋에는 웹, DO, 서울 VM 세 곳의 배포 버전을 따로 적는다. 그래야 셋 중 하나만 올라가고 나머지가 옛 버전으로 남은 상태를 나중에 찾을 수 있다.
진행 중인 판을 지키는 드레인
제일 신경 쓴 건 재배포하는 순간 판을 하고 있던 사람이다. 랜선포차 서버는 종료 신호(SIGTERM)를 받으면 바로 죽지 않고 드레인 상태가 된다.
/health가 503과draining: true를 돌려준다. 새 입장은 받지 않는다.- 진행 중인 판은 끝날 때까지 기다린다. 기다리는 최대 시간
DRAIN_SEC의 기본값은 660초, 11분이다. 개발 중 자동 시뮬레이션 기준 한 판이 약 7분이니, 막 시작한 판도 끝낼 수 있는 시간이다. - 판이 모두 끝나면 정상 종료 로그를 남기고 내려간다.
기록은 VM 안의 SQLite에 있어서 백업도 붙였다. backup.sh가 VACUUM INTO로 스냅샷을 떠서 VM 볼륨과 내 맥 양쪽에 둔다. 로컬 검증 때 컨테이너를 재시작한 뒤에도 데이터가 남는지, 백업 파일이 integrity_check ok를 내는지 확인했다.
배포는 요청받았을 때만
마지막 규칙은 기술이 아니라 습관이다. 랜선포차 저장소 문서에는 “배포는 요청받았을 때만 한다”고 적어 두었다. 작업은 기능 단위로 커밋하고 푸시하지만, 실제로 사람들이 접속하는 서버에 올리는 건 내가 확인한 뒤다. 절차는 자동화했지만 올릴지 말지는 내가 정한다.
각 게임이 어떤 서버 위에서 도는지는 서버 지도 글에, 배포 뒤 문제를 찾는 방법은 텔레메트리 글에 정리했다. 이렇게 올린 게임은 Salmonholic, AvoidNow, 랜선포차에서 해 볼 수 있다.


