Skip to content

Latest commit

 

History

History
624 lines (528 loc) · 35.8 KB

File metadata and controls

624 lines (528 loc) · 35.8 KB

nsetup 架构设计

本文面向开发与设计维护;安装、部署和认证操作分别见 使用与运维和 Authelia 认证。

nsetup 是 Linux 主机上的 Docker Compose 应用管理工具,面向家庭服务器场景: 一台机器、一个反向代理入口、若干容器化应用。

单个 musl 静态链接二进制包含两种角色:

  • daemon:以 root 运行的 systemd 服务,独占 Docker 操作权限;
  • CLI:普通用户使用的管理命令,通过本机 Unix socket 上的 gRPC 调用 daemon。 nihility 用户组成员即可操作,无需 sudo。
flowchart LR
    CLI["nsetup CLI"] -- "gRPC over Unix socket<br/>(可选 TCP + token 远程)" --> D["nsetup daemon"]
    D --> T["template<br/>内置模板"]
    D --> S["spec<br/>统一数据结构 IR"]
    D --> O["orchestrator<br/>项目编排"]
    T --> S --> O
    O --> K["docker compose"] --> Docker[(Docker)]
    O --> FS[("stacks_root/<br/>项目目录")]
Loading

统一数据结构(IR)

整个系统只有一种「应用运行所需数据」的表示(IR)。任何输入(TOML 配置、CLI 标志、compose 文件)都先转换为 IR,daemon 只从 IR 生成 compose.yaml;修改时 从 compose.yaml + .env 解析回 IR,改完重新生成。compose 文件是项目状态的 唯一持久化真相。

classDiagram
    class StackSpec {
        +String name
        +Document document
        +BTreeMap~String,String~ environment
        +load(dir)$
        +compose_yaml() String
        +env_file() String
    }
    class Document {
        +BTreeMap~String,Service~ services
        +BTreeMap~String,Network~ networks
    }
    class Service {
        +String image
        +Option~String~ container_name
        +Vec~String~ command
        +Option~String~ restart
        +Option~String~ network_mode
        +Vec~String~ networks
        +Vec~String~ ports
        +Vec~String~ volumes
        +BTreeMap~String,String~ environment
        +Vec~String~ env_file
        +Vec~String~ labels
        +Option~String~ user
        +Vec~String~ group_add
        +Option~Healthcheck~ healthcheck
        +Option~Logging~ logging
    }
    StackSpec --> Document
    Document --> Service

    class SemanticView["语义视图(由字段双向派生)"] {
        +routes() Vec~Route~
        +set_routes(routes, middlewares)
        +user_route_identities() Vec~RouteIdentity~
        +route_bindings() Vec~RouteBinding~
        +image_version() / set_image_version()
        +host_ports() Vec~PublishedPort~
        +project_hooks() / service_hooks(stage)
    }
    Service ..> SemanticView : Traefik labels / image 字段解析
Loading
  • Document/Service 是 compose YAML 的强类型模型,serde 双向(序列化 + 解析),全部 deny_unknown_fields:不在上表中的 compose 指令(如 depends_on、build、顶层 volumes、list 形式的 environment/labels)不会 进入 IR。nsetup import 在解析前先按白名单剔除这些键并在结果中列出被忽略的 字段,因此接管既有 Compose 项目不会因为无关字段整体失败;受支持字段本身无效 时仍然报错。
  • 路由冲突判定使用 host + path_prefix + entrypoint + protocol 组合 (RouteIdentity),因此同一 host 下可以按路径或协议拆分多条路由。手写 labels 中的 router 只提取 Host(...) 身份用于诊断,不参与冲突校验; traefik.enable 原样保留,仅在声明了路由时才由 nsetup 补齐。
  • 启动钩子([services.*.hooks])不属于 compose 字段,序列化到 .env 的 NSETUP_APP_HOOKS_JSON 中,由 daemon 在 compose up 前后以项目目录为工作目录 执行。
  • 语义视图不独立存储:路由、中间件、镜像版本由 labels 和 image 字段按需解析; 修改时清除旧的生成 labels 再重新生成,派生数据与字段永远一致。
  • .env 解析为 environment 映射。模板附属文件(Traefik 中间件定义、站点 内容)不属于 IR,只在应用配置时写盘,编辑不触碰。

应用配置格式(TOML)

TOML 是唯一的声明式配置格式,用 template 字段选择内置模板,缺省 app。 nsetup template <名称> 输出带注释的配置骨架。

app 模板(缺省):容器应用,支持单服务或多服务

