@mingo_789/nas-deploy
nas-deploy
把「本机构建 Docker 镜像 → 压缩 → SSH/SCP 上传 → NAS 加载 → 健康检查 → 清理」封装成一个 npm 命令行包。适合个人 NAS 上的单容器应用,应用仍维护自己的 Dockerfile。
包名为 @mingo_789/nas-deploy。零第三方运行时依赖,使用 Node.js 内置模块、本机 Docker/OpenSSH 和 NAS 上的 Bash/Docker。本文的 0.1.0 是首版安装示例,实际版本以 package.json 和维护者发布结果为准;完成发包准备不等于已发布 npm。
文档入口
- 使用者:从下方安装和接入教程开始,日常操作见「命令和恢复」。
- 配置参考:使用案例,说明端口映射、host 网络、健康检查和应用变量如何从项目实际要求转成配置。
- AI 接入:先读 AGENT_GUIDE.md,按需读取配置、部署和迁移教程,所有页面都随包发布。
- 维护者:npm 发布教程,包含升版、预演、正式发包、预发布标签和失败重试。
注意命令所在项目:本工具源码仓库的 npm run release 是发 npm 包;应用中的 nas-deploy release 才是部署到 NAS。
默认行为
- 每次构建生成独立镜像标签,例如
my-app:20260924093000-a1b2c3d4e5f6,不覆盖latest。 docker save的输出流经过 gzip 压缩,以.partial写入;进程和压缩流全部成功后,才原子重命名为.tar.gz。- 记录镜像 ID、平台、文件大小、SHA-256;部署前本地校验,上传后 NAS 再校验。
- 上传镜像和应用环境文件也使用
.partial,校验后再改为正式文件名。 - 先加载并验证镜像、创建候选容器,再停止旧容器。候选容器健康后,才替换正式名称并清理。
- 默认仅保留当前成功版本。 删除旧容器、未使用的本项目镜像和本次本地/NAS 压缩包。NAS 临时运行环境文件也会删除;Docker 已在创建容器时读取环境变量。
- 本地旧的本项目镜像也会清理,保留本次部署镜像,以及任何仍被容器使用的镜像。
- 不执行全局
image prune、system prune或builder prune;不删除数据目录、卷、其他项目镜像、手动添加的镜像标签或构建缓存。 - 发布失败保留恢复容器、归档和诊断状态,不自动回滚共享数据库。可显式运行
rollback恢复旧容器。 - 本地和 NAS 两端都有发布锁;错误配置、无健康检查、平台不匹配、其他工具创建的同名容器会被拒绝。
这是停服替换流程,会有短暂中断,不提供零停机切流、多容器编排或镜像仓库推送。
环境准备
本机:Node.js 22+、Docker CLI 和正在运行的 Docker 服务、SSH/SCP。gzip 压缩由 Node.js 完成,无需 Python。跨架构构建能力由本机 Docker 提供。
NAS:Bash 3+、sha256sum、支持健康检查和 --mount 的 Docker/Container Manager。用户需要 Docker 权限,或可通过 sudo -n 调用 Docker。工具自动尝试群晖 Container Manager 和旧 Docker 套件的路径。
SSH 使用密钥或 SSH agent,要求目标已在 known_hosts 中。首次使用先通过普通 SSH 登录核对主机指纹。工具启用 BatchMode 和严格主机校验,不接收 SSH 密码、不自动接受主机指纹,也不提供交互 sudo。默认使用 scp -O 兼容没有 SFTP 子系统的 NAS。
安装
包正式发布后,在需要部署的应用项目内安装固定版本:
npm install --save-dev --save-exact @mingo_789/nas-deploy@0.1.0
# pnpm 项目:
pnpm add -D -E @mingo_789/nas-deploy@0.1.0
若该版本尚未发布,使用维护者提供的 .tgz,或按下面步骤从源码生成,不需要全局安装。
从本地源码打包和安装
cd /path/to/nas-deploy
npm ci
npm run pack:check
pack:check 执行语法检查、全部测试,并在仓库外离线安装真实归档,验证 CLI、通用初始化、dry-run 和随包文档。产物为 artifacts/packages/mingo_789-nas-deploy-0.1.0.tgz,报告在 artifacts/pack-check.json。归档文件名随版本变化。普通 npm pack 也保留可用,会检查并测试,但不执行隔离消费验收。
在使用方项目中安装本地产物并固定版本:
npm install --save-dev --save-exact /path/to/nas-deploy/artifacts/packages/mingo_789-nas-deploy-0.1.0.tgz
# pnpm 项目:
pnpm add -D -E /path/to/nas-deploy/artifacts/packages/mingo_789-nas-deploy-0.1.0.tgz
以上是本机验证方式,锁文件会记录本地压缩包路径。跨机器/CI 使用前,应分发这个固定版本的压缩包,或发布到可访问的 npm registry,再改成包版本依赖;不要在团队锁文件中保留个人电脑的绝对路径。
新项目接入
以下命令都在应用项目根目录执行。应用应已有可用 Dockerfile;先确认 NAS 架构、应用端口、数据目录和健康端点。
每个项目只需初始化一次,编辑并保存项目自己的配置。后续发布复用这份配置,不用再次运行 init。
1. 初始化配置
npx --no-install nas-deploy init
cp .env.deploy.example .env.deploy
2. 填写连接与应用配置
编辑 nas-deploy.config.json,将默认 3000 端口、数据挂载和平台改为应用实际值,再填写 .env.deploy:
NAS_HOST=192.168.1.194
NAS_USER=nas
# NAS_SSH_PORT=22
# NAS_IDENTITY_FILE=/absolute/path/to/private-key
Dockerfile 有 HEALTHCHECK 时可直接使用;没有时,在 JSON 中配置容器内执行的 health.command。例如应用运行于 Node.js 且提供 /health:
{
"health": {
"command": "node -e \"fetch('http://127.0.0.1:3000/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))\"",
"timeout": 120
}
}
将该字段合入配置,而不是用它替换整个文件。命令中的端口应为容器内部端口,检查程序必须已安装在镜像内。应用没有该端点时,应按实际情况提供健康检查。
3. 添加应用脚本
init 不覆盖已有配置,并向 .gitignore 和 .dockerignore 添加必要排除规则。它不会自动修改应用的 package.json;添加以下命令即可:
{
"scripts": {
"build:image": "nas-deploy build",
"deploy": "nas-deploy deploy",
"release": "nas-deploy release"
}
}
npx --no-install nas-deploy release --dry-run
npx --no-install nas-deploy doctor
npm run release
npx --no-install nas-deploy status
上面依次预览配置、检查连接和 Docker、发布、查看状态。pnpm 项目用 pnpm exec nas-deploy ... 和 pnpm run release。第一次成功后,再发布一次可验证旧镜像与归档的自动清理。
--dry-run 只读取本地配置并展示计划,不连接 NAS、不执行 Docker、不写文件,也不展示应用环境变量值。真实 release 会短暂停止旧容器;有不兼容数据迁移时,先按应用要求备份。
4. 日常更新和工具升级
应用代码更新后直接执行 npm run release;要先构建再择时部署,用 npm run build:image 后再 npm run deploy。默认成功后压缩包会删除,下次更新应重新构建。
升级公共工具时,在应用中安装经过验证的新版本并更新锁文件,例如 npm install -D -E @mingo_789/nas-deploy@<新版本>;阅读新版本 CHANGELOG,先 dry-run 再做一次实际发布验证。部署工具修复不需要再同步多份 Shell 脚本。
如果只做接入配置,到 dry-run 为止即可;尚未完成真实 NAS 发布时,不应报告部署成功。
配置案例教程
工具只生成通用初始配置,项目差异由配置文件表达。使用案例 参考两个项目,给出完整配置及推导步骤:
- plot-assistant:端口映射、镜像自带健康检查、采集令牌和持久化目录。
- blood-games:host 网络、额外健康检查、SQLite 路径和模型环境变量。
案例是阅读参考,不是 CLI 内置能力。结合自己的项目调整一次并保存配置即可。
从旧脚本迁移
旧的 .env.deploy 是 Bash 配置;新工具使用字面量环境文件,不会执行 source、变量引用或命令替换。把 REMOTE_HOST 改为 NAS_HOST,REMOTE_USER 改为 NAS_USER,把网络、端口、挂载和 Docker 设置迁到 JSON。REMOTE_PASS 不再使用,REMOTE_DOCKER='sudo docker' 对应 remote.sudo: "always"。
首次接管已有部署需要安排维护窗口:
- 先备份持久化数据,确认新配置挂载的是原来的数据目录。
- 执行
build和doctor,确认配置、镜像和连接可用。 - 在 NAS 停止原容器,将其改名为另一个明确的备份名称,例如
plot-assistant-legacy,释放原名称和端口。 - 使用新工具执行
deploy,验证应用和已有数据。 - 确认成功后,手动清理旧工具创建的备份容器和镜像。
新工具不修改、删除或接管缺少本项目管理标签的同名容器;旧备份也不在自动清理范围内。首次迁移失败时通过旧容器手动恢复,后续由本工具发布的版本才支持工具内的失败恢复。
配置
所有路径以配置文件所在目录为基准,和执行命令时的目录无关。配置文件使用严格 JSON,未知字段会报错。
{
"project": "my-app",
"image": "my-app",
"platform": "linux/amd64",
"build": {
"context": ".",
"dockerfile": "Dockerfile",
"args": { "PUBLIC_BASE": "/" }
},
"remote": {
"directory": "/volume1/docker/my-app",
"port": 22,
"sudo": "auto",
"legacyScp": true,
"connectTimeout": 10
},
"container": {
"name": "my-app",
"network": "bridge",
"ports": [{ "address": "0.0.0.0", "host": 3000, "container": 3000, "protocol": "tcp" }],
"mounts": [{ "source": "/volume1/docker/my-app/data", "target": "/app/data", "owner": "1000:1000" }],
"env": { "NODE_ENV": "production", "PORT": "3000" },
"envFrom": ["APP_TOKEN"],
"restart": "unless-stopped"
},
"health": {
"timeout": 120
},
"cleanup": {
"retainPrevious": 0,
"localArchive": true,
"localImages": true
},
"artifactDir": ".nas-deploy"
}
| 字段 | 说明 |
|---|---|
project |
必填,小写项目标识,用于资源所有权标签;同一 NAS 上不同应用应使用不同标识 |
image |
镜像仓库名,默认 project,版本标签由工具生成;不支持带端口的 registry 名称 |
platform |
linux/amd64(默认)或 linux/arm64 |
build |
context、Dockerfile、非敏感构建参数 args、可选阶段 target |
remote.host/user |
可写入 JSON,也可通过 NAS_HOST/NAS_USER 提供 |
remote.directory |
项目专属工作目录,默认 /volume1/docker/<project>;NAS_DIRECTORY 可覆盖 |
remote.docker |
可选 Docker 可执行文件绝对路径,不是 Shell 命令 |
remote.sudo |
auto、never 或 always,只使用免交互 sudo -n |
remote.identityFile |
可选密钥路径;支持 NAS_IDENTITY_FILE 覆盖 |
remote.port |
SSH/SCP 端口;支持 NAS_SSH_PORT 覆盖 |
container.network |
bridge 或 host,host 模式不能同时配置 ports |
container.ports |
host/container 端口,支持 IPv4 绑定地址和 tcp/udp |
container.mounts |
目录 bind mount;可选 readOnly 和 owner,二者不能同时使用 |
container.env |
非敏感固定环境变量,值必须是字符串 |
container.envFrom |
从环境文件/进程环境中提取的变量名白名单,发布时未配置会报错;可显式设为空。构建、状态和恢复不要求这些变量齐全 |
container.user |
可选数值 UID 或 UID:GID,默认沿用镜像 |
container.command |
可选字符串数组,覆盖镜像 CMD |
health.command |
可选容器内 Shell 检查命令;不配置时要求镜像有 HEALTHCHECK |
health.timeout |
整体等待健康的秒数,默认 120 |
health.interval/checkTimeout/startPeriod/retries |
配置 command 时生效,默认 5/3/20 秒、6 次 |
cleanup.retainPrevious |
默认 0:只留当前;可设 1:保留一个 previous 容器及其镜像 |
cleanup.localArchive |
默认 true:部署成功后删除本次本地压缩包和对应 manifest |
cleanup.localImages |
默认 true:清理未被使用的本项目旧版本本地镜像;不清理构建缓存 |
envFile |
环境文件路径,默认 .env.deploy;显式指定的文件必须存在 |
artifactDir |
本机构建产物及锁目录,默认 .nas-deploy |
mount.owner 会在旧容器停止后、启动新容器前,使用新镜像的 chown 程序递归设置所有权。因此仅应用于项目专属目录,且镜像需要有 chown;不指定则不改权限。只读挂载必须预先准备合适权限。
对于 health command,可参考 使用案例 中的 Node.js fetch 检查,也可使用镜像内已有的其他工具;命令会在容器内部执行。健康检查必须有意义,仅容器进程启动不算发布成功。
环境变量与敏感信息
应用变量优先级:进程环境 > .env.deploy > container.env,其中只有 envFrom 显式列出的名字会从外部读取。NAS 连接变量优先级:进程环境 > 环境文件 > JSON。
环境文件支持 KEY=value、可选 export 前缀、整行注释、空值及单/双引号。引号内内容原样使用,不处理反斜线转义、$VARIABLE、${VARIABLE} 或 $(command);每个值必须是单行。未加引号的值中,空白后面的 # 开始注释。
APP_TOKEN='literal-value-with-$-characters'
OPENAI_API_KEY=
应用环境文件只包含白名单变量,使用临时目录和 0600 权限,通过 SCP 传递,不拼接到 SSH 命令参数中。部署日志不主动打印应用日志或变量值。Docker 管理员仍可通过容器配置查看运行环境,这不是独立密钥管理系统。
构建时保留项目已有的 Dockerfile 专属 .dockerignore 或根 .dockerignore 规则,使用临时 Dockerfile 追加排除 .git、.env*、本工具产物目录和当前环境文件,避免把这些内容带入构建上下文。原项目文件不被改写。不要通过 build.args 传递密钥;本版本不封装 BuildKit secrets。
命令和恢复
nas-deploy build # 只构建,不连接 NAS
nas-deploy deploy # 部署最新的本地完整产物
nas-deploy deploy --release <release-id> # 部署仍保留的指定产物
nas-deploy release # 构建并部署
nas-deploy status # 查看当前、候选和恢复容器
nas-deploy rollback # 显式恢复保留的旧容器
nas-deploy cleanup # 重试健康部署后的清理
nas-deploy doctor # 检查本机 Docker、NAS Docker/架构/sha256sum
nas-deploy release --config ./deploy/nas.json --env-file ./production.env
正常健康发布后,NAS 目录只保留归属标记、last-success 发布记录及应用持久化目录。本地 .nas-deploy/last-deployed.json 记录最后一次部署产物,帮助重试本地清理。build 单独运行时保留产物,失败的本地构建/发布产物不自动删除。
如果 NAS 已经健康,但后续清理失败,命令返回非零。日志会指出已进入清理阶段;先 status 核实,再运行 cleanup 重试,不必重新构建。若清理在 NAS 阶段失败,本地尚未写入 last-deployed.json,本次本地归档会保留,之后可手动删除。
失败的候选容器使用 <name>-candidate,旧容器一般仍为 <name>;晋升中断时旧容器可能名为 <name>-previous。rollback 会识别这些情况,先启动旧版本并等待健康,再删除候选容器和无用文件。首次部署没有旧容器可恢复时会明确报错,需检查 NAS 容器日志并手动删除候选容器后重试。
默认成功发布会删除旧版本,所以成功后没有历史版本可供 rollback。需要保留恢复点时,事先设 cleanup.retainPrevious: 1。容器恢复不恢复数据库、文件或权限变更;不兼容的数据迁移需停止应用并恢复部署前备份后,再启动旧版本。
强制终止或断电可能留下 .nas-deploy/.lock 或 NAS 的 <remote.directory>/.deploy-lock。先确认没有本机命令、SSH 会话或 NAS 部署进程仍在执行,再手动移除对应锁;工具不会按时间自动抢占锁。普通失败会尝试释放本次操作持有的锁,绝不会删除其他操作的锁。
验证范围
npm test 使用临时目录、独立的本地/NAS Docker 状态以及 SSH/SCP 替身,执行实际 CLI 和远端 Bash 脚本。覆盖压缩包写入失败、校验失败、上传失败、容器创建失败、健康失败、晋升中断、恢复、并发锁、跨项目清理隔离、保留策略和参数转义。测试不访问真实 NAS,也不要求 Docker 服务运行。
实机验收需要运行 Docker 服务,在测试 NAS 或独立测试目录执行 doctor 和连续两次 release,确认真实健康检查和清理结果后,再接入生产项目。