| name | remote-server-deployment |
| description | 将 Node.js 服务部署到远程服务器(如 HP 192.168.0.111)的模式。涵盖 rsync 同步、npm 离线安装、systemd 服务管理、Cloudflare Tunnel 暴露、网络受限环境下的镜像配置,以及已吸收的 Next.js Docker 部署与 cloudflared Tunnel 深度操作指南。 |
远程服务器 Node.js 服务部署
触发条件
- 需要将 Node.js CLI 或 HTTP 服务部署到远程 Linux 服务器
- 服务器网络受限(无法直连 npm registry / Docker Hub / GitHub)
- 服务器有 Cloudflare Tunnel 需要为新增服务添加路由
- 目标:开机自启 + 自动恢复 + Tunnel 公网暴露
架构
本地开发机 远程服务器 (HP, 192.168.0.111)
│ │
├─ rsync 代码/包 ─────────────→ ├─ 数据目录 (/opt/<service>/)
│ ├─ systemd 服务
│ ├─ cloudflared Tunnel
│ └─ HTTP 端口 (8003/8005/...)
│
└─ 首次: npm pack + rsync ───→ └─ npm install -g <tarball>
本地打包 (能联网) (自动下载 270 依赖)
安装模式
模式 A:npm registry 可访问
ssh hp "npm install -g @scope/package"
模式 B:registry 不可访问(通用离线安装)
- 在本地打包:
npm pack @scope/package
- rsync 到服务器:
rsync -avz scope-package-*.tgz hp:/tmp/
- 在服务器上安装(npm 会从 tarball 下载依赖):
ssh hp "npm install -g /tmp/scope-package-*.tgz"
模式 C:Docker 不可用时
当 Docker 构建因网络代理问题(apk 源不可达、Docker Hub IPv6 超时)失败时:
ssh hp "node --version"
ssh hp "npm install -g @scope/package"
Docker vs systemd 的选择
优先选择 Docker(--restart unless-stopped),理由:
- systemd 管理的服务在服务器重启后偶发失效(服务依赖未就绪、PATH 变化等)
- Docker 提供统一的 restart policy + 日志管理 + 资源隔离
- 迁移成本:
systemctl stop → docker run --restart unless-stopped
仅当 Docker 构建因网络/架构问题不可行时,退而使用 systemd。
Docker 化:网络受限环境的构建策略
当 docker build 因网络问题(代理不通、镜像源超时)反复失败时,有两条替代路径:
路径 A:docker save/load(跨机器搬运)
docker build -t my-service .
docker save my-service | gzip > /tmp/my-service.tar.gz
rsync -avzP /tmp/my-service.tar.gz hp:/tmp/
ssh hp "docker load < /tmp/my-service.tar.gz"
注意架构:构建机的架构必须与目标服务器一致(uname -m 检查)。ARM Mac → x86_64 服务器需要用 buildx --platform linux/amd64。
路径 B:docker commit(从运行中的容器创建镜像)
当 docker build 内 RUN 指令反复失败(apt/apk/npm 网络问题)时,在容器内手动安装再 commit:
ssh hp "docker run -d --name builder node:22-slim sleep 120"
ssh hp "docker exec builder npm install -g /tmp/package.tgz"
ssh hp "docker cp /opt/service/config.yml builder:/etc/service/config.yml"
ssh hp "docker commit builder my-service && docker rm builder"
陷阱:native addon 架构不匹配
Node.js 的 native addon(better-sqlite3、node-llama-cpp 等)编译为特定 CPU 架构和 Node.js 版本的二进制 .node 文件。跨架构/版本移植必报错:
| 症状 | 错误信息 | 原因 | 修复 |
|---|
| 架构不匹配 | invalid ELF header | ARM 编译的 binary 跑在 x86_64 上 | binary 必须在目标架构上编译 |
| Node 版本不匹配 | NODE_MODULE_VERSION 141. This version requires 127 | Node v25 binary 跑在 Node v22 上 | 容器 Node 版本需匹配编译时的版本 |
| libc 不兼容 | Error loading shared library ld-linux-x86-64.so.2 | glibc binary 跑在 musl (Alpine) 上 | 用 node:22-slim (Debian) 而非 Alpine |
修复方案:
# 对 Node 版本不匹配:在容器内 rebuild
RUN cd /path/to/app && npm rebuild better-sqlite3
# 或使用与宿主相同 Node 版本的基础镜像
FROM node:25-slim # 匹配宿主机的 v25
最佳基础镜像选择:
| 镜像 | 优点 | 缺点 | 适用场景 |
|---|
node:22-slim (Debian) | glibc 兼容性好,apt 源多 | 较大(~75MB) | 推荐—大多数 native addon 场景 |
node:22-alpine | 小(~7MB) | musl libc 不兼容 glibc binary | 纯 JS 项目,无 native addon |
node:22-bookworm-slim | 更全的包 | 更大 | 需要系统包(python3, git) |
systemd 服务模板
[Unit]
Description=<Service Description>
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/<command> <args>
Restart=on-failure
RestartSec=5
Environment=HOME=/root
Environment=HF_ENDPOINT=https://hf-mirror.com
[Install]
WantedBy=multi-user.target
scp service.service hp:/etc/systemd/system/
ssh hp "systemctl daemon-reload && systemctl enable <service> && systemctl start <service>"
ssh hp "systemctl status <service> --no-pager | head -5"
ssh hp "journalctl -u <service> --no-pager -n 20"
ssh hp "systemctl restart <service>"
Cloudflare Tunnel 配置
拆除 / 下线 (Teardown)
废弃服务器对外访问(如 HP 下线)时,完整拆除 Tunnel + Zero Trust 通道。要点:
- 服务器
/root/.cloudflared/cert.pem 是账户级授权 → 可直接 cloudflared tunnel delete <name> 删隧道本体。
- 删隧道不会自动删 DNS 记录,proxied 记录会残留并解析到 CF anycast IP(死 origin → 502),须控制台或 API token 删。
- cloudflared CLI 管不了 Zero Trust Access 应用/服务 token,须控制台(one.dash.cloudflare.com → Access)删。
- patch 工具拒写
~/.ssh/config(受保护凭据文件),删失效 tunnel Host 段用 sed -i ''。
完整分步流程 + 验证 + 控制台指引见 references/cloudflare-tunnel-teardown.md。
新增 DNS 路由
ssh hp "cloudflared tunnel route dns <tunnel-name> <subdomain>.jinguo.tech"
更新 ingress 规则
tunnel: <tunnel-uuid>
credentials-file: /root/.cloudflared/<tunnel-uuid>.json
ingress:
- hostname: <subdomain>.jinguo.tech
service: http://localhost:<PORT>
- hostname: ai.jinguo.tech
service: http://localhost:8003
- hostname: qmd.jinguo.tech
service: http://localhost:8005
- service: http_status:404
ssh hp "systemctl restart cloudflared"
网络受限环境的通用解决
| 场景 | 问题 | 解决 |
|---|
| npm registry 被墙 | npm install 超时 | 使用 npm pack + rsync tarball |
| Docker Hub 被墙 | FROM 镜像下载失败 | 配置 Docker registry mirrors |
| Alpine apk 被墙 | apk add 超时 | 换国内镜像源 sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories |
| HuggingFace 被墙 | 模型下载 0 字节 | export HF_ENDPOINT=https://hf-mirror.com |
| PyPI 被墙 | pip install 超时 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple |
文件目录规范
/opt/<service>/ ← 代码/数据目录
/root/.cache/<service>/ ← 索引/缓存
/etc/systemd/system/<service>.service ← 服务配置
部署审计:盘点远程服务器(HP)上到底跑了什么
用户问"某服务是否还部署在 HP 上 / 还有哪些服务在跑"时,用一次 SSH 会话做全量盘点。刚重启的机器(uptime 显示 up 几分钟)尤其要逐项查——隧道路由可能指向已死的端口:
ssh hp "uptime"
ssh hp "docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'"
ssh hp "systemctl list-units --type=service --state=running" | grep -iE 'cloudflared|openmaic|qmd|openclaw|relay|channel'
ssh hp "ss -tlnp | grep LISTEN"
ssh hp "cat /root/.cloudflared/config.yml; cloudflared tunnel list"
ssh hp "ls -la /usr/local/bin/" | grep -iE 'codex|qmd|openmaic|hermes'
关键判断点:
- 监听端口 vs 隧道路由对不上 = 服务掉了。cloudflared ingress 还留着
codex-hp.jinguo.tech → localhost:19080,但 ss -tlnp 里没有 19080 → relay 进程没自启。DNS 路由存在 ≠ 服务存活。
- 找自启机制:
ps -o pid,ppid,cmd -p <pid> 逐级看父进程。父进程是 /lib/systemd/systemd --user → 开机自启、重启自动恢复;若没有 systemd/cron/rc.local → 重启后可能不回来,需手动拉起。
- OpenClaw 类消息网关:配置在
~/.openclaw/openclaw.json(非 yaml,用 python3 -c 解析 JSON),channels 段列出启用的渠道(如 feishu websocket + allowlist);绑定 127.0.0.1 表示本地-only、未走隧道;/proc/<pid>/cwd + tr '\0' ' ' </proc/<pid>/cmdline 可确认启动方式。
验证清单
□ systemctl status <service> → active (running)
□ curl localhost:<PORT>/health → 200
□ curl https://<subdomain>.jinguo.tech/health → 200 (通过 Tunnel)
□ journalctl -u <service> 无 ERROR 日志
□ systemctl is-enabled <service> → enabled
□ 重启后自动恢复测试
已吸收的部署子技能 (absorbed)
references/nextjs-docker-deployment.md(吸收自 nextjs-docker-deployment)— Next.js Docker 化与静态部署:multi-stage standalone Dockerfile(output: 'standalone'、deps 阶段必须 COPY scripts/、TypeScript 类型错误在干净构建中暴露)、docker-compose 模式(bind mount 而非 named volume)、静态导出到 Cloudflare Pages(Free 计划)、以及常见陷阱(params Promise、Tailwind、TS 类型断言)。附带 references/classroom-data-export.md、references/openmaic-docker-fixes.md、references/video-export-pipeline.md。
references/cloudflare-tunnel-setup.md(吸收自 cloudflare-tunnel-setup)— cloudflared 深度操作:安装、tunnel login(浏览器授权)、named tunnel 创建、DNS 路由、ingress 配置、systemd 服务;关键陷阱:全局代理(v2rayN/clash)干扰必须清空全部 proxy 环境变量(NO_PROXY=* 不够)、手动进程与 systemd 双跑导致 502、--protocol http2。附带 references/zero-trust-ssh-setup.md、references/codex-websocket-ingress.md、references/docker-permission-fix.md、references/browser-autoplay-workaround.md。
关联 skill
openmaic-hp-deploy — OpenMAIC (Next.js) 部署到 HP 的具体实现
rag-client-migration-pattern — 当前 skill 的 QMD 参考含 systemd/Tunnel 细节
cloudflare-tunnel-setup — Tunnel 安装、认证、排错