nsetup template app 输出逐字段注释骨架:必填项与常用最小路由保持启用,具有宿主机 副作用或通常可沿用默认行为的字段以注释示例展示;取消注释前应按实际应用修改。

format = 1
name = "media"

[services.web]
image = "ghcr.io/example/media"    # 镜像仓库,不含标签
version = "1.0"                    # 镜像版本标签(独立配置,升级即改此项)
port = 8080                        # 容器端口,供路由与发布使用
publish = ["12780:8080/tcp"]       # 宿主机端口映射
volumes = ["/srv/media:/data"]     # 绝对路径 bind mount
environment = { LOG_LEVEL = "info" }
command = ["--serve"]
network = "bridge"                 # bridge | host | external
external_network = "my-net"        # network = "external" 时使用
labels = ["com.example.team=infra"]
[services.web.healthcheck]
command = "curl -fsS http://localhost:8080/health"
interval = "30s"
timeout = "3s"
retries = 3

[services.web.traefik]             # 反向代理路由(可多个 host)
hosts = ["media", "media.example.com"]
middlewares = ["gzip", "internal-only"]
protocol = "http"                  # http | https | h2c
sticky_cookie = false
priority = 100

[services.worker]
image = "ghcr.io/example/worker"
version = "1.2"

多个子路由必须使用具名映射,而不是顺序数组:

[services.web.traefik.routes.api]
hosts = ["api"]
port = 8080

[services.web.traefik.routes.admin]
hosts = ["admin"]
port = 9090

路由键(api / admin)是稳定身份,分别生成 nsetup-<项目>-<服务>-api / nsetup-<项目>-<服务>-admin router 与 backend; 域名、端口和其他路由参数都在同一个具名表内声明,增删或重排其他路由不会改变身份。 旧的 [[services.*.traefik.routes]] 数组语法不再接受。

image 与 version 在生成时合成 image:version 写入 IR,解析时拆开;image 本身带标签或摘要属于配置错误。各模板的版本字段语义一致(其中基础设施模板的 version 分别表示所属模板的容器镜像版本),版本校验(C1) 统一作用于该字段。

服务只要有路由,就同时接入两个网络:外部共享的 nsetup-proxy(Traefik 回源) 和项目网络 project。项目网络的 Docker 名被钉为 <项目>_default,与未显式声明 networks 的服务所处的隐式默认网络同名,因此同一项目里带路由与不带路由的服务 仍在同一个网络上。服务接入多个网络时额外生成 traefik.docker.network=nsetup-proxy,避免 Traefik 选到项目网络而无法回源。

traefik 模板:反向代理基础设施

format = 1
template = "traefik"
domain = "example.com"
acme_email = "admin@example.com"
cloudflare_token = "..."
version = "v3.8.0"
http_port = 80
https_port = 443
dashboard_authelia = true

生成 Traefik 项目:ACME + Cloudflare DNS 证书、HTTP→HTTPS 重定向、HTTP/3、 仅内网可访问的 dashboard,以及供所有应用引用的中间件配置(authelia / gzip / forwarded-headers / internal-only / tls)。Traefik 项目与应用项目完全同构: 同样经 IR 生成、受同样的约束、用同样的命令运维。升级 Traefik 就是修改 version 后重新 up --force。

dashboard_authelia = true 会在固定的 internal-only@file 之后追加 authelia@file,使控制台同时受内网来源限制与 Authelia 登录保护;关闭或省略时 只保留内网限制。启用前应先准备可用的 Authelia 配置。

dashboard 只生成一条 router(nsetup-traefik-traefik-dashboard):后端固定是内置的 api@internal(永远不写 loadbalancer.server.port),entrypoint 为 https, priority 1000,带内网来源限制与主域名通配符证书域名。0.2.1 曾经额外生成一条把容器 8080 当后端的应用式路由,两条 router 抢同一个 Host(),实际命中的是回源到容器端口 的那条(表现为 dashboard 404);0.2.2 起不再生成。

metrics = true 时除启动参数外,还会在 config/dynamic/nsetup.yml 里为指标入口 生成两条 file provider router:Path(/metrics) → prometheus@internal,以及 PathPrefix(/api) || PathPrefix(/debug) → api@internal。没有 router 时该入口 一律 404,Prometheus 的 traefik:8081 job 与 nsetup doctor 都无法工作。

内置 tls 是响应头中间件(HSTS:stsSeconds + stsIncludeSubdomains),不是路由级 TLS 开关;路由是否终止 TLS 由 entrypoint 决定。它必须继续生成,因为历史配置把 tls 写在 middlewares 列表里,缺失会让整条路由报 middleware "tls@file" does not exist。

