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

14 KiB
Raw Permalink Blame History

部署手册(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)。

初始化命令(仅重建机器时参考)

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. 后端连库走宿主网关:
    DB_HOST: host.docker.internal
    # backend-base 下:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    
  3. proxy 只绑宿主回环(80/443 已被 OpenResty 占用):
    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:

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,绝不入库)。

# 数据库(复用 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:

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

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 旧静态站:

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,不是守护进程,跑完即退出:
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 容器里,备份命令:

docker exec 1Panel-postgresql-4Ndp pg_dump -U user_JA83HF fashion \
  | gzip > /opt/fashion/backups/db-$(date +%F).sql.gz

每日 4 点、保留 7 天(示例):

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. 本地构建发布(前后端都不在服务器编译)

服务器资源紧张时用这条路径:构建全在本机完成,服务器只负责打镜像层 + 蓝绿切换。

# 本机(后端仓)执行,一条命令跑完
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. 排障(真实案例)

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),否则端口发布不可达