feat: 移除仓库内 nginx 并拆分前端运行时
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
:4173
|
||||
|
||||
root * /srv
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
+7
-3
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user