Switch production deploy to native systemd instead of Docker.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric 2026-08-29 01:51:46 -05:00
parent f6019e050f
commit e8d225390d
5 changed files with 117 additions and 43 deletions

View File

@ -220,24 +220,24 @@ NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1
| API 文档 | https://nomadweb.nomadro.com/docs | | API 文档 | https://nomadweb.nomadro.com/docs |
| Git 仓库 | https://gitea.dsx2020.com/eric/nomadweb | | Git 仓库 | https://gitea.dsx2020.com/eric/nomadweb |
**服务器架构:** **服务器架构(原生优先,非 Docker):**
- 服务器:`107.173.30.245` - 服务器:`107.173.30.245`
- 反向代理:Caddy(自动 HTTPS) - 反向代理:Caddy(自动 HTTPS)→ `deploy/caddy/nomadweb.caddy`
- 前端容器:`127.0.0.1:3055` → Next.js - 前端:`systemd` `nomadro-web` → `127.0.0.1:3055`(Next.js)
- 后端容器:`127.0.0.1:8055` → FastAPI - 后端:`systemd` `nomadro-api` → `127.0.0.1:8055`(uvicorn)
- 数据库:PocketBase(Docker 内部网络) - 数据库:主机 PocketBase → `127.0.0.1:8090`
**更新部署:** **更新部署(推荐):**
本地一键增量部署(推荐):
```bash ```bash
python scripts/deploy_update.py python scripts/deploy_update.py
``` ```
脚本会对比上次成功部署的 commit:仅前端改动只重建 `frontend`,仅后端改动只重建 `backend`,并启用 BuildKit npm/pip 缓存;健康检查改为短轮询。 脚本会:`git pull` → 按变更增量 `npm run build` / `pip install` → `systemctl restart`,并自动停掉占用端口的旧 Docker 前后端容器。仅前端改动时通常明显快于镜像重建。
服务器手动全量: 单元文件:`deploy/systemd/nomadro-web.service`、`deploy/systemd/nomadro-api.service`。
**备选:Docker Compose**
```bash ```bash
cd /opt/nomadweb cd /opt/nomadweb

View File

@ -0,0 +1,18 @@
[Unit]
Description=nomadro FastAPI backend
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/nomadweb/backend
Environment=POCKETBASE_URL=http://127.0.0.1:8090
Environment=POCKETBASE_ADMIN_EMAIL=admin@nomadro.com
Environment=POCKETBASE_ADMIN_PASSWORD=admin123456
Environment=CORS_ORIGINS=https://nomadweb.nomadro.com,http://nomadweb.nomadro.com
ExecStart=/opt/nomadweb/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8055 --workers 2
Restart=always
RestartSec=3
User=root
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,21 @@
[Unit]
Description=nomadro Next.js frontend
After=network.target
Wants=nomadro-api.service
[Service]
Type=simple
WorkingDirectory=/opt/nomadweb/frontend
Environment=NODE_ENV=production
Environment=PORT=3055
Environment=HOSTNAME=127.0.0.1
Environment=NEXT_TELEMETRY_DISABLED=1
Environment=NEXT_PUBLIC_API_URL=https://nomadweb.nomadro.com/api/v1
Environment=NEXT_PUBLIC_SITE_URL=https://nomadweb.nomadro.com
ExecStart=/usr/bin/npm run start -- -H 127.0.0.1 -p 3055
Restart=always
RestartSec=3
User=root
[Install]
WantedBy=multi-user.target

View File

