release(main): 集成 nginx 移除里程碑

This commit is contained in:
Cheng Zhou
2026-03-30 21:03:35 +08:00
parent 956c47218e
commit 5677f6823a
22 changed files with 287 additions and 160 deletions
+12 -8
View File
@@ -4,20 +4,21 @@
- 生产入口:`docker-compose.yaml` - 生产入口:`docker-compose.yaml`
- 初始化方式:`docker compose run --rm backend-init` - 初始化方式:`docker compose run --rm backend-init`
- 启动方式:`docker compose up -d --build` - 启动方式:`docker compose up -d --build`
- 对外入口:Nginx 提供前端静态资源,并反代后端 API - 运行拓扑:`frontend``backend``db`
- 对外入口:仓库外部统一反向代理;建议将 `/` 转发到 `frontend`,将 `/api``/health` 转发到 `backend`
- 数据库 schema 来源:Alembic migration,不再依赖 `database/init.sql` - 数据库 schema 来源:Alembic migration,不再依赖 `database/init.sql`
- 默认无任何 demo 数据;生产初始化只确保固定管理员 `admin@huapont.cn / admin123` 存在 - 默认无任何 demo 数据;生产初始化只确保固定管理员 `admin@huapont.cn / admin123` 存在
- 验证方式: - 验证方式:
- `docker compose config` - `docker compose config`
- `curl -i http://127.0.0.1/` - `curl -i http://127.0.0.1:4173/`
- `curl -i http://127.0.0.1/health` - `curl -i http://127.0.0.1:8000/health`
## 腾讯云私有镜像仓库 ## 腾讯云私有镜像仓库
- GitHub Actions 会在推送到 `dev``release``release/*``main` 时,通过 SSH 连接腾讯云服务器,在服务器本机完成 `backend``nginx` 镜像的构建与推送。 - GitHub Actions 会在推送到 `dev``release``release/*``main` 时,通过 SSH 连接腾讯云服务器,在服务器本机完成 `backend``frontend` 镜像的构建与推送。
- 私有仓库地址默认使用 `${REGISTRY_HOST}`,仓库命名为 `${REGISTRY_HOST}/ctms/ctms-backend``${REGISTRY_HOST}/ctms/ctms-nginx` - 私有仓库地址默认使用 `${REGISTRY_HOST}`,仓库命名为 `${REGISTRY_HOST}/ctms/ctms-backend``${REGISTRY_HOST}/ctms/ctms-frontend`
- `dev` 分支发布 `dev-latest``dev-<short_sha>``release` 分支发布 `rc-release``rc-<short_sha>``release/*` 分支发布 `rc-<release-branch>``rc-<short_sha>``main` 分支发布 `latest``sha-<short_sha>` - `dev` 分支发布 `dev-latest``dev-<short_sha>``release` 分支发布 `rc-release``rc-<short_sha>``release/*` 分支发布 `rc-<release-branch>``rc-<short_sha>``main` 分支发布 `latest``sha-<short_sha>`
- 部署机需要先 `docker login <IP>:5000`,并使用私有仓库的 `htpasswd` 账号密码完成认证。 - 部署机需要先 `docker login <IP>:5000`,并使用私有仓库的 `htpasswd` 账号密码完成认证。
- `backend-init` 复用 `BACKEND_IMAGE`,所以生产机可在设置 `REGISTRY_HOST`,或直接指定 `BACKEND_IMAGE``NGINX_IMAGE` 后执行 `docker compose pull && docker compose run --rm backend-init && docker compose up -d` - `backend-init` 复用 `BACKEND_IMAGE`,所以生产机可在设置 `REGISTRY_HOST`,或直接指定 `BACKEND_IMAGE``FRONTEND_IMAGE` 后执行 `docker compose pull && docker compose run --rm backend-init && docker compose up -d`
- 本地开发仍可继续使用 `docker compose up -d --build` 走本地构建。 - 本地开发仍可继续使用 `docker compose up -d --build` 走本地构建。
## 账号与注册 ## 账号与注册
@@ -35,8 +36,11 @@
- 普通成员(无项目角色):仅浏览 - 普通成员(无项目角色):仅浏览
## 访问方式 ## 访问方式
- 前端:`http://localhost`Nginx 直接托管前端静态文件) - 前端:`http://localhost:4173`
- 后端 API同域 `/api/v1/*`(已在前端代理) - 后端 API`http://localhost:8000/api/v1/*`
- 生产环境建议通过外部反向代理提供同域访问,前端代码默认请求同域 `/api/v1/*`
- 如果本地直接访问 `http://localhost:4173`,可在 `frontend/.env` 中设置 `VITE_API_BASE_URL=http://localhost:8000`,让前端直连后端 API
- `docker compose` 本地构建前端容器时,默认会把 `VITE_API_BASE_URL` 注入为 `http://localhost:8000`;如果是生产构建,请显式覆盖为空值或你的正式 API 地址
## 仓库治理文档 ## 仓库治理文档
- 分支治理规范:`docs/branch-governance.md` - 分支治理规范:`docs/branch-governance.md`
+15
View File
@@ -166,6 +166,21 @@ def create_app() -> FastAPI:
register_exception_handlers(app) register_exception_handlers(app)
@app.get(
"/",
tags=["health"],
summary="服务入口说明",
description="返回后端服务入口说明,避免将根路径误认为前端页面。",
)
async def root() -> dict[str, str]:
return {
"service": "ctms-backend",
"status": "ok",
"health": "/health",
"docs": "/docs",
"api": "/api/v1",
}
@app.get( @app.get(
"/health", "/health",
tags=["health"], tags=["health"],
+14
View File
@@ -62,6 +62,20 @@ async def test_register_creates_pending_user(client_and_db):
assert user.status == UserStatus.PENDING assert user.status == UserStatus.PENDING
@pytest.mark.asyncio
async def test_root_returns_service_metadata(client_and_db):
client, _ = client_and_db
resp = await client.get("/")
assert resp.status_code == 200
assert resp.json() == {
"service": "ctms-backend",
"status": "ok",
"health": "/health",
"docs": "/docs",
"api": "/api/v1",
}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_login_blocked_before_approval(client_and_db): async def test_login_blocked_before_approval(client_and_db):
client, SessionLocal = client_and_db client, SessionLocal = client_and_db
+8 -13
View File
@@ -52,22 +52,17 @@ services:
networks: networks:
- ctms_net - ctms_net
nginx: frontend:
image: ${NGINX_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-nginx:latest} image: ${FRONTEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-frontend:latest}
build: build:
context: . context: ./frontend
dockerfile: nginx/Dockerfile dockerfile: Dockerfile
container_name: ctms_nginx args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:8000}
container_name: ctms_frontend
restart: always restart: always
ports: ports:
- "80:80" # HTTP - "4173:4173"
- "443:443" # HTTPS
- "8888:8888"
volumes:
- ./nginx/certs:/etc/nginx/certs:ro
- ./frontend/public/favicon.ico:/usr/share/nginx/html/favicon.ico:ro
depends_on:
- backend
networks: networks:
- ctms_net - ctms_net
+4 -4
View File
@@ -10,10 +10,10 @@
- [ ] `cd frontend && npm run build` - [ ] `cd frontend && npm run build`
## 1.1 腾讯云私有镜像校验 ## 1.1 腾讯云私有镜像校验
- [ ] `dev` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``dev-latest` / `dev-<short_sha>` 标签已出现在私有仓库 - [ ] `dev` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``dev-latest` / `dev-<short_sha>` 标签已出现在私有仓库
- [ ] `release` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``rc-release` / `rc-<short_sha>` 标签已出现在私有仓库 - [ ] `release` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``rc-release` / `rc-<short_sha>` 标签已出现在私有仓库
- [ ] `release/*` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``rc-<release-branch>` / `rc-<short_sha>` 标签已出现在私有仓库 - [ ] `release/*` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``rc-<release-branch>` / `rc-<short_sha>` 标签已出现在私有仓库
- [ ] `main` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``latest` / `sha-<short_sha>` 标签已出现在私有仓库 - [ ] `main` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``latest` / `sha-<short_sha>` 标签已出现在私有仓库
- [ ] GitHub Actions 已成功 SSH 登录腾讯云服务器并执行远程构建脚本 - [ ] GitHub Actions 已成功 SSH 登录腾讯云服务器并执行远程构建脚本
- [ ] 部署机已执行 `docker login <IP>:5000`,且使用的是私有仓库 `htpasswd` 账号密码 - [ ] 部署机已执行 `docker login <IP>:5000`,且使用的是私有仓库 `htpasswd` 账号密码
@@ -4,24 +4,25 @@
**Decisions** **Decisions**
- Keep single-host `docker-compose` deployment. - Keep single-host `docker-compose` deployment.
- Convert the frontend to a static build served directly by Nginx. - Keep the frontend as an independently deployable runtime image.
- Keep PostgreSQL and the existing `pg_data` persistence model. - Keep PostgreSQL and the existing `pg_data` persistence model.
- Keep one backend container and remove development-only runtime behavior. - 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. - Update the existing `docker-compose.yaml` in place instead of introducing a separate production file.
- Leave public ingress to an external reverse proxy outside this repository.
**Architecture** **Architecture**
- `db` remains a long-lived PostgreSQL service with the existing bind-mounted data directory. - `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. - `backend` runs from the backend image default command and exposes port `8000`.
- `nginx` becomes the public entrypoint. It serves the built frontend assets and proxies `/api/` and `/health` to the backend service. - `frontend` runs from its own image and serves the SPA on a dedicated frontend port.
- 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. - An external reverse proxy becomes the public entrypoint and routes browser traffic to `frontend` and API traffic to `backend`.
**Cleanup Scope** **Cleanup Scope**
- Remove development-only compose services such as toolbox/init containers. - 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 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. - Remove development-only frontend runtime behavior such as bind mounts and hot-reload-only startup behavior.
- Keep data persistence and core service names stable where practical to limit operational change. - Keep data persistence and core service names stable where practical to limit operational change.
**Operational Notes** **Operational Notes**
- Updating `docker-compose.yaml` in place means local development will no longer use the previous hot-reload setup by default. - 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`. - Frontend SPA routing and same-origin `/api` behavior must be handled by the external reverse proxy.
- Verification is done through `docker compose config`, image builds, container startup, and HTTP health checks. - Verification is done through `docker compose config`, image builds, container startup, and direct HTTP health checks.
@@ -2,11 +2,11 @@
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. > **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. **Goal:** Convert the repository's only `docker-compose.yaml` from development deployment to production deployment using standalone frontend and backend runtime images.
**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. **Architecture:** The backend runs as a production FastAPI container, the frontend is built into a standalone runtime image that serves the SPA itself, and an external reverse proxy routes browser and API traffic to the correct container. PostgreSQL remains persistent through the existing `pg_data` bind mount.
**Tech Stack:** Docker Compose, FastAPI, Vue/Vite, Nginx, PostgreSQL **Tech Stack:** Docker Compose, FastAPI, Vue/Vite, Node.js, PostgreSQL
--- ---
@@ -19,27 +19,27 @@
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`. 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** **Step 2: Keep the output suitable for frontend runtime consumption**
Ensure the final stage stores frontend assets in a predictable directory such as `/usr/share/nginx/html`. Ensure the final stage stores frontend assets in a predictable directory and starts a static server on a stable port.
**Step 3: Verify the image definition is syntactically valid** **Step 3: Verify the image definition is syntactically valid**
Run: `docker compose config` Run: `docker compose config`
Expected: compose renders successfully with no Dockerfile-related errors. Expected: compose renders successfully with no Dockerfile-related errors.
### Task 2: Productionize Nginx Runtime ### Task 2: Productionize Frontend Runtime
**Files:** **Files:**
- Modify: `nginx/nginx.conf` - Modify: `frontend/Dockerfile`
**Step 1: Add static frontend serving** **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. Use the runtime image to expose a frontend port and serve the compiled SPA directly.
**Step 2: Keep backend reverse proxy behavior** **Step 2: Leave reverse proxy behavior to external infrastructure**
Preserve `/api/` and `/health` proxying to `backend:8000` with standard forwarded headers. Do not embed repository-owned reverse proxy configuration; document that `/` routes to `frontend` and `/api` plus `/health` route to `backend`.
**Step 3: Verify config structure through compose-backed startup** **Step 3: Verify config structure through compose-backed startup**
@@ -55,13 +55,13 @@ Expected: compose renders successfully.
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. 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** **Step 2: Wire backend and frontend 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. 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** **Step 3: Add standalone frontend service**
Point the Nginx service build to a production image definition that includes the built frontend output. Point the frontend service build to `frontend/Dockerfile` and give it a registry-backed image reference.
**Step 4: Verify rendered compose** **Step 4: Verify rendered compose**
@@ -79,12 +79,12 @@ Document that `docker-compose.yaml` is now the production deployment entrypoint.
**Step 2: Add minimal verification commands** **Step 2: Add minimal verification commands**
Document `docker compose up -d --build`, plus health checks for `/` and `/health`. Document `docker compose up -d --build`, plus direct health checks for the frontend and backend containers.
**Step 3: Verify behavior** **Step 3: Verify behavior**
Run: `docker compose up -d --build` Run: `docker compose up -d --build`
Expected: `db`, `backend`, and `nginx` start successfully. Expected: `db`, `backend`, and `frontend` start successfully.
### Task 5: End-to-End Verification ### Task 5: End-to-End Verification
@@ -103,8 +103,8 @@ Expected: PASS
**Step 3: Check HTTP endpoints** **Step 3: Check HTTP endpoints**
Run: `curl -i http://127.0.0.1/` Run: `curl -i http://127.0.0.1:4173/`
Expected: `200 OK` and HTML payload Expected: `200 OK` and HTML payload
Run: `curl -i http://127.0.0.1/health` Run: `curl -i http://127.0.0.1:8000/health`
Expected: successful backend health response Expected: successful backend health response
@@ -25,7 +25,7 @@
**Operational Flow** **Operational Flow**
1. Start PostgreSQL with an empty data directory. 1. Start PostgreSQL with an empty data directory.
2. Run the one-shot backend initialization command. 2. Run the one-shot backend initialization command.
3. Start long-lived backend and Nginx services. 3. Start long-lived backend and frontend services behind the external reverse proxy.
4. Log in with `admin@huapont.cn / admin123`, then change the password. 4. Log in with `admin@huapont.cn / admin123`, then change the password.
**Non-Goals** **Non-Goals**
@@ -163,7 +163,7 @@ Expected: migrations run successfully and the protected admin is ensured.
**Step 4: Validate service health** **Step 4: Validate service health**
Run: `docker compose up -d` Run: `docker compose up -d`
Expected: `db`, `backend`, and `nginx` start successfully. Expected: `db`, `backend`, and `frontend` start successfully.
Run: `curl -i http://127.0.0.1/health` Run: `curl -i http://127.0.0.1:8000/health`
Expected: `200 OK` Expected: `200 OK`
@@ -0,0 +1,21 @@
# Remove In-Repo Nginx Design
**Goal:** Remove all in-repository Nginx runtime, build, publish, and documentation responsibilities while keeping the frontend as an independently deployable image behind an external reverse proxy.
**Decisions**
- Remove the `nginx/` directory and every repository-owned Nginx configuration reference.
- Keep `docker-compose.yaml` as the deployment entrypoint, but change the production topology to `frontend + backend + db`.
- Keep `frontend` as a standalone runtime image that serves the built SPA directly.
- Keep `backend` as the only API container and let the external reverse proxy route `/api` and `/health` to it.
- Update historical docs so they describe the current topology instead of preserving obsolete Nginx-based guidance.
**Architecture**
- `db` remains the persistent PostgreSQL service.
- `backend` remains the FastAPI service exposed on port `8000`.
- `frontend` becomes a standalone container image built from `frontend/Dockerfile` and serves the compiled SPA on its own port.
- The external reverse proxy is now the only public entrypoint and is responsible for routing browser traffic to `frontend` and API traffic to `backend`.
**Operational Notes**
- The frontend still assumes same-origin API access, so the external proxy must route `/api/*` and `/health` to `backend`.
- The repository publish flow should only build and push `ctms-backend` and `ctms-frontend`.
- Verification should prove that no runtime, CI, or document references to repository-managed Nginx remain.
@@ -0,0 +1,114 @@
# Remove In-Repo Nginx Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Remove repository-managed Nginx and replace it with a standalone frontend image plus backend image that are intended to sit behind an external reverse proxy.
**Architecture:** The compose stack runs `db`, `backend`, and `frontend`. The frontend image builds the Vite SPA and serves it directly from its own container, while the backend continues serving the API on port `8000`. External infrastructure performs the only reverse proxy routing.
**Tech Stack:** Docker Compose, FastAPI, Vue/Vite, Node.js, PostgreSQL, GitHub Actions
---
### Task 1: Define the frontend runtime image
**Files:**
- Modify: `frontend/Dockerfile`
**Step 1: Keep the multi-stage frontend build**
Retain the existing build stage so `npm ci` and `npm run build` still produce `dist`.
**Step 2: Turn the final image into a runnable frontend container**
Use a lightweight runtime image that copies `dist`, exposes a stable frontend port, and starts a static file server suitable for SPA delivery.
**Step 3: Verify Dockerfile shape**
Run: `sed -n '1,200p' frontend/Dockerfile`
Expected: final stage exposes a frontend port and contains a runnable `CMD`.
### Task 2: Replace nginx topology with frontend topology
**Files:**
- Modify: `docker-compose.yaml`
**Step 1: Remove the `nginx` service**
Delete the Nginx image, build, ports, and volume mounts.
**Step 2: Add a `frontend` service**
Build from `frontend/Dockerfile`, define a private-registry-backed `FRONTEND_IMAGE` default, and expose the frontend runtime port.
**Step 3: Verify rendered compose**
Run: `docker compose config`
Expected: only `db`, `backend`, `backend-init`, and `frontend` render.
### Task 3: Update image publishing
**Files:**
- Modify: `scripts/build-and-push-registry.sh`
- Modify: `.github/workflows/publish-images.yml`
**Step 1: Publish `frontend` instead of `nginx`**
Build and push `${REGISTRY_HOST}/ctms/ctms-frontend` alongside `${REGISTRY_HOST}/ctms/ctms-backend`.
**Step 2: Keep branch-based tag logic unchanged**
Preserve the existing `dev`, `release`, `release/*`, and `main` tag conventions.
**Step 3: Verify script syntax**
Run: `bash -n scripts/build-and-push-registry.sh`
Expected: PASS
### Task 4: Rewrite docs to match the new topology
**Files:**
- Modify: `README.md`
- Modify: `docs/guides/release-checklist.md`
- Modify: `docs/plans/2026-03-27-production-compose-design.md`
- Modify: `docs/plans/2026-03-27-production-compose-implementation.md`
- Modify: `docs/plans/2026-03-27-production-init-design.md`
- Modify: `docs/plans/2026-03-27-production-init-implementation.md`
- Modify: `docs/plans/2026-03-30-tcloud-private-registry-design.md`
- Modify: `docs/plans/2026-03-30-tcloud-private-registry.md`
**Step 1: Remove repository-managed Nginx language**
Replace every statement that says Nginx is the public entrypoint, serves the SPA, or is a published runtime image.
**Step 2: Document the external reverse proxy expectation**
Describe the runtime as frontend and backend containers behind an external proxy.
**Step 3: Verify search results**
Run: `rg -n --hidden -g '!/.git' -S "nginx|ctms-nginx|NGINX_IMAGE|Nginx" README.md docker-compose.yaml .github scripts docs`
Expected: no active repository-managed Nginx references remain.
### Task 5: Remove obsolete nginx files and verify end-to-end cleanup
**Files:**
- Delete: `nginx/Dockerfile`
- Delete: `nginx/nginx.conf`
**Step 1: Delete the obsolete files**
Remove the Nginx directory because nothing in the repository should depend on it anymore.
**Step 2: Validate compose and script syntax**
Run: `docker compose config`
Expected: PASS
Run: `bash -n scripts/build-and-push-registry.sh`
Expected: PASS
**Step 3: Validate frontend and backend image references**
Run: `sed -n '1,160p' README.md`
Expected: deployment instructions mention `frontend` and `backend`, not `nginx`.
@@ -1,9 +1,9 @@
# Tencent Cloud Private Registry Design # Tencent Cloud Private Registry Design
**Goal:** Add CI that automatically builds and pushes private `backend` and `nginx` images to a self-hosted Docker Registry running on a Tencent Cloud CVM, with `dev` publishing test tags, `release` and `release/*` publishing release-candidate tags, and `main` publishing formal tags. **Goal:** Add CI that automatically builds and pushes private `backend` and `frontend` images to a self-hosted Docker Registry running on a Tencent Cloud CVM, with `dev` publishing test tags, `release` and `release/*` publishing release-candidate tags, and `main` publishing formal tags.
**Decisions** **Decisions**
- Publish only two runtime images: `<registry-host>/ctms/ctms-backend` and `<registry-host>/ctms/ctms-nginx`. - Publish only two runtime images: `<registry-host>/ctms/ctms-backend` and `<registry-host>/ctms/ctms-frontend`.
- Keep the private registry on the Tencent Cloud CVM as a self-hosted `registry:2` instance protected by `htpasswd`. - Keep the private registry on the Tencent Cloud CVM as a self-hosted `registry:2` instance protected by `htpasswd`.
- Trigger automation on pushes to `dev`, `release`, `release/*`, and `main`. - Trigger automation on pushes to `dev`, `release`, `release/*`, and `main`.
- Use GitHub Actions only as the orchestrator. Actual Docker build and push happen on the Tencent Cloud server over SSH. - Use GitHub Actions only as the orchestrator. Actual Docker build and push happen on the Tencent Cloud server over SSH.
@@ -18,34 +18,33 @@
- A new GitHub Actions workflow triggers on pushes to `dev`, `release`, `release/*`, and `main`. - A new GitHub Actions workflow triggers on pushes to `dev`, `release`, `release/*`, and `main`.
- The workflow authenticates to the Tencent Cloud server using SSH credentials stored in GitHub Secrets. - The workflow authenticates to the Tencent Cloud server using SSH credentials stored in GitHub Secrets.
- The workflow syncs the repository contents to a fixed build directory such as `/opt/ctms-build/<repo>`. - The workflow syncs the repository contents to a fixed build directory such as `/opt/ctms-build/<repo>`.
- A server-local script logs in to the private registry, builds `backend` and `nginx`, applies branch-specific tags, and pushes the images to the registry. - A server-local script logs in to the private registry, builds `backend` and `frontend`, applies branch-specific tags, and pushes the images to the registry.
**Registry Naming** **Registry Naming**
- Backend image: `<registry-host>/ctms/ctms-backend` - Backend image: `<registry-host>/ctms/ctms-backend`
- Nginx image: `<registry-host>/ctms/ctms-nginx` - Frontend image: `<registry-host>/ctms/ctms-frontend`
- `frontend` is intentionally not published because the current runtime topology already builds the frontend assets into the `nginx` image.
**Tagging Strategy** **Tagging Strategy**
- `dev` branch pushes publish: - `dev` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:dev-latest` - `<registry-host>/ctms/ctms-backend:dev-latest`
- `<registry-host>/ctms/ctms-backend:dev-<short_sha>` - `<registry-host>/ctms/ctms-backend:dev-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:dev-latest` - `<registry-host>/ctms/ctms-frontend:dev-latest`
- `<registry-host>/ctms/ctms-nginx:dev-<short_sha>` - `<registry-host>/ctms/ctms-frontend:dev-<short_sha>`
- `release` branch pushes publish: - `release` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:rc-release` - `<registry-host>/ctms/ctms-backend:rc-release`
- `<registry-host>/ctms/ctms-backend:rc-<short_sha>` - `<registry-host>/ctms/ctms-backend:rc-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:rc-release` - `<registry-host>/ctms/ctms-frontend:rc-release`
- `<registry-host>/ctms/ctms-nginx:rc-<short_sha>` - `<registry-host>/ctms/ctms-frontend:rc-<short_sha>`
- `release/*` branch pushes publish: - `release/*` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:rc-<release-branch>` - `<registry-host>/ctms/ctms-backend:rc-<release-branch>`
- `<registry-host>/ctms/ctms-backend:rc-<short_sha>` - `<registry-host>/ctms/ctms-backend:rc-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:rc-<release-branch>` - `<registry-host>/ctms/ctms-frontend:rc-<release-branch>`
- `<registry-host>/ctms/ctms-nginx:rc-<short_sha>` - `<registry-host>/ctms/ctms-frontend:rc-<short_sha>`
- `main` branch pushes publish: - `main` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:latest` - `<registry-host>/ctms/ctms-backend:latest`
- `<registry-host>/ctms/ctms-backend:sha-<short_sha>` - `<registry-host>/ctms/ctms-backend:sha-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:latest` - `<registry-host>/ctms/ctms-frontend:latest`
- `<registry-host>/ctms/ctms-nginx:sha-<short_sha>` - `<registry-host>/ctms/ctms-frontend:sha-<short_sha>`
**Server Requirements** **Server Requirements**
- Docker installed on the Tencent Cloud server. - Docker installed on the Tencent Cloud server.
@@ -64,7 +63,7 @@
- `REGISTRY_PASSWORD` - `REGISTRY_PASSWORD`
**Compose Integration** **Compose Integration**
- `docker-compose.yaml` should define `image:` for `backend`, `backend-init`, and `nginx`, with defaults based on private registry variables such as `REGISTRY_HOST`. - `docker-compose.yaml` should define `image:` for `backend`, `backend-init`, and `frontend`, with defaults based on private registry variables such as `REGISTRY_HOST`.
- Existing `build:` blocks stay in place so local `docker compose up -d --build` continues to work. - Existing `build:` blocks stay in place so local `docker compose up -d --build` continues to work.
- Deployment hosts can export `REGISTRY_HOST`, then run `docker login`, `docker compose pull`, `docker compose run --rm backend-init`, and `docker compose up -d`. - Deployment hosts can export `REGISTRY_HOST`, then run `docker login`, `docker compose pull`, `docker compose run --rm backend-init`, and `docker compose up -d`.
@@ -2,11 +2,11 @@
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add CI that triggers remote builds on a Tencent Cloud CVM and pushes private `backend` and `nginx` images into a self-hosted Docker Registry protected by `htpasswd`, for `dev`, `release`, `release/*`, and `main`. **Goal:** Add CI that triggers remote builds on a Tencent Cloud CVM and pushes private `backend` and `frontend` images into a self-hosted Docker Registry protected by `htpasswd`, for `dev`, `release`, `release/*`, and `main`.
**Architecture:** GitHub Actions runs on pushes to `dev`, `release`, `release/*`, and `main`, connects to the Tencent Cloud server over SSH, syncs the repository into a fixed build directory, and invokes a repository-owned shell script that logs in to the private registry, builds the images locally on the server, tags them according to branch, and pushes them. **Architecture:** GitHub Actions runs on pushes to `dev`, `release`, `release/*`, and `main`, connects to the Tencent Cloud server over SSH, syncs the repository into a fixed build directory, and invokes a repository-owned shell script that logs in to the private registry, builds the images locally on the server, tags them according to branch, and pushes them.
**Tech Stack:** GitHub Actions, SSH, Docker, self-hosted `registry:2`, Docker Compose, FastAPI, Nginx **Tech Stack:** GitHub Actions, SSH, Docker, self-hosted `registry:2`, Docker Compose, FastAPI, Node.js
--- ---
@@ -65,7 +65,7 @@ Fail fast for unsupported branches.
Log in to `${REGISTRY_HOST}` using the supplied username and password, then build: Log in to `${REGISTRY_HOST}` using the supplied username and password, then build:
- `${REGISTRY_HOST}/ctms/ctms-backend` - `${REGISTRY_HOST}/ctms/ctms-backend`
- `${REGISTRY_HOST}/ctms/ctms-nginx` - `${REGISTRY_HOST}/ctms/ctms-frontend`
**Step 3: Push both tags for both images** **Step 3: Push both tags for both images**
@@ -86,7 +86,7 @@ Expected: PASS
Set: Set:
- `backend.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}` - `backend.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}`
- `backend-init.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}` - `backend-init.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}`
- `nginx.image` to `${NGINX_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-nginx:latest}` - `frontend.image` to `${FRONTEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-frontend:latest}`
Keep the current `build:` blocks intact. Keep the current `build:` blocks intact.
+1
View File
@@ -1,4 +1,5 @@
# Frontend feature flags and timeline overrides # Frontend feature flags and timeline overrides
VITE_API_BASE_URL=
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3 VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
VITE_STARTUP_ACCEPT_TIMEOUT_MONTHS=3 VITE_STARTUP_ACCEPT_TIMEOUT_MONTHS=3
VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6 VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6
+5
View File
@@ -0,0 +1,5 @@
:4173
root * /srv
try_files {path} /index.html
file_server
+7 -3
View File
@@ -2,14 +2,18 @@ FROM node:20-alpine AS build
WORKDIR /app WORKDIR /app
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM alpine:3.21 FROM caddy:2-alpine
WORKDIR /opt/frontend COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=build /app/dist /srv
COPY --from=build /app/dist ./dist EXPOSE 4173
+4 -1
View File
@@ -7,9 +7,12 @@ import { useStudyStore } from "../store/study";
import { TEXT } from "../locales"; import { TEXT } from "../locales";
import { extendAccessToken, forceLogout, lockSession, markNetworkActive } from "../session/sessionManager"; import { extendAccessToken, forceLogout, lockSession, markNetworkActive } from "../session/sessionManager";
import { useSessionStore } from "../store/session"; import { useSessionStore } from "../store/session";
import { resolveApiBaseUrl } from "./baseUrl";
const apiBaseUrl = resolveApiBaseUrl(import.meta.env.VITE_API_BASE_URL);
const instance: AxiosInstance = axios.create({ const instance: AxiosInstance = axios.create({
baseURL: "/", baseURL: apiBaseUrl,
timeout: 15000, timeout: 15000,
}); });
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { resolveApiBaseUrl } from "./baseUrl";
describe("resolveApiBaseUrl", () => {
it("defaults to same-origin root when no env override is set", () => {
expect(resolveApiBaseUrl(undefined)).toBe("/");
expect(resolveApiBaseUrl("")).toBe("/");
expect(resolveApiBaseUrl(" ")).toBe("/");
});
it("preserves explicit root", () => {
expect(resolveApiBaseUrl("/")).toBe("/");
});
it("trims whitespace and trailing slashes from absolute base urls", () => {
expect(resolveApiBaseUrl(" http://localhost:8000/ ")).toBe("http://localhost:8000");
expect(resolveApiBaseUrl("https://api.example.com///")).toBe("https://api.example.com");
});
});
+12
View File
@@ -0,0 +1,12 @@
const trimTrailingSlashes = (value: string): string => value.replace(/\/+$/, "");
export const resolveApiBaseUrl = (rawValue: string | undefined): string => {
const normalized = rawValue?.trim() ?? "";
if (!normalized) {
return "/";
}
if (normalized === "/") {
return "/";
}
return trimTrailingSlashes(normalized);
};
-14
View File
@@ -1,14 +0,0 @@
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
-65
View File
@@ -1,65 +0,0 @@
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
resolver 127.0.0.11 valid=10s ipv6=off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
root /usr/share/nginx/html;
index index.html;
upstream backend {
zone backend 64k;
server backend:8000 resolve;
}
server {
listen 80;
server_name _;
client_max_body_size 20m;
location = /favicon.ico {
root /usr/share/nginx/html;
try_files /favicon.ico =204;
access_log off;
log_not_found off;
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
}
location /api/ {
proxy_pass http://backend;
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;
}
location /health {
proxy_pass http://backend;
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;
}
location / {
try_files $uri $uri/ /index.html;
}
}
}
+6 -7
View File
@@ -45,7 +45,7 @@ case "${BRANCH_NAME}" in
esac esac
backend_image="${REGISTRY_HOST}/ctms/ctms-backend" backend_image="${REGISTRY_HOST}/ctms/ctms-backend"
nginx_image="${REGISTRY_HOST}/ctms/ctms-nginx" frontend_image="${REGISTRY_HOST}/ctms/ctms-frontend"
echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" --username "${REGISTRY_USERNAME}" --password-stdin echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" --username "${REGISTRY_USERNAME}" --password-stdin
@@ -55,12 +55,11 @@ docker build \
./backend ./backend
docker build \ docker build \
-t "${nginx_image}:${mutable_tag}" \ -t "${frontend_image}:${mutable_tag}" \
-t "${nginx_image}:${immutable_tag}" \ -t "${frontend_image}:${immutable_tag}" \
-f ./nginx/Dockerfile \ ./frontend
.
docker push "${backend_image}:${mutable_tag}" docker push "${backend_image}:${mutable_tag}"
docker push "${backend_image}:${immutable_tag}" docker push "${backend_image}:${immutable_tag}"
docker push "${nginx_image}:${mutable_tag}" docker push "${frontend_image}:${mutable_tag}"
docker push "${nginx_image}:${immutable_tag}" docker push "${frontend_image}:${immutable_tag}"