diff --git a/DESIGN.md b/DESIGN.md index cbd9914..2c8d131 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -930,6 +930,13 @@ API 暴露边界: - SEO metadata 只能使用公开业务数据和系统文案;不得暴露邮箱、权限 key、token/hash、内部审计 payload、调试信息或实现说明。 - 多语言 metadata 使用当前前端语言和系统文案回退机制;当前没有语言专属 URL,因此暂不输出 `hreflang`。 +## 部署与升级维护 + +- Docker 部署时公开前端端口由 `frontend_gateway` 承载,正常流量代理到 `frontend` 服务。 +- `frontend` 因 `docker compose up -d --build` 重建、启动中或临时不可达时,`frontend_gateway` 返回静态升级维护页并保持公开端口可访问;后端 `/health` 不可用时,前端网关也返回同一维护页,避免用户看到静态页面后遇到 API 不可用。 +- 升级维护页是基础设施级静态 fallback,不依赖 Vue、Vue I18n、后端 API 或数据库;页面只展示正式用户文案和品牌视觉,不展示构建日志、调试信息、内部字段或实现说明。 +- 升级维护页使用 `503`、`Retry-After: 300`、`Cache-Control: no-store` 和 `noindex`,提示用户 Pokopia Wiki 正在升级并将在约 5 分钟内恢复。 + ## API 概览 公开浏览 API: diff --git a/docker-compose.yml b/docker-compose.yml index 65a60a5..b51aeb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,11 +44,21 @@ services: VITE_SITE_URL: ${VITE_SITE_URL:-https://pokopiawiki.tootaio.com} environment: PORT: 20015 - ports: - - "20015:20015" + expose: + - "20015" depends_on: - backend + frontend_gateway: + image: nginx:1.29-alpine + ports: + - "20015:20015" + volumes: + - ./frontend/gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro + - ./frontend/gateway/maintenance.html:/usr/share/nginx/html/maintenance.html:ro + depends_on: + - frontend + volumes: postgres18_data: backend_uploads: diff --git a/frontend/gateway/maintenance.html b/frontend/gateway/maintenance.html new file mode 100644 index 0000000..1cd1433 --- /dev/null +++ b/frontend/gateway/maintenance.html @@ -0,0 +1,224 @@ + + + + + + + + Pokopia Wiki is upgrading + + + +
+
+ +
+
+ +
+ Pokopia + Wiki +
+
+ Upgrading +

Pokopia Wiki is upgrading

+

We'll be online within 5 minutes.

+ +
+
+
+ + diff --git a/frontend/gateway/nginx.conf b/frontend/gateway/nginx.conf new file mode 100644 index 0000000..9fc6165 --- /dev/null +++ b/frontend/gateway/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 20015; + server_name _; + + resolver 127.0.0.11 valid=5s ipv6=off; + + location / { + auth_request /backend-health; + error_page 500 502 503 504 =503 /maintenance.html; + + set $frontend_upstream http://frontend:20015; + + 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 $scheme; + + proxy_connect_timeout 1s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + proxy_intercept_errors on; + + proxy_pass $frontend_upstream; + } + + location = /backend-health { + internal; + set $backend_upstream http://backend:3001/health; + + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_connect_timeout 1s; + proxy_read_timeout 1s; + + proxy_pass $backend_upstream; + } + + location = /maintenance.html { + internal; + root /usr/share/nginx/html; + add_header Cache-Control "no-store" always; + add_header Retry-After "300" always; + } +}