# 部署手册(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/` 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/.conf` - 证书目录:`/opt/1panel/www/sites//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),否则端口发布不可达 |