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 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-<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` 账号密码完成认证。
- `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`
+15
View File
@@ -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"],
+14
View File
@@ -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
+8 -13
View File
@@ -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
+4 -4
View File
@@ -10,10 +10,10 @@
- [ ] `cd frontend && npm run build`
## 1.1 腾讯云私有镜像校验
- [ ] `dev` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``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-nginx``rc-<release-branch>` / `rc-<short_sha>` 标签已出现在私有仓库
- [ ] `main` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-nginx``latest` / `sha-<short_sha>` 标签已出现在私有仓库
- [ ] `dev` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``dev-latest` / `dev-<short_sha>` 标签已出现在私有仓库
- [ ] `release` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``rc-release` / `rc-<short_sha>` 标签已出现在私有仓库
- [ ] `release/*` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``rc-<release-branch>` / `rc-<short_sha>` 标签已出现在私有仓库
- [ ] `main` 推送后,`<IP>:5000/ctms/ctms-backend``ctms-frontend``latest` / `sha-<short_sha>` 标签已出现在私有仓库
- [ ] GitHub Actions 已成功 SSH 登录腾讯云服务器并执行远程构建脚本
- [ ] 部署机已执行 `docker login <IP>:5000`,且使用的是私有仓库 `htpasswd` 账号密码
@@ -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.
@@ -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
@@ -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**
@@ -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`
@@ -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
**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: `<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`.
- 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/<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**
- Backend image: `<registry-host>/ctms/ctms-backend`
- Nginx image: `<registry-host>/ctms/ctms-nginx`
- `frontend` is intentionally not published because the current runtime topology already builds the frontend assets into the `nginx` image.
- Frontend image: `<registry-host>/ctms/ctms-frontend`
**Tagging Strategy**
- `dev` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:dev-latest`
- `<registry-host>/ctms/ctms-backend:dev-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:dev-latest`
- `<registry-host>/ctms/ctms-nginx:dev-<short_sha>`
- `<registry-host>/ctms/ctms-frontend:dev-latest`
- `<registry-host>/ctms/ctms-frontend:dev-<short_sha>`
- `release` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:rc-release`
- `<registry-host>/ctms/ctms-backend:rc-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:rc-release`
- `<registry-host>/ctms/ctms-nginx:rc-<short_sha>`
- `<registry-host>/ctms/ctms-frontend:rc-release`
- `<registry-host>/ctms/ctms-frontend:rc-<short_sha>`
- `release/*` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:rc-<release-branch>`
- `<registry-host>/ctms/ctms-backend:rc-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:rc-<release-branch>`
- `<registry-host>/ctms/ctms-nginx:rc-<short_sha>`
- `<registry-host>/ctms/ctms-frontend:rc-<release-branch>`
- `<registry-host>/ctms/ctms-frontend:rc-<short_sha>`
- `main` branch pushes publish:
- `<registry-host>/ctms/ctms-backend:latest`
- `<registry-host>/ctms/ctms-backend:sha-<short_sha>`
- `<registry-host>/ctms/ctms-nginx:latest`
- `<registry-host>/ctms/ctms-nginx:sha-<short_sha>`
- `<registry-host>/ctms/ctms-frontend:latest`
- `<registry-host>/ctms/ctms-frontend:sha-<short_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`.
@@ -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.
+1
View File
@@ -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
+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
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
+4 -1
View File
@@ -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,
});
+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
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}"