Traefik 的运行默认值以 main 分支既有基础设施生成器为基线:dashboard 路由固定 指向 api@internal;路由使用 cloudflare resolver 和主域名 + 通配符 SAN;关闭 匿名统计、版本检查、access log 与 tracing;默认启用 Prometheus 指标、ping 健康 检查、HTTP/3、EC256、指定 DNS resolver、30 秒 DNS 传播等待,以及 json-file 日志 滚动。健康检查固定为 CMD ["traefik", "healthcheck", "--ping"]:Docker 的 HEALTHCHECK 是独立进程,读不到容器 command 里的 --ping=true,缺 --ping 会一直报 「please enable ping to use health check」而恒为 unhealthy。当前架构只调整受管路径、 网络名和新增的 Authelia 中间件,不重新猜测基础设施默认值。

authelia 模板:基础认证设施

format = 1
template = "authelia"
host = "auth"
version = "4.39.20"
default_redirection_url = "https://example.com"
default_policy = "one_factor"
jwt_secret = "...至少 32 字符..."
session_secret = "...至少 32 字符..."
storage_encryption_key = "...至少 32 字符..."

[users.admin]
display_name = "Administrator"
password_hash = '$argon2id$...'
email = "admin@example.com"
groups = ["admins"]

OIDC provider 是 Authelia 模板的可选全局能力,只拥有 provider HMAC 与签名 私钥:

[oidc]
hmac_secret = "...至少 64 个 RFC3986 非保留字符..."
jwk_private_key = """
-----BEGIN PRIVATE KEY-----
...PKCS#8 或 PKCS#1 RSA 私钥...
-----END PRIVATE KEY-----
"""

# 可选:把 ID Token 之外的 claim 直接写入 ID Token。
# 键名即客户端 claims_policy 引用的策略名。
[oidc.claims_policies.default]
id_token = ["groups", "email", "email_verified", "preferred_username", "name"]

Authelia 默认只把协议必需的 claim 放进 ID Token,其余 claim 按规范通过 UserInfo 端点交付。Grafana 等客户端不会主动请求 UserInfo,因此需要 claims_policies 这一逃生舱把 email、name、groups、preferred_username 写进 ID Token,否则登录可用但邮箱、显示名与角色映射全部为空。每个 policy 至少 声明 id_token 或 access_token 之一;policy 属于 provider,由客户端用 claims_policy = "<名称>" 引用,引用了未声明的策略时 Authelia 启动即报错。

客户端不属于 Authelia TOML,而是由对应的 app TOML 声明。具名映射键是稳定的 client_id:

[authelia.oidc_clients.gitea]
client_name = "Gitea"
client_secret_hash = '$pbkdf2-sha512$...'
authorization_policy = "two_factor"
redirect_uris = ["https://git.example.com/user/oauth2/authelia/callback"]
scopes = ["openid", "profile", "email", "groups"]
grant_types = ["authorization_code", "refresh_token"]
require_pkce = false
token_endpoint_auth_method = "client_secret_basic"
claims_policy = "default"          # 可选;引用 Authelia 项目的 claims policy

机密客户端只接受密钥摘要,不接受明文;客户端应用保存生成摘要时对应的明文。 公共客户端不配置摘要,必须启用 S256 PKCE 并使用 none token endpoint 认证。 回调 URI 是区分大小写的完整 URI,非回环 HTTP 回调会被拒绝。OIDC 客户端的 authorization_policy 与用于 ForwardAuth 的 access_control.default_policy 相互独立。

应用客户端映射作为 app IR 的受管元数据存入该应用 .env,以便独立导出。 部署 app 时,编排层将它原子同步为 authelia/config/oidc-clients/<应用项目>.yml;取消声明或删除 app 时删除同名 片段。重新部署 Authelia 时会从所有现有 app IR 重建该目录。跨项目校验保证 client_id 全局唯一;已部署的 Authelia 未启用 [oidc] 时拒绝新客户端。 片段变更后会明确提示重启 Authelia,不隐式改变其运行状态。

users 下的 TOML 表名是认证用户名:[users.admin] 生成用户名 admin; display_name 只用于展示,email 默认也不作为登录别名。修改登录名必须修改表名, 例如将其改为 [users.nihilityer],再执行 up --force 与 restart authelia。 用户名改名按删除旧用户并新增用户处理,不隐式迁移以旧用户名关联的认证状态。

