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
This commit is contained in:
12
.env.example
12
.env.example
@ -5,10 +5,18 @@
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
# ---- 数据库 ----
|
# ---- 数据库 ----
|
||||||
|
# 生产复用宿主机上的 Postgres(如 1Panel 自带),容器内经 host-gateway 访问
|
||||||
|
# 下列 DB_* 同时用于容器与 init-db.sh / backup-db.sh(脚本走 --network host)
|
||||||
DB_USER=fashion
|
DB_USER=fashion
|
||||||
DB_NAME=fashion
|
DB_NAME=fashion
|
||||||
# 上线必改:不要沿用开发库口令
|
# 上线必改:不要沿用开发库口令
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
# 容器内访问宿主的固定写法;若数据库在另一台机器,改成那台的地址
|
||||||
|
DB_HOST=host.docker.internal
|
||||||
|
DB_PORT=5432
|
||||||
|
# 1Panel 管理的 Postgres 容器名(init-db.sh / backup-db.sh 直接 docker exec 进去)
|
||||||
|
# 查法:docker ps --format '{{.Names}}' | grep -i postgres
|
||||||
|
PG_CONTAINER=1Panel-postgresql-4Ndp
|
||||||
|
|
||||||
# ---- 后端密钥(必填,缺了容器拒绝启动)----
|
# ---- 后端密钥(必填,缺了容器拒绝启动)----
|
||||||
# 公开 ID 混淆盐:上线后不要再改,改了所有旧链接失效
|
# 公开 ID 混淆盐:上线后不要再改,改了所有旧链接失效
|
||||||
@ -31,8 +39,8 @@ S4_REGION=cn-east-1
|
|||||||
S4_BASE_URL=
|
S4_BASE_URL=
|
||||||
|
|
||||||
# ---- 目录与访问 ----
|
# ---- 目录与访问 ----
|
||||||
# 前端仓库位置(服务器 /opt/fashion 下默认 ../test)
|
# 前端仓库位置(服务器 /opt/fashion 下是 ../frontend_v2)
|
||||||
WEB_DIR=../test
|
WEB_DIR=../frontend_v2
|
||||||
# 前端 nginx 监听端口(有宿主机 nginx/宝塔占用 80 时改成 8080 再由外层反代)
|
# 前端 nginx 监听端口(有宿主机 nginx/宝塔占用 80 时改成 8080 再由外层反代)
|
||||||
HTTP_PORT=80
|
HTTP_PORT=80
|
||||||
CORS_ALLOW_ORIGINS=https://xisoa.com,https://www.xisoa.com
|
CORS_ALLOW_ORIGINS=https://xisoa.com,https://www.xisoa.com
|
||||||
|
|||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Shell 脚本必须保持 LF:CRLF 会让 Linux 上的 bash 直接报语法错误
|
||||||
|
# (典型报错:syntax error near unexpected token `$'do\r'')
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Dockerfile 同理(CRLF 会导致某些解析器把续行符后的 \r 当成参数)
|
||||||
|
Dockerfile text eol=lf
|
||||||
|
Dockerfile.* text eol=lf
|
||||||
|
|
||||||
|
# compose / nginx 配置也统一 LF
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.conf text eol=lf
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -51,4 +51,7 @@ admin_cookies.txt
|
|||||||
|
|
||||||
# 部署状态与备份(deploy.sh 生成,不进版本库)
|
# 部署状态与备份(deploy.sh 生成,不进版本库)
|
||||||
.deploy/
|
.deploy/
|
||||||
backups/
|
backups/
|
||||||
|
|
||||||
|
# 本地预构建产物(scripts/push-build.sh 生成)
|
||||||
|
/server-linux-amd64
|
||||||
38
DEPLOY.md
38
DEPLOY.md
@ -275,7 +275,43 @@ docker exec 1Panel-postgresql-4Ndp pg_dump -U user_JA83HF fashion \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 排障(真实案例)
|
## 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
|
```bash
|
||||||
cd /opt/fashion/backend_v2
|
cd /opt/fashion/backend_v2
|
||||||
|
|||||||
24
Dockerfile.prebuilt
Normal file
24
Dockerfile.prebuilt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 预构建镜像:二进制在本机(GOOS=linux)编好,服务器只负责打层。
|
||||||
|
# 用途:服务器资源紧张时,跳过容器内的 go build。
|
||||||
|
# 由 scripts/push-build.sh 与 deploy.sh --prebuilt 使用;构建上下文里的
|
||||||
|
# server-linux-amd64 由本机交叉编译产出(已在 .gitignore / .dockerignore 中)。
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY server-linux-amd64 /app/server
|
||||||
|
COPY configs ./configs
|
||||||
|
|
||||||
|
RUN mkdir -p /app/uploads
|
||||||
|
|
||||||
|
ENV CONFIG_PATH=/app/configs/config.yml \
|
||||||
|
GIN_MODE=release \
|
||||||
|
UPLOAD_DIR=/app/uploads
|
||||||
|
|
||||||
|
EXPOSE 8090 8091 8092
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:8090/api/health || exit 1
|
||||||
|
|
||||||
|
CMD ["/app/server"]
|
||||||
12
README.md
12
README.md
@ -212,3 +212,15 @@ cp .env.example .env # 填 6 个必填密钥
|
|||||||
镜像按 `RELEASE=时间戳-提交号` 打不可变标签,历史版本不被覆盖。
|
镜像按 `RELEASE=时间戳-提交号` 打不可变标签,历史版本不被覆盖。
|
||||||
|
|
||||||
完整步骤、发布流程、镜像管理、数据库兼容约束与排障见 **[`DEPLOY.md`](./DEPLOY.md)**。
|
完整步骤、发布流程、镜像管理、数据库兼容约束与排障见 **[`DEPLOY.md`](./DEPLOY.md)**。
|
||||||
|
|
||||||
|
### 本地构建(服务器不编译)
|
||||||
|
|
||||||
|
服务器资源紧张时,前后端都可以在本机构建,只把产物传上去:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVER=user@server ./scripts/push-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
它会建 SSH 隧道让前端读**线上** SSG 数据 → 本机 `astro build` → 本机交叉编译后端 →
|
||||||
|
上传 `dist/` 与 `server-linux-amd64` → 远程 `deploy.sh all --prebuilt`。
|
||||||
|
详见 [`DEPLOY.md` 第 10 节](./DEPLOY.md)。
|
||||||
|
|||||||
40
deploy.sh
40
deploy.sh
@ -8,6 +8,7 @@
|
|||||||
# ./deploy.sh all --no-pull 用服务器上现有代码构建
|
# ./deploy.sh all --no-pull 用服务器上现有代码构建
|
||||||
# ./deploy.sh web --no-cache 强制不用构建缓存(前端 SSG 依赖后端数据,数据变了要强制重建)
|
# ./deploy.sh web --no-cache 强制不用构建缓存(前端 SSG 依赖后端数据,数据变了要强制重建)
|
||||||
# ./deploy.sh --prune 只清理旧镜像(保留最近 KEEP_IMAGES 个,默认 5)
|
# ./deploy.sh --prune 只清理旧镜像(保留最近 KEEP_IMAGES 个,默认 5)
|
||||||
|
# ./deploy.sh all --prebuilt 用已上传的预构建产物(不拉代码、不在容器内编译)
|
||||||
# GIT_BRANCH=dev ./deploy.sh all 临时指定分支(优先于 .env)
|
# GIT_BRANCH=dev ./deploy.sh all 临时指定分支(优先于 .env)
|
||||||
#
|
#
|
||||||
# 每次发布生成 RELEASE=<时间戳>-<后端提交号>,镜像以此打标签(不可变,不覆盖历史)。
|
# 每次发布生成 RELEASE=<时间戳>-<后端提交号>,镜像以此打标签(不可变,不覆盖历史)。
|
||||||
@ -19,6 +20,7 @@ TARGET="backend"
|
|||||||
NO_PULL=0
|
NO_PULL=0
|
||||||
NO_CACHE=0
|
NO_CACHE=0
|
||||||
PRUNE_ONLY=0
|
PRUNE_ONLY=0
|
||||||
|
PREBUILT=0
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
backend|web|all) TARGET="$arg" ;;
|
backend|web|all) TARGET="$arg" ;;
|
||||||
@ -26,7 +28,8 @@ for arg in "$@"; do
|
|||||||
--no-pull) NO_PULL=1 ;;
|
--no-pull) NO_PULL=1 ;;
|
||||||
--no-cache) NO_CACHE=1 ;;
|
--no-cache) NO_CACHE=1 ;;
|
||||||
--prune) PRUNE_ONLY=1 ;;
|
--prune) PRUNE_ONLY=1 ;;
|
||||||
*) echo "用法: ./deploy.sh [backend|web|all|--rollback|--prune] [--no-pull] [--no-cache]"; exit 1 ;;
|
--prebuilt) PREBUILT=1 ;;
|
||||||
|
*) echo "用法: ./deploy.sh [backend|web|all|--rollback|--prune] [--no-pull] [--no-cache] [--prebuilt]"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
@ -38,7 +41,7 @@ set -a; . ./.env; set +a
|
|||||||
[ -n "$CLI_GIT_BRANCH" ] && GIT_BRANCH="$CLI_GIT_BRANCH"
|
[ -n "$CLI_GIT_BRANCH" ] && GIT_BRANCH="$CLI_GIT_BRANCH"
|
||||||
GIT_BRANCH="${GIT_BRANCH:-main}"
|
GIT_BRANCH="${GIT_BRANCH:-main}"
|
||||||
KEEP_IMAGES="${KEEP_IMAGES:-5}"
|
KEEP_IMAGES="${KEEP_IMAGES:-5}"
|
||||||
WEB_DIR="${WEB_DIR:-../test}"
|
WEB_DIR="${WEB_DIR:-../frontend_v2}"
|
||||||
|
|
||||||
STATE_DIR=".deploy"
|
STATE_DIR=".deploy"
|
||||||
ACTIVE_CONF="$STATE_DIR/nginx/active.conf"
|
ACTIVE_CONF="$STATE_DIR/nginx/active.conf"
|
||||||
@ -201,12 +204,45 @@ prune_images() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---- 预构建产物校验 ----
|
||||||
|
# 本地构建 + 上传的路径:镜像只负责打包,不在服务器上编译。
|
||||||
|
# 同时强制 --no-pull:代码已在本地构建完毕,服务器仓库不需要(也不应该)再更新。
|
||||||
|
use_prebuilt() {
|
||||||
|
PREBUILT=1
|
||||||
|
NO_PULL=1
|
||||||
|
export BACKEND_DOCKERFILE=Dockerfile.prebuilt
|
||||||
|
export WEB_DOCKERFILE=Dockerfile.prebuilt
|
||||||
|
echo "==> 使用预构建产物(不在服务器编译)"
|
||||||
|
case "$TARGET" in
|
||||||
|
backend|all)
|
||||||
|
[ -f server-linux-amd64 ] || {
|
||||||
|
echo "!! 缺少 server-linux-amd64:先在本机执行 scripts/push-build.sh"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " 后端二进制:$(du -h server-linux-amd64 | cut -f1)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case "$TARGET" in
|
||||||
|
web|all)
|
||||||
|
[ -f "$WEB_DIR/dist/server/entry.mjs" ] || {
|
||||||
|
echo "!! 缺少 $WEB_DIR/dist/server/entry.mjs:前端还没构建或没上传"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " 前端产物:$WEB_DIR/dist"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ---- 主流程 ----
|
# ---- 主流程 ----
|
||||||
if [ "$PRUNE_ONLY" = "1" ]; then
|
if [ "$PRUNE_ONLY" = "1" ]; then
|
||||||
prune_images
|
prune_images
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$PREBUILT" = "1" ] && [ "$TARGET" != "rollback" ]; then
|
||||||
|
use_prebuilt
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$TARGET" != "rollback" ]; then
|
if [ "$TARGET" != "rollback" ]; then
|
||||||
export RELEASE="${RELEASE:-$(new_release)}"
|
export RELEASE="${RELEASE:-$(new_release)}"
|
||||||
echo "==> 发布号 $RELEASE"
|
echo "==> 发布号 $RELEASE"
|
||||||
|
|||||||
@ -116,6 +116,8 @@ services:
|
|||||||
<<: *backend-base
|
<<: *backend-base
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
# 默认在容器内编译;本地预构建时由 deploy.sh --prebuilt 切到 Dockerfile.prebuilt
|
||||||
|
dockerfile: ${BACKEND_DOCKERFILE:-Dockerfile}
|
||||||
args:
|
args:
|
||||||
GOPROXY: ${GOPROXY:-https://goproxy.cn,direct}
|
GOPROXY: ${GOPROXY:-https://goproxy.cn,direct}
|
||||||
|
|
||||||
@ -127,6 +129,8 @@ services:
|
|||||||
<<: *web-base
|
<<: *web-base
|
||||||
build:
|
build:
|
||||||
context: ${WEB_DIR:-../test}
|
context: ${WEB_DIR:-../test}
|
||||||
|
# 默认在容器内 npm ci + astro build;本地预构建时切到 Dockerfile.prebuilt
|
||||||
|
dockerfile: ${WEB_DOCKERFILE:-Dockerfile}
|
||||||
args:
|
args:
|
||||||
# 走宿主回环的代理 18091,由代理转发到活跃后端槽
|
# 走宿主回环的代理 18091,由代理转发到活跃后端槽
|
||||||
BASE_API_SSG: http://127.0.0.1:18091
|
BASE_API_SSG: http://127.0.0.1:18091
|
||||||
|
|||||||
52
init-db.sh
52
init-db.sh
@ -1,16 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 首次建库:把导出的 SQL 灌进空的 PostgreSQL 容器
|
# 首次建库:把导出的 SQL 灌进 PostgreSQL
|
||||||
#
|
#
|
||||||
# ./init-db.sh /path/to/dump.sql
|
# ./init-db.sh /path/to/dump.sql
|
||||||
#
|
#
|
||||||
# 前置:在开发机导出最新结构与数据(含 pgvector 扩展):
|
# 数据库由 1Panel 管理(独立的 postgres 容器,端口不对外发布),所以直接
|
||||||
# go run ./cmd/dbtool dump -with-extension -out db/backups/db_dump.sql
|
# docker exec 进那个容器跑 psql —— 服务器上不需要装 psql,也不依赖宿主端口。
|
||||||
#
|
#
|
||||||
# ⚠️ 仓库里 db/backups/db_dump.sql 是 2026-09-21 导出的,比当前代码少了之后两个月
|
# 容器名在 .env 的 PG_CONTAINER 里;不确定就看:
|
||||||
# 的新增迁移(向量表 / 近邻对表 / 品牌重编号 / published_at / image_size 等),
|
# docker ps --format '{{.Names}}' | grep -i postgres
|
||||||
# 直接拿它建库会导致后端启动后字段缺失——所以这里必须显式传入文件路径。
|
|
||||||
#
|
#
|
||||||
# 注意:只在库为空时执行一次;生产库已有数据时重跑会与现有行冲突。
|
# 注意:只在库为空时执行一次;已有数据时重跑会与现有行冲突。
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
@ -22,24 +21,39 @@ if [ -z "$DUMP" ]; then
|
|||||||
echo "用法:./init-db.sh <dump.sql>"
|
echo "用法:./init-db.sh <dump.sql>"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 请传入一份与当前代码匹配的 SQL(含全部新增迁移)。"
|
echo " 请传入一份与当前代码匹配的 SQL(含全部新增迁移)。"
|
||||||
echo " 导出命令:go run ./cmd/dbtool dump -with-extension -out db/backups/db_dump.sql"
|
echo " 导出命令(开发机):go run ./cmd/dbtool dump -with-extension -out db/backups/db_dump.sql"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
[ -f "$DUMP" ] || { echo "!! 找不到 dump 文件:$DUMP"; exit 1; }
|
[ -f "$DUMP" ] || { echo "!! 找不到 dump 文件:$DUMP"; exit 1; }
|
||||||
|
|
||||||
echo "==> 启动数据库"
|
PG_CONTAINER="${PG_CONTAINER:?PG_CONTAINER 未设置:填 .env 里那个 postgres 容器名}"
|
||||||
docker compose up -d db
|
if ! docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
|
||||||
|
echo "!! 容器不存在:$PG_CONTAINER"
|
||||||
|
echo " docker ps --format '{{.Names}}' | grep -i postgres"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> 等待数据库就绪"
|
run_psql() {
|
||||||
for i in $(seq 1 30); do
|
docker exec -i -e PGPASSWORD="${DB_PASSWORD:?DB_PASSWORD 未设置}" "$PG_CONTAINER" \
|
||||||
if docker compose exec -T db pg_isready -U "${DB_USER:-fashion}" -d "${DB_NAME:-fashion}" >/dev/null 2>&1; then
|
psql -U "${DB_USER:-fashion}" -d "${DB_NAME:-fashion}" "$@"
|
||||||
break
|
}
|
||||||
fi
|
|
||||||
[ "$i" -eq 30 ] && { echo "!! 数据库 60 秒未就绪"; docker compose logs --tail 30 db; exit 1; }
|
echo "==> 检查连接(容器 $PG_CONTAINER)"
|
||||||
sleep 2
|
if ! run_psql -c 'select 1' >/dev/null 2>&1; then
|
||||||
done
|
echo "!! 连不上。检查 DB_USER / DB_NAME / DB_PASSWORD 是否与该容器一致"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 检查 pgvector"
|
||||||
|
if run_psql -tAc "select 1 from pg_available_extensions where name = 'vector'" 2>/dev/null | grep -q 1; then
|
||||||
|
echo " pgvector 可用"
|
||||||
|
else
|
||||||
|
echo " !! 该 Postgres 没有 pgvector:dump 里的 vector 类型会导入失败"
|
||||||
|
echo " 换用带 pgvector 的镜像,或先安装扩展后重试"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> 导入 $DUMP"
|
echo "==> 导入 $DUMP"
|
||||||
docker compose exec -T db psql -U "${DB_USER:-fashion}" -d "${DB_NAME:-fashion}" -v ON_ERROR_STOP=1 < "$DUMP"
|
run_psql -v ON_ERROR_STOP=1 < "$DUMP"
|
||||||
|
|
||||||
echo "==> 完成。可以执行 ./deploy.sh all 启动全部服务"
|
echo "==> 完成。可以执行 ./deploy.sh all 启动全部服务"
|
||||||
|
|||||||
30
scripts/backup-db.sh
Normal file
30
scripts/backup-db.sh
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 备份 1Panel 管理的 Postgres(直接 docker exec 进它的容器,不依赖宿主端口)
|
||||||
|
#
|
||||||
|
# ./scripts/backup-db.sh # 输出 backups/db-YYYY-MM-DD-HHMMSS.sql.gz
|
||||||
|
# KEEP_DAYS=7 ./scripts/backup-db.sh # 顺手删掉 7 天前的备份
|
||||||
|
#
|
||||||
|
# cron(每天 4 点):
|
||||||
|
# 0 4 * * * cd /opt/fashion/backend && KEEP_DAYS=7 ./scripts/backup-db.sh >> .deploy/backup.log 2>&1
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
[ -f .env ] || { echo "!! 缺少 .env"; exit 1; }
|
||||||
|
set -a; . ./.env; set +a
|
||||||
|
|
||||||
|
PG_CONTAINER="${PG_CONTAINER:?PG_CONTAINER 未设置:填 .env 里那个 postgres 容器名}"
|
||||||
|
KEEP_DAYS="${KEEP_DAYS:-0}"
|
||||||
|
mkdir -p backups
|
||||||
|
|
||||||
|
OUT="backups/db-$(date +%F-%H%M%S).sql.gz"
|
||||||
|
echo "==> 备份到 $OUT"
|
||||||
|
docker exec -e PGPASSWORD="${DB_PASSWORD:?DB_PASSWORD 未设置}" "$PG_CONTAINER" \
|
||||||
|
pg_dump -U "${DB_USER:-fashion}" "${DB_NAME:-fashion}" | gzip > "$OUT"
|
||||||
|
|
||||||
|
echo " $(du -h "$OUT" | cut -f1)"
|
||||||
|
|
||||||
|
if [ "$KEEP_DAYS" -gt 0 ]; then
|
||||||
|
find backups -name 'db-*.sql.gz' -mtime +"$KEEP_DAYS" -print -delete
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 完成"
|
||||||
105
scripts/push-build.sh
Normal file
105
scripts/push-build.sh
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 本地构建 + 上传 + 远程发布(前后端都走预构建,服务器不编译)
|
||||||
|
#
|
||||||
|
# SERVER=user@server ./scripts/push-build.sh
|
||||||
|
# SERVER=user@server REMOTE_DIR=/opt/fashion WEB_DIR=../test ./scripts/push-build.sh
|
||||||
|
#
|
||||||
|
# 流程:
|
||||||
|
# 1. 建 SSH 隧道(本地 18091 → 服务器回环 18091):前端构建期靠它读线上 SSG 数据
|
||||||
|
# 2. 本机 npm run build → 静态页内容与线上数据一致,且不占服务器资源
|
||||||
|
# 3. 本机交叉编译 linux/amd64 → 产出 server-linux-amd64
|
||||||
|
# 4. 上传 dist/ 与二进制
|
||||||
|
# 5. 远程 ./deploy.sh all --prebuilt → 服务器只打镜像 + 蓝绿切换
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.." # 后端仓根
|
||||||
|
|
||||||
|
SERVER="${SERVER:-}"
|
||||||
|
if [ -z "$SERVER" ]; then
|
||||||
|
echo "用法: SERVER=user@host ./scripts/push-build.sh"
|
||||||
|
echo ""
|
||||||
|
echo "可选环境变量:"
|
||||||
|
echo " REMOTE_DIR 服务器上的项目根(默认 /opt/fashion)"
|
||||||
|
echo " WEB_DIR 本地前端仓(默认 ../test)"
|
||||||
|
echo " SSH_OPTS 额外 ssh 参数(默认 -o ConnectTimeout=10)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
REMOTE_DIR="${REMOTE_DIR:-/opt/fashion}"
|
||||||
|
REMOTE_BACKEND="${REMOTE_BACKEND:-${REMOTE_DIR}/backend_v2}"
|
||||||
|
REMOTE_FRONTEND="${REMOTE_FRONTEND:-${REMOTE_DIR}/frontend_v2}"
|
||||||
|
WEB_DIR="${WEB_DIR:-../frontend_v2}"
|
||||||
|
SSH_OPTS="${SSH_OPTS:--o ConnectTimeout=10}"
|
||||||
|
TUNNEL_PORT="${TUNNEL_PORT:-18091}"
|
||||||
|
|
||||||
|
# 本机也放一份 .env(复制服务器那份即可),用于取 SSG_TOKEN 等构建期密钥
|
||||||
|
SSG_TOKEN="${SSG_TOKEN:-}"
|
||||||
|
if [ -f .env ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
set -a; . ./.env; set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
TUNNEL_PID=""
|
||||||
|
cleanup() { [ -n "$TUNNEL_PID" ] && kill "$TUNNEL_PID" 2>/dev/null || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "==> 0/5 检查 SSH 免密登录"
|
||||||
|
# 隧道是后台起的(ssh -N ... &),一旦需要交互输密码就会静默卡住,所以必须免密。
|
||||||
|
if ! ssh $SSH_OPTS -o BatchMode=yes "$SERVER" true 2>/dev/null; then
|
||||||
|
echo "!! 无法免密登录 $SERVER"
|
||||||
|
echo ""
|
||||||
|
echo " Windows(PowerShell):"
|
||||||
|
echo " ssh-keygen -t ed25519 -C deploy@local"
|
||||||
|
echo " type \$env:USERPROFILE\\.ssh\\id_ed25519.pub | ssh $SERVER \"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys\""
|
||||||
|
echo ""
|
||||||
|
echo " Linux / macOS / WSL:"
|
||||||
|
echo " ssh-keygen -t ed25519 -C deploy@local && ssh-copy-id $SERVER"
|
||||||
|
echo ""
|
||||||
|
echo " 配好后用这条确认:ssh $SERVER true && echo ok"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " 通过"
|
||||||
|
|
||||||
|
# 服务器若开了 SSG_TOKEN,构建期必须带同一个值。自动从服务器 .env 取,避免本机再维护一份密钥。
|
||||||
|
if [ -z "${SSG_TOKEN:-}" ]; then
|
||||||
|
SSG_TOKEN="$(ssh $SSH_OPTS -o BatchMode=yes "$SERVER" \
|
||||||
|
"grep -m1 '^SSG_TOKEN=' '${REMOTE_BACKEND}/.env' 2>/dev/null | cut -d= -f2-" 2>/dev/null || true)"
|
||||||
|
if [ -n "$SSG_TOKEN" ]; then
|
||||||
|
echo " 已从服务器读取 SSG_TOKEN(${#SSG_TOKEN} 字符)"
|
||||||
|
else
|
||||||
|
echo " 服务器未设置 SSG_TOKEN,构建期不带 token"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> 1/5 建立 SSH 隧道(本地 ${TUNNEL_PORT} → ${SERVER}:127.0.0.1:18091)"
|
||||||
|
ssh -N $SSH_OPTS -o ExitOnForwardFailure=yes \
|
||||||
|
-L "${TUNNEL_PORT}:127.0.0.1:18091" "$SERVER" &
|
||||||
|
TUNNEL_PID=$!
|
||||||
|
|
||||||
|
for i in $(seq 1 15); do
|
||||||
|
if (echo > "/dev/tcp/127.0.0.1/${TUNNEL_PORT}") >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 15 ]; then
|
||||||
|
echo "!! 隧道 15 秒内没建起来(检查 SSH 连通性、服务器上 proxy 是否在跑)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> 2/5 构建前端(构建期数据来自线上)"
|
||||||
|
(cd "$WEB_DIR" && BASE_API_SSG="http://127.0.0.1:${TUNNEL_PORT}" SSG_TOKEN="$SSG_TOKEN" npm run build)
|
||||||
|
[ -f "$WEB_DIR/dist/server/entry.mjs" ] || { echo "!! 前端产物缺失:$WEB_DIR/dist/server/entry.mjs"; exit 1; }
|
||||||
|
|
||||||
|
echo "==> 3/5 交叉编译后端(linux/amd64)"
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags "-s -w" -o server-linux-amd64 ./cmd/server
|
||||||
|
|
||||||
|
echo "==> 4/5 上传产物到 ${SERVER}:${REMOTE_DIR}"
|
||||||
|
ssh $SSH_OPTS "$SERVER" "mkdir -p '${REMOTE_BACKEND}' '${REMOTE_FRONTEND}'"
|
||||||
|
tar -C "$WEB_DIR" -czf - dist | ssh $SSH_OPTS "$SERVER" "tar -xzf - -C '${REMOTE_FRONTEND}'"
|
||||||
|
scp $SSH_OPTS server-linux-amd64 "${SERVER}:${REMOTE_BACKEND}/server-linux-amd64"
|
||||||
|
|
||||||
|
echo "==> 5/5 远程发布(预构建 + 蓝绿切换)"
|
||||||
|
RELEASE="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD 2>/dev/null || echo nogit)"
|
||||||
|
ssh $SSH_OPTS "$SERVER" "cd '${REMOTE_BACKEND}' && RELEASE='${RELEASE}' ./deploy.sh all --prebuilt"
|
||||||
|
|
||||||
|
echo "完成:$RELEASE"
|
||||||
Reference in New Issue
Block a user