diff --git a/README.md b/README.md index 0aeaf83..461a5a4 100644 --- a/README.md +++ b/README.md @@ -220,24 +220,24 @@ NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 | API 文档 | https://nomadweb.nomadro.com/docs | | Git 仓库 | https://gitea.dsx2020.com/eric/nomadweb | -**服务器架构:** +**服务器架构(原生优先,非 Docker):** - 服务器:`107.173.30.245` -- 反向代理:Caddy(自动 HTTPS) -- 前端容器:`127.0.0.1:3055` → Next.js -- 后端容器:`127.0.0.1:8055` → FastAPI -- 数据库:PocketBase(Docker 内部网络) +- 反向代理:Caddy(自动 HTTPS)→ `deploy/caddy/nomadweb.caddy` +- 前端:`systemd` `nomadro-web` → `127.0.0.1:3055`(Next.js) +- 后端:`systemd` `nomadro-api` → `127.0.0.1:8055`(uvicorn) +- 数据库:主机 PocketBase → `127.0.0.1:8090` -**更新部署:** - -本地一键增量部署(推荐): +**更新部署(推荐):** ```bash 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 cd /opt/nomadweb diff --git a/deploy/systemd/nomadro-api.service b/deploy/systemd/nomadro-api.service new file mode 100644 index 0000000..022e5d6 --- /dev/null +++ b/deploy/systemd/nomadro-api.service @@ -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 diff --git a/deploy/systemd/nomadro-web.service b/deploy/systemd/nomadro-web.service new file mode 100644 index 0000000..6580455 --- /dev/null +++ b/deploy/systemd/nomadro-web.service @@ -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 diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index d20cd74..6eed1b2 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -28,7 +28,7 @@ const LOGS = [ "行程支持下载 Markdown 文件", "新增饮食饮水注意事项", "修复生产环境 CSS 解析失败导致的页面异常", - "部署加速:BuildKit 缓存 + 按变更增量重建前后端", + "部署改为原生 systemd(非 Docker 优先),增量构建更快", ], }, { diff --git a/scripts/deploy_update.py b/scripts/deploy_update.py index 694c2ca..a31a1d1 100644 --- a/scripts/deploy_update.py +++ b/scripts/deploy_update.py @@ -1,11 +1,9 @@ """ -Fast incremental deploy for nomadro. +Native (non-Docker) deploy for nomadro. -- Pulls latest origin/dev1 -- Rebuilds only services touched since last deploy -- Uses BuildKit cache mounts (npm/pip) -- Single remote critical section under flock (no concurrent collide) -- Polls health instead of fixed long sleep +Preferred path: git pull → npm/pip build → systemctl restart +Keeps Caddy → 127.0.0.1:3055 / 8055 unchanged. +PocketBase is expected as a host process on :8090. """ from __future__ import annotations @@ -24,19 +22,18 @@ GIT = "https://eric:Xiao4669805@gitea.dsx2020.com/eric/nomadweb.git" DOMAIN = "nomadweb.nomadro.com" REMOTE = "/opt/nomadweb" 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]: - print("\n>>", cmd[:200].replace("\n", " ")) + print("\n>>", cmd[:220].replace("\n", " ")) _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() if out.strip(): - print(out[-5000:]) + print(out[-6000:]) if err.strip(): - print("LOG:", err[-2000:]) + print("LOG:", err[-2500:]) print("exit:", code) return code, out, err @@ -53,15 +50,6 @@ DOMAIN="__DOMAIN__" GIT="__GIT__" REMOTE="__REMOTE__" 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" git remote set-url origin "$GIT" @@ -69,6 +57,20 @@ git fetch --depth=80 origin git checkout -B dev1 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) 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 echo "$CHANGED" | grep -qE '^frontend/' && FE=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 MODE=noop 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 +# 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 "NEW=$NEW" 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 noop) - echo "No app changes — skip build" + echo "No app changes — ensure services up" + systemctl start nomadro-api nomadro-web ;; frontend) - docker compose -f "$COMPOSE" build frontend - docker compose -f "$COMPOSE" up -d --no-deps frontend + build_frontend + systemctl start nomadro-api ;; backend) - docker compose -f "$COMPOSE" build backend - docker compose -f "$COMPOSE" up -d --no-deps backend + build_backend + systemctl start nomadro-web ;; full) - docker compose -f "$COMPOSE" build - docker compose -f "$COMPOSE" up -d + build_backend + build_frontend ;; esac 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) api=$(curl -s http://127.0.0.1:8055/api/v1/health || true) echo "try=$i web=$web api=$api" @@ -126,10 +161,12 @@ for i in $(seq 1 25); do sleep 1 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 -sk "https://${DOMAIN}/api/v1/health" echo "$NEW" > "$MARKER" +echo "DEPLOY_ENGINE=native" [ "$ok" = 1 ] """ @@ -147,10 +184,8 @@ def main() -> int: .replace("__GIT__", GIT) .replace("__REMOTE__", REMOTE) .replace("__MARKER__", MARKER) - .replace("__COMPOSE__", COMPOSE) ) - # Upload and run as one locked remote job code, out, _ = run( client, 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})") return code - print(f"\nDeploy OK in {elapsed}s (mode={mode})") + print(f"\nDeploy OK in {elapsed}s (mode={mode}, engine=native)") return 0