Files
backend_v2/DEPLOY.md
toom1996 2e76507d37 deploy: 本地预构建发布(push-build),对齐 1Panel 与 backend_v2 目录
- 新增 Dockerfile.prebuilt / scripts/push-build.sh:本机构建、只上传产物,服务器零编译
- deploy.sh 增加 --prebuilt(自动 --no-pull + 产物校验),并保留 --no-cache
- compose 支持 BACKEND_DOCKERFILE / WEB_DOCKERFILE 切换;预构建时用 prebuilt 变体
- init-db.sh / scripts/backup-db.sh 改为 docker exec 1Panel 的 Postgres 容器
- WEB_DIR 默认改为 ../frontend_v2;nginx 切换文件改到 .deploy/ 避免弄脏工作区
- 新增 .gitattributes 固定脚本与配置为 LF
2026-10-21 00:02:18 +08:00

366 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 部署手册(1Panel 单机 · Docker 蓝绿 · Gitea Actions)
> 本文记录**当前实际线上环境**的部署方式。与老模板的差异见文末「与默认模板的差异」。
>
> 最后更新:2026-10-20
---
## 0. 一图看懂
```
浏览器
│ https://fashion.23cm.cn (443, Let's Encrypt)
▼
1Panel OpenResty (80/443,宿主机,已服务 git.23cm.cn 等站点)
│ proxy_pass 127.0.0.1:8080
▼
proxy 容器 (nginx:1.27-alpine,常驻,发布时不重建)
├─ /api/ ──▶ backend_a 或 backend_b :8090
├─ / ──▶ web_a 或 web_b :3000
├─ :18090(仅容器网络)SSR 取数 → 当前活跃后端
├─ 宿主 127.0.0.1:18091 → 后端 SSG :8091(构建期取数)
└─ 宿主 127.0.0.1:18092 → 后端管理后台 :8092
│
▼
1Panel PostgreSQL 容器 (5432, 库 fashion, 用户 user_JA83HF)
```
发布时只重建**空闲槽**,等它 healthy 后改写切换文件并 `nginx -s reload`,在途请求由旧槽跑完。
镜像以 `RELEASE`(`时间戳-后端提交号`)打标签,不可变,历史版本不被覆盖。
部署入口在本仓库(后端仓 `deploy.sh`),脚本会同时同步前端仓。
---
## 1. 服务器现状(只做一次,已就绪)
| 项 | 值 |
| --- | --- |
| 主机 | `43.161.253.59`,Ubuntu 24.04,**2 核 / 3.6G 内存 / 1.9G swap** |
| 面板 | 1Panel v2.0.3(已装 Docker 28.3 + Compose v2.38) |
| Gitea | 容器 `1Panel-gitea-1fEW`(EE 24.2.3,Web `:3000` / SSH `:222`,域名 `git.23cm.cn`) |
| Gitea Runner | 容器 `gitea-runner`(act_runner,label `docker`,job 镜像 `node:22-alpine`) |
| 数据库 | 容器 `1Panel-postgresql-4Ndp`(postgres:18.6,`0.0.0.0:5432`,库 `fashion`,用户 `user_JA83HF`) |
| 反向代理 | 容器 `1Panel-openresty-2mkA`(占 80/443,站点配置在 `/opt/1panel/www/conf.d`) |
| 其它 | MySQL / MariaDB / Redis / PHP-FPM×2(供其它站点用,勿动) |
### 目录
```
/opt/fashion/
├── backend_v2/ # 后端仓(部署入口,含 deploy.sh / docker-compose.yml / nginx/)
├── frontend_v2/ # 前端仓(Astro)
└── spider/ # 爬虫仓(Go CLI,当前暂不部署)
```
三个仓库都以 **`dev`** 分支为准(不是 main)。
### 初始化命令(仅重建机器时参考)
```bash
mkdir -p /opt/fashion && cd /opt/fashion
git clone -b dev https://git.23cm.cn/toom1996/backend_v2.git backend_v2
git clone -b dev https://git.23cm.cn/toom1996/frontend_v2.git frontend_v2
git clone -b dev https://git.23cm.cn/toom1996/spider.git spider
cd backend_v2
cp .env.example .env # 按第 3 节填写
chmod +x deploy.sh
```
> 数据库已存在且有数据,**不需要** `init-db.sh` / 导入 dump。
---
## 2. 关键配置(已改好,重建机器时照抄)
### 2.1 docker-compose.yml
相对上游模板做了 3 处适配:
1. **删掉自带 `db` 服务**:改用 1Panel 现有 Postgres(不再另起 pgvector)。
2. **后端连库走宿主网关**:
```yaml
DB_HOST: host.docker.internal
# backend-base 下:
extra_hosts:
- "host.docker.internal:host-gateway"
```
3. **proxy 只绑宿主回环**(80/443 已被 OpenResty 占用):
```yaml
ports:
- "127.0.0.1:${HTTP_PORT:-8080}:80"
- "127.0.0.1:18091:18091"
- "127.0.0.1:18092:18092"
```
### 2.2 nginx/default.conf(重要)
容器内的 18091 / 18092 **必须监听 0.0.0.0**,不能监听 `127.0.0.1`:
```nginx
listen 18091; # 容器内回环监听会导致 docker 端口发布不可达
listen 18092;
```
原因:Docker 端口发布是转发到**容器网卡**,不是容器回环。监听 `127.0.0.1` 时,
宿主机 `127.0.0.1:18091` 会连接被重置,导致**前端构建期 SSG 取数为空、首页无数据**。
宿主机侧仍只发布到 `127.0.0.1`,不会外泄。
### 2.3 deploy.sh(相对模板新增)
- 首次部署顺序修复:先 `render_active` 再 `--no-deps` 起代理(否则 map 为空 nginx 起不来)。
- 新增 `--no-cache`:强制前端不用构建缓存(见第 5 节 SSG)。
---
## 3. `.env`(敏感值只放服务器,600 权限)
路径:`/opt/fashion/backend_v2/.env`(已 gitignore,**绝不入库**)。
```ini
# 数据库(复用 1Panel 现有 Postgres)
DB_USER=user_JA83HF
DB_NAME=fashion
DB_PASSWORD=<真实口令,见下方说明>
# 密钥
HASHID_SECRET= # 留空=内置默认盐,必须与爬虫 ingest.hashid_secret 一致
JWT_SECRET=<随机长串>
INGEST_SECRET=<与爬虫 ingest.secret 一致>
SSG_TOKEN=<随机串,构建期取 SSG 用>
# 对象存储(当前关闭;现有图片走 CDN 公开桶,仍可正常显示)
S4_ENABLED=false
S4_AK=disabled
S4_SK=disabled
# 目录 / 端口
WEB_DIR=../frontend_v2
HTTP_PORT=8080
CORS_ALLOW_ORIGINS=https://fashion.23cm.cn,https://test.23cm.cn
# 其它
GIT_BRANCH=dev
GOPROXY=https://goproxy.cn,direct
```
> **DB 密码从哪来**:以 Postgres 容器的环境变量为准,而不是旧 `config.yml`:
> ```bash
> docker exec 1Panel-postgresql-4Ndp printenv POSTGRES_PASSWORD
> ```
> 旧 `/opt/fashionapi/config.yml` 里的密码被行尾 `#` 注释截断过,是错的(28P01 的根因)。
---
## 4. 数据库
- 复用 1Panel 的 `postgres:18.6`,库 `fashion`,用户 `user_JA83HF`。
- 已开启 pgvector(`vector` 扩展)。
- 主要表:`brands` / `brand_runways` / `brand_runway_images` / `street_snaps` / `street_snap_images` / `users` / `favorites` / `histories` / `ingest_jobs` …
- 后端不再自动迁移,表结构变更走 `cmd/dbtool`。
---
## 5. 部署方式 A:Gitea Actions(手动触发)
三个仓库的 `.gitea/workflows/deploy.yml` 都是 `workflow_dispatch`,**不会 push 自动上线**。
### 使用
1. 打开 `https://git.23cm.cn/toom1996/<repo>`
2. 顶部 **Actions → deploy → Run workflow**
3. 分支选 **`dev`** → Run
各仓行为:
| 仓库 | 实际执行 |
| --- | --- |
| `backend_v2` | `cd /opt/fashion/backend_v2 && ./deploy.sh backend` |
| `frontend_v2` | `cd /opt/fashion/backend_v2 && ./deploy.sh web --no-cache` |
| `spider` | `git pull` + `go build -o bin/spider .` |
### 原理与 Secrets
- Runner 与服务器同机,job 容器用 `node:22-alpine`,通过 SSH 连 `127.0.0.1` 执行部署。
- 仓库 Secrets:`DEPLOY_HOST=127.0.0.1`、`DEPLOY_USER=root`、`DEPLOY_KEY_B64`(SSH 私钥 base64)。
- 注意:**不要用公网 IP 43.161.253.59 做 DEPLOY_HOST**,云主机常拦截「访问自己公网 IP」的 hairpin,会卡住。
---
## 6. 部署方式 B:手动 SSH
```bash
ssh -i ~/.ssh/id_rsa_hongkong root@43.161.253.59
cd /opt/fashion/backend_v2
./deploy.sh # 只更新后端(默认)
./deploy.sh web # 只更新前端
./deploy.sh all # 前后端都更新
./deploy.sh all --no-pull
./deploy.sh web --no-cache # 强制重建前端(刷新首页 SSG 数据)
./deploy.sh --rollback # 秒级回滚到上一槽
./deploy.sh --prune # 清理旧镜像(默认保留最近 5 个)
GIT_BRANCH=dev ./deploy.sh all
```
**发布流程**:
| 步 | 动作 | 用户感受 |
| --- | --- | --- |
| 1 | 生成 `RELEASE`,`git pull --ff-only`(后端 + 前端) | 无感 |
| 2 | `docker compose build` 构建 `fashion-*:$RELEASE` | 无感(旧槽在跑) |
| 3 | 重建空闲槽 | 无感 |
| 4 | 轮询 health,未通过则退出 | 旧槽继续服务 |
| 5 | 改切换文件 → `nginx -t` → `nginx -s reload` | 在途请求不丢 |
| 6 | 写 `.deploy/releases.log` | — |
---
## 7. 域名与 HTTPS
| 域名 | 指向 | 说明 |
| --- | --- | --- |
| `fashion.23cm.cn` | 127.0.0.1:8080 | **正式入口**,Let's Encrypt 证书,80/443 都通 |
| `test.23cm.cn` | 127.0.0.1:8080 | 同一应用;原为旧静态站,旧配置备份在 `test.23cm.cn.conf.bak` |
- OpenResty 站点配置:`/opt/1panel/www/conf.d/<domain>.conf`
- 证书目录:`/opt/1panel/www/sites/<domain>/ssl/{fullchain.pem,privkey.pem}`
- `fashion.23cm.cn` 证书由 **certbot(webroot)** 签发,自动续期;续期 hook:
`/etc/letsencrypt/renewal-hooks/deploy/fashion-23cm.sh`(复制证书 + `nginx -s reload`)
- 验证:`curl -I https://fashion.23cm.cn/`
> 注意:1Panel 面板里的证书是自签(`CN=localhost`),与 certbot 那张不是同一张;面板「证书」列表里看不到 fashion 的证书属正常。
还原 `test.23cm.cn` 旧静态站:
```bash
cp /opt/1panel/www/conf.d/test.23cm.cn.conf.bak /opt/1panel/www/conf.d/test.23cm.cn.conf
docker exec 1Panel-openresty-2mkA nginx -s reload
```
---
## 8. 爬虫(当前暂不部署)
- 已构建:`/opt/fashion/spider/bin/spider`
- 生产配置:`/opt/fashion/spider/config.prod.yml`(ingest 指向 `http://127.0.0.1:18092`)
- 是 **CLI**,不是守护进程,跑完即退出:
```bash
cd /opt/fashion/spider
./bin/spider -c config.prod.yml vogue -b 1
./bin/spider -c config.prod.yml theimpression -n 5
```
- 若要定时执行,可加 systemd timer(需要时再配)。
---
## 9. 备份
数据库在 1Panel Postgres 容器里,备份命令:
```bash
docker exec 1Panel-postgresql-4Ndp pg_dump -U user_JA83HF fashion \
| gzip > /opt/fashion/backups/db-$(date +%F).sql.gz
```
每日 4 点、保留 7 天(示例):
```cron
0 4 * * * docker exec 1Panel-postgresql-4Ndp pg_dump -U user_JA83HF fashion | gzip > /opt/fashion/backups/db-$(date +\%F).sql.gz && find /opt/fashion/backups -name 'db-*.sql.gz' -mtime +7 -delete
```
---
## 10. 本地构建发布(前后端都不在服务器编译)
服务器资源紧张时用这条路径:**构建全在本机完成**,服务器只负责打镜像层 + 蓝绿切换。
```bash
# 本机(后端仓)执行,一条命令跑完
SERVER=user@server ./scripts/push-build.sh
```
脚本做五件事:
| 步 | 动作 | 说明 |
|---|---|---|
| 1 | 建 SSH 隧道 `本地18091 → 服务器 127.0.0.1:18091` | 前端构建期靠它读**线上** SSG 数据,公网零暴露 |
| 2 | 本机 `npm run build` | 静态页内容与线上一致,不吃服务器内存 |
| 3 | 本机 `CGO_ENABLED=0 GOOS=linux go build` | 产出 35MB 的 `server-linux-amd64` |
| 4 | `tar`/`scp` 上传 `dist/` 与二进制 | 只传产物,不传源码 |
| 5 | 远程 `./deploy.sh all --prebuilt` | 用 `Dockerfile.prebuilt` 打层,蓝绿切换 |
前置条件:
- 本机有 `node` / `npm` / `go` / `ssh` / `scp` / `tar`
- 本机后端仓放一份 `.env`(复制服务器那份,用于取 `SSG_TOKEN`)
- 服务器上 `proxy` 正在跑(18091 是它的端口)
与「服务器构建」的区别:
| | 服务器构建 | 本地构建(push-build) |
|---|---|---|
| 服务器 CPU/内存 | 前端 2~4 分钟 + 1GB 内存 | 只 `COPY` 产物,秒级 |
| 构建期数据 | 线上(天然一致) | 线上(走隧道,同样一致) |
| git 拉取 | 会拉两个仓库 | **跳过**(`--prebuilt` 自动 `--no-pull`) |
| 发布号 | 服务器仓库的 sha | 本机 sha,通过 `RELEASE` 传入 |
两种方式可以混用:后端改动小、想走服务器构建,就 `./deploy.sh backend`;想省资源就 `push-build.sh`。
## 11. 排障(真实案例)
```bash
cd /opt/fashion/backend_v2
docker compose ps
docker compose logs --tail 100 proxy
docker compose logs --tail 100 backend_a
docker compose logs --tail 100 web_b
```
| 现象 | 原因 / 处理 |
| --- | --- |
| 连库报 `28P01 password authentication failed` | 密码不对。以容器 `POSTGRES_PASSWORD` 为准;旧 config 里的可能被 `#` 截断 |
| 首页只有 `Loading`、没有图集 | 构建期 SSG 取数失败。确认 nginx 内部 `listen 18091`(不是 `127.0.0.1:18091`)、proxy 在跑、`curl 127.0.0.1:18091/api/internal/ssg/home/runway?token=…` 有数据;再 `./deploy.sh web --no-cache` |
| 改了内容但首页没变 | 首页是构建期 SSG,需重新部署前端(workflow 已带 `--no-cache`) |
| `nginx: bind() to 0.0.0.0:18091 failed (Address in use)` | 旧进程还占着回环端口,`docker compose restart proxy` |
| 全站接口 404 | `nginx/default.conf` 的 `proxy_pass` 带了 URI/斜杠(会剥掉 `/api` 前缀) |
| 详情页 500 | 前端 `BASE_API_SSR` 必须是 `http://proxy:18090`(容器里的 localhost 是它自己) |
| 图片全 404 | S4 关闭但现有图片走 CDN 正常;新入库图片需补 `S4_AK/S4_SK` 再开 `S4_ENABLED` |
| 发布后仍是旧版本 | 分支不对(用 `dev`);或看 `docker compose logs proxy` 的 reload 报错 |
| 回滚提示「槽不可用」 | 该槽被更早发布重建过,回滚只能退一格 |
| 磁盘涨得快 | `./deploy.sh --prune`;`docker builder prune -f` |
---
## 11. 性能说明(当前瓶颈)
服务器 **2 核 / 3.6G**,且已常驻 Gitea、Postgres、MySQL、MariaDB、Redis、PHP×2、OpenResty、两个 Go 后端、Node 前端。体检时 **swap 1.9G 已用 1.7G**,构建期容易换页,`npm ci + astro build` 明显慢。
建议(按性价比):
1. **长期(推荐)**:构建移到本地 **WSL2 + Docker Engine**,推送到 Gitea 容器仓库,服务器只 `pull + up`。
- 不要把构建目录放在 `/mnt/d/...`(9p 极慢),放 WSL 自己的 `~/project`。
2. **短期(零成本)**:
- 前端用 `--no-cache` 只用于刷新 SSG;进一步可改 `CACHEBUST` 构建参数,只重跑 `npm run build`,`npm ci` 走缓存。
- Dockerfile 加 BuildKit cache mount(npm/go 依赖缓存)。
- `docker builder prune -f` 回收构建缓存;`./deploy.sh --prune` 控制镜像数。
- 可再加 4G swap 降低 OOM 风险。
---
## 12. 与默认模板的差异(为什么这么改)
| 模板假设 | 实际 |
| --- | --- |
| 自带 `db` 服务 + `init-db.sh` | 复用 1Panel 现有 Postgres,库已有数据 |
| 目录 `backend` / `test` | `backend_v2` / `frontend_v2` |
| 默认分支 `main` | `dev` |
| 外层 Caddy / 宿主机 nginx 空着 | 1Panel OpenResty 已占 80/443,应用 proxy 改绑 `127.0.0.1:8080` |
| `S4_AK/S4_SK` 必填 | 当前 `S4_ENABLED=false`,图片走 CDN 公开桶 |
| push 自动部署 | 改为 `workflow_dispatch` 手动触发 |
| nginx 内部 `listen 127.0.0.1:18091` | 容器内必须 `listen 18091`(0.0.0.0),否则端口发布不可达 |