[feat] deploy,solution/frontend: 공개 진입점을 한 오리진으로 통합 — nginx 가 앱·사이트·API 를 가른다
개발은 Vite 프록시가 `/s`·`/assets` 를 :3001 로 넘겨 한 오리진을 만드는데, 운영에는 그 몫을 받는 자리가 없었다. site.conf 가 정적 서빙 전용이라 `/`(빌더)와 `/v1`(API)이 전부 404 였다 — 실측(w4ai.o2o.kr): DNS·TLS·프록시는 정상인데 우리 404 페이지만 나왔다. - nginx/site.conf.example: `/`→solution-frontend:3000, `/v1|healthz|docs`→solution-backend:9800 프록시 추가. `/s/`·`/assets/`·`/fonts/` 는 `^~` 로 잡아 정규식 location 이 못 끼어들게 한다. 업스트림을 변수+resolver 로 둔 건 기동 시점 이름풀이를 피하려는 것 — 프론트가 아직 안 떴을 때 nginx 자체가 죽는다. IndexNow 키 파일(`<key>.txt`)도 루트에서 받는다. - vite.config.ts: allowedHosts 에 VITE_PUBLISH_HOST. Vite 6 는 모르는 Host 를 403 "Blocked request" 로 막는다 — 프록시는 정상인데 앱만 전부 403 이라 원인이 안 보인다. nginx -t 통과
This commit is contained in:
parent
45cc80b60a
commit
36b9e25b92
@ -1,11 +1,22 @@
|
||||
# 발행 사이트 정적 서빙.
|
||||
# 공개 진입점 하나. 사장님 앱 · 발행 사이트 · API 가 **같은 오리진**을 쓴다.
|
||||
#
|
||||
# ★ 산출물은 named volume(site-out)으로 들어온다. 프리렌더 컨테이너가 쓰고 여기서 읽기만 한다 —
|
||||
# / → 사장님 앱(solution-frontend:3000)
|
||||
# /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)으로 들어온다. 프리렌더가 쓰고 여기서 읽기만 한다 —
|
||||
# 호스트 경로가 등장하지 않으므로 재배포로 코드를 갈아엎어도 사이트가 죽지 않는다.
|
||||
#
|
||||
# ★ 규칙은 하나뿐이다: 디렉토리 요청 → index.html.
|
||||
# `/s/butter` (끝 슬래시 없음)가 사장님이 주소창에 치는 형태다. 이게 404 면
|
||||
# "발행했는데 안 나온다"가 된다.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@ -18,11 +29,15 @@ server {
|
||||
# 그게 크롤러에 잡힌다. 빈 경로에 마운트하면 복사될 것이 없다.
|
||||
root /srv/sites;
|
||||
|
||||
# index 지시를 끈다. 오리진 루트에는 페이지가 없다(사이트는 /s/<slug> 아래에 있다).
|
||||
index index.html;
|
||||
|
||||
charset utf-8;
|
||||
server_tokens off;
|
||||
client_max_body_size 20m;
|
||||
|
||||
# ★ Docker 내장 DNS. 업스트림을 변수로 두면 nginx 가 **기동할 때** 이름을 풀지 않는다 —
|
||||
# 안 그러면 solution-frontend 가 아직 안 떴을 때 nginx 자체가 죽는다.
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
set $builder http://solution-frontend:3000;
|
||||
set $api http://solution-backend:9800;
|
||||
|
||||
# 텍스트 산출물은 압축이 크게 먹는다(HTML 55KB → 10KB 안팎).
|
||||
gzip on;
|
||||
@ -34,17 +49,23 @@ server {
|
||||
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";
|
||||
}
|
||||
|
||||
# 파일명에 해시가 박혀 있다. 내용이 바뀌면 이름이 바뀌므로 영구 캐시가 안전하다.
|
||||
# ★ expires 와 add_header 를 함께 쓰면 Cache-Control 헤더가 두 줄로 나간다.
|
||||
# 합쳐서 읽히긴 하지만 의도가 흐려지므로 add_header 하나로 통일한다.
|
||||
location /assets/ {
|
||||
location ^~ /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /fonts/ {
|
||||
location ^~ /fonts/ {
|
||||
add_header Cache-Control "public, max-age=604800";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
@ -55,18 +76,47 @@ server {
|
||||
# 크롤러가 옛 파일을 계속 본다.
|
||||
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 를 굽고 검색엔진이 대조한다.
|
||||
location ~ ^/[A-Za-z0-9_-]{8,128}\.txt$ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# ── 사장님 앱 (그 외 전부) ─────────────────────────────────
|
||||
# ★ Vite dev 서버다. Host 헤더를 그대로 넘기므로 vite.config.ts 의 allowedHosts 에
|
||||
# 발행 호스트가 들어 있어야 한다 — 없으면 전부 403 "Blocked request" 다.
|
||||
location / {
|
||||
# $uri/ 를 거치면 nginx 가 끝 슬래시로 301 을 내보낸다. 크롤러가 리다이렉트를
|
||||
# 한 번 더 타야 하므로 index.html 을 바로 준다.
|
||||
try_files $uri $uri/index.html =404;
|
||||
add_header Cache-Control "public, max-age=300, must-revalidate";
|
||||
proxy_pass $builder;
|
||||
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;
|
||||
# HMR 웹소켓. 없으면 화면은 뜨는데 콘솔이 재연결 실패로 계속 시끄럽다.
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# 발행되지 않은 주소. 사장님이 오타를 냈을 때 흰 화면 대신 이유를 보여준다.
|
||||
|
||||
@ -19,6 +19,10 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// ★ Vite 6 는 모르는 Host 헤더를 403 "Blocked request" 로 막는다(DNS rebinding 방어).
|
||||
// 운영은 앞단 nginx 가 Host 를 그대로 넘기므로 발행 호스트를 여기 넣어야 화면이 뜬다.
|
||||
// 빠뜨리면 프록시는 정상인데 앱만 전부 403 이다.
|
||||
allowedHosts: [process.env.VITE_PUBLISH_HOST ?? 'w4ai.o2o.kr'],
|
||||
// 발행 사이트는 별도 정적 서버(:3001)가 만들고 서빙하지만, 사용자는 admin 과 같은
|
||||
// origin(:3000)의 `/s/<slug>` 로 접근한다. 프로세스 포트만 분리하고 공개 URL은 하나로 둔다.
|
||||
proxy: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user