生成固定名 authelia 项目,使用文件用户库、SQLite 和文件型密钥。配置与用户库 位于项目目录并只读挂载,密钥文件使用 0600 且只读挂载;SQLite、TOTP 等运行 状态位于第一个 data_root 的 authelia/,删除项目不会删除认证状态。用户库由 TOML 声明管理,watch = false,并禁用容器内密码修改与重置,避免运行时文件和 导出状态形成双重真相。用户名或密码哈希变更后必须显式重启容器加载新用户库。 storage_encryption_key 是持久化状态的加密根密钥,数据库初始化后必须保持稳定; 轮换必须先用旧密钥执行 Authelia 的 storage encryption change-key,不能仅重写配置。

可选的 [telemetry] 只生成 telemetry.metrics.address 与 telemetry.tracing.*: Authelia 4.39.x 的指标路径固定为 /metrics,写出的 telemetry.metrics.path 会被 当成未知配置键并 fatal 退出(configuration key not expected: telemetry.metrics.path), 容器随即进入无限重启。声明里的 metrics_path 仍被接受并原样保留在导出结果中 (export → up 无损),但不是 /metrics 时只记一条警告,不写入 YAML。

二次验证固定使用 TOTP:将其显式启用并设为默认方法,同时禁用 WebAuthn;是否要求 二次验证仍分别由 ForwardAuth 的 default_policy 和 OIDC 客户端的 authorization_policy 决定。

启用 OIDC 时,模板额外生成 OIDC_HMAC_SECRET 与 OIDC_JWK_PRIVATE_KEY 两个 0600 文件,通过 Authelia template 配置过滤器从只读 /secrets 挂载读取; configuration.yml 不包含这两个 provider 密钥的明文。它在模板过滤阶段遍历各应用 片段,为 Authelia 构造单一 identity_providers.oidc.clients 列表;provider 配置仍 持久化在 Authelia 项目的受管 .env 中以支持独立导出。

claims_policies 是 provider 级结构,应用片段无法贡献,且 Authelia 的配置模板 过滤器不支持命名模板,因此由模板直接内联到生成的 YAML 中;没有已声明的客户端时 整个 identity_providers 块都不会生成,也就不需要 provider 密钥。应用片段目录 为空时不会产生空的 clients 键。

Traefik 模板始终生成 authelia@file ForwardAuth 中间件,地址固定为共享 nsetup-proxy 网络内的 http://authelia:9091/api/authz/forward-auth。普通应用在 路由中使用 middlewares = ["authelia"] 即可启用认证;认证门户自身不使用该 中间件,避免重定向循环。

static 模板:Nginx 静态站点

format = 1
template = "static"
name = "docs"
host = "docs"
version = "1.27"
middlewares = ["gzip"]

站点文件不写入 TOML,由 up --assets <目录> 随请求上传,落盘到项目目录的 site/,再挂到 nginx 的 /usr/share/nginx/html。容器同时以只读方式获得整个 项目目录(/opt/nsetup),把 nginx.conf 放进项目目录即可改写服务方式;默认 站点配置由模板拥有的 config/nginx/default.conf 提供。--assets-mode merge (默认)只覆盖同名文件,--assets-mode replace 才清空站点目录;上传文件使用 0644、目录 0755,使容器内非 root 进程可以读取。

数据流

创建与更新

CLI 标志类命令(如 edit 之外的创建入口)在客户端构建 TOML;daemon 侧只有 两条输入路径:TOML(Apply)与 compose 文件(ImportCompose),二者都汇入 IR 后由唯一的写盘收口生成项目。

flowchart LR
    A["nsetup up -f app.toml<br/>(含 authelia / traefik / static 模板)"] --> AP["Apply RPC<br/>config_toml (+assets)"]
    B["nsetup import name -f compose.yaml"] --> IC["ImportCompose RPC"]
    AP -->|解析 TOML → 模板生成| IR["StackSpec (IR)"]
    IC -->|解析 compose<br/>未知字段报错| IR
    IR --> D["deploy 收口<br/>白名单校验 → 规范化生成 → 原子写<br/>→ compose config 验证 → 失败回滚"]
    D --> E[("stacks_root/名称/<br/>compose.yaml + .env")]
Loading

修改

sequenceDiagram
    participant C as CLI
    participant D as daemon
    participant IR as StackSpec
    participant F as compose.yaml + .env
    participant K as docker compose

    C->>D: nsetup edit(含 --version 升级)
    D->>F: 读取
    F->>IR: 解析 compose + .env,反解语义视图
    D->>IR: 应用修改(字段 / set_routes / set_image_version)
    IR->>D: 重新生成规范 YAML
    D->>F: 原子写(验证失败自动回滚)
    D->>K: compose up -d [service]
