moby-daemon-config配置

章节 关键文件 解答
1. 模块定位与边界 config.go 顶部常量 config 包做什么、不做什么
2. 数据模型总览 CommonConfig + 平台 Config 一个 Config 结构由哪些字段组成
3. 嵌入式子结构 LogConfig/DNSConfig/TLSOptions/... 8 个内嵌结构体的语义
4. 平台分叉 config_linux.go / config_windows.go Linux/Windows 在配置上的差异
5. 默认值装配 New() + setPlatformDefaults 默认值从哪来
6. 文件读取与编码兼容 getConflictFreeConfiguration BOM/UTF-16/UTF-8 兼容、空文件
7. 冲突检测 findConfigurationConflicts 命令行 vs 文件、unknown key、迁移键
8. 合并策略 MergeDaemonConfigurations flagConfig 覆盖 fileConfig(mergo)
9. 校验体系 Validate + validatePlatformConfig 跨平台 + 平台特定校验
10. 热重载 Reload + reloadConfig SIGHUP → reload 回调链
11. Builder 子配置 builder.go BuildKit 的 GC/Entitlements/History 配置
12. 客户端使用 command/daemon.go::loadDaemonCliConfig 整套调用链
13. 设计要点小结 --- 易踩坑点 + 设计哲学

1. 模块定位与边界

daemon/config 是 dockerd 的纯配置数据模型 + 加载/校验工具

  • 数据模型ConfigCommonConfigBuilderConfigBridgeConfigLogConfigDNSConfigTLSOptionsNetworkConfigProxiesDaemonLogConfig,以及它们的平台变体。
  • 加载工具New()(默认值)、MergeDaemonConfigurations(启动时合并)、Reload(SIGHUP 热重载)。
  • 校验工具Validate(跨平台)、ValidateMinAPIVersionGetConflictFreeLabelsGetExecOptSanitize(脱敏)。
  • 平台钩子setPlatformDefaults / validatePlatformConfig / validatePlatformExecOpt / IsRootless / IsSwarmCompatible / GetExecRoot / GetInitPath,按 config_<os>.go 文件分叉。

它不做

  • 修改 Daemon 运行时状态------所有"应用配置"的动作都在调用方(daemon.Daemon.ReloadDaemonCli.reloadConfig);
  • 直接读 pflag------flags *pflag.FlagSet 由调用方传入;
  • 做 plugin 加载、不建容器、不连 containerd;
  • 写文件------daemon.json 由用户维护。

它是一套"纯函数式的数据 + 校验"工具,便于单元测试(参见 config_test.go ~855 行)。

顶层常量速查

ini 复制代码
// config.go
const (
    DefaultMaxConcurrentDownloads = 3
    DefaultMaxConcurrentUploads   = 5
    DefaultDownloadAttempts       = 5
    DefaultShmSize  int64 = 64 * 1024 * 1024 // 64 MiB
    DefaultNetworkMtu             = 1500
    DisableNetworkBridge          = "none"
    DefaultLogDriver              = "json-file"
    DefaultShutdownTimeout        = 15
    DefaultInitBinary             = "docker-init"
    DefaultRuntimeBinary          = "runc"
    DefaultContainersNamespace    = "moby"
    DefaultPluginNamespace        = "plugins.moby"
    MaxAPIVersion                 = "1.54"
    defaultMinAPIVersion          = "1.40"
    MinAPIVersion                 = "1.24"
    SeccompProfileDefault         = "builtin"
    SeccompProfileUnconfined      = "unconfined"
    LibnetDataPath                = "network/files"
)

注意几个细节:

  • MaxAPIVersion 是当前 daemon 支持的最高 API;defaultMinAPIVersion 是默认最低;MinAPIVersion 是绝对下限(不可突破)。三者形成 [MinAPIVersion, MaxAPIVersion] 的合法区间。
  • DefaultContainersNamespaceDefaultPluginNamespace 决定 dockerd 在 containerd 中给业务容器和插件用的命名空间,是 dockerd 与 containerd 的"领地划分"。
  • DisableNetworkBridge = "none"--bridge=none 的禁用 bridge 关键字。

三张"白名单"map

config.go 顶部有三张关键 map,控制文件解析时的特殊行为:

go 复制代码
// 不能被 flatten 的字段------这些 key 在 daemon.json 里要整体当 map 处理
var flatOptions = map[string]bool{
    "cluster-store-opts": true, "default-network-opts": true,
    "log-opts": true, "runtimes": true, "default-ulimits": true,
    "features": true, "builder": true, "nri-opts": true,
}

// unknown-key 校验时跳过这些(只在 daemon.json 出现、没有对应 CLI flag)
var skipValidateOptions = map[string]bool{
    "features": true, "builder": true, "min-api-version": true,
    "deprecated-key-path": true,
    "allow-nondistributable-artifacts": true,
}

// 冲突检测时跳过这些(允许在 flag 和 file 同时出现,会被合并)
var skipDuplicates = map[string]bool{
    "runtimes": true,
}

理解这三张 map 是读懂后面 getConflictFreeConfiguration / findConfigurationConflicts 的钥匙。


2. 数据模型总览

