PuppetGC 生产服务器部署复盘

木偶AI正在绞尽脑汁想思路ING···
木偶のAI摘要
DeepSeek-Chat

这篇不是单纯命令清单,而是一份可复盘的上线笔记:部署路径、脚本、检查命令、故障现象和处理方式都放在一起,后面排查同类问题可以直接按章节跳转。

一、部署目标

这次要把 PuppetGC 部署到 Ubuntu 服务器,项目目录固定为:

1
/opt/PuppetGC

整体结构如下:

1
2
3
4
5
6
7
8
Nginx
├─ puppetgc.cn / www.puppetgc.cn 官网静态资源
├─ app.puppetgc.cn 创作者端静态资源
├─ admin.puppetgc.cn 管理后台静态资源
└─ api.puppetgc.cn 反向代理到 127.0.0.1:9620

FastAPI
└─ puppetgc-api.service systemd 托管,仅监听本机 9620

核心原则:

API 不直接暴露公网,只允许 Nginx 反代访问

真实生产密钥只放在 config/production.env,不提交 Git

SQLite 数据库和上传文件每天自动备份

所有上线前检查都以 /health 为准

部署过程

  1. 推送代码

本地完成提交并推送到 GitHub,排除 PuppetGC.tarPuppetGC.tar.zip 这类大包产物。

  1. 服务器拉取

服务器 /opt/PuppetGC 拉取最新代码,准备 config/production.env

  1. 启动 API

执行 deploy/scripts/deploy-api.sh,创建 Python 3.13 虚拟环境,安装依赖并写入 systemd 服务。

  1. 排查失败

curl 127.0.0.1:9620/health 失败后,通过 journalctl 定位到生产弱密码校验失败。

  1. 补齐备份

配置 backup-sqlite.sh + root crontab,每天凌晨自动备份数据库和上传文件。

二、服务器基础准备

先安装基础依赖:

1
2
sudo apt update
sudo apt install -y nginx git curl rsync sqlite3 python3.13 python3.13-venv

确认版本:

1
2
3
python3.13 --version
sqlite3 --version
nginx -v

准备目录:

1
2
3
sudo mkdir -p /opt/PuppetGC
sudo mkdir -p /var/log/puppetgc
sudo mkdir -p /var/backups/puppetgc

三、拉取代码

进入部署目录:

1
2
3
cd /opt
sudo git clone https://github.com/Pupper0601/PuppetGC.git
cd /opt/PuppetGC

后续更新代码:

1
2
cd /opt/PuppetGC
git pull --ff-only

如果服务器网络访问 GitHub 不稳定,可以临时配置代理再拉取:

1
2
3
export HTTP_PROXY=http://127.0.0.1:7890
export HTTPS_PROXY=http://127.0.0.1:7890
git pull --ff-only

四、生产配置

复制模板:

1
2
3
cd /opt/PuppetGC
cp config/production.env.example config/production.env
nano config/production.env

关键配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PORT=9620
APP_ENV=production
JWT_SECRET=替换为至少32位随机字符串
ENCRYPTION_KEY=替换为生产随机密钥
CORS_ALLOW_ORIGINS=https://admin.puppetgc.cn,https://app.puppetgc.cn,https://puppetgc.cn,https://www.puppetgc.cn

ADMIN_SEED_EMAIL=admin@example.com
ADMIN_SEED_PASSWORD=替换为至少12位强密码

SMTP_HOST=smtp.example.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=mail@example.com
SMTP_PASS=替换为SMTP授权码
SMTP_FROM=mail@example.com
SMTP_FROM_NAME=PuppetGC
MAIL_LOG_CODE=false

生成随机密钥可以用:

1
openssl rand -hex 32

设置权限:

1
2
sudo chown root:www-data config/production.env
sudo chmod 640 config/production.env

注意:production.env 是真实生产配置,必须在 .gitignore 中排除,不能提交。

APP_ENV=production 打开后,API 会在启动阶段主动校验生产配置。弱密码、默认密钥、不安全 CORS 都会让服务启动失败,这是故意设计的安全闸门。

五、部署 API 服务

项目里已经准备了部署脚本:

1
2
cd /opt/PuppetGC
bash deploy/scripts/deploy-api.sh

脚本主要做这些事:

  1. 检查 config/production.env 是否存在。
  2. 拉取最新代码。
  3. 创建或复用 apps/api-py/.venv
  4. 安装 Python 依赖。
  5. 初始化数据目录和日志目录权限。
  6. 写入 systemd 服务 puppetgc-api.service
  7. 重启 API。
  8. 请求 http://127.0.0.1:9620/health 做健康检查。

部署后检查:

1
2
sudo systemctl status puppetgc-api --no-pager
curl http://127.0.0.1:9620/health

正常应该返回类似:

1
{"ok":true}
1
2
cd /opt/PuppetGC
bash deploy/scripts/deploy-api.sh
1
2
sudo systemctl status puppetgc-api --no-pager
sudo journalctl -u puppetgc-api -n 100 --no-pager
1
2
curl http://127.0.0.1:9620/health
sudo ss -lntp | grep 9620 || true

六、Nginx 配置

API 域名的重点是反向代理到本机 9620:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name api.puppetgc.cn;

location / {
proxy_pass http://127.0.0.1:9620;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

检查并重载:

1
2
sudo nginx -t
sudo systemctl reload nginx

公网验证:

1
curl http://api.puppetgc.cn/health

如果已经配置 HTTPS:

1
curl https://api.puppetgc.cn/health

七、每日备份

备份脚本:

1
/opt/PuppetGC/deploy/scripts/backup-sqlite.sh

脚本默认备份:

  • SQLite 数据库:/opt/PuppetGC/apps/api-py/data/puppetgc.db
  • 上传文件:/opt/PuppetGC/apps/api-py/data/uploads
  • 备份目录:/var/backups/puppetgc
  • 保留天数:14 天

先手动验证:

1
2
3
sudo chmod +x /opt/PuppetGC/deploy/scripts/backup-sqlite.sh
sudo /opt/PuppetGC/deploy/scripts/backup-sqlite.sh
ls -lh /var/backups/puppetgc

配置每日凌晨 03:20 自动备份:

1
sudo crontab -e

加入:

1
20 3 * * * /opt/PuppetGC/deploy/scripts/backup-sqlite.sh >/var/log/puppetgc-backup.log 2>&1

如果想保留 30 天:

1
20 3 * * * KEEP_DAYS=30 /opt/PuppetGC/deploy/scripts/backup-sqlite.sh >/var/log/puppetgc-backup.log 2>&1

查看备份日志:

1
sudo tail -n 100 /var/log/puppetgc-backup.log

备份脚本要先手动跑通一次,再交给 crontab。这样可以提前发现 sqlite3 未安装、目录权限不对、数据库文件路径不一致等问题。

八、部署脚本留档

这些脚本是这次部署闭环的关键,放在文章里是为了后面复盘时不用再翻仓库。真实生产密钥仍然只在服务器 config/production.env,脚本里不能写死。

deploy-api.sh:部署并重启 API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="${REPO_DIR:-/opt/PuppetGC}"
PYTHON_BIN="${PYTHON_BIN:-python3.13}"
SERVICE_NAME="${SERVICE_NAME:-puppetgc-api.service}"
SERVICE_TARGET="/etc/systemd/system/${SERVICE_NAME}"

cd "${REPO_DIR}"

if [ ! -f "config/production.env" ]; then
echo "缺少 config/production.env,请先复制 production.env.example 并填写真实生产配置。"
exit 1
fi

sudo chown root:www-data "${REPO_DIR}/config/production.env"
sudo chmod 640 "${REPO_DIR}/config/production.env"

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git pull --ff-only
fi

cd "${REPO_DIR}/apps/api-py"

if ! command -v "${PYTHON_BIN}" >/dev/null 2>&1; then
echo "未找到 ${PYTHON_BIN}。请先安装 Python 3.13,或用 PYTHON_BIN=/path/to/python3.13 指定解释器。"
exit 1
fi

if [ -d ".venv" ]; then
VENV_VERSION="$(.venv/bin/python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
if [ "${VENV_VERSION}" != "3.13" ]; then
echo "当前 apps/api-py/.venv 使用 Python ${VENV_VERSION},项目生产部署要求 Python 3.13。"
echo "请执行:rm -rf ${REPO_DIR}/apps/api-py/.venv"
echo "然后重新运行:bash deploy/scripts/deploy-api.sh"
exit 1
fi
else
"${PYTHON_BIN}" -m venv .venv
fi

. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt

sudo mkdir -p "${REPO_DIR}/apps/api-py/data"
sudo mkdir -p /var/log/puppetgc
sudo chown -R www-data:www-data "${REPO_DIR}/apps/api-py/data" /var/log/puppetgc

sed "s#__REPO_DIR__#${REPO_DIR}#g" "${REPO_DIR}/deploy/systemd/puppetgc-api.service" | sudo tee "${SERVICE_TARGET}" >/dev/null

sudo systemctl daemon-reload
sudo systemctl enable puppetgc-api
sudo systemctl restart puppetgc-api

sleep 2
curl -fsS http://127.0.0.1:9620/health
echo
echo "PuppetGC API 已启动。"
backup-sqlite.sh:每日备份 SQLite 和上传文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="${REPO_DIR:-/opt/PuppetGC}"
BACKUP_DIR="${BACKUP_DIR:-/var/backups/puppetgc}"
KEEP_DAYS="${KEEP_DAYS:-14}"
STAMP="$(date +%Y%m%d-%H%M%S)"
DATA_DIR="${REPO_DIR}/apps/api-py/data"
DB_PATH="${DATA_DIR}/puppetgc.db"
TARGET_DIR="${BACKUP_DIR}/${STAMP}"

mkdir -p "${TARGET_DIR}"

if [ -f "${DB_PATH}" ]; then
sqlite3 "${DB_PATH}" ".backup '${TARGET_DIR}/puppetgc.db'"
else
echo "未找到数据库文件:${DB_PATH}"
fi

if [ -d "${DATA_DIR}/uploads" ]; then
tar -czf "${TARGET_DIR}/uploads.tar.gz" -C "${DATA_DIR}" uploads
fi

find "${BACKUP_DIR}" -mindepth 1 -maxdepth 1 -type d -mtime +"${KEEP_DAYS}" -exec rm -rf {} +

echo "备份完成:${TARGET_DIR}"
deploy-static.sh:构建并发布静态站点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="${REPO_DIR:-/opt/PuppetGC}"
WEB_ROOT="${WEB_ROOT:-/var/www/puppetgc}"
VITE_API_BASE_URL="${VITE_API_BASE_URL:-https://api.puppetgc.cn}"
VITE_APP_URL="${VITE_APP_URL:-https://app.puppetgc.cn}"
VITE_ADMIN_URL="${VITE_ADMIN_URL:-https://admin.puppetgc.cn}"
VITE_DOWNLOAD_URL="${VITE_DOWNLOAD_URL:-https://download.puppetgc.cn}"

export VITE_API_BASE_URL
export VITE_APP_URL
export VITE_ADMIN_URL
export VITE_DOWNLOAD_URL

cd "${REPO_DIR}"

if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git pull --ff-only
fi

pnpm install --frozen-lockfile
pnpm build:web
pnpm build:admin
pnpm build:website

sudo mkdir -p "${WEB_ROOT}/app" "${WEB_ROOT}/admin" "${WEB_ROOT}/www" "${WEB_ROOT}/download/windows" "${WEB_ROOT}/download/mac"
sudo rsync -a --delete "${REPO_DIR}/apps/desktop/src/renderer/dist/" "${WEB_ROOT}/app/"
sudo rsync -a --delete "${REPO_DIR}/apps/admin/dist/" "${WEB_ROOT}/admin/"
sudo rsync -a --delete "${REPO_DIR}/apps/website/dist/" "${WEB_ROOT}/www/"
sudo chown -R www-data:www-data "${WEB_ROOT}"

echo "静态站点已部署:${WEB_ROOT}"
apply-upload-package.sh:无 Git 场景下应用上传包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env bash
set -euo pipefail

PACKAGE_PATH="${1:-/tmp/PuppetGC.zip}"
REPO_DIR="${REPO_DIR:-/opt/PuppetGC}"
WORK_DIR="$(mktemp -d /tmp/puppetgc-upload.XXXXXX)"

cleanup() {
rm -rf "${WORK_DIR}"
}
trap cleanup EXIT

if [ ! -f "${PACKAGE_PATH}" ]; then
echo "未找到上传包:${PACKAGE_PATH}"
exit 1
fi

if ! command -v unzip >/dev/null 2>&1; then
echo "缺少 unzip,请先执行:sudo apt install -y unzip"
exit 1
fi

if ! command -v rsync >/dev/null 2>&1; then
echo "缺少 rsync,请先执行:sudo apt install -y rsync"
exit 1
fi

mkdir -p "${REPO_DIR}"
unzip -q "${PACKAGE_PATH}" -d "${WORK_DIR}"

SOURCE_DIR="${WORK_DIR}"
if [ -d "${WORK_DIR}/PuppetGC" ]; then
SOURCE_DIR="${WORK_DIR}/PuppetGC"
fi

if [ ! -f "${SOURCE_DIR}/package.json" ] || [ ! -d "${SOURCE_DIR}/apps" ]; then
echo "上传包结构不正确:需要包含 package.json 和 apps/。"
exit 1
fi

rsync -a --delete \
--exclude '.git/' \
--exclude 'node_modules/' \
--exclude '.venv/' \
--exclude 'dist/' \
--exclude 'out/' \
--exclude 'config/production.env' \
--exclude 'apps/api-py/data/' \
"${SOURCE_DIR}/" "${REPO_DIR}/"

echo "上传包已应用到 ${REPO_DIR}。生产配置与 API 数据目录已保留。"

这几个脚本的共同点是:先失败、再退出。set -euo pipefail 可以避免命令失败后继续往下跑,部署脚本尤其需要这种保守策略。

九、问题一:curl 本机 9620 失败

现象:

1
2
curl http://127.0.0.1:9620/health
curl: (7) Failed to connect to 127.0.0.1 port 9620 after 0 ms: Couldn't connect to server

判断:

这不是 Nginx 问题,而是 API 进程没有监听 9620。

排查命令:

1
2
3
sudo systemctl status puppetgc-api --no-pager
sudo journalctl -u puppetgc-api -n 100 --no-pager
sudo ss -lntp | grep 9620 || true

这三个命令分别确认:

  • systemd 服务是否启动。
  • 服务启动失败的具体异常。
  • 9620 端口是否真的有进程监听。

curl 127.0.0.1:9620 都连不上时,说明本机端口没有进程监听。此时不要先查域名、证书、Nginx,先查 puppetgc-api 服务。

十、问题二:生产配置弱密码导致 API 启动失败

日志里看到:

1
2
RuntimeError: 生产配置不安全:ADMIN_SEED_PASSWORD 必须替换为强密码
ERROR: Application startup failed. Exiting.

原因:

APP_ENV=production 后,API 启动时会执行生产安全校验。ADMIN_SEED_PASSWORD 如果还是默认值,或长度小于 12 位,就会拒绝启动。

解决:

1
2
cd /opt/PuppetGC
sudo nano config/production.env

修改:

1
ADMIN_SEED_PASSWORD=一个至少12位的强密码

然后重启:

1
2
3
sudo systemctl restart puppetgc-api
sudo systemctl status puppetgc-api --no-pager
curl http://127.0.0.1:9620/health

这次真正的根因就是 ADMIN_SEED_PASSWORD 没换成强密码。生产校验报错虽然看起来烦,但它避免了弱口令上线。

如果还有其他配置问题,日志会继续指出,例如:

  • JWT_SECRET 太短或仍是默认值。
  • ENCRYPTION_KEY 太短或仍是默认值。
  • CORS_ALLOW_ORIGINS 为空,或包含 *nulllocalhost127.0.0.1

十一、问题三:Git 推送 TLS 中断

本地提交后推送 GitHub 时遇到:

1
2
fatal: unable to access 'https://github.com/Pupper0601/PuppetGC.git/':
TLS connect error: unexpected eof while reading

原因:

网络到 GitHub 的 TLS 连接被中断,不是代码问题。

解决:

1
2
3
$env:HTTP_PROXY='http://127.0.0.1:7890'
$env:HTTPS_PROXY='http://127.0.0.1:7890'
git push origin main

推送成功后用下面命令确认本地和远端同步:

1
git rev-list --left-right --count origin/main...HEAD

返回:

1
0 0

说明本地和远端一致。

十二、问题四:误以为还有 21 个文件没提交

现象:

IDE 里看到还有一批未提交文件,以为 PuppetGC 没有提交干净。

实际情况:

E:\PuppetGC 仓库已经提交并推送完成,只剩两个未跟踪的大包:

1
2
PuppetGC.tar
PuppetGC.tar.zip

这两个是 100MB 以上的打包产物,不应该当源码提交。

IDE 里看到的另一批文件来自另一个仓库:

1
C:\Users\puppe\.openclaw\workspace

排查方式:

1
2
git status --short --branch
git -C C:\Users\puppe\.openclaw\workspace status --short --branch

教训:

看到 IDE 的 Git 面板提示时,要先确认当前打开的是哪个 Git 仓库,尤其是同时打开多个项目目录时。

十三、问题五:部署文档误写真实密钥

部署文档里曾经出现过真实的 JWT_SECRETENCRYPTION_KEYADMIN_SEED_PASSWORDSMTP_PASS

处理方式:

  1. 提交前先用搜索扫敏感字段。
  2. 把文档里的真实值替换成占位符。
  3. 确认暂存区没有真实密钥。
  4. 如果密钥曾经推送到远端,必须立刻轮换。

本地检查命令:

1
rg -n --hidden --glob '!.git/**' --glob '!node_modules/**' -i "(api[_-]?key|secret|token|password|passwd|private[_-]?key|SMTP_PASS|JWT_SECRET|ENCRYPTION_KEY)" .

提交前再查暂存区:

1
2
git diff --cached --check
git diff --cached --name-only

密钥类配置的原则:

示例文档只写占位符

.envproduction.env 不进 Git

如果真实值被复制进文档,就当作已经泄漏处理,重新生成

十四、最终检查清单

上线前按这个清单过一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cd /opt/PuppetGC

# API 服务
sudo systemctl status puppetgc-api --no-pager
curl http://127.0.0.1:9620/health

# Nginx
sudo nginx -t
sudo systemctl reload nginx
curl https://api.puppetgc.cn/health

# 备份
sudo /opt/PuppetGC/deploy/scripts/backup-sqlite.sh
ls -lh /var/backups/puppetgc

# 日志
sudo journalctl -u puppetgc-api -n 100 --no-pager
sudo tail -n 100 /var/log/puppetgc-backup.log

只要这些都通过,说明后端、反代和备份链路都已经闭环。

1
2
3
sudo systemctl status puppetgc-api --no-pager
curl http://127.0.0.1:9620/health
sudo journalctl -u puppetgc-api -n 100 --no-pager
1
2
3
sudo nginx -t
sudo systemctl reload nginx
curl https://api.puppetgc.cn/health
1
2
3
sudo /opt/PuppetGC/deploy/scripts/backup-sqlite.sh
ls -lh /var/backups/puppetgc
sudo tail -n 100 /var/log/puppetgc-backup.log
1
2
git status --short --branch
git rev-list --left-right --count origin/main...HEAD

十五、这次部署的关键经验

curl 127.0.0.1:9620/health 失败时,优先查 systemd 和端口监听,不要先怀疑 Nginx

生产环境必须让配置校验严格一点,宁可启动失败,也不要带弱密码上线

部署文档不能写真实密钥,哪怕只是临时记录

大包、构建产物、数据库文件不要提交到代码仓库

多仓库同时打开时,先确认 git status 的工作目录

备份必须手动跑通一次,再交给 crontab

这次的部署问题都不复杂,但很典型:服务没监听看日志,生产配置不安全就改配置,敏感信息进文档就替换并轮换。把这些流程固定下来,后续上线就会稳很多。