Switch production deploy to native systemd instead of Docker.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f6019e050f
commit
e8d225390d
20
README.md
20
README.md
@ -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
|
||||||
|
|||||||
18
deploy/systemd/nomadro-api.service
Normal file
18
deploy/systemd/nomadro-api.service
Normal 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
|
||||||
21
deploy/systemd/nomadro-web.service
Normal file
21
deploy/systemd/nomadro-web.service
Normal 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
|
||||||
@ -28,7 +28,7 @@ const LOGS = [
|
|||||||
"行程支持下载 Markdown 文件",
|
"行程支持下载 Markdown 文件",
|
||||||
"新增饮食饮水注意事项",
|
"新增饮食饮水注意事项",
|
||||||
"修复生产环境 CSS 解析失败导致的页面异常",
|
"修复生产环境 CSS 解析失败导致的页面异常",
|
||||||
"部署加速:BuildKit 缓存 + 按变更增量重建前后端",
|
"部署改为原生 systemd(非 Docker 优先),增量构建更快",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user