c 复制代码
config.Config                              ← 平台相关结构(Linux 有更多字段,Windows 几乎空)
└── embeds config.CommonConfig             ← 跨平台公共字段
      ├── AuthorizationPlugins []string
      ├── ExecOptions          []string
      ├── GraphDriver          string        (storage-driver)
      ├── GraphOptions         []string      (storage-opts)
      ├── Labels               []string
      ├── Root / ExecRoot / Pidfile
      ├── SocketGroup          string        (group)
      ├── Proxies                          ← 内嵌(http-proxy/https-proxy/no-proxy)
      ├── LiveRestoreEnabled   bool
      ├── MaxConcurrentDownloads/Uploads/DownloadAttempts
      ├── ShutdownTimeout      int
      ├── Hosts []string
      ├── TLS *bool / TLSVerify *bool       ← 指针,三态:未设置 / true / false
      ├── SwarmDefaultAdvertiseAddr / SwarmRaftHeartbeatTick / SwarmRaftElectionTick
      ├── MetricsAddress       string
      ├── DaemonLogConfig                   ← 内嵌(log-level / log-format / raw-logs)
      ├── TLSOptions                        ← 内嵌(tlscacert / tlscert / tlskey)
      ├── DNSConfig                         ← 内嵌(dns / dns-opts / dns-search / host-gateway-ips)
      ├── LogConfig                         ← 内嵌(log-driver / log-opts)
      ├── BridgeConfig                      ← 内嵌(bridge / fixed-cidr / icc / iptables / ip-masq / ...)
      ├── NetworkConfig                     ← 内嵌(default-address-pools / firewall-backend / ...)
      ├── registry.ServiceOptions           ← 内嵌(registry-mirrors / insecure-registries / ...)
      ├── Experimental bool
      ├── NodeGenericResources []string
      ├── ContainerdAddr / CriContainerd / ContainerdNamespace / ContainerdPluginNamespace
      ├── Features map[string]bool
      ├── Builder BuilderConfig             ← BuildKit 子配置
      ├── DefaultRuntime string
      ├── CDISpecDirs []string
      ├── NRIOpts opts.NRIOpts
      ├── MinAPIVersion string
      └── ValuesSet map[string]any          ← 不序列化,记录"哪些字段是文件里显式设置的"

为什么这样设计? 通过 Go 内嵌(anonymous embed),JSON 反序列化时 daemon.json 里的扁平 key(如 "log-driver": "json-file")可以直接落到内嵌结构体(LogConfig.Type)的字段上------避免在 CommonConfig 里写一堆重复字段。json: tag 的命名跟 dockerd 命令行 flag 完全对齐(log-driverlog-optsstorage-driver ...),保证同一份文档既适合 daemon.json 也适合 CLI。

ValuesSet:用于回答"这个选项是否在文件里显式设置过"

go 复制代码
type CommonConfig struct {
    // ...
    ValuesSet map[string]any `json:"-"`  // 不参与序列化
}

func (conf *Config) IsValueSet(name string) bool {
    _, ok := conf.ValuesSet[name]
    return ok
}

ValuesSetgetConflictFreeConfiguration 里被填充:它记录"用户在 daemon.json 里实际写了哪些 key"。reloadConfigcfg.IsValueSet("debug") 来判断要不要修改 debug 状态------只有用户显式 写了 "debug": true/false 才去动它,避免没写就被默认值覆盖。


3. 嵌入式子结构逐一解读

3.1 LogConfig(容器默认日志)

go 复制代码
type LogConfig struct {
    Type   string            `json:"log-driver,omitempty"`
    Config map[string]string `json:"log-opts,omitempty"`
}

注意 "log-opts"flatOptions 白名单里,意味着 daemon.json 里的 log-opts不会 被展平到顶层,而是作为 map[string]string 整体保存------因为不同 log-driver 的 opts 完全不同(json-file 有 max-size/max-file,fluentd 有 fluentd-address,syslog 有 syslog-address...)。

3.2 DaemonLogConfig(daemon 自身日志)

c 复制代码
type DaemonLogConfig struct {
    LogLevel  string           `json:"log-level,omitempty"`      // panic/fatal/error/warn/info/debug/trace
    LogFormat log.OutputFormat `json:"log-format,omitempty"`     // text/json
    RawLogs   bool             `json:"raw-logs,omitempty"`       // 不做 daemon 序号前缀
}

log.OutputFormat 是 containerd 日志包定义的类型,自带 Marshal/Unmarshal。

3.3 TLSOptions(API server mTLS)

go 复制代码
type TLSOptions struct {
    CAFile   string `json:"tlscacert,omitempty"`
    CertFile string `json:"tlscert,omitempty"`
    KeyFile  string `json:"tlskey,omitempty"`
}

注意 CommonConfig.TLSTLSVerify 都是 *bool------三态:

  • nil:未设置(用默认行为);
  • &true:启用;
  • &false:显式禁用。

loadDaemonCliConfig 里有相应处理:--tlsverify 一旦设置就强制 TLS = true,无论 --tls 是否传过。

3.4 DNSConfig(容器默认 DNS)

go 复制代码
type DNSConfig struct {
    DNS            []netip.Addr `json:"dns,omitempty"`
    DNSOptions     []string     `json:"dns-opts,omitempty"`
    DNSSearch      []string     `json:"dns-search,omitempty"`
    HostGatewayIP  net.IP       `json:"host-gateway-ip,omitempty"`  // Deprecated
    HostGatewayIPs []netip.Addr `json:"host-gateway-ips,omitempty"`
}

HostGatewayIP(单 IP)已被 HostGatewayIPs(多 IP,含 v4+v6)取代。migratedNamedConfig 描述了从单 IP 到多 IP 的迁移规则:

go 复制代码
var migratedNamedConfig = map[string]struct {
    newName  string
    migrate func(*Config)
}{
    "host-gateway-ip": {
        newName:  "host-gateway-ips",
        migrate:  migrateHostGatewayIP,   // 把老的 net.IP 转成 []netip.Addr
    },
}

迁移逻辑:在新老 key 都没同时出现、且老 key 显式设置时,把 HostGatewayIPHostGatewayIPs

3.5 Proxies(daemon 出网代理)

go 复制代码
type Proxies struct {
    HTTPProxy  string `json:"http-proxy,omitempty"`
    HTTPSProxy string `json:"https-proxy,omitempty"`
    NoProxy    string `json:"no-proxy,omitempty"`
}

用于 docker build/pull 时 daemon 自己出网的代理,不是容器内进程的代理。Sanitize(cfg) 会用 MaskCredentials 把 URL 里的用户名密码脱敏后再打印,避免 reload 日志泄漏凭证。

