feat(deploy): 正式環境部署設定

- frontend/api.js: API URL 自動判斷 (localhost=dev, 其他=相對路徑 /api/v1)
- main.py: 加入 StaticFiles 掛載 admin-portal,CORS 開放
- schedule_tenant: Traefik 路由範本更新
  - 管理租戶加入 /api 路由 (priority 200)
  - /admin 加入 StripPrefix middleware
  - admin 服務改指向 vmis-backend:10281
- docker/vmis: 新增 Dockerfile + docker-compose.yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VMIS Developer
2026-03-15 18:11:31 +08:00
parent 62baadb06f
commit 8eb3909ca9
5 changed files with 74 additions and 6 deletions

View File

@@ -1,6 +1,8 @@
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from app.core.database import SessionLocal from app.core.database import SessionLocal
from app.services.seed import seed_initial_data from app.services.seed import seed_initial_data
@@ -31,7 +33,7 @@ app = FastAPI(
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["http://localhost:10280", "http://localhost:10290"], allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -43,3 +45,9 @@ app.include_router(api_router, prefix="/api/v1")
@app.get("/health") @app.get("/health")
def health(): def health():
return {"status": "ok", "service": "vmis-backend"} return {"status": "ok", "service": "vmis-backend"}
# 靜態前端admin portal— 掛載在最後,讓 API 路由優先
_frontend_dir = os.path.join(os.path.dirname(__file__), "..", "frontend")
if os.path.isdir(_frontend_dir):
app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")

View File

@@ -195,13 +195,33 @@ def _generate_tenant_route_yaml(tenant, is_active: bool) -> str:
domain = tenant.domain domain = tenant.domain
nc_url = f"http://nc-{code}:80" if is_active else f"http://nc-{code}-test:80" nc_url = f"http://nc-{code}:80" if is_active else f"http://nc-{code}-test:80"
lines = ["http:", " routers:"] lines = ["http:"]
if tenant.is_manager:
lines += [
" middlewares:",
" vmis-strip-admin:",
" stripPrefix:",
' prefixes: ["/admin"]',
"",
]
lines += [" routers:"]
if tenant.is_manager: if tenant.is_manager:
lines += [ lines += [
f" {code}-admin:", f" {code}-admin:",
f' rule: "Host(`{domain}`) && PathPrefix(`/admin`)"', f' rule: "Host(`{domain}`) && PathPrefix(`/admin`)"',
f" service: {code}-admin", f" service: {code}-vmis",
" entryPoints: [websecure]",
" middlewares: [vmis-strip-admin]",
" tls:",
" certResolver: letsencrypt",
" priority: 200",
"",
f" {code}-api:",
f' rule: "Host(`{domain}`) && PathPrefix(`/api`)"',
f" service: {code}-vmis",
" entryPoints: [websecure]", " entryPoints: [websecure]",
" tls:", " tls:",
" certResolver: letsencrypt", " certResolver: letsencrypt",
@@ -232,10 +252,10 @@ def _generate_tenant_route_yaml(tenant, is_active: bool) -> str:
if tenant.is_manager: if tenant.is_manager:
lines += [ lines += [
f" {code}-admin:", f" {code}-vmis:",
" loadBalancer:", " loadBalancer:",
" servers:", " servers:",
' - url: "http://10.1.0.245:10280"', ' - url: "http://vmis-backend:10281"',
] ]
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"

19
docker/vmis/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/app ./app
COPY backend/alembic ./alembic
COPY backend/alembic.ini .
COPY frontend/admin-portal ./frontend
EXPOSE 10281
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "10281"]

View File

@@ -0,0 +1,19 @@
services:
vmis-backend:
build:
context: ../..
dockerfile: docker/vmis/Dockerfile
image: vmis-backend:latest
container_name: vmis-backend
restart: unless-stopped
env_file: .env
volumes:
- /home/porsche/.ssh:/root/.ssh:ro
networks:
- traefik-network
labels:
- "traefik.enable=false"
networks:
traefik-network:
external: true

View File

@@ -1,6 +1,8 @@
/* VMIS Admin Portal - API utilities */ /* VMIS Admin Portal - API utilities */
const API = 'http://localhost:10281/api/v1'; const API = (location.hostname === 'localhost' || location.hostname === '127.0.0.1')
? 'http://localhost:10281/api/v1'
: '/api/v1';
/* ── Auth state ── */ /* ── Auth state ── */
let _sysSettings = null; let _sysSettings = null;