296 lines
11 KiB
Markdown
296 lines
11 KiB
Markdown
# 自动构建与自动重启流程
|
||
|
||
本文档说明当前项目通过 Git remote + `post-receive` hook 实现自动部署、自动重启的流程。当前服务器端运行方式已经容器化,核心文件为 [.githooks/post-receive](../.githooks/post-receive)、[Dockerfile](../Dockerfile) 和 [docker-compose.yml](../docker-compose.yml)。
|
||
|
||
服务器默认仍会本地构建 Docker 镜像;如果服务器工作树中存在 `.deploy.env` 且配置 `COZSWEET_DEPLOY_IMAGE_SOURCE=pull`,则会改为从镜像仓库拉取由 Gitea Actions 构建好的镜像。
|
||
|
||
## 部署入口
|
||
|
||
当前涉及两个服务器 remote:
|
||
|
||
| 环境 | Git remote | 推送分支 | 宿主机端口 | 容器端口 | 部署脚本 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 测试环境 | `test` | `test` | `9135` | `3000` | `scripts/deploy/deploy_web_test.sh` |
|
||
| 生产环境 | `production` | `main` | `9185` | `3000` | `scripts/deploy/deploy_web.sh` |
|
||
|
||
当前 remote 配置示例:
|
||
|
||
```bash
|
||
test root@43.106.13.130:/root/cozsweet-repos/test
|
||
production root@43.106.13.130:/root/cozsweet-repos/main
|
||
```
|
||
|
||
常用发布入口:
|
||
|
||
```bash
|
||
# 发布测试环境
|
||
./scripts/release/pre_release_web.sh
|
||
|
||
# 发布生产环境
|
||
./scripts/release/release_web.sh
|
||
```
|
||
|
||
也可以只执行推送脚本:
|
||
|
||
```bash
|
||
# 推送 test remote 的 test 分支
|
||
./scripts/deploy/deploy_web_test.sh
|
||
|
||
# 推送 production remote 的 main 分支
|
||
./scripts/deploy/deploy_web.sh
|
||
```
|
||
|
||
## 本地发布流程
|
||
|
||
测试环境发布流程:
|
||
|
||
1. 进入 test worktree:`cozsweet-frontend-nextjs.worktrees/cozsweet-nextjs-test`
|
||
2. 确认当前分支。
|
||
3. 执行 `git rebase dev`,把 `dev` 的最新代码变基到 `test` 分支。
|
||
4. 复制测试环境图标到 `public/`。
|
||
5. 复制 `env-example/.env.local.example` 到 `.env.local`。
|
||
6. 执行 `scripts/deploy/deploy_web_test.sh`。
|
||
7. `deploy_web_test.sh` 推送 `test` remote 的 `test` 分支。
|
||
8. 推送成功后尝试清除 Cloudflare CDN 缓存。
|
||
|
||
生产环境发布流程:
|
||
|
||
1. 进入 main worktree:`cozsweet-frontend-nextjs.worktrees/cozsweet-nextjs-main`
|
||
2. 确认当前分支。
|
||
3. 执行 `git rebase dev`,把 `dev` 的最新代码变基到 `main` 分支。
|
||
4. 复制生产环境图标到 `public/`。
|
||
5. 复制 `env-example/.env.production.example` 到 `.env.production`。
|
||
6. 执行 `scripts/deploy/deploy_web.sh`。
|
||
7. `deploy_web.sh` 强制推送 `production` remote 的 `main` 分支。
|
||
8. 推送成功后尝试清除 Cloudflare CDN 缓存。
|
||
|
||
## 服务器端 post-receive 流程
|
||
|
||
服务器收到 push 后,`post-receive` hook 会自动执行以下步骤:
|
||
|
||
1. 解析当前仓库目录,进入服务端工作树。
|
||
2. 初始化日志文件:`logs/post-receive.log`。
|
||
3. 覆盖写入本次执行元信息,包括时间、分支、commit、cwd。
|
||
4. 执行 `git reset --hard HEAD`,让服务端工作树同步到刚推送的最新 commit。
|
||
5. 根据当前分支复制环境变量文件。
|
||
6. 根据当前分支选择 Next.js 启动端口。
|
||
7. 根据分支导出 Docker Compose 所需变量,包括镜像名、容器名、env 文件和端口。
|
||
8. 如果使用默认 `build` 模式,执行 `docker compose build web` 构建环境专属镜像。
|
||
9. 如果使用 `pull` 模式,执行 `docker compose pull web` 拉取 CI 已发布镜像。
|
||
10. 如果镜像构建或拉取失败,写入失败日志并中止,旧容器继续运行。
|
||
11. 如果镜像准备成功,执行 `docker compose up -d --remove-orphans web` 重建并启动容器。
|
||
|
||
## 分支、环境变量、端口映射
|
||
|
||
`post-receive` 当前按服务端工作树所在分支判断环境:
|
||
|
||
| 服务端分支 | 环境变量复制 | 宿主机端口 | 容器名 | 镜像标签 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `main` | `env-example/.env.production.example` → `.env.production` | `9185` | `cozsweet-web-prod` | `cozsweet-web:prod-<commit>` |
|
||
| `test` | `env-example/.env.local.example` → `.env.local` | `9135` | `cozsweet-web-test` | `cozsweet-web:test-<commit>` |
|
||
| `dev` | `env-example/.env.development.example` → `.env.local` | `9135` | `cozsweet-web-dev` | `cozsweet-web:dev-<commit>` |
|
||
| 其他分支 | 使用默认 `.env.local` | `9135` | `cozsweet-web-<branch>` | `cozsweet-web:<branch>-<commit>` |
|
||
|
||
如果 `.deploy.env` 配置为 pull 模式,镜像标签会从本地标签切换为远程仓库标签:
|
||
|
||
```env
|
||
COZSWEET_DEPLOY_IMAGE_SOURCE=pull
|
||
COZSWEET_REGISTRY_IMAGE=gitea.banlv-ai.com/admin/cozsweet-web
|
||
```
|
||
|
||
默认远程镜像 tag 为 `<deploy_env>-<short_sha>`:
|
||
|
||
```text
|
||
gitea.banlv-ai.com/admin/cozsweet-web:test-<commit>
|
||
gitea.banlv-ai.com/admin/cozsweet-web:prod-<commit>
|
||
```
|
||
|
||
也可以在服务器本地 `.deploy.env` 中覆盖为 latest tag:
|
||
|
||
```env
|
||
COZSWEET_REMOTE_IMAGE_TAG=prod-latest
|
||
```
|
||
|
||
推荐生产发布使用默认精确 tag,避免 `latest` 因缓存或并发发布导致版本不可追踪。
|
||
|
||
本地生产发布脚本和服务器端 `post-receive` 已统一使用 `.env.production`。Next.js 在生产构建时会读取 `.env.production`;测试和开发发布仍使用 `.env.local`。
|
||
|
||
本地部署脚本清除 Cloudflare CDN 缓存时,也会按当前分支读取 env:`main` 优先读取 `.env.production`,其他分支优先读取 `.env.local`。如果优先文件不存在或缺少 `CF_ZONE_ID` / `CF_API_TOKEN`,会尝试读取另一个文件作为兜底。
|
||
|
||
## 生产容器化启用与验证
|
||
|
||
生产环境容器化通过 `main` 分支触发。发布入口为:
|
||
|
||
```bash
|
||
./scripts/release/release_web.sh
|
||
```
|
||
|
||
发布完成后,可在生产服务器工作树中验证:
|
||
|
||
```bash
|
||
cd /root/cozsweet-repos/main
|
||
|
||
# 确认生产工作树已更新到 main 最新 commit
|
||
git branch --show-current
|
||
git rev-parse --short HEAD
|
||
|
||
# 查看生产容器
|
||
docker ps --filter 'name=cozsweet-web-prod'
|
||
docker inspect cozsweet-web-prod --format '{{.State.Health.Status}}'
|
||
|
||
# 验证生产宿主机端口和 PWA service worker
|
||
curl -I http://127.0.0.1:9185/
|
||
curl -I http://127.0.0.1:9185/serwist/sw.js
|
||
```
|
||
|
||
如果本次发布同时更新了 `.githooks/post-receive` 本身,而服务器这次 push 仍由旧 hook 执行,可在生产工作树中手动触发一次新 hook:
|
||
|
||
```bash
|
||
cd /root/cozsweet-repos/main
|
||
./.githooks/post-receive
|
||
```
|
||
|
||
后续生产发布会自动使用新的容器化 hook。
|
||
|
||
## 镜像来源
|
||
|
||
### 默认:服务器本机构建
|
||
|
||
未配置 `.deploy.env` 时,`post-receive` 使用原有模式:
|
||
|
||
```bash
|
||
docker compose build web
|
||
docker compose up -d --remove-orphans web
|
||
```
|
||
|
||
### 推荐:CI 构建,服务器拉取
|
||
|
||
Gitea Actions 通过 [.gitea/workflows/docker-image.yml](../.gitea/workflows/docker-image.yml) 构建并推送镜像。服务器只负责拉取镜像:
|
||
|
||
```bash
|
||
docker compose pull web
|
||
docker compose up -d --remove-orphans --no-build web
|
||
```
|
||
|
||
服务器需要提前执行一次 registry 登录:
|
||
|
||
```bash
|
||
docker login REGISTRY_HOST
|
||
```
|
||
|
||
服务器本地 `.deploy.env` 不提交到仓库,示例见 [env-example/.deploy.env.example](../env-example/.deploy.env.example)。
|
||
|
||
## 容器构建机制
|
||
|
||
Docker 构建使用 Next.js standalone 输出:
|
||
|
||
1. `next.config.ts` 设置 `output: "standalone"`。
|
||
2. `Dockerfile` 使用 multi-stage build。
|
||
3. pnpm 依赖安装使用 BuildKit cache mount 复用 `/pnpm/store`。
|
||
4. Next.js 构建使用 BuildKit cache mount 复用 `.next/cache`。
|
||
5. build 阶段通过 Docker BuildKit secret 挂载 `.env.local` 或 `.env.production`。
|
||
6. build 完成后删除临时 env 文件,避免 env 文件被复制到最终镜像。
|
||
7. runtime 阶段复制 `.next/standalone`、`.next/static`、`public` 和生产依赖 `node_modules`。
|
||
8. 生产依赖用于支持 Serwist 在运行时生成 `/serwist/sw.js`,避免手动修复 pnpm symlink。
|
||
9. 容器通过 `node server.js` 启动,不再使用 `next start`。
|
||
|
||
注意:`NEXT_PUBLIC_*` 变量会在 `next build` 时固化到前端 bundle,因此测试环境和生产环境必须分别构建镜像,不能共用同一个镜像。
|
||
|
||
## 自动重启机制
|
||
|
||
重启由 Docker Compose 实现:
|
||
|
||
1. `post-receive` 先准备镜像:默认执行 `docker compose build web`,pull 模式执行 `docker compose pull web`。
|
||
2. 镜像准备失败时直接中止,旧容器继续运行。
|
||
3. 镜像准备成功后执行:
|
||
|
||
```bash
|
||
docker compose up -d --remove-orphans web
|
||
```
|
||
|
||
pull 模式下会额外加上 `--no-build`,确保服务器不会意外本地构建:
|
||
|
||
```bash
|
||
docker compose up -d --remove-orphans --no-build web
|
||
```
|
||
|
||
4. Compose 会使用新镜像重建服务容器。
|
||
5. 容器配置了 `restart: unless-stopped`,容器异常退出后由 Docker 自动拉起。
|
||
6. 容器内固定监听 `3000`,宿主机端口按分支映射为 `9135` 或 `9185`。
|
||
7. hook 不再主动清理宿主机端口进程,端口和容器生命周期交给 Docker Compose 管理。
|
||
|
||
## 日志与排查
|
||
|
||
服务器端日志文件:
|
||
|
||
```bash
|
||
logs/post-receive.log
|
||
```
|
||
|
||
当前日志策略是每次 `post-receive` 执行时覆盖旧日志,而不是追加。
|
||
|
||
常见排查命令:
|
||
|
||
```bash
|
||
# 查看最近一次部署日志
|
||
tail -n 200 logs/post-receive.log
|
||
|
||
# 查看容器状态
|
||
docker ps --filter 'name=cozsweet-web'
|
||
|
||
# 查看容器日志
|
||
docker logs --tail=200 cozsweet-web-test
|
||
docker logs --tail=200 cozsweet-web-prod
|
||
|
||
# 查看宿主机端口占用
|
||
ss -tulnp | grep ':9135'
|
||
ss -tulnp | grep ':9185'
|
||
|
||
# 查看当前分支和 commit
|
||
git branch --show-current
|
||
git rev-parse --short HEAD
|
||
```
|
||
|
||
镜像构建或拉取失败时,日志中会出现:
|
||
|
||
```text
|
||
docker build EXIT_CODE=<非 0>
|
||
ABORTED ... (docker build failed, old container kept running)
|
||
```
|
||
|
||
或:
|
||
|
||
```text
|
||
docker pull EXIT_CODE=<非 0>
|
||
ABORTED ... (docker pull failed, old container kept running)
|
||
```
|
||
|
||
这种情况下不会停止旧容器,也不会启动新容器。
|
||
|
||
容器启动失败时,日志中会出现:
|
||
|
||
```text
|
||
docker compose up EXIT_CODE=<非 0>
|
||
ABORTED ... (docker compose up failed)
|
||
```
|
||
|
||
这种情况下需要人工查看 `docker compose ps`、`docker logs <container>` 和宿主机端口占用。
|
||
|
||
## 关键约束
|
||
|
||
1. `scripts/deploy/*` 只负责推送代码,不负责本地构建和本地启动。
|
||
2. 自动构建和自动重启只发生在服务器端 `post-receive` hook。
|
||
3. `main` 分支对应生产环境,`test` 分支对应测试环境。
|
||
4. 生产推送当前使用 `git push --force production main`。
|
||
5. 构建失败时不会重启容器,旧容器继续运行。
|
||
6. 服务启动由 Docker Compose 管理,不再使用 `nohup pnpm run start`。
|
||
7. 服务器需要安装 Docker,并支持 `docker compose` 或旧版 `docker-compose` 命令。
|
||
8. 测试环境和生产环境都通过同一套 `post-receive` 容器化流程部署,差异仅由分支、环境变量文件和端口决定。
|
||
|
||
## 后续优化建议
|
||
|
||
1. 保留最近 N 次部署日志,而不是只保留最后一次日志,方便排查历史问题。
|
||
2. 将镜像推送到私有镜像仓库,服务器只负责拉取和启动镜像,减少服务器构建压力。
|
||
3. 增加部署后健康检查,确认新容器真正可访问后再判定部署成功。
|
||
4. 增加旧镜像清理策略,避免服务器磁盘被历史镜像占满。
|