npm.io
0.1.0 • Published 7h agoCLI

@mingo_789/nas-deploy

Licence
UNLICENSED
Version
0.1.0
Deps
0
Size
88 kB
Vulns
0
Weekly
0

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 prunesystem prunebuilder 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_HOSTREMOTE_USER 改为 NAS_USER,把网络、端口、挂载和 Docker 设置迁到 JSON。REMOTE_PASS 不再使用,REMOTE_DOCKER='sudo docker' 对应 remote.sudo: "always"

首次接管已有部署需要安排维护窗口:

  1. 先备份持久化数据,确认新配置挂载的是原来的数据目录。
  2. 执行 builddoctor,确认配置、镜像和连接可用。
  3. 在 NAS 停止原容器,将其改名为另一个明确的备份名称,例如 plot-assistant-legacy,释放原名称和端口。
  4. 使用新工具执行 deploy,验证应用和已有数据。
  5. 确认成功后,手动清理旧工具创建的备份容器和镜像。

新工具不修改、删除或接管缺少本项目管理标签的同名容器;旧备份也不在自动清理范围内。首次迁移失败时通过旧容器手动恢复,后续由本工具发布的版本才支持工具内的失败恢复。

配置

所有路径以配置文件所在目录为基准,和执行命令时的目录无关。配置文件使用严格 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 autoneveralways,只使用免交互 sudo -n
remote.identityFile 可选密钥路径;支持 NAS_IDENTITY_FILE 覆盖
remote.port SSH/SCP 端口;支持 NAS_SSH_PORT 覆盖
container.network bridgehost,host 模式不能同时配置 ports
container.ports host/container 端口,支持 IPv4 绑定地址和 tcp/udp
container.mounts 目录 bind mount;可选 readOnlyowner,二者不能同时使用
container.env 非敏感固定环境变量,值必须是字符串
container.envFrom 从环境文件/进程环境中提取的变量名白名单,发布时未配置会报错;可显式设为空。构建、状态和恢复不要求这些变量齐全
container.user 可选数值 UIDUID: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>-previousrollback 会识别这些情况,先启动旧版本并等待健康,再删除候选容器和无用文件。首次部署没有旧容器可恢复时会明确报错,需检查 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,确认真实健康检查和清理结果后,再接入生产项目。

Keywords