From 5677f6823abd24d56a032f4972704f79ebc08089 Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 30 Mar 2026 21:03:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?release(main):=20=E9=9B=86=E6=88=90=20nginx?= =?UTF-8?q?=20=E7=A7=BB=E9=99=A4=E9=87=8C=E7=A8=8B=E7=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +-- backend/app/main.py | 15 +++ backend/tests/test_registration.py | 14 +++ docker-compose.yaml | 21 ++-- docs/guides/release-checklist.md | 8 +- .../2026-03-27-production-compose-design.md | 15 +-- ...03-27-production-compose-implementation.md | 34 +++--- .../2026-03-27-production-init-design.md | 2 +- ...26-03-27-production-init-implementation.md | 4 +- docs/plans/2026-03-30-remove-nginx-design.md | 21 ++++ .../2026-03-30-remove-nginx-implementation.md | 114 ++++++++++++++++++ ...26-03-30-tcloud-private-registry-design.md | 27 ++--- .../2026-03-30-tcloud-private-registry.md | 8 +- frontend/.env.example | 1 + frontend/Caddyfile | 5 + frontend/Dockerfile | 10 +- frontend/src/api/axios.ts | 5 +- frontend/src/api/baseUrl.test.ts | 19 +++ frontend/src/api/baseUrl.ts | 12 ++ nginx/Dockerfile | 14 --- nginx/nginx.conf | 65 ---------- scripts/build-and-push-registry.sh | 13 +- 22 files changed, 287 insertions(+), 160 deletions(-) create mode 100644 docs/plans/2026-03-30-remove-nginx-design.md create mode 100644 docs/plans/2026-03-30-remove-nginx-implementation.md create mode 100644 frontend/Caddyfile create mode 100644 frontend/src/api/baseUrl.test.ts create mode 100644 frontend/src/api/baseUrl.ts delete mode 100644 nginx/Dockerfile delete mode 100644 nginx/nginx.conf diff --git a/README.md b/README.md index b8ecf344..d9f70323 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,21 @@ - 生产入口:`docker-compose.yaml` - 初始化方式:`docker compose run --rm backend-init` - 启动方式:`docker compose up -d --build` -- 对外入口:Nginx 提供前端静态资源,并反代后端 API +- 运行拓扑:`frontend`、`backend`、`db` +- 对外入口:仓库外部统一反向代理;建议将 `/` 转发到 `frontend`,将 `/api` 与 `/health` 转发到 `backend` - 数据库 schema 来源:Alembic migration,不再依赖 `database/init.sql` - 默认无任何 demo 数据;生产初始化只确保固定管理员 `admin@huapont.cn / admin123` 存在 - 验证方式: - `docker compose config` - - `curl -i http://127.0.0.1/` - - `curl -i http://127.0.0.1/health` + - `curl -i http://127.0.0.1:4173/` + - `curl -i http://127.0.0.1:8000/health` ## 腾讯云私有镜像仓库 -- GitHub Actions 会在推送到 `dev`、`release`、`release/*` 和 `main` 时,通过 SSH 连接腾讯云服务器,在服务器本机完成 `backend` 和 `nginx` 镜像的构建与推送。 -- 私有仓库地址默认使用 `${REGISTRY_HOST}`,仓库命名为 `${REGISTRY_HOST}/ctms/ctms-backend` 和 `${REGISTRY_HOST}/ctms/ctms-nginx`。 +- GitHub Actions 会在推送到 `dev`、`release`、`release/*` 和 `main` 时,通过 SSH 连接腾讯云服务器,在服务器本机完成 `backend` 和 `frontend` 镜像的构建与推送。 +- 私有仓库地址默认使用 `${REGISTRY_HOST}`,仓库命名为 `${REGISTRY_HOST}/ctms/ctms-backend` 和 `${REGISTRY_HOST}/ctms/ctms-frontend`。 - `dev` 分支发布 `dev-latest` 和 `dev-`,`release` 分支发布 `rc-release` 和 `rc-`,`release/*` 分支发布 `rc-` 和 `rc-`,`main` 分支发布 `latest` 和 `sha-`。 - 部署机需要先 `docker login :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` 走本地构建。 ## 账号与注册 @@ -35,8 +36,11 @@ - 普通成员(无项目角色):仅浏览 ## 访问方式 -- 前端:`http://localhost`(Nginx 直接托管前端静态文件) -- 后端 API:同域 `/api/v1/*`(已在前端代理) +- 前端:`http://localhost:4173` +- 后端 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` diff --git a/backend/app/main.py b/backend/app/main.py index 28304e0d..fd415f37 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -166,6 +166,21 @@ def create_app() -> FastAPI: 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( "/health", tags=["health"], diff --git a/backend/tests/test_registration.py b/backend/tests/test_registration.py index 1921fe1b..6603a54e 100644 --- a/backend/tests/test_registration.py +++ b/backend/tests/test_registration.py @@ -62,6 +62,20 @@ async def test_register_creates_pending_user(client_and_db): 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 async def test_login_blocked_before_approval(client_and_db): client, SessionLocal = client_and_db diff --git a/docker-compose.yaml b/docker-compose.yaml index 0cc84fcd..160ec7b1 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -52,22 +52,17 @@ services: networks: - ctms_net - nginx: - image: ${NGINX_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-nginx:latest} + frontend: + image: ${FRONTEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-frontend:latest} build: - context: . - dockerfile: nginx/Dockerfile - container_name: ctms_nginx + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:8000} + container_name: ctms_frontend restart: always ports: - - "80:80" # HTTP - - "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 + - "4173:4173" networks: - ctms_net diff --git a/docs/guides/release-checklist.md b/docs/guides/release-checklist.md index a98a5efa..41bfa740 100644 --- a/docs/guides/release-checklist.md +++ b/docs/guides/release-checklist.md @@ -10,10 +10,10 @@ - [ ] `cd frontend && npm run build` ## 1.1 腾讯云私有镜像校验 -- [ ] `dev` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `dev-latest` / `dev-` 标签已出现在私有仓库 -- [ ] `release` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `rc-release` / `rc-` 标签已出现在私有仓库 -- [ ] `release/*` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `rc-` / `rc-` 标签已出现在私有仓库 -- [ ] `main` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `latest` / `sha-` 标签已出现在私有仓库 +- [ ] `dev` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-frontend` 的 `dev-latest` / `dev-` 标签已出现在私有仓库 +- [ ] `release` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-frontend` 的 `rc-release` / `rc-` 标签已出现在私有仓库 +- [ ] `release/*` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-frontend` 的 `rc-` / `rc-` 标签已出现在私有仓库 +- [ ] `main` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-frontend` 的 `latest` / `sha-` 标签已出现在私有仓库 - [ ] GitHub Actions 已成功 SSH 登录腾讯云服务器并执行远程构建脚本 - [ ] 部署机已执行 `docker login :5000`,且使用的是私有仓库 `htpasswd` 账号密码 diff --git a/docs/plans/2026-03-27-production-compose-design.md b/docs/plans/2026-03-27-production-compose-design.md index 46109937..470307e3 100644 --- a/docs/plans/2026-03-27-production-compose-design.md +++ b/docs/plans/2026-03-27-production-compose-design.md @@ -4,24 +4,25 @@ **Decisions** - 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 one backend container and remove development-only runtime behavior. - 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** - `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. +- `backend` runs from the backend image default command and exposes port `8000`. +- `frontend` runs from its own image and serves the SPA on a dedicated frontend port. +- An external reverse proxy becomes the public entrypoint and routes browser traffic to `frontend` and API traffic to `backend`. **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. +- 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. **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. +- 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 direct 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 index 9a39add1..d1688a3d 100644 --- a/docs/plans/2026-03-27-production-compose-implementation.md +++ b/docs/plans/2026-03-27-production-compose-implementation.md @@ -2,11 +2,11 @@ > **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`. -**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** Run: `docker compose config` Expected: compose renders successfully with no Dockerfile-related errors. -### Task 2: Productionize Nginx Runtime +### Task 2: Productionize Frontend Runtime **Files:** -- Modify: `nginx/nginx.conf` +- Modify: `frontend/Dockerfile` **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** @@ -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. -**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. -**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** @@ -79,12 +79,12 @@ 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`. +Document `docker compose up -d --build`, plus direct health checks for the frontend and backend containers. **Step 3: Verify behavior** 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 @@ -103,8 +103,8 @@ Expected: PASS **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 -Run: `curl -i http://127.0.0.1/health` +Run: `curl -i http://127.0.0.1:8000/health` Expected: successful backend health response diff --git a/docs/plans/2026-03-27-production-init-design.md b/docs/plans/2026-03-27-production-init-design.md index 53b5d3ca..9c8ed7eb 100644 --- a/docs/plans/2026-03-27-production-init-design.md +++ b/docs/plans/2026-03-27-production-init-design.md @@ -25,7 +25,7 @@ **Operational Flow** 1. Start PostgreSQL with an empty data directory. 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. **Non-Goals** diff --git a/docs/plans/2026-03-27-production-init-implementation.md b/docs/plans/2026-03-27-production-init-implementation.md index fc7a11c1..5698b4af 100644 --- a/docs/plans/2026-03-27-production-init-implementation.md +++ b/docs/plans/2026-03-27-production-init-implementation.md @@ -163,7 +163,7 @@ Expected: migrations run successfully and the protected admin is ensured. **Step 4: Validate service health** 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` diff --git a/docs/plans/2026-03-30-remove-nginx-design.md b/docs/plans/2026-03-30-remove-nginx-design.md new file mode 100644 index 00000000..e07bcaa5 --- /dev/null +++ b/docs/plans/2026-03-30-remove-nginx-design.md @@ -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. diff --git a/docs/plans/2026-03-30-remove-nginx-implementation.md b/docs/plans/2026-03-30-remove-nginx-implementation.md new file mode 100644 index 00000000..1c530f5f --- /dev/null +++ b/docs/plans/2026-03-30-remove-nginx-implementation.md @@ -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`. diff --git a/docs/plans/2026-03-30-tcloud-private-registry-design.md b/docs/plans/2026-03-30-tcloud-private-registry-design.md index 8509bfc1..8e059230 100644 --- a/docs/plans/2026-03-30-tcloud-private-registry-design.md +++ b/docs/plans/2026-03-30-tcloud-private-registry-design.md @@ -1,9 +1,9 @@ # 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** -- Publish only two runtime images: `/ctms/ctms-backend` and `/ctms/ctms-nginx`. +- Publish only two runtime images: `/ctms/ctms-backend` and `/ctms/ctms-frontend`. - 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`. - 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`. - 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/`. -- 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** - Backend image: `/ctms/ctms-backend` -- Nginx image: `/ctms/ctms-nginx` -- `frontend` is intentionally not published because the current runtime topology already builds the frontend assets into the `nginx` image. +- Frontend image: `/ctms/ctms-frontend` **Tagging Strategy** - `dev` branch pushes publish: - `/ctms/ctms-backend:dev-latest` - `/ctms/ctms-backend:dev-` - - `/ctms/ctms-nginx:dev-latest` - - `/ctms/ctms-nginx:dev-` + - `/ctms/ctms-frontend:dev-latest` + - `/ctms/ctms-frontend:dev-` - `release` branch pushes publish: - `/ctms/ctms-backend:rc-release` - `/ctms/ctms-backend:rc-` - - `/ctms/ctms-nginx:rc-release` - - `/ctms/ctms-nginx:rc-` + - `/ctms/ctms-frontend:rc-release` + - `/ctms/ctms-frontend:rc-` - `release/*` branch pushes publish: - `/ctms/ctms-backend:rc-` - `/ctms/ctms-backend:rc-` - - `/ctms/ctms-nginx:rc-` - - `/ctms/ctms-nginx:rc-` + - `/ctms/ctms-frontend:rc-` + - `/ctms/ctms-frontend:rc-` - `main` branch pushes publish: - `/ctms/ctms-backend:latest` - `/ctms/ctms-backend:sha-` - - `/ctms/ctms-nginx:latest` - - `/ctms/ctms-nginx:sha-` + - `/ctms/ctms-frontend:latest` + - `/ctms/ctms-frontend:sha-` **Server Requirements** - Docker installed on the Tencent Cloud server. @@ -64,7 +63,7 @@ - `REGISTRY_PASSWORD` **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. - Deployment hosts can export `REGISTRY_HOST`, then run `docker login`, `docker compose pull`, `docker compose run --rm backend-init`, and `docker compose up -d`. diff --git a/docs/plans/2026-03-30-tcloud-private-registry.md b/docs/plans/2026-03-30-tcloud-private-registry.md index 55a063c2..23dfb0c6 100644 --- a/docs/plans/2026-03-30-tcloud-private-registry.md +++ b/docs/plans/2026-03-30-tcloud-private-registry.md @@ -2,11 +2,11 @@ > **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. -**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: - `${REGISTRY_HOST}/ctms/ctms-backend` -- `${REGISTRY_HOST}/ctms/ctms-nginx` +- `${REGISTRY_HOST}/ctms/ctms-frontend` **Step 3: Push both tags for both images** @@ -86,7 +86,7 @@ Expected: PASS Set: - `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}` -- `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. diff --git a/frontend/.env.example b/frontend/.env.example index 73a9682d..81f4a110 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,4 +1,5 @@ # Frontend feature flags and timeline overrides +VITE_API_BASE_URL= VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3 VITE_STARTUP_ACCEPT_TIMEOUT_MONTHS=3 VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6 diff --git a/frontend/Caddyfile b/frontend/Caddyfile new file mode 100644 index 00000000..38576a03 --- /dev/null +++ b/frontend/Caddyfile @@ -0,0 +1,5 @@ +:4173 + +root * /srv +try_files {path} /index.html +file_server diff --git a/frontend/Dockerfile b/frontend/Dockerfile index b145d8c4..106ffcac 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -2,14 +2,18 @@ FROM node:20-alpine AS build WORKDIR /app +ARG VITE_API_BASE_URL +ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} + COPY package*.json ./ RUN npm ci COPY . . 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 diff --git a/frontend/src/api/axios.ts b/frontend/src/api/axios.ts index e669ed03..0ae6aba3 100644 --- a/frontend/src/api/axios.ts +++ b/frontend/src/api/axios.ts @@ -7,9 +7,12 @@ import { useStudyStore } from "../store/study"; import { TEXT } from "../locales"; import { extendAccessToken, forceLogout, lockSession, markNetworkActive } from "../session/sessionManager"; import { useSessionStore } from "../store/session"; +import { resolveApiBaseUrl } from "./baseUrl"; + +const apiBaseUrl = resolveApiBaseUrl(import.meta.env.VITE_API_BASE_URL); const instance: AxiosInstance = axios.create({ - baseURL: "/", + baseURL: apiBaseUrl, timeout: 15000, }); diff --git a/frontend/src/api/baseUrl.test.ts b/frontend/src/api/baseUrl.test.ts new file mode 100644 index 00000000..521baee7 --- /dev/null +++ b/frontend/src/api/baseUrl.test.ts @@ -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"); + }); +}); diff --git a/frontend/src/api/baseUrl.ts b/frontend/src/api/baseUrl.ts new file mode 100644 index 00000000..17657095 --- /dev/null +++ b/frontend/src/api/baseUrl.ts @@ -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); +}; diff --git a/nginx/Dockerfile b/nginx/Dockerfile deleted file mode 100644 index d394f9fc..00000000 --- a/nginx/Dockerfile +++ /dev/null @@ -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 diff --git a/nginx/nginx.conf b/nginx/nginx.conf deleted file mode 100644 index 37ff0e79..00000000 --- a/nginx/nginx.conf +++ /dev/null @@ -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; - } - } -} diff --git a/scripts/build-and-push-registry.sh b/scripts/build-and-push-registry.sh index b38f945d..b1c25b8c 100644 --- a/scripts/build-and-push-registry.sh +++ b/scripts/build-and-push-registry.sh @@ -45,7 +45,7 @@ case "${BRANCH_NAME}" in esac 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 @@ -55,12 +55,11 @@ docker build \ ./backend docker build \ - -t "${nginx_image}:${mutable_tag}" \ - -t "${nginx_image}:${immutable_tag}" \ - -f ./nginx/Dockerfile \ - . + -t "${frontend_image}:${mutable_tag}" \ + -t "${frontend_image}:${immutable_tag}" \ + ./frontend docker push "${backend_image}:${mutable_tag}" docker push "${backend_image}:${immutable_tag}" -docker push "${nginx_image}:${mutable_tag}" -docker push "${nginx_image}:${immutable_tag}" +docker push "${frontend_image}:${mutable_tag}" +docker push "${frontend_image}:${immutable_tag}" From e15f3dd8875c0246f28e5ce097d9f72138892adf Mon Sep 17 00:00:00 2001 From: Cheng Zhou Date: Mon, 30 Mar 2026 22:17:39 +0800 Subject: [PATCH 2/2] fix: use writable remote build path --- .github/workflows/publish-images.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index 1eb3186e..e157dd73 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -19,7 +19,7 @@ jobs: publish: runs-on: ubuntu-latest env: - REMOTE_BASE_DIR: /opt/ctms-build + REMOTE_BASE_DIR: /home/zcc/registry/build/ctms-build SSH_KEY_PATH: /tmp/tcloud_ci_key steps: - name: Checkout