Loading

导出

nsetup export:解析项目的 compose + .env,反解出完整 TOML(app 模板还原 各服务字段与路由选项;traefik / static 模板还原模板参数)。导出内容永远等于 当前真实状态,可直接用于 up 重建项目。

命令设计

远程访问是全局标志而非独立命令树:任何命令都可加 --endpoint <地址> --token-file <路径> 连接远程 daemon,本机缺省走 Unix socket。

命令 说明
nsetup init [--domain D] [--stacks-root P] [--data-root P]... [--docker-socket P] [--force] 安装 daemon:写配置、安装二进制与 systemd unit、启动服务。--force 覆盖已存在的安装,未指定的配置项沿用已有值
nsetup status daemon 版本、Docker 连通性、项目根目录、主域名
nsetup template [app|authelia|traefik|static] 输出带注释的配置骨架
nsetup up -f <配置.toml> [--assets <目录>] [--start] [--force] 创建或整体更新项目;流式返回排队、执行与完成阶段
nsetup import <名称> -f <compose.yaml> [--env-file <路径>] [--start] 导入 compose 文件(字段子集),新建或整体替换
nsetup export <名称> [-o <输出.toml>] 导出当前状态的 TOML
nsetup edit <名称> [--service <服务>] [选项...] 局部修改:--version(升级镜像)、--image、--port、--publish、--volume、--env、--host、--middleware、--label、--healthcheck-cmd、--remove-healthcheck、--start 等;未指定的项保持不变
nsetup list / nsetup show <名称> 项目列表 / 项目与容器详情
nsetup start / stop / restart <名称> 流式显示生命周期阶段;停止与重启使用 30 秒容器关闭上限
nsetup pull <名称> 拉取镜像;TTY 原地刷新单行进度,管道保留逐事件稳定输出
nsetup build <名称> 构建镜像
nsetup logs <名称> [--tail N] [-f] 只读日志流;不占用变更锁,客户端断开后终止 Compose 子进程
nsetup rm <名称> [--force] 停止并删除项目(--force 跳过确认);绑定挂载的数据不受影响

nsetup daemon 为隐藏命令,仅供 systemd unit 的 ExecStart 调用。daemon 的 启停与开机自启用 systemctl 管理。

RPC 接口

proto 定义与命令一一对应,共 14 个方法。声明式输入一律是 TOML 字符串;只有 Edit 使用类型化字段表达局部修改:

service Orchestrator {
  rpc Status(StatusRequest) returns (StatusResponse);

  rpc Apply(ApplyRequest) returns (stream OperationProgress);
  rpc ImportCompose(ImportComposeRequest) returns (stream OperationProgress);
  rpc Export(ExportRequest) returns (ExportResponse);
  rpc Edit(EditRequest) returns (stream OperationProgress);

  rpc List(ListRequest) returns (ListResponse);
  rpc Get(GetRequest) returns (Stack);
  rpc Remove(RemoveRequest) returns (stream OperationProgress);
  rpc Start(ActionRequest) returns (stream OperationProgress);
  rpc Stop(ActionRequest) returns (stream OperationProgress);
  rpc Restart(ActionRequest) returns (stream OperationProgress);
  rpc Pull(ActionRequest) returns (stream PullProgress);
  rpc Build(ActionRequest) returns (stream OperationProgress);
  rpc Logs(LogsRequest) returns (stream LogLine);
}

message ApplyRequest {
  string config_toml = 1;
  repeated Asset assets = 2;    // static 模板的站点文件
  bool start = 3;
  bool force = 4;               // 覆盖已存在的项目
}

message ImportComposeRequest {
  string name = 1;
  string compose_yaml = 2;
  optional string env_file = 3; // 缺省:新项目为空,已有项目沿用原文件
  bool start = 4;
}

message EditRequest {
  string name = 1;
  optional string service = 2;  // 单服务项目可省略
  optional string image = 3;
  optional string version = 4;
  repeated string command = 5;
  optional uint32 container_port = 6;
  repeated Route routes = 7;        // 提供时整体重建路由
  repeated PublishedPort published_ports = 8;
  repeated Volume volumes = 9;      // 追加
  map<string, string> environment = 10; // 合并
  optional NetworkMode network_mode = 11;
  optional string external_network = 12;
  repeated Middleware middlewares = 13;
  repeated string labels = 14;      // 按键替换
  optional Healthcheck healthcheck = 15;
  bool remove_healthcheck = 16;
  bool start = 17;
}

配置文件

/etc/nsetup/config.toml,扁平键:

