diff --git a/README.md b/README.md index c2faeb4..81c28b2 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,22 @@ Caddy의 runtime logger는 reverse proxy 오류에도 요청 정보를 남길 `X-Session-Token` 값을 encoder 단계에서 삭제합니다. 장애 로그를 수집할 때도 인증 헤더, cookie, token과 secret을 보고서나 채팅에 복사하지 않습니다. +## 프런트엔드 캐시 정책 + +Gateway와 profile frontend의 Vite build는 `/gateway/assets/`, +`//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는 같은 구성으로 @@ -181,3 +197,10 @@ docker compose -f compose.yaml -f compose.smoke.yaml up -d --build --wait 다른 env 파일은 `ENV_FILE=/path/to/file ./scripts/check.sh`로 검사합니다. `docker compose config` 전체 출력에는 펼쳐진 비밀값이 포함될 수 있으므로 CI artifact나 이슈에 그대로 첨부하지 않습니다. + +Caddy cache matcher의 적용·제외 경계는 다음처럼 비밀값 없이 별도로 검사할 수 +있습니다. + +```sh +node --test test/caddy-cache-policy.test.mjs +``` diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 3cb2a9d..51f0ce9 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -10,6 +10,11 @@ {$SITE_ADDRESS} { encode zstd gzip + # Vite emits content-hashed files below each frontend's assets directory. + # Defer this header so it replaces vite preview's default no-cache value. + @immutableFrontendAssets path_regexp immutableFrontendAssets ^/(gateway|che|kwe|pwe|twe|nya|pya|hwe)/assets/.+-[A-Za-z0-9_-]{8}\.[^/]+$ + header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable" + redir /gateway /gateway/ 308 redir /che /che/ 308 redir /kwe /kwe/ 308 diff --git a/test/caddy-cache-policy.test.mjs b/test/caddy-cache-policy.test.mjs new file mode 100644 index 0000000..eac4e95 --- /dev/null +++ b/test/caddy-cache-policy.test.mjs @@ -0,0 +1,27 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +test('Caddy caches only content-hashed Vite frontend assets as immutable', async () => { + const caddyfile = await fs.readFile(new URL('../caddy/Caddyfile', import.meta.url), 'utf8'); + const matcher = caddyfile.match( + /@immutableFrontendAssets path_regexp immutableFrontendAssets (\S+)/, + ); + + assert.ok(matcher, 'immutable frontend asset matcher must exist'); + const assetPath = new RegExp(matcher[1]); + + for (const profile of ['gateway', 'che', 'kwe', 'pwe', 'twe', 'nya', 'pya', 'hwe']) { + assert.equal(assetPath.test(`/${profile}/assets/index-CcXtLSVk.js`), true); + assert.equal(assetPath.test(`/${profile}/assets/index-CcXtLSVk.js.map`), true); + } + + assert.equal(assetPath.test('/gateway/'), false); + assert.equal(assetPath.test('/gateway/terms.1.html'), false); + assert.equal(assetPath.test('/gateway/assets/index.js'), false); + assert.equal(assetPath.test('/image/game/figure.png'), false); + assert.match( + caddyfile, + /header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable"/, + ); +});