这篇不是单纯命令清单,而是一份可复盘的上线笔记:部署路径、脚本、检查命令、故障现象和处理方式都放在一起,后面排查同类问题可以直接按章节跳转。
一、部署目标
这次要把 PuppetGC 部署到 Ubuntu 服务器,项目目录固定为:
整体结构如下:
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
|
核心原则:
本地完成提交并推送到 GitHub,排除 PuppetGC.tar、PuppetGC.tar.zip 这类大包产物。
服务器 /opt/PuppetGC 拉取最新代码,准备 config/production.env。
执行 deploy/scripts/deploy-api.sh,创建 Python 3.13 虚拟环境,安装依赖并写入 systemd 服务。
curl 127.0.0.1:9620/health 失败后,通过 journalctl 定位到生产弱密码校验失败。
配置 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 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
|
脚本主要做这些事:
- 检查
config/production.env 是否存在。
- 拉取最新代码。
- 创建或复用
apps/api-py/.venv。
- 安装 Python 依赖。
- 初始化数据目录和日志目录权限。
- 写入 systemd 服务
puppetgc-api.service。
- 重启 API。
- 请求
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 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
| 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 为空,或包含 *、null、localhost、127.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
|
返回:
说明本地和远端一致。
十二、问题四:误以为还有 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_SECRET、ENCRYPTION_KEY、ADMIN_SEED_PASSWORD 和 SMTP_PASS。
处理方式:
- 提交前先用搜索扫敏感字段。
- 把文档里的真实值替换成占位符。
- 确认暂存区没有真实密钥。
- 如果密钥曾经推送到远端,必须立刻轮换。
本地检查命令:
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
|
密钥类配置的原则:
十四、最终检查清单
上线前按这个清单过一遍:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| cd /opt/PuppetGC
sudo systemctl status puppetgc-api --no-pager curl http://127.0.0.1:9620/health
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
|
十五、这次部署的关键经验
这次的部署问题都不复杂,但很典型:服务没监听看日志,生产配置不安全就改配置,敏感信息进文档就替换并轮换。把这些流程固定下来,后续上线就会稳很多。