domain = "example.com"                       # 短子域名拼接的主域名
stacks_root = "/var/lib/nsetup/stacks"       # 项目根目录
data_roots = ["/var/lib/nsetup/data"]        # bind mount 允许的路径前缀
listen = "unix:///run/nsetup/nsetup.sock"    # gRPC 监听;TCP 地址需配合 auth token
docker_socket = "/var/run/docker.sock"       # Docker API socket

bind mount 白名单 = data_roots ∪ stacks_root ∪ docker_socket(精确匹配), 完全由配置推导。

磁盘布局

内容 路径
二进制 /usr/local/bin/nsetup
systemd unit /etc/systemd/system/nsetup.service
配置文件 /etc/nsetup/config.toml(root:nihility、0640)
TCP 认证 token /etc/nsetup/auth.token(0600)
gRPC socket /run/nsetup/nsetup.sock(root:nihility、0660)
项目 stacks_root/<名称>/{compose.yaml, .env, config/, secrets/, site/, files/}
项目 --files 上传内容 stacks_root/<名称>/files/(0644/0755,容器内挂到 /opt/nsetup/files)
Traefik 动态配置 stacks_root/traefik/config/dynamic/{nsetup.yml, custom.yml}(providers.file.directory)
Authelia 状态 data_roots[0]/authelia/{db.sqlite3, notification.txt}

模块划分

模块 职责
cli CLI 稳定入口,重新导出参数模型和命令执行器
cli/args clap 参数、子命令以及中文帮助模板
cli/runner 本地命令和远程 RPC 命令分发
cli/edit 编辑参数到类型化 protobuf 请求的转换
cli/io 配置、静态资源、导出文件与 stdout 操作
config 配置文件模型、加载、白名单推导
install init:自安装与 systemd unit 管理
spec IR 公共数据模型与稳定入口;具体职责拆入 spec/project、spec/service、spec/value
spec/project 项目级 compose YAML、.env 加载、校验与规范化序列化
spec/service 服务镜像版本与 Traefik label 语义视图的双向转换
spec/value 路由、端口、bind mount、健康检查、镜像与名称值对象校验
template 模板公共 TOML 模型、注册表与稳定入口
template/authelia Authelia TOML ↔ IR、服务定义与基础配置生成
template/authelia/oidc Authelia OIDC provider 校验、配置与文件型密钥
template/authelia/users 文件用户库模型、校验与 YAML 生成
template/oidc app 拥有的 Authelia OIDC 客户端模型、校验与 YAML 片段
template/generate app/traefik/static TOML → IR,并生成模板附属文件
template/reverse 当前 IR → 规范化 TOML,恢复模板参数与服务语义
template/skeleton CLI 输出的带注释 TOML 配置骨架
orchestrator 编排器及其请求、响应公共模型
orchestrator/operations 应用、导入、查询和项目生命周期操作
orchestrator/edit 服务局部编辑、标签合并和网络语义修改
orchestrator/oidc 跨项目 OIDC client ID 校验及 app 片段向 Authelia 同步
orchestrator/deploy deploy 写盘收口、冲突检查和受管目录解析
orchestrator/storage 安全路径、附属文件复制、原子文件写入和回滚辅助
docker docker compose 子进程封装(统一使用配置的 docker_socket)
rpc 生成协议模块及 RPC 稳定入口
rpc/service gRPC 服务方法、变更互斥、只读并发和流式阶段调度
rpc/client Unix socket 与认证 TCP 客户端
rpc/transport UDS/TCP 监听、Bearer 认证和关闭信号
rpc/conversion protobuf 线路表示与领域模型之间的转换和校验

0.2.0 兼容性说明

升级到 0.2.0 时注意以下行为变化:

变化 说明
Traefik 动态配置改为目录 config/dynamic.yml 变为 config/dynamic/nsetup.yml,启动参数改用 --providers.file.directory;同目录的 custom.yml 与其它 *.yml 不会被 up 覆盖。重新部署 traefik 项目即可迁移。
指标默认开启 traefik 模板默认 metrics = true,在内网入口暴露 Prometheus 指标并绑定到宿主机回环地址;不需要时显式写 metrics = false。
静态站点挂载点变化 项目目录整体挂到 /opt/nsetup,站点根目录由 /usr/share/nginx/html 变为 /opt/nsetup/site。
路由冲突判定放宽 冲突按 host + path_prefix + entrypoint + protocol 判定,原先因同 host 被判重的配置升级后可以直接应用。
OIDC 客户端 PKCE 字段 仅在 require_pkce = true 时输出 pkce_challenge_method: S256,不再对全部客户端强制 PKCE。
应用后重建容器 部署会整体替换项目目录,up --start(以及 edit --start)在项目有运行中容器时使用 --force-recreate,避免容器继续持有已被替换的目录 inode。
RPC 线路协议变化 Route.middlewares 与 EditRequest.middlewares 由枚举改为字符串,并新增 Doctor、SetDomain 两个 RPC。升级时 CLI 与 daemon 必须来自同一版本;跨版本混用时新客户端调用新 RPC 会得到 unimplemented,中间件参数会自行校验失败而不是静默丢弃。

