From f2f856ea24b5df8fdc10cb49004f7c695c6f3a7b Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Fri, 27 Mar 2026 16:13:28 +0800 Subject: [PATCH] chore: productionize compose deployment --- README.md | 13 ++- docker-compose.yaml | 68 +---------- .../2026-03-27-production-compose-design.md | 27 +++++ ...03-27-production-compose-implementation.md | 110 ++++++++++++++++++ frontend/Dockerfile | 16 ++- nginx/Dockerfile | 14 +++ nginx/nginx.conf | 13 +-- 7 files changed, 182 insertions(+), 79 deletions(-) create mode 100644 docs/plans/2026-03-27-production-compose-design.md create mode 100644 docs/plans/2026-03-27-production-compose-implementation.md create mode 100644 nginx/Dockerfile diff --git a/README.md b/README.md index 98f3a51b..96642efb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # CTMS 项目快速上手 +## 生产部署 +- 生产入口:`docker-compose.yaml` +- 启动方式:`docker compose up -d --build` +- 对外入口:Nginx 提供前端静态资源,并反代后端 API +- 验证方式: + - `docker compose config` + - `curl -i http://127.0.0.1/` + - `curl -i http://127.0.0.1/health` + ## 账号与注册 - 初始化管理员:`admin@example.com / admin123`(已修正邮箱域名,后端启动时自动创建) - 演示账号:PM `pm@example.com / pm123456`,CRA `cra@example.com / cra123456`(仅用于体验,正式环境请修改密码或禁用) @@ -16,7 +25,7 @@ - 普通成员(无项目角色):仅浏览 ## 访问方式 -- 前端:`http://localhost`(Nginx 反代到 Vite 开发容器) +- 前端:`http://localhost`(Nginx 直接托管前端静态文件) - 后端 API:同域 `/api/v1/*`(已在前端代理) ## 常用流程 @@ -35,4 +44,4 @@ ## 注意 - 登录使用邮箱 + 密码,未审核/已拒绝/已停用账号无法登录。 - 令牌与当前项目上下文保存在浏览器 LocalStorage 中,清除后需重新登录/选择项目。 -- 若后端重启,确认数据库容器仍健康,前端会弹出错误提示。 +- 若后端重启,确认数据库容器仍健康,前端会弹出错误提示。 diff --git a/docker-compose.yaml b/docker-compose.yaml index ba85f58a..37825af8 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,9 +1,4 @@ services: - # ========================================= - # [主演] 业务服务 (日常开发一直开着) - # ========================================= - - # 1. 数据库 (PostgreSQL) db: image: postgres:15-alpine container_name: ctms_db @@ -27,18 +22,14 @@ services: networks: - ctms_net - # 2. 后端 (FastAPI) backend: - build: + build: context: ./backend dockerfile: Dockerfile container_name: ctms_backend - command: python -m debugpy --listen 0.0.0.0:5678 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --proxy-headers --forwarded-allow-ips '*' - volumes: - - ./backend:/code + restart: always ports: - "8000:8000" - - "5678:5678" environment: DATABASE_URL: postgresql+asyncpg://ctms_user:secret_password@db/ctms_db depends_on: @@ -47,56 +38,10 @@ services: networks: - ctms_net - # 3. 前端 (Vue3) - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: ctms_frontend - restart: always - # 开启 Host 模式以支持 Vite 热更新 - command: npm run dev -- --host - volumes: - - ./frontend:/app - # [关键] 让容器使用自己的 node_modules,而不是宿主机的 - - /app/node_modules - ports: - - "5173:5173" - environment: - VITE_API_BASE_URL: / - NODE_OPTIONS: --max-old-space-size=4096 - depends_on: - - backend - networks: - - ctms_net - - # ========================================= - # [剧务] 初始化工具 (只在需要时召唤) - # profiles: ["init"] 标记了它们平时不会启动 - # ========================================= - - # Node 工具箱:用于生成 Vue 代码、安装 npm 包 - frontend-init: - image: node:latest - profiles: ["init"] - working_dir: /app - volumes: - - ./frontend:/app - networks: - - ctms_net - - # Python 工具箱:用于生成 requirements.txt 等 - backend-init: - image: python:3.10-slim - profiles: ["init"] - working_dir: /code - volumes: - - ./backend:/code - networks: - - ctms_net - nginx: - image: nginx:latest + build: + context: . + dockerfile: nginx/Dockerfile container_name: ctms_nginx restart: always ports: @@ -104,16 +49,13 @@ services: - "443:443" # HTTPS - "8888:8888" volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./nginx/certs:/etc/nginx/certs:ro - ./frontend/public/favicon.ico:/usr/share/nginx/html/favicon.ico:ro depends_on: - backend - - frontend networks: - ctms_net - networks: ctms_net: driver: bridge diff --git a/docs/plans/2026-03-27-production-compose-design.md b/docs/plans/2026-03-27-production-compose-design.md new file mode 100644 index 00000000..46109937 --- /dev/null +++ b/docs/plans/2026-03-27-production-compose-design.md @@ -0,0 +1,27 @@ +# Production Compose Design + +**Goal:** Convert the current single-host `docker-compose` deployment from a development-oriented stack into a production-oriented stack while keeping the existing `docker-compose.yaml` as the only deployment entrypoint. + +**Decisions** +- Keep single-host `docker-compose` deployment. +- Convert the frontend to a static build served directly by Nginx. +- Keep PostgreSQL and the existing `pg_data` persistence model. +- Keep one backend container and remove development-only runtime behavior. +- Update the existing `docker-compose.yaml` in place instead of introducing a separate production file. + +**Architecture** +- `db` remains a long-lived PostgreSQL service with the existing bind-mounted data directory. +- `backend` runs from the backend image default command and exposes only the application port needed inside the compose network. +- `nginx` becomes the public entrypoint. It serves the built frontend assets and proxies `/api/` and `/health` to the backend service. +- The frontend is no longer a long-running service in production. Its build artifacts are produced during the image build and copied into the Nginx image. + +**Cleanup Scope** +- Remove development-only compose services such as toolbox/init containers. +- Remove development-only backend startup overrides such as `debugpy` and `uvicorn --reload`. +- Remove development-only frontend runtime behavior such as `npm run dev`, bind mounts, and exposed Vite ports. +- Keep data persistence and core service names stable where practical to limit operational change. + +**Operational Notes** +- Updating `docker-compose.yaml` in place means local development will no longer use the previous hot-reload setup by default. +- Frontend SPA routing must be handled by Nginx with `try_files`. +- Verification is done through `docker compose config`, image builds, container startup, and HTTP health checks. diff --git a/docs/plans/2026-03-27-production-compose-implementation.md b/docs/plans/2026-03-27-production-compose-implementation.md new file mode 100644 index 00000000..9a39add1 --- /dev/null +++ b/docs/plans/2026-03-27-production-compose-implementation.md @@ -0,0 +1,110 @@ +# Production Compose Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Convert the repository's only `docker-compose.yaml` from development deployment to production deployment using static frontend assets served by Nginx. + +**Architecture:** The backend runs as a production FastAPI container, the frontend is built into static assets during image build, and Nginx serves the SPA while reverse proxying backend routes. PostgreSQL remains persistent through the existing `pg_data` bind mount. + +**Tech Stack:** Docker Compose, FastAPI, Vue/Vite, Nginx, PostgreSQL + +--- + +### Task 1: Productionize Frontend Image + +**Files:** +- Modify: `frontend/Dockerfile` + +**Step 1: Replace dev runtime with a multi-stage production build** + +Update the Dockerfile so a Node build stage runs dependency install and `npm run build`, then a lightweight stage exposes the built `dist` directory for downstream use instead of running `npm run dev`. + +**Step 2: Keep the output suitable for Nginx consumption** + +Ensure the final stage stores frontend assets in a predictable directory such as `/usr/share/nginx/html`. + +**Step 3: Verify the image definition is syntactically valid** + +Run: `docker compose config` +Expected: compose renders successfully with no Dockerfile-related errors. + +### Task 2: Productionize Nginx Runtime + +**Files:** +- Modify: `nginx/nginx.conf` + +**Step 1: Add static frontend serving** + +Set the web root to the built frontend assets and add `try_files $uri $uri/ /index.html;` for SPA routing. + +**Step 2: Keep backend reverse proxy behavior** + +Preserve `/api/` and `/health` proxying to `backend:8000` with standard forwarded headers. + +**Step 3: Verify config structure through compose-backed startup** + +Run: `docker compose config` +Expected: compose renders successfully. + +### Task 3: Productionize Compose Topology + +**Files:** +- Modify: `docker-compose.yaml` + +**Step 1: Remove dev-only services and settings** + +Delete `frontend`, `frontend-init`, and `backend-init`, and remove bind mounts, debug ports, and development commands that are only useful for hot reload or scaffolding. + +**Step 2: Wire backend and nginx for production** + +Allow the backend to use its Dockerfile default command, keep the database dependency, and expose only the ports needed for the production stack. + +**Step 3: Build frontend assets into nginx** + +Point the Nginx service build to a production image definition that includes the built frontend output. + +**Step 4: Verify rendered compose** + +Run: `docker compose config` +Expected: only the production services remain and configuration renders cleanly. + +### Task 4: Add Deployment Verification + +**Files:** +- Modify: `README.md` + +**Step 1: Update deployment entry description** + +Document that `docker-compose.yaml` is now the production deployment entrypoint. + +**Step 2: Add minimal verification commands** + +Document `docker compose up -d --build`, plus health checks for `/` and `/health`. + +**Step 3: Verify behavior** + +Run: `docker compose up -d --build` +Expected: `db`, `backend`, and `nginx` start successfully. + +### Task 5: End-to-End Verification + +**Files:** +- Verify only + +**Step 1: Render final compose** + +Run: `docker compose config` +Expected: PASS + +**Step 2: Start stack** + +Run: `docker compose up -d --build` +Expected: PASS + +**Step 3: Check HTTP endpoints** + +Run: `curl -i http://127.0.0.1/` +Expected: `200 OK` and HTML payload + +Run: `curl -i http://127.0.0.1/health` +Expected: successful backend health response diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 742b04e5..b145d8c4 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,7 +1,15 @@ -FROM node:20-alpine +FROM node:20-alpine AS build + WORKDIR /app + COPY package*.json ./ -RUN npm install +RUN npm ci + COPY . . -EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host", "--port", "5173"] +RUN npm run build + +FROM alpine:3.21 + +WORKDIR /opt/frontend + +COPY --from=build /app/dist ./dist diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..d394f9fc --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,14 @@ +FROM node:20-alpine AS frontend-build + +WORKDIR /app + +COPY frontend/package*.json ./ +RUN npm ci + +COPY frontend/ ./ +RUN npm run build + +FROM nginx:1.27-alpine + +COPY nginx/nginx.conf /etc/nginx/nginx.conf +COPY --from=frontend-build /app/dist /usr/share/nginx/html diff --git a/nginx/nginx.conf b/nginx/nginx.conf index f8533c24..37ff0e79 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -20,11 +20,8 @@ http { sendfile on; keepalive_timeout 65; - - upstream frontend { - zone frontend 64k; - server frontend:5173 resolve; - } + root /usr/share/nginx/html; + index index.html; upstream backend { zone backend 64k; @@ -62,11 +59,7 @@ http { } location / { - proxy_pass http://frontend; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; + try_files $uri $uri/ /index.html; } } }