킹서버에 `vite dev` 가 떠 있었다. 요청마다 트랜스파일하고, 컨테이너 기동이 npm install 네트워크에 의존하고, /src 원본과 소스맵이 그대로 나간다. 발행물을 파는 사이트의 진입점이 dev 서버일 이유가 없다. - nginx/Dockerfile 신규: @o2o/frontend 를 굽는 build 스테이지 + 번들을 담은 nginx. 발행 사이트는 이미지에 안 넣는다 — 사이트가 늘 때마다 이미지를 다시 굽지 않으려고 site-out 볼륨에서 읽는다. - vite.config.ts: build.assetsDir='builder-assets'. 발행본과 같은 오리진이라 `/assets/` 를 서로 뺏는다 — 안 가르면 빌더 JS·CSS 가 404 인데 화면은 떠서 원인이 안 보인다. - site.conf: `/` 를 /srv/app 정적으로. index.html 은 no-cache — 번들 해시가 여기 박혀 있어 캐시되면 재배포해도 옛 번들 주소를 계속 부른다. `/fonts/` 는 발행본 먼저 보고 없으면 빌더로 떨어뜨린다(둘이 같은 경로를 각자 쓴다). - compose: solution-frontend(dev 서버)를 profile dev 로 내리고, 굽기만 하는 solution-prerender 를 기본으로 올린다. VITE_* 는 번들에 구워지므로 build args 다 — 주소를 바꾸면 재빌드해야 한다. docker compose config 통과
137 lines
6.7 KiB
Plaintext
137 lines
6.7 KiB
Plaintext
# 공개 진입점 하나. 사장님 앱 · 발행 사이트 · API 가 **같은 오리진**을 쓴다.
|
|
#
|
|
# / → 사장님 앱 (이미지에 구워 넣은 정적 번들, /srv/app)
|
|
# /s/<slug> → 발행 사이트 (site-out 볼륨에서 정적)
|
|
# /assets/ → 발행본 공용 번들 (정적)
|
|
# /robots.txt · /sitemap.xml → 크롤러가 읽는 파일 (정적)
|
|
# /v1/... /healthz → API(solution-backend:9800)
|
|
#
|
|
# ★ 오리진을 가르지 않는 이유: robots.txt·sitemap.xml 은 RFC 9309 상 **오리진 루트에서만**
|
|
# 읽힌다. 앱과 사이트를 다른 호스트에 두면 인증서도 DNS 도 두 벌이 되고 CORS 가 붙는다.
|
|
# 개발에서는 Vite 프록시가 같은 일을 한다(solution/frontend/vite.config.ts).
|
|
#
|
|
# ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더가 쓰고 여기서 읽기만 한다 —
|
|
# 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다.
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name _;
|
|
|
|
# ★ nginx 이미지의 기본 문서 루트(/usr/share/nginx/html)를 쓰지 않는다.
|
|
# named volume 을 거기 마운트하면 Docker 가 **이미지에 들어 있던 index.html 을
|
|
# 빈 볼륨으로 복사한다** — 그러면 오리진 루트가 "Welcome to nginx!" 를 띄우고,
|
|
# 그게 크롤러에 잡힌다. 빈 경로에 마운트하면 복사될 것이 없다.
|
|
root /srv/sites;
|
|
|
|
charset utf-8;
|
|
server_tokens off;
|
|
client_max_body_size 20m;
|
|
|
|
# ★ Docker 내장 DNS. 업스트림을 변수로 두면 nginx 가 **기동할 때** 이름을 풀지 않는다 —
|
|
# 안 그러면 solution-backend 가 아직 안 떴을 때 nginx 자체가 죽는다.
|
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
set $api http://solution-backend:9800;
|
|
|
|
# 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎).
|
|
gzip on;
|
|
gzip_comp_level 6;
|
|
gzip_min_length 1024;
|
|
gzip_vary on;
|
|
gzip_types
|
|
text/plain text/css text/xml
|
|
application/javascript application/json application/xml
|
|
image/svg+xml;
|
|
|
|
# ── 발행 사이트 ────────────────────────────────────────────
|
|
# ^~ 로 잡아 아래 정규식 location 들이 끼어들지 못하게 한다.
|
|
location ^~ /s/ {
|
|
# $uri/ 를 거치면 nginx 가 끝 슬래시로 301 을 내보낸다. 크롤러가 리다이렉트를
|
|
# 한 번 더 타야 하므로 index.html 을 바로 준다.
|
|
try_files $uri $uri/index.html =404;
|
|
add_header Cache-Control "public, max-age=300, must-revalidate";
|
|
}
|
|
|
|
# 파일명에 해시가 박혀 있다. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다.
|
|
location ^~ /assets/ {
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
access_log off;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# ★ 빌더와 발행본이 `/fonts/` 를 **각자** 쓴다(vite.config.ts 주석). 발행본을 먼저 보고
|
|
# 없으면 빌더 것으로 떨어뜨린다 — 한쪽만 잡으면 다른 쪽 폰트가 조용히 404 다.
|
|
location ^~ /fonts/ {
|
|
root /srv/sites;
|
|
add_header Cache-Control "public, max-age=604800";
|
|
access_log off;
|
|
try_files $uri @app_fonts;
|
|
}
|
|
location @app_fonts {
|
|
root /srv/app;
|
|
access_log off;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# ── 크롤러가 읽는 파일 ─────────────────────────────────────
|
|
# 발행하면 곧바로 반영되어야 한다. 길게 캐시하면 새 사업장이 사이트맵에 들어가도
|
|
# 크롤러가 옛 파일을 계속 본다.
|
|
location = /robots.txt {
|
|
add_header Cache-Control "public, max-age=300, must-revalidate";
|
|
try_files $uri =404;
|
|
}
|
|
|
|
location = /sitemap.xml {
|
|
add_header Cache-Control "public, max-age=300, must-revalidate";
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# ── API ────────────────────────────────────────────────────
|
|
# 앱과 같은 오리진이라 프리플라이트가 아예 발생하지 않는다.
|
|
location ~ ^/(v1/|healthz$|openapi\.json$|docs|redoc) {
|
|
proxy_pass $api;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
|
# 발행·수집 잡은 분 단위다. 기본 60s 면 게이트웨이가 먼저 끊는다.
|
|
proxy_read_timeout 300s;
|
|
proxy_send_timeout 300s;
|
|
}
|
|
|
|
# IndexNow 키 파일. 프리렌더가 루트에 <key>.txt 를 굽고 검색엔진이 대조한다.
|
|
# ★ `{8,128}` 같은 수량자는 못 쓴다 — nginx 는 `{`·`}` 를 블록 구분자로 먼저 읽는다.
|
|
location ~ ^/[A-Za-z0-9_-]+\.txt$ {
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# ── 사장님 앱 (그 외 전부) ─────────────────────────────────
|
|
# 해시가 박힌 번들. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다.
|
|
# ★ 발행본 `/assets/` 와 겹치지 않게 빌더만 `builder-assets` 로 뺐다
|
|
# (solution/frontend/vite.config.ts 의 build.assetsDir).
|
|
location ^~ /builder-assets/ {
|
|
root /srv/app;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
access_log off;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# SPA 다. 없는 경로는 index.html 로 넘겨 클라이언트 라우터가 받게 한다.
|
|
# ★ index.html 은 캐시하지 않는다 — 여기에 번들 해시가 박혀 있어서, 캐시되면
|
|
# 새로 배포해도 브라우저가 옛 번들 주소를 계속 부른다(404 → 흰 화면).
|
|
location / {
|
|
root /srv/app;
|
|
try_files $uri $uri/index.html /index.html;
|
|
add_header Cache-Control "no-cache";
|
|
}
|
|
|
|
# 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다.
|
|
error_page 404 /404.html;
|
|
location = /404.html {
|
|
internal;
|
|
return 404 '<!doctype html><html lang="ko"><meta charset="utf-8"><title>페이지를 찾을 수 없습니다</title><body style="font-family:system-ui;padding:3rem;text-align:center"><h1>페이지를 찾을 수 없습니다</h1><p>주소를 다시 확인해 주세요.</p></body></html>';
|
|
add_header Content-Type "text/html; charset=utf-8";
|
|
}
|
|
}
|