0.2.1 兼容性说明

升级到 0.2.1 时注意以下行为变化:

变化 说明
traefik / authelia 声明可省略 name 两个模板的项目名固定为 traefik / authelia,name 可省略也可显式写出;写其它值会明确报错。0.2.0 中这两个模板任何情况都无法应用,0.2.1 修复。nsetup template traefik|authelia 的骨架与 nsetup export 的产出都能直接 nsetup up。
--files / --assets 权限改为 a+rX 上传资源目录由 0750 改为 0755、文件 0644,容器内非 root 进程(如 nginx worker)可以直接读取;需要收紧时用 nsetup up --assets-perms private(0750/0640)。
static 模板支持 user / group_add / [hooks] 站点侧可以像 app 模板一样在 TOML 内修正属主与权限,不必再维护 chmod 兜底脚本。
nsetup show --routes 完整 修复了 router 前缀拼接错误:模板生成的路由(来源 nsetup)与用户手写 label 路由(来源 labels)会一起列出,并补上 BACKEND 与 PRIORITY 列。
volumes 支持相对项目目录的挂载源 files/x.yaml、./config 与绝对路径等价,stacks_root 变更时不需要改仓库里的 TOML;命名卷与 ../ 仍被拒绝,展开后仍走白名单。
新增 up --files-only 只重新上传 --files / --assets 并保留现有容器(项目目录 inode 不变),用于同步配置文件内容;不触碰 compose.yaml、.env,也不执行钩子与 OIDC 同步。
钩子输出与失败提示 钩子的 stdout/stderr 会作为进度信息回显;失败时的错误包含退出码、完整输出、容器当前状态与补救命令。nsetup up --help 说明钩子运行环境与 /tmp 只读。
Authelia /config 改为可写挂载 官方镜像 entrypoint 会执行 chown -R ${PUID}:${PGID} /config(镜像默认 0:0),只读挂载会让每次启动都往容器日志写 chown: ... Read-only file system;/secrets 保持只读。OIDC 片段改为原地重写,不再替换 inode。
--restart-dependents 文案 实际执行了重启时提示「已重启 authelia 使新客户端生效」,未重启时附带确切命令 nsetup restart authelia。

0.2.2 兼容性说明

升级到 0.2.2 时注意以下行为变化(全部来自 0.2.1 的实测反馈):

变化 说明
Authelia 不再生成 telemetry.metrics.path metrics_path 仍可写出、仍原样保留在 export 结果里,但不再进入 configuration.yml;Authelia 4.39.x 的指标路径固定为 /metrics,多出该键会让容器以 configuration key not expected: telemetry.metrics.path fatal 退出并无限重启。写上非 /metrics 的值时只记一条 warning。
traefik dashboard 只保留一条路由 不再额外生成把容器 8080 当后端的应用式路由。dashboard 固定 api@internal、priority 1000、internal-only@file(开启 dashboard_authelia 时再叠加 authelia@file),并显式声明主域名通配符证书域名。升级后重新 nsetup up -f traefik.toml --force --start,原先为绕开 404 而在 dynamic/custom.yml 里补的 dashboard-managed router 应当删除。
指标入口自动生成 router metrics = true 时 config/dynamic/nsetup.yml 里生成 Path(/metrics) → prometheus@internal 与 PathPrefix(/api) || PathPrefix(/debug) → api@internal;nsetup doctor 因此不再降级为仅检查容器 label。为绕开 404 而在 custom.yml 里补的 metrics-internal / api-internal router 应当删除。
内置 tls 中间件保证存在 tls 是 HSTS 响应头中间件,继续随 nsetup.yml 生成,历史配置里的 middlewares = ["tls"] 不需要改;在 custom.yml 里补的 tls 定义应当删除(同名 file provider 中间件会被后加载的文件覆盖)。骨架与文档补上「不是路由级 TLS 开关」的说明。
traefik 健康检查带 --ping 健康检查固定为 CMD ["traefik", "healthcheck", "--ping"],容器不再恒为 unhealthy;用 nsetup edit … --healthcheck-cmd 打的补丁不再需要。
nsetup doctor 归一化 router 名 比对前去掉 Traefik API 名字里的 @<provider> 后缀,不再把正常路由同时报成「未被加载」和「已不再声明」,完整报告重新可用。
traefik 路由表 BACKEND 显示内置服务 nsetup show traefik --routes 里 dashboard 的后端显示为 api@internal(不带端口),容器路由仍是 服务:端口。