@ -28,7 +28,7 @@ const LOGS = [
"行程支持下载 Markdown 文件", "行程支持下载 Markdown 文件",
"新增饮食饮水注意事项", "新增饮食饮水注意事项",
"修复生产环境 CSS 解析失败导致的页面异常", "修复生产环境 CSS 解析失败导致的页面异常",
"部署加速:BuildKit 缓存 + 按变更增量重建前后端", "部署改为原生 systemd(非 Docker 优先),增量构建更快",
], ],
}, },
{ {

View File

@ -1,11 +1,9 @@
""" """
Fast incremental deploy for nomadro. Native (non-Docker) deploy for nomadro.
- Pulls latest origin/dev1 Preferred path: git pull → npm/pip build → systemctl restart
- Rebuilds only services touched since last deploy Keeps Caddy → 127.0.0.1:3055 / 8055 unchanged.
- Uses BuildKit cache mounts (npm/pip) PocketBase is expected as a host process on :8090.
- Single remote critical section under flock (no concurrent collide)
- Polls health instead of fixed long sleep
""" """
from __future__ import annotations from __future__ import annotations
@ -24,19 +22,18 @@ GIT = "https://eric:Xiao4669805@gitea.dsx2020.com/eric/nomadweb.git"
DOMAIN = "nomadweb.nomadro.com" DOMAIN = "nomadweb.nomadro.com"
REMOTE = "/opt/nomadweb" REMOTE = "/opt/nomadweb"
MARKER = f"{REMOTE}/.last_deploy_sha" MARKER = f"{REMOTE}/.last_deploy_sha"
COMPOSE = "docker-compose.prod.yml"
def run(client: paramiko.SSHClient, cmd: str, timeout: int = 900) -> tuple[int, str, str]: def run(client: paramiko.SSHClient, cmd: str, timeout: int = 900) -> tuple[int, str, str]:
print("\n>>", cmd[:200].replace("\n", " ")) print("\n>>", cmd[:220].replace("\n", " "))
_, stdout, stderr = client.exec_command(cmd, timeout=timeout) _, stdout, stderr = client.exec_command(cmd, timeout=timeout)
out = stdout.read().decode("utf-8", errors="replace") out = stdout.read().decode("utf-8", errors="replace")
err = stderr.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace")
code = stdout.channel.recv_exit_status() code = stdout.channel.recv_exit_status()
if out.strip(): if out.strip():
print(out[-5000:]) print(out[-6000:])
if err.strip(): if err.strip():
print("LOG:", err[-2000:]) print("LOG:", err[-2500:])
print("exit:", code) print("exit:", code)
return code, out, err return code, out, err
@ -53,15 +50,6 @@ DOMAIN="__DOMAIN__"
GIT="__GIT__" GIT="__GIT__"
REMOTE="__REMOTE__" REMOTE="__REMOTE__"
MARKER="__MARKER__" MARKER="__MARKER__"
COMPOSE="__COMPOSE__"
export DOMAIN \
NEXT_PUBLIC_API_URL="https://${DOMAIN}/api/v1" \
NEXT_PUBLIC_SITE_URL="https://${DOMAIN}" \
CORS_ORIGINS="https://${DOMAIN},http://${DOMAIN}" \
DOCKER_BUILDKIT=1 \
COMPOSE_DOCKER_CLI_BUILD=1 \
BUILDKIT_PROGRESS=plain
cd "$REMOTE" cd "$REMOTE"
git remote set-url origin "$GIT" git remote set-url origin "$GIT"
@ -69,6 +57,20 @@ git fetch --depth=80 origin
git checkout -B dev1 origin/dev1 git checkout -B dev1 origin/dev1
git reset --hard origin/dev1 git reset --hard origin/dev1
# Install / refresh systemd units
install -m 644 "$REMOTE/deploy/systemd/nomadro-web.service" /etc/systemd/system/nomadro-web.service
install -m 644 "$REMOTE/deploy/systemd/nomadro-api.service" /etc/systemd/system/nomadro-api.service
systemctl daemon-reload
systemctl enable nomadro-web nomadro-api >/dev/null 2>&1 || true
# Stop Docker app containers if they still hold ports (keep other stacks)
if command -v docker >/dev/null 2>&1; then
docker stop nomadflow-web nomadflow-api 2>/dev/null || true
docker rm nomadflow-web nomadflow-api 2>/dev/null || true
# optional: stop unused compose pb if host pb already serves :8090
docker stop nomadflow-pb 2>/dev/null || true
fi
NEW=$(git rev-parse HEAD) NEW=$(git rev-parse HEAD)
OLD=$(cat "$MARKER" 2>/dev/null || true) OLD=$(cat "$MARKER" 2>/dev/null || true)
@ -78,7 +80,7 @@ if [ -n "$OLD" ] && git cat-file -e "${OLD}^{commit}" 2>/dev/null; then
FE=0; BE=0 FE=0; BE=0
echo "$CHANGED" | grep -qE '^frontend/' && FE=1 || true echo "$CHANGED" | grep -qE '^frontend/' && FE=1 || true
echo "$CHANGED" | grep -qE '^backend/' && BE=1 || true echo "$CHANGED" | grep -qE '^backend/' && BE=1 || true
echo "$CHANGED" | grep -qE '^docker-compose\.prod\.yml$' && FE=1 && BE=1 || true echo "$CHANGED" | grep -qE '^deploy/systemd/' && FE=1 && BE=1 || true
if [ -z "$CHANGED" ]; then if [ -z "$CHANGED" ]; then
MODE=noop MODE=noop
elif [ "$FE" = 1 ] && [ "$BE" = 0 ]; then elif [ "$FE" = 1 ] && [ "$BE" = 0 ]; then
@ -92,30 +94,63 @@ if [ -n "$OLD" ] && git cat-file -e "${OLD}^{commit}" 2>/dev/null; then
fi fi
fi fi
# First native boot or missing venv/node_modules → full
[ -d "$REMOTE/backend/.venv" ] || MODE=full
[ -d "$REMOTE/frontend/node_modules" ] || MODE=full
[ -d "$REMOTE/frontend/.next" ] || MODE=full
echo "MODE=$MODE" echo "MODE=$MODE"
echo "NEW=$NEW" echo "NEW=$NEW"
echo "OLD=${OLD:-none}" echo "OLD=${OLD:-none}"
export NEXT_TELEMETRY_DISABLED=1
export NEXT_PUBLIC_API_URL="https://${DOMAIN}/api/v1"
export NEXT_PUBLIC_SITE_URL="https://${DOMAIN}"
export NODE_ENV=production
build_frontend() {
cd "$REMOTE/frontend"
if [ ! -d node_modules ] || echo "${CHANGED:-}" | grep -qE '^frontend/package(-lock)?\.json$'; then
npm ci --legacy-peer-deps --prefer-offline
fi
npm run build
systemctl restart nomadro-web
}
build_backend() {
cd "$REMOTE/backend"
if [ ! -d .venv ]; then
python3 -m venv .venv
fi
if [ ! -f .venv/.reqsha ] || ! cmp -s requirements.txt .venv/.reqsha 2>/dev/null; then
.venv/bin/pip install -q -r requirements.txt
cp requirements.txt .venv/.reqsha
fi
systemctl restart nomadro-api
}
CHANGED="${CHANGED:-}"
case "$MODE" in case "$MODE" in
noop) noop)
echo "No app changes — skip build" echo "No app changes — ensure services up"
systemctl start nomadro-api nomadro-web
;; ;;
frontend) frontend)
docker compose -f "$COMPOSE" build frontend build_frontend
docker compose -f "$COMPOSE" up -d --no-deps frontend systemctl start nomadro-api
;; ;;
backend) backend)
docker compose -f "$COMPOSE" build backend build_backend
docker compose -f "$COMPOSE" up -d --no-deps backend systemctl start nomadro-web
;; ;;
full) full)
docker compose -f "$COMPOSE" build build_backend
docker compose -f "$COMPOSE" up -d build_frontend
;; ;;
esac esac
ok=0 ok=0
for i in $(seq 1 25); do for i in $(seq 1 30); do
web=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3055/ || true) web=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3055/ || true)
api=$(curl -s http://127.0.0.1:8055/api/v1/health || true) api=$(curl -s http://127.0.0.1:8055/api/v1/health || true)
echo "try=$i web=$web api=$api" echo "try=$i web=$web api=$api"
@ -126,10 +161,12 @@ for i in $(seq 1 25); do
sleep 1 sleep 1
done done
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep nomadflow || true systemctl --no-pager --full status nomadro-web nomadro-api | sed -n '1,40p' || true
ss -lntp | grep -E '3055|8055|8090' || true
curl -skI "https://${DOMAIN}" | head -6 curl -skI "https://${DOMAIN}" | head -6
curl -sk "https://${DOMAIN}/api/v1/health" curl -sk "https://${DOMAIN}/api/v1/health"
echo "$NEW" > "$MARKER" echo "$NEW" > "$MARKER"
echo "DEPLOY_ENGINE=native"
[ "$ok" = 1 ] [ "$ok" = 1 ]
""" """
@ -147,10 +184,8 @@ def main() -> int:
.replace("__GIT__", GIT) .replace("__GIT__", GIT)
.replace("__REMOTE__", REMOTE) .replace("__REMOTE__", REMOTE)
.replace("__MARKER__", MARKER) .replace("__MARKER__", MARKER)
.replace("__COMPOSE__", COMPOSE)
) )
# Upload and run as one locked remote job
code, out, _ = run( code, out, _ = run(
client, client,
f"cat > /tmp/nomadro-deploy.sh <<'EOF'\n{script}\nEOF\nbash /tmp/nomadro-deploy.sh", f"cat > /tmp/nomadro-deploy.sh <<'EOF'\n{script}\nEOF\nbash /tmp/nomadro-deploy.sh",
@ -171,7 +206,7 @@ def main() -> int:
print(f"\nDeploy FAILED in {elapsed}s (mode={mode})") print(f"\nDeploy FAILED in {elapsed}s (mode={mode})")
return code return code
print(f"\nDeploy OK in {elapsed}s (mode={mode})") print(f"\nDeploy OK in {elapsed}s (mode={mode}, engine=native)")
return 0 return 0