3.6 BridgeConfig(默认网桥)

平台分叉:

Linux( config_linux.go

go 复制代码
type BridgeConfig struct {
    DefaultBridgeConfig                // 内嵌默认网桥字段
    EnableIPTables           bool      // iptables
    EnableIP6Tables          bool      // ip6tables
    EnableIPForward          bool      // ip-forward
    DisableFilterForwardDrop bool      // ip-forward-no-drop
    EnableIPMasq             bool      // ip-masq
    EnableUserlandProxy      bool      // userland-proxy
    UserlandProxyPath        string    // userland-proxy-path
    AllowDirectRouting       bool
    BridgeAcceptFwMark       string    // bridge-accept-fwmark
}

type DefaultBridgeConfig struct {
    commonBridgeConfig                  // iface + fixed-cidr(跨平台)
    EnableIPv6                  bool
    FixedCIDRv6                 string
    MTU                         int
    DefaultIP                   net.IP
    IP                          string  // bip
    IP6                         string  // bip6
    DefaultGatewayIPv4          net.IP
    DefaultGatewayIPv6          net.IP
    InterContainerCommunication bool    // icc
}

Windows( config_windows.go

go 复制代码
type BridgeConfig struct {
    DefaultBridgeConfig
    // nat 驱动没有专门参数,只有默认 nat 网络的几个字段
}
type DefaultBridgeConfig struct {
    commonBridgeConfig
    MTU int  // Windows 上不生效,但 flag 始终存在
}

commonBridgeConfig 是真正跨平台的子集:

go 复制代码
type commonBridgeConfig struct {
    Iface     string `json:"bridge,omitempty"`
    FixedCIDR string `json:"fixed-cidr,omitempty"`
}

这种"两层内嵌 + 共享 common"的结构是为了让平台代码能复用最通用的字段,同时各平台可以加自己的扩展字段。

3.7 NetworkConfig(daemon 级网络)

go 复制代码
type NetworkConfig struct {
    DefaultAddressPools     opts.PoolsOpt                  `json:"default-address-pools"`
    NetworkControlPlaneMTU  int                            `json:"network-control-plane-mtu,omitempty"`
    DefaultNetworkOpts      map[string]map[string]string   `json:"default-network-opts,omitempty"`
    FirewallBackend         string                         `json:"firewall-backend,omitempty"` // iptables/nftables
}

DefaultNetworkOpts 也在 flatOptions 白名单里,所以 JSON 里它是嵌套 map,不会被展平。FirewallBackend 仅 Linux 可用(Windows 在 validatePlatformConfig 直接报错)。

3.8 registry.ServiceOptions

直接复用 pkg/registry.ServiceOptions

arduino 复制代码
CommonConfig struct {
    // ...
    registry.ServiceOptions  // Mirrors / InsecureRegistries / ...
}

注释里 thaJeztah 留了 TODO 想把这个类型挪进 daemon/config,但暂时还依赖 pkg/registry


4. 平台分叉

config_linux.goconfig_windows.go 是用 build constraint 自然分叉的两个文件。两者都必须实现这几个平台钩子函数,签名固定:

函数 Linux 行为 Windows 行为
setPlatformDefaults(cfg) 设默认 root/exec-root/pidfile/seccomp/IPC/cgroupns/ulimits/shm-size/runtimes;rootless 时改用 XDG 路径 设默认 root=%PROGRAMDATA%\docker
validatePlatformConfig(conf) 校验 userland-proxy、IPC mode、fixed-cidr-v6、firewall-backend、fwmark、cgroupns 拒绝 firewall-backend;警告 MTU 不可配
validatePlatformExecOpt(opt, val) 拒绝 isolation;接受 native.cgroupdriver 接受 isolation;拒绝 native.cgroupdriver
IsRootless() 返回 cfg.Rootless 恒返回 false
IsSwarmCompatible() 检查 live-restore 与 nftables 兼容 恒 nil
GetExecRoot() 返回 cfg.ExecRoot 返回空串
GetInitPath() 优先 InitPath → DefaultInitBinary → "docker-init" 恒空串

Config 结构本身的平台差异

go 复制代码
// config_linux.go
type Config struct {
    CommonConfig
    Runtimes             map[string]system.Runtime
    DefaultInitBinary    string
    CgroupParent         string
    EnableSelinuxSupport bool
    RemappedRoot         string                       // userns-remap
    Ulimits              map[string]*container.Ulimit
    CPURealtimePeriod    int64
    CPURealtimeRuntime   int64
    Init                 bool
    InitPath             string
    SeccompProfile       string
    ShmSize              opts.MemBytes
    NoNewPrivileges      bool
    IpcMode              string
    CgroupNamespaceMode  string
    ResolvConf           string
    Rootless             bool
}

// config_windows.go
type Config struct {
    CommonConfig
    // 当前没有 Windows 专属字段
}

注意:

  • Runtimes 是 map 而非 slice ------每个 runtime 是 name -> {path, args} 的键值对。这就是为什么 skipDuplicates["runtimes"] = true:CLI 上多次 --add-runtime 与 daemon.json 里的 runtimes map 是合并关系而不是冲突。
  • Init 是 bool 不是 *bool------因为容器级 init 是单值开关,不存在"未设置"语义。
  • Linux 才有 RemappedRoot / CgroupParent / CPURealtimePeriod / EnableSelinuxSupport ------这些都是 Linux namespace/cgroup/selinux 专有的概念。
  • StockRuntimeName:Linux 是 "runc",Windows 是空串(让 dockerd 自动从其他选项推断 HCS runtime)。

lookupBinPath:FHS 兼容的二进制查找

go 复制代码
// config_linux.go
func lookupBinPath(binary string) (string, error) {
    if filepath.IsAbs(binary) {
        return binary, nil
    }
    lookupPaths := []string{
        "/usr/local/libexec/docker",
        "/usr/libexec/docker",
        "/usr/local/lib/docker",
        "/usr/lib/docker",
    }
    if strings.HasPrefix(binary, "docker-") {
        lookupPaths = append(lookupPaths, "/usr/local/libexec", "/usr/libexec")
    }
    for _, dir := range lookupPaths {
        if file, err := exec.LookPath(filepath.Join(dir, binary)); err == nil {
            return file, nil
        }
    }
    return exec.LookPath(binary)
}

用于找 docker-initdocker-proxy。注释明确引用了 FHS 3.0 / 2.3 规范------/usr/libexec 是 vendor 内部二进制的标准位置,避免污染用户 PATH。


5. 默认值装配:New()

go 复制代码
func New() (*Config, error) {
    cfg := &Config{
        CommonConfig: CommonConfig{
            ShutdownTimeout:          DefaultShutdownTimeout,    // 15
            LogConfig:                LogConfig{
                Type:   DefaultLogDriver,                       // "json-file"
                Config: make(map[string]string),
            },
            DaemonLogConfig:          DaemonLogConfig{
                LogLevel:  "info",
                LogFormat: log.TextFormat,
            },
            MaxConcurrentDownloads:   DefaultMaxConcurrentDownloads, // 3
            MaxConcurrentUploads:     DefaultMaxConcurrentUploads,   // 5
            MaxDownloadAttempts:      DefaultDownloadAttempts,       // 5
            BridgeConfig:             BridgeConfig{
                DefaultBridgeConfig: DefaultBridgeConfig{
                    MTU: DefaultNetworkMtu,                        // 1500
                },
            },
            NetworkConfig:            NetworkConfig{
                NetworkControlPlaneMTU: DefaultNetworkMtu,
                DefaultNetworkOpts:     make(map[string]map[string]string),
            },
            ContainerdNamespace:      DefaultContainersNamespace,    // "moby"
            ContainerdPluginNamespace:DefaultPluginNamespace,        // "plugins.moby"
            Features:                 make(map[string]bool),
            DefaultRuntime:           StockRuntimeName,              // Linux: "runc", Windows: ""
            MinAPIVersion:            defaultMinAPIVersion,          // "1.40"
        },
    }

    if err := setPlatformDefaults(cfg); err != nil {    // 平台默认值
        return nil, err
    }
    return cfg, nil
}

关键设计

  • New() 返回的是带默认值的空配置,所有 slice/map 都已初始化,调用方可以直接 merge。
  • setPlatformDefaults 是平台钩子,必须被调用 ------否则 RootExecRootPidfile 都为空,daemon 起不来。
  • rootless 模式下(rootless.RunningWithRootlessKit() 为 true),setPlatformDefaults 会把路径切到 $XDG_DATA_HOME/docker / $XDG_RUNTIME_DIR/docker,让非 root 用户也能跑。

6. 文件读取:getConflictFreeConfiguration

这是配置加载的核心。流程:

arduino 复制代码
os.ReadFile(configFile)
    ↓
BOM 检测 + UTF-8 验证(transform.Chain)
    ↓
TrimSpace(检测空文件)
    ↓
如果 flags != nil:
    ├─ json.Unmarshal → map[string]any
    ├─ configValuesSet:flatten 嵌套 map(除 flatOptions)
    ├─ findConfigurationConflicts:unknown key + flag/file 双重设置
    ├─ 对 boolValue flag 用文件值覆盖(修复 issue #20289)
    └─ config.ValuesSet = configSet
    ↓
json.Unmarshal → Config(完整结构反序列化)
    ↓
migrate(如 host-gateway-ip → host-gateway-ips)
    ↓
return &config

6.1 BOM 与编码兼容

css 复制代码
b, n, err := transform.Bytes(
    transform.Chain(unicode.BOMOverride(transform.Nop), encoding.UTF8Validator),
    b,
)

源码注释非常详细地解释了为什么要处理 BOM:

  • RFC 8259 规定 JSON 默认 UTF-8 不带 BOM,但允许实现忽略 UTF-8 BOM;
  • Windows PowerShell 5.1(Server 自带)默认写 UTF-16 LE with BOM 或 UTF-8 with BOM;
  • 老 Notepad 默认写 UTF-8 with BOM。

为了 Windows Server 用户的体验,dockerd 接受这三种编码:UTF-8 (no BOM)、UTF-8 (with BOM)、UTF-16 LE (with BOM)。其他编码会触发 UTF8Validator 报错,错误里带 offset n 方便定位。

6.2 configValuesSet:展平嵌套

go 复制代码
func configValuesSet(config map[string]any) map[string]any {
    flatten := make(map[string]any)
    for k, v := range config {
        if m, isMap := v.(map[string]any); isMap && !flatOptions[k] {
            maps.Copy(flatten, m)   // 展开
            continue
        }
        flatten[k] = v
    }
    return flatten
}

举例,daemon.json

json 复制代码
{
  "log-driver": "json-file",
  "log-opts": {"max-size": "10m"},
  "dns": ["8.8.8.8"],
  "labels": ["foo=bar"]
}

展平后:

ini 复制代码
log-driver = "json-file"
max-size   = "10m"        ← log-opts 被展开
dns        = ["8.8.8.8"]
labels     = ["foo=bar"]

flatOptions["log-opts"] 在白名单里,所以展开的是 log-opts 这个 key 本身被替换为其内层 key 。等等------这里需要细看:flatOptions[k] 是检查 key 是否"不能被展平"。如果 k == "log-opts"flatOptions[k] == true,所以不展平 ,整体作为 flatten["log-opts"] = map[string]string{"max-size":"10m"}

反过来,如果一个 map 字段不在 flatOptions(比如 tls-options 这种假设性的),它的内层 key 会被展开到顶层。这是为了和某些 CLI flag(如 --tlscert)的扁平命名对齐。

6.3 findConfigurationConflicts:4 个检测

go 复制代码
func findConfigurationConflicts(config map[string]any, flags *pflag.FlagSet) error {
    // 1. unknown key:file 里有但 flag 里没有(且不在 skipValidateOptions)
    unknownKeys := ...
    if len(unknownKeys) > 0 {
        // 进一步:是否是 NamedOption(labels↔label 这种别名)
        flags.VisitAll(func(f) {
            if namedOption, ok := f.Value.(opts.NamedOption); ok {
                delete(unknownKeys, namedOption.Name())
            }
        })
        return "the following directives don't match any configuration option: ..."
    }

    // 2. flag/file 双重设置(且不在 skipDuplicates)
    flags.Visit(func(f) {
        if namedOption, ok := f.Value.(opts.NamedOption); ok {
            if optsValue, ok := config[namedOption.Name()]; ok && !skipDuplicates[...] {
                conflicts = append(conflicts, ...)
            }
        } else {
            for _, name := range []string{f.Name, f.Shorthand} {
                if value, ok := config[name]; ok && !skipDuplicates[name] {
                    conflicts = append(...)
                }
            }
        }
    })

    // 3. 迁移键的新老版本不能共存
    for oldName, migration := range migratedNamedConfig {
        oldNameVal, haveOld := config[oldName]
        _, haveNew := config[migration.newName]
        if haveOld && haveNew { errs = append(errs, ...) }
        if f := flags.Lookup(oldName); f != nil && f.Changed {
            conflicts = append(conflicts, ...)
        }
    }

    // 4. 把 conflicts 拼成错误返回
    return stderrors.Join(errs...)
}

四个检测点:

  • unknown key :用户写错字段名(如 "data-root" 写成 "data_root")会被立刻发现;但对 NamedOption 有例外(CLI 上 --label 对应 JSON labels,名字不同但语义一致)。
  • flag vs file :CLI 显式传过的 flag 不能和 daemon.json 同字段同时存在------避免"哪个胜出"的歧义。runtimes 是例外,因为它是 map,可以累加。
  • migration 键:老键和新键不能共存,也不能"老键在 file + 新键在 CLI"。
  • 凭证脱敏http-proxy/https-proxy 在错误信息里会被 MaskCredentials 脱敏,避免日志泄漏。

6.4 boolValue 覆盖

go 复制代码
for key, value := range configSet {
    f := flags.Lookup(key)
    if f == nil { namedOptions[key] = value; continue }
    if _, ok := f.Value.(boolValue); ok {
        f.Value.Set(fmt.Sprintf("%v", value))
    }
}

这段修复了 GitHub issue #20289 的坑:用户在 daemon.json 写 "tls": false,但 pflag 的 --tls 默认值可能是 false(值相同,但 Changed() 是 false),后续 mergo Merge 时会把 fileConfig(false)被 flagsConfig(false-default)覆盖......实际效果是用户写的 false 被忽略。

通过把 fileConfig 里的 bool 值提前 Set 到对应的 flag 上,让 flags.Visit 在 conflict 检测时认为 flag 已经被改变------后续逻辑就能正确处理。这是配置加载里最微妙的细节之一。


7. 合并策略:MergeDaemonConfigurations

启动时调用:

go 复制代码
func MergeDaemonConfigurations(flagsConfig *Config, flags *pflag.FlagSet, configFile string) (*Config, error) {
    fileConfig, err := getConflictFreeConfiguration(configFile, flags)
    if err != nil {
        return nil, err
    }

    // flagsConfig 覆盖到 fileConfig 上(fileConfig 是 dst)
    if err := mergo.Merge(fileConfig, flagsConfig); err != nil {
        return nil, err
    }

    if err := Validate(fileConfig); err != nil {
        return nil, errors.Wrap(err, "merged configuration validation from file and command line flags failed")
    }

    return fileConfig, nil
}

mergo.Merge(dst, src)深合并:src 的非零值覆盖 dst,零值不覆盖。这样:

  • fileConfig 已经有默认值(如果用户在 daemon.json 没写某字段);
  • flagsConfig 提供的"显式设置"覆盖 fileConfig;
  • 默认零值不会反过来覆盖用户配置。

但 mergo 有个经典坑 :slice 和 map 的合并语义有时与预期不符。config_test.go 里有大量用例来覆盖这些边界情况。

整套启动时配置流程(command/daemon.go::loadDaemonCliConfig

scss 复制代码
1. opts.setDefaultOptions()                       // CLI 默认值
2. conf := opts.daemonConfig                      // 来自 New() 的默认值
3. 处理 DOCKER_MIN_API_VERSION 环境变量
4. 处理 TLS / TLSVerify(指针三态)
5. (Windows)确定 configFile 默认路径
6. config.MergeDaemonConfigurations(conf, flags, configFile)
   └─ 失败时:如果用户没显式 --config-file 且文件不存在,忽略;否则硬错
7. 删除 conf.Hosts 里的空白项
8. 若 conf.Hosts 仍空:根据 TLS 选 defaultTLSHost 或 unix socket
9. normalizeHosts(conf)
10. config.Validate(conf)
11. config.GetConflictFreeLabels(conf.Labels) → 去重
12. 处理 TLSVerify 联动
13. validateCPURealtimeOptions(conf)
14. 设置默认 CDISpecDirs(如未配)

整个流程串起了 config 包提供的工具:默认值 → 合并 → 校验 → 后处理。注意 config.Validate MergeDaemonConfigurations 内部已经跑过一次,但 loadDaemonCliConfig 在合并后还会再跑一次------这是为了应对后续步骤(如 Hosts 注入)可能引入的新问题。


8. 校验体系:Validate

go 复制代码
func Validate(config *Config) error {
    if err := validateDaemonLogConfig(config.DaemonLogConfig); err != nil { return err }

    // DNSSearch
    for _, dnsSearch := range config.DNSSearch { ... opts.ValidateDNSSearch ... }

    // HostGatewayIPs
    if err := dopts.ValidateHostGatewayIPs(config.HostGatewayIPs); err != nil { return err }

    // Labels
    for _, label := range config.Labels { ... opts.ValidateLabel ... }

    // 数值边界(注意允许 0)
    if config.MTU < 0 { ... }
    if config.MaxConcurrentDownloads < 0 { ... }
    if config.MaxConcurrentUploads < 0 { ... }
    if config.MaxDownloadAttempts < 0 { ... }
    if config.NetworkDiagnosticPort < 0 || > 65535 { ... }

    // Swarm 通用资源
    if _, err := ParseGenericResources(config.NodeGenericResources); err != nil { return err }

    // Hosts 格式
    for _, h := range config.Hosts { ... opts.ValidateHost ... }

    // Registry mirrors
    for _, mirror := range config.ServiceOptions.Mirrors { ... registry.ValidateMirror ... }

    // ExecOptions
    if _, err := parseExecOptions(config.ExecOptions); err != nil { return err }

    // 平台特定
    return validatePlatformConfig(config)
}

值得注意的几个设计选择

  1. 允许 0MTU=0MaxConcurrentDownloads=0 都被接受,意味着"用默认"。这是个 TODO------thaJeztah 在注释里说"应该把 0 也拒绝"。但在历史上 0 已经成为"未设置"的约定。
  1. 校验顺序 :先做跨平台基础校验,最后才调 validatePlatformConfig。这样 platform hook 可以假设公共字段已经合法。
  1. 错误返回风格 :单个错误立刻返回(fail-fast),而不是收集所有错误。这是缺点------用户改 daemon.json 时一次只能看到一个错误。TODO 里有人提过要改成 errors.Join

validateDaemonLogConfig

c 复制代码
switch strings.ToLower(cfg.LogLevel) {
case "panic", "fatal", "error", "warn", "info", "debug", "trace":
    // OK
default:
    return fmt.Errorf("invalid logging level: %s", cfg.LogLevel)
}

switch logFormat := cfg.LogFormat; logFormat {
case log.TextFormat, log.JSONFormat:
    // OK
default:
    return fmt.Errorf("invalid log format: %s", logFormat)
}

注释提到这依赖 containerd log 包的内部知识("FIXME find a better way"),不太干净。

parseExecOptions

go 复制代码
for _, keyValue := range execOptions {
    k, v, ok := strings.Cut(keyValue, "=")
    k = strings.ToLower(strings.TrimSpace(k))
    v = strings.TrimSpace(v)
    if !ok || k == "" || v == "" {
        return nil, fmt.Errorf("invalid exec-opt (%s): must be formatted 'opt=value'", keyValue)
    }
    if err := validatePlatformExecOpt(k, v); err != nil { ... }
    o[k] = v
}

exec-opt 接受 opt=value 形式。当前合法的 opt 只有 native.cgroupdriver(Linux)和 isolation(Windows),其他全部 reject------这是个保守策略,避免用户瞎配。


9. 热重载:Reload

go 复制代码
func Reload(configFile string, flags *pflag.FlagSet, reload func(*Config)) error {
    newConfig, err := getConflictFreeConfiguration(configFile, flags)
    if err != nil {
        if flags.Changed("config-file") || !os.IsNotExist(err) {
            return errors.Wrapf(err, "unable to configure the Docker daemon with file %s", configFile)
        }
        // 用户没显式 --config-file 且文件消失:回退到默认
        newConfig, err = New()
        if err != nil { return err }
    }

    // 标签去重
    newLabels, err := GetConflictFreeLabels(newConfig.Labels)
    if err != nil { return err }
    newConfig.Labels = newLabels

    // TODO 注释:在 reload 回调前先 Validate 不对,应该合并后再 Validate
    if err := Validate(newConfig); err != nil {
        return errors.Wrap(err, "file configuration validation failed")
    }

    reload(newConfig)
    return nil
}

reload 是回调函数,由调用方提供。在 dockerd 主流程里:

go 复制代码
// daemon/command/daemon.go::reloadConfig
reload := func(cfg *config.Config) {
    if err := validateAuthzPlugins(cfg.AuthorizationPlugins, cli.d.PluginStore); err != nil {
        log.Fatal(...)
        return
    }
    if err := cli.d.Reload(cfg); err != nil {        // ← Daemon 实际更新内部状态
        log.Error(...)
        return
    }
    cli.authzMiddleware.SetPlugins(cfg.AuthorizationPlugins)
    if cfg.IsValueSet("debug") {
        // 根据 cfg.Debug 切换 debug 模式
    }
}

config.Reload(*cli.configFile, cli.flags, reload)

cli.d.Reloaddaemon.Daemon.Reload,它真正应用配置到运行时 :更新 log level、重建 SDN 规则、调整 MTU、切流量等。config 包不参与这些副作用------它只负责"读取 + 校验"。

大段 TODO 注释的解释

源码里 thaJeztah 留了一大段注释,承认 Reload 现有顺序有问题:

sql 复制代码
1. get (a copy of) the active configuration
2. get the new configuration
3. apply the (reloadable) options from the new configuration
4. validate the merged results
5. apply the new configuration.

理想流程是"先合并、再校验、最后应用",但现状是"先校验 newConfig、再 reload 回调里再合并+应用"。后果:某些字段(只有合并后才有意义)可能在 Validate 阶段被误判为非法。这是个长期重构目标。

触发 reload 的入口

不同平台触发 SIGHUP/refresh 的方式:

平台 触发
Linux SIGHUPcommand/daemon_unix.go::handleSignalscli.reloadConfig()
Windows 服务控制码 SERVICE_CONTROL_PARAMCHANGEservice_windows.gocli.reloadConfig()

无论哪种,最终都走到同一个 config.Reload + cli.d.Reload


10. Builder 子配置:builder.go

CommonConfig.Builder 字段:

go 复制代码
type BuilderConfig struct {
    GC           BuilderGCConfig
    Entitlements BuilderEntitlements
    History      *BuilderHistoryConfig `json:",omitempty"`
}

对应 daemon.json:

json 复制代码
{
  "builder": {
    "gc": {
      "enabled": true,
      "defaultKeepStorage": "10GB",       // 已弃用,等价于 defaultReservedSpace
      "defaultReservedSpace": "10GB",
      "defaultMaxUsedSpace": "20GB",
      "defaultMinFreeSpace": "5GB",
      "policy": [
        { "keepStorage": "1GB", "filter": ["unused-for=168h"] },
        { "all": true, "filter": ["type==regular"] }
      ]
    },
    "entitlements": {
      "network-host": true,
      "security-insecure": false,
      "device": true
    },
    "history": {
      "maxAge": "168h",
      "maxEntries": 1000
    }
  }
}

自定义 UnmarshalJSON 的设计

BuilderGCConfigBuilderGCRuleBuilderGCFilter 都重写了 UnmarshalJSON,主要解决两个问题:

  1. 兼容弃用字段defaultKeepStoragedefaultReservedSpacekeepStoragereservedSpace。重写时用临时结构体接受所有可能的字段名,然后映射到正式字段。
  1. 默认值填充BuilderGCConfig.Enabled*bool,UnmarshalJSON 里把临时结构体的 Enabled bool 默认值设为 true,然后取地址------这样 JSON 里不写 enabled 时也得到 true(默认开启 GC)。

BuilderGCFilterfilters.Args 的别名,自定义 Marshal/Unmarshal 是为了在 JSON 里用 ["key=value", "key2=value2"] 的字符串数组形式(更人性化),同时兼容老的 map 形式("backwards compat for deprecated buggy form")。

为什么 Builder 配置独立成文件?

  • BuildKit 的配置语义复杂(GC 策略可以多档叠加),不适合塞进 CommonConfig
  • 字段名和 BuildKit 上游对齐(bkconfig "github.com/moby/buildkit/cmd/buildkitd/config"),便于 daemon/internal/builder-next/controller.go::getGCPolicy 直接复用;
  • 独立文件便于单元测试。

11. 其他工具函数

GetConflictFreeLabels

go 复制代码
func GetConflictFreeLabels(labels []string) ([]string, error) {
    labelMap := map[string]string{}
    for _, label := range labels {
        key, val, ok := strings.Cut(label, "=")
        if ok {
            if v, ok := labelMap[key]; ok && v != val {
                return nil, errors.Errorf("conflict labels for %s=%s and %s=%s", key, val, key, v)
            }
            labelMap[key] = val
        }
    }
    // ... 重组为 slice
}

允许同 key 同 val 的重复(取最后一个),拒绝同 key 不同 val。Swarm 会自动去重,所以这里宽松一点。

GetExecOpt

go 复制代码
func (conf *Config) GetExecOpt(name string) (val string, found bool, _ error) {
    o, err := parseExecOptions(conf.ExecOptions)
    if err != nil { return "", false, err }
    val, found = o[name]
    return val, found, nil
}

每次查都重新 parse------不缓存,但 ExecOptions 通常很小,开销可忽略。

ParseGenericResources

go 复制代码
func ParseGenericResources(value []string) ([]swarm.GenericResource, error) {
    resources, err := genericresource.Parse(value)
    if err != nil { return nil, err }
    obj := convert.GenericResourcesFromGRPC(resources)
    return obj, nil
}

Swarm 节点的"通用资源"声明(GPU 数量、特殊硬件等)。委托给 swarmkit 解析,再转成 moby 的 swarm 类型。

ValidateMinAPIVersion

go 复制代码
func ValidateMinAPIVersion(ver string) error {
    if ver == "" { return errors.New("value is empty") }
    if strings.EqualFold(ver[0:1], "v") {
        return errors.New(`API version must be provided without "v" prefix`)
    }
    if versions.LessThan(ver, MinAPIVersion) {
        return errors.Errorf(`minimum supported API version is %s: %s`, MinAPIVersion, ver)
    }
    if versions.GreaterThan(ver, MaxAPIVersion) {
        return errors.Errorf(`maximum supported API version is %s: %s`, MaxAPIVersion, ver)
    }
    return nil
}

仅用于 DOCKER_MIN_API_VERSION 环境变量校验------这是 daemon.json 的 min-api-version 不能覆盖的环境变量级覆盖。

Sanitize / MaskCredentials

go 复制代码
func MaskCredentials(rawURL string) string {
    parsedURL, err := url.Parse(rawURL)
    if err != nil || parsedURL.User == nil { return rawURL }
    parsedURL.User = url.UserPassword("xxxxx", "xxxxx")
    return parsedURL.String()
}

func Sanitize(cfg Config) Config {
    cfg.CommonConfig.Proxies = Proxies{
        HTTPProxy:  MaskCredentials(cfg.HTTPProxy),
        HTTPSProxy: MaskCredentials(cfg.HTTPSProxy),
        NoProxy:    MaskCredentials(cfg.NoProxy),
    }
    return cfg
}

reload 后打印日志前会调 Sanitize,把 http://user:pass@proxy/foo 脱敏成 http://xxxxx:xxxxx@proxy/foo


12. 调用链总览

scss 复制代码
启动 dockerd
└── cmd/dockerd/main → DaemonCli.Start
    └── loadDaemonCliConfig(opts)                          [daemon/command/daemon.go]
        ├── opts.setDefaultOptions()                       // CLI 默认
        ├── conf := opts.daemonConfig                       // = config.New() 的产物
        ├── 处理 DOCKER_MIN_API_VERSION / TLS / TLSVerify
        └── config.MergeDaemonConfigurations(conf, flags, configFile)
            ├── getConflictFreeConfiguration
            │   ├── ReadFile + BOM/UTF-8 解码
            │   ├── configValuesSet(flatten)
            │   ├── findConfigurationConflicts(4 项检查)
            │   ├── boolValue flag 覆盖(修 #20289)
            │   ├── json.Unmarshal → Config
            │   └── migrate(host-gateway-ip → host-gateway-ips)
            ├── mergo.Merge(fileConfig, flagsConfig)
            └── Validate(fileConfig)
        ├── 删 Hosts 空白 + 注入默认 socket
        ├── normalizeHosts
        ├── config.Validate(conf)                          // 再校验一次
        ├── config.GetConflictFreeLabels(conf.Labels)
        ├── validateCPURealtimeOptions
        └── 默认 CDISpecDirs

运行中(SIGHUP / Windows SERVICE_CONTROL_PARAMCHANGE)
└── DaemonCli.reloadConfig                                  [daemon/command/daemon.go]
    ├── reload 回调:validateAuthzPlugins + cli.d.Reload + 切 debug
    └── config.Reload(configFile, flags, reload)
        ├── getConflictFreeConfiguration
        ├── GetConflictFreeLabels
        ├── Validate
        └── reload(newConfig)                              // 真正应用

13. 设计要点小结

  1. 数据 + 行为分离Config 只描述"应该是什么样","怎么应用"由 Daemon 决定。这让 config 包极易于测试(无副作用)。
  1. JSON tag CLI flag 完全对齐 :内嵌结构体的 json tag 直接用 dockerd CLI 的 flag 名(log-driverstorage-driverfixed-cidr-v6),保证文档单一来源。
  1. 三态布尔TLS *bool / TLSVerify *bool 区分"未设置 / true / false",避免零值覆盖问题。
  1. ValuesSet 解决"是否显式设置" :reload 时只有显式设置的字段才触发副作用,避免默认值意外覆盖运行时状态。
  1. 三张白名单 map 是关键flatOptions / skipValidateOptions / skipDuplicates 分别处理"不展平 / 不校验 / 不冲突",是配置加载语义的核心。改任何一张都要慎重。
  1. 平台钩子签名固定setPlatformDefaults / validatePlatformConfig / validatePlatformExecOpt / IsRootless / IsSwarmCompatible 等,新增平台只要实现这套钩子。
  1. mergo Merge 的方向Merge(fileConfig, flagsConfig) ------ flagsConfig 覆盖 fileConfig。这符合"CLI 优先级更高"的常识,但 mergo 对 slice/map 的合并语义要小心。
  1. 冲突检测 fail-fast:unknown key、flag vs file 双重设置、迁移键新老共存------任何一个冲突都拒绝启动,避免歧义。
  1. boolValue 提前覆盖 :修复 daemon.json"tls": false 被 CLI 默认值覆盖的历史 bug(#20289),是配置加载里最微妙的细节。
  1. 凭证脱敏贯穿始终SanitizeMaskCredentials、conflict 报错时对 proxy 脱敏------多道防线避免 reload 日志泄漏凭证。
  1. Builder 配置独立builder.go 单独管理 BuildKit GC/Entitlements/History,自定义 UnmarshalJSON 处理字段迁移和默认值,便于跟随 BuildKit 上游演进。
  1. Reload 顺序是已知 TODO:thaJeztah 在源码里明说 Reload 现有顺序不对(先校验后合并),未来要重构成"合并 → 校验 → 应用"的原子流程。
  1. FHS 兼容的二进制查找lookupBinPath 按 FHS 3.0/2.3 在 /usr/libexec/docker 等位置找 docker-init / docker-proxy,避免污染用户 PATH。
  1. BOM/UTF-16 兼容是 Windows 友好性的关键:PowerShell 5.1 / 老 Notepad 默认带 BOM,dockerd 兼容这些编码以减少 Windows 用户的踩坑。
  1. 校验允许 0MTU=0 / MaxConcurrentDownloads=0 被接受为"未设置"。这是个历史决定,源码里有 TODO 想改成更严格的语义。

14. 学习路径建议

按以下顺序读,效率最高:

  1. config.go::Config / CommonConfig------先看数据模型,理解有哪些字段;同时浏览顶部常量。
  1. config.go::New() + config_linux.go::setPlatformDefaults------默认值从哪来。
  1. config.go::getConflictFreeConfiguration------配置加载核心,重点理解三张白名单 map 和 4 项冲突检测。
  1. config.go::MergeDaemonConfigurations------理解 mergo 合并的方向和坑。
  1. config.go::Validate + config_linux.go::validatePlatformConfig------校验体系。
  1. config.go::Reload + command/daemon.go::reloadConfig------热重载链路。
  1. builder.go------BuildKit 子配置,自定义 UnmarshalJSON 的范本。
  1. config_test.go(~855 行)------大量边界用例,看完后基本能应对任何 daemon.json 配置问题。

读这套代码时建议备着一份 daemon.json 示例对照字段名(参考官方文档 https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file),文档与本仓源码的字段名一一对应。

相关推荐
拉斯特当思1 小时前
设备总掉线却复现不了?把弱网“造“出来:tcconfig 五种网络故障模拟实战
后端
RainCity1 小时前
Java Swing 自定义组件库分享(十五)
java·笔记·后端
程序员麻辣烫1 小时前
Memory向量记忆系统4-文本向量化
后端·aigc
Java编程爱好者1 小时前
Java 并发锁体系全解:从 synchronized、Lock 到 AQS 底层原理与源码深度剖析
后端
程序员天天困1 小时前
Java 项目用 Single-flight 一招终结 AI 接口重复调用
redis·后端
farerboy1 小时前
23-Java 构造函数
java·后端
程序员麻辣烫1 小时前
Memory向量记忆系统3-数据选取
后端·aigc
程序员麻辣烫1 小时前
Memory向量记忆系统2-SQLite
后端·aigc
阳光是sunny1 小时前
LangGraph高级教程:Multi Schema多状态管理详解
前端·人工智能·后端