0.2.3 兼容性说明

升级到 0.2.3 时注意以下行为变化(来自 0.2.2 的实测反馈):

变化 说明
播种的 dynamic/custom.yml 只含注释 骨架不再写 http: {}、routers: {}、services: {}、middlewares: {}:Traefik 的 structures 解码器把空映射判定为 standalone element,file provider 按整目录构建,一个文件解析失败就让同目录 nsetup.yml 里的 metrics / api 路由与内置 tls 中间件一起失效(/metrics 404、响应缺 HSTS、doctor 降级),而容器健康检查只看 --ping,仍然是 healthy。注释与空文件都能正常加载,示例因此全部保持注释状态。
旧骨架自动升级 0.2.0–0.2.2 播种的 custom.yml 会在下次 nsetup up(含 --force)时换成只含注释的新骨架,否则升级后故障仍在。只有内容与旧模板逐字节相同才替换;用户改过的内容仍逐字节保留。改过、但里面还留着空映射的文件不会被改写,需要手工删掉那些行。
config/dynamic 只保留两个文件 该目录是受管目录,nsetup up 会整体重写它,nsetup.yml 由模板拥有、custom.yml 由用户拥有,其余 *.yml 一律清除。0.2.0 起的骨架注释曾称「同目录新增其它 *.yml 效果相同」,该说法不成立,手工配置必须写在 custom.yml 里。

设计约束

  • C1 镜像钉版本:所有镜像必须带明确标签,拒绝 latest 与缺省标签;标签 须以字母、数字或下划线开头,仅含字母、数字、点、下划线、连字符,不超过 128 字符。
  • C2 校验先行:项目名(小写字母开头,仅小写字母/数字/-/_,≤63 字符)、 服务名、镜像引用、域名、宿主端口与路由冲突都在写入前校验,失败无副作用。
  • C3 写盘单点收口:所有 compose 写入经过同一个 deploy 收口:白名单校验 → 规范化生成 → 原子写 → docker compose config 验证 → 失败恢复原文件。
  • C4 密钥与权限:token、ACME、Authelia 密钥等写入 .env(0600),不进入 命令行参数;Authelia 额外生成 0600 密钥文件并只读挂载,容器环境只保存 _FILE 路径,生成的 configuration.yml 同样不含密钥明文;socket 与配置文件 权限见磁盘布局表。
  • C5 compose 字段子集:只接受 IR 模型覆盖的 compose 指令,未知字段导入时 报错。这保证任何项目都能被解析回 IR 进行编辑与导出。
  • C6 数据路径白名单:bind mount 源路径可以写成绝对路径(data_roots、 stacks_root 或 docker_socket 内),也可以写成相对受管项目目录的路径 (files/x.yaml、./config)。相对写法在生成 Compose 之前展开为项目目录下的 绝对路径,再与绝对路径一起做归一化(解析 ./..、符号链接)与白名单校验。 命名卷(mydata:/data)与越出项目目录的 ../ 仍然报错。白名单完全由配置 推导,对所有项目(含 Traefik)一视同仁。多个应用挂载同一路径以共享数据是 合法用法。Docker 启动容器时会自动创建缺失的宿主目录,因此校验发生在任何 compose up 之前。
  • C7 禁止命名卷:持久化数据一律使用白名单内的 bind mount,位置可审计、 可备份。compose 顶层 volumes 段与 NAME:/path 语法在导入时报错。 删除项目只停止容器并删除项目目录,绑定挂载的数据不受影响。
  • C8 变量镜像只读:镜像引用含环境变量(如 Traefik 的 ${TRAEFIK_VERSION} 或 Authelia 的 ${AUTHELIA_VERSION})的项目不能用 edit --version 升级;其版本由所属模板的参数管理,通过 up --force 整体应用。
  • C9 操作可观测与可取消:变更 RPC 在等待锁、执行和完成时发送阶段事件;中间 事件写 stderr,最终结果保持 stdout 稳定。查询和日志不占用变更锁;持续日志与 拉取每 200ms 检查客户端连接,断开时终止所属 Compose 子进程。