chore(deploy): containerize server deployment
This commit is contained in:
+62
-39
@@ -1,15 +1,15 @@
|
||||
# 自动构建与自动重启流程
|
||||
|
||||
本文档说明当前项目通过 Git remote + `post-receive` hook 实现自动构建、自动重启的流程。核心文件为 [.githooks/post-receive](../.githooks/post-receive)。
|
||||
本文档说明当前项目通过 Git remote + `post-receive` hook 实现自动构建、自动重启的流程。当前服务器端运行方式已经容器化,核心文件为 [.githooks/post-receive](../.githooks/post-receive)、[Dockerfile](../Dockerfile) 和 [docker-compose.yml](../docker-compose.yml)。
|
||||
|
||||
## 部署入口
|
||||
|
||||
当前涉及两个服务器 remote:
|
||||
|
||||
| 环境 | Git remote | 推送分支 | 服务端端口 | 部署脚本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 测试环境 | `test` | `test` | `9135` | `scripts/deploy/deploy_web_test.sh` |
|
||||
| 生产环境 | `production` | `main` | `9185` | `scripts/deploy/deploy_web.sh` |
|
||||
| 环境 | Git remote | 推送分支 | 宿主机端口 | 容器端口 | 部署脚本 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 测试环境 | `test` | `test` | `9135` | `3000` | `scripts/deploy/deploy_web_test.sh` |
|
||||
| 生产环境 | `production` | `main` | `9185` | `3000` | `scripts/deploy/deploy_web.sh` |
|
||||
|
||||
当前 remote 配置示例:
|
||||
|
||||
@@ -73,43 +73,57 @@ production root@43.106.13.130:/root/cozsweet-repos/main
|
||||
5. 执行 `git reset --hard HEAD`,让服务端工作树同步到刚推送的最新 commit。
|
||||
6. 根据当前分支复制环境变量文件。
|
||||
7. 根据当前分支选择 Next.js 启动端口。
|
||||
8. 执行 `pnpm run build:deploy`。
|
||||
9. 如果构建失败,写入失败日志并中止,不会重启服务。
|
||||
10. 如果构建成功,查找并强杀当前端口上的旧 Next.js 进程。
|
||||
11. 等待端口释放,并重试确认。
|
||||
12. 端口释放成功后,执行 `nohup pnpm run start` 后台启动新服务。
|
||||
8. 根据分支导出 Docker Compose 所需变量,包括镜像名、容器名、env 文件和端口。
|
||||
9. 执行 `docker compose build web` 构建环境专属镜像。
|
||||
10. 如果镜像构建失败,写入失败日志并中止,旧容器继续运行。
|
||||
11. 如果镜像构建成功,执行 `docker compose up -d --remove-orphans web` 重建并启动容器。
|
||||
12. 第一次从裸机 `next start` 迁移到容器时,如果目标端口上没有同名容器但存在旧进程,会清理旧进程后再启动容器。
|
||||
|
||||
## 分支、环境变量、端口映射
|
||||
|
||||
`post-receive` 当前按服务端工作树所在分支判断环境:
|
||||
|
||||
| 服务端分支 | 环境变量复制 | 启动端口 |
|
||||
| --- | --- | --- |
|
||||
| `main` | `env-example/.env.production.example` → `.env.production` | `9185` |
|
||||
| `test` | `env-example/.env.local.example` → `.env.local` | `9135` |
|
||||
| `dev` | `env-example/.env.development.example` → `.env.local` | `9135` |
|
||||
| 其他分支 | 跳过 env 复制 | `9135` |
|
||||
| 服务端分支 | 环境变量复制 | 宿主机端口 | 容器名 | 镜像标签 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `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>` |
|
||||
|
||||
本地生产发布脚本和服务器端 `post-receive` 已统一使用 `.env.production`。Next.js 在生产构建时会读取 `.env.production`;测试和开发发布仍使用 `.env.local`。
|
||||
|
||||
本地部署脚本清除 Cloudflare CDN 缓存时,也会按当前分支读取 env:`main` 优先读取 `.env.production`,其他分支优先读取 `.env.local`。如果优先文件不存在或缺少 `CF_ZONE_ID` / `CF_API_TOKEN`,会尝试读取另一个文件作为兜底。
|
||||
|
||||
## 容器构建机制
|
||||
|
||||
Docker 构建使用 Next.js standalone 输出:
|
||||
|
||||
1. `next.config.ts` 设置 `output: "standalone"`。
|
||||
2. `Dockerfile` 使用 multi-stage build。
|
||||
3. build 阶段通过 Docker BuildKit secret 挂载 `.env.local` 或 `.env.production`。
|
||||
4. build 完成后删除临时 env 文件,避免 env 文件被复制到最终镜像。
|
||||
5. runtime 阶段只复制 `.next/standalone`、`.next/static` 和 `public`。
|
||||
6. 容器通过 `node server.js` 启动,不再依赖完整 `node_modules` 或 `next start`。
|
||||
|
||||
注意:`NEXT_PUBLIC_*` 变量会在 `next build` 时固化到前端 bundle,因此测试环境和生产环境必须分别构建镜像,不能共用同一个镜像。
|
||||
|
||||
## 自动重启机制
|
||||
|
||||
重启由 `post-receive` 中的端口进程管理实现:
|
||||
重启由 Docker Compose 实现:
|
||||
|
||||
1. 使用 `ss -tulnp` 查询占用目标端口的进程。
|
||||
2. 如果找到旧进程,执行 `kill -9` 强制停止。
|
||||
3. 默认等待 `3` 秒。
|
||||
4. 最多重试 `3` 次确认端口已经释放。
|
||||
5. 如果端口仍未释放,中止启动,避免新服务出现 `EADDRINUSE`。
|
||||
6. 如果端口释放成功,执行:
|
||||
1. `post-receive` 先执行 `docker compose build web`。
|
||||
2. 构建失败时直接中止,旧容器继续运行。
|
||||
3. 构建成功后执行:
|
||||
|
||||
```bash
|
||||
PORT="$START_PORT" nohup pnpm run start >> "$LOG_FILE" 2>&1 &
|
||||
docker compose up -d --remove-orphans web
|
||||
```
|
||||
|
||||
`package.json` 中的 `start` 脚本会优先使用传入的 `PORT`,因此测试环境固定使用 `9135`,生产环境固定使用 `9185`。
|
||||
4. Compose 会使用新镜像重建服务容器。
|
||||
5. 容器配置了 `restart: unless-stopped`,容器异常退出后由 Docker 自动拉起。
|
||||
6. 容器内固定监听 `3000`,宿主机端口按分支映射为 `9135` 或 `9185`。
|
||||
|
||||
首次迁移时,如果宿主机端口仍被旧的裸机 `next start` 进程占用,hook 会在构建成功后、启动容器前清理旧进程。后续如果同名容器已经存在,则不再手动清理端口,由 Docker Compose 管理容器替换。
|
||||
|
||||
## 日志与排查
|
||||
|
||||
@@ -127,7 +141,14 @@ logs/post-receive.log
|
||||
# 查看最近一次部署日志
|
||||
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'
|
||||
|
||||
@@ -136,22 +157,23 @@ git branch --show-current
|
||||
git rev-parse --short HEAD
|
||||
```
|
||||
|
||||
构建失败时,日志中会出现:
|
||||
镜像构建失败时,日志中会出现:
|
||||
|
||||
```text
|
||||
build EXIT_CODE=<非 0>
|
||||
ABORTED ... (build failed, start skipped)
|
||||
docker build EXIT_CODE=<非 0>
|
||||
ABORTED ... (docker build failed, old container kept running)
|
||||
```
|
||||
|
||||
这种情况下不会停止旧服务,也不会启动新服务。
|
||||
这种情况下不会停止旧容器,也不会启动新容器。
|
||||
|
||||
端口释放失败时,日志中会出现:
|
||||
容器启动失败时,日志中会出现:
|
||||
|
||||
```text
|
||||
stop FAILED: port <端口> still occupied
|
||||
docker compose up EXIT_CODE=<非 0>
|
||||
ABORTED ... (docker compose up failed)
|
||||
```
|
||||
|
||||
这种情况下不会启动新服务,需要人工检查占用该端口的进程。
|
||||
这种情况下需要人工查看 `docker compose ps`、`docker logs <container>` 和宿主机端口占用。
|
||||
|
||||
## 关键约束
|
||||
|
||||
@@ -159,12 +181,13 @@ stop FAILED: port <端口> still occupied
|
||||
2. 自动构建和自动重启只发生在服务器端 `post-receive` hook。
|
||||
3. `main` 分支对应生产环境,`test` 分支对应测试环境。
|
||||
4. 生产推送当前使用 `git push --force production main`。
|
||||
5. 构建失败时不会启动新服务,避免用旧 `.next` 跑新代码。
|
||||
6. 服务启动使用 `nohup` 后台进程,不依赖 PM2 或 systemd。
|
||||
5. 构建失败时不会重启容器,旧容器继续运行。
|
||||
6. 服务启动由 Docker Compose 管理,不再使用 `nohup pnpm run start`。
|
||||
7. 服务器需要安装 Docker,并支持 `docker compose` 或旧版 `docker-compose` 命令。
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. 将服务进程交给 PM2 或 systemd 管理,提升崩溃自动恢复能力。
|
||||
2. 保留最近 N 次部署日志,而不是只保留最后一次日志,方便排查历史问题。
|
||||
3. 构建命令可以保留完整输出到单独日志文件,当前 `pnpm run build:deploy` 输出被丢弃,只记录退出码。
|
||||
4. 可以在启动后增加健康检查,确认新服务真正可访问后再判定部署成功。
|
||||
1. 保留最近 N 次部署日志,而不是只保留最后一次日志,方便排查历史问题。
|
||||
2. 将镜像推送到私有镜像仓库,服务器只负责拉取和启动镜像,减少服务器构建压力。
|
||||
3. 增加部署后健康检查,确认新容器真正可访问后再判定部署成功。
|
||||
4. 增加旧镜像清理策略,避免服务器磁盘被历史镜像占满。
|
||||
|
||||
Reference in New Issue
Block a user