| 章节 | 关键文件 | 解答 |
|---|---|---|
| 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 的纯配置数据模型 + 加载/校验工具:
- 数据模型 :
Config、CommonConfig、BuilderConfig、BridgeConfig、LogConfig、DNSConfig、TLSOptions、NetworkConfig、Proxies、DaemonLogConfig,以及它们的平台变体。
- 加载工具 :
New()(默认值)、MergeDaemonConfigurations(启动时合并)、Reload(SIGHUP 热重载)。
- 校验工具 :
Validate(跨平台)、ValidateMinAPIVersion、GetConflictFreeLabels、GetExecOpt、Sanitize(脱敏)。
- 平台钩子 :
setPlatformDefaults/validatePlatformConfig/validatePlatformExecOpt/IsRootless/IsSwarmCompatible/GetExecRoot/GetInitPath,按config_<os>.go文件分叉。
它不做:
- 不 修改 Daemon 运行时状态------所有"应用配置"的动作都在调用方(
daemon.Daemon.Reload、DaemonCli.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]的合法区间。
DefaultContainersNamespace与DefaultPluginNamespace决定 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-driver、log-opts、storage-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
}
ValuesSet 在 getConflictFreeConfiguration 里被填充:它记录"用户在 daemon.json 里实际写了哪些 key"。reloadConfig 用 cfg.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.TLS 和 TLSVerify 都是 *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 显式设置时,把 HostGatewayIP → HostGatewayIPs。
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.go 与 config_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 里的runtimesmap 是合并关系而不是冲突。
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-init 和 docker-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是平台钩子,必须被调用 ------否则Root、ExecRoot、Pidfile都为空,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对应 JSONlabels,名字不同但语义一致)。
- 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)
}
值得注意的几个设计选择:
- 允许 0 :
MTU=0、MaxConcurrentDownloads=0都被接受,意味着"用默认"。这是个 TODO------thaJeztah 在注释里说"应该把 0 也拒绝"。但在历史上 0 已经成为"未设置"的约定。
- 校验顺序 :先做跨平台基础校验,最后才调
validatePlatformConfig。这样 platform hook 可以假设公共字段已经合法。
- 错误返回风格 :单个错误立刻返回(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.Reload 是 daemon.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 | SIGHUP → command/daemon_unix.go::handleSignals → cli.reloadConfig() |
| Windows | 服务控制码 SERVICE_CONTROL_PARAMCHANGE → service_windows.go → cli.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 的设计
BuilderGCConfig、BuilderGCRule、BuilderGCFilter 都重写了 UnmarshalJSON,主要解决两个问题:
- 兼容弃用字段 :
defaultKeepStorage→defaultReservedSpace、keepStorage→reservedSpace。重写时用临时结构体接受所有可能的字段名,然后映射到正式字段。
- 默认值填充 :
BuilderGCConfig.Enabled是*bool,UnmarshalJSON 里把临时结构体的Enabled bool默认值设为true,然后取地址------这样 JSON 里不写enabled时也得到true(默认开启 GC)。
BuilderGCFilter 是 filters.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. 设计要点小结
- 数据 + 行为分离 :
Config只描述"应该是什么样","怎么应用"由 Daemon 决定。这让config包极易于测试(无副作用)。
- JSON tag ↔ CLI flag 完全对齐 :内嵌结构体的 json tag 直接用 dockerd CLI 的 flag 名(
log-driver、storage-driver、fixed-cidr-v6),保证文档单一来源。
- 三态布尔 :
TLS *bool/TLSVerify *bool区分"未设置 / true / false",避免零值覆盖问题。
ValuesSet解决"是否显式设置" :reload 时只有显式设置的字段才触发副作用,避免默认值意外覆盖运行时状态。
- 三张白名单 map 是关键 :
flatOptions/skipValidateOptions/skipDuplicates分别处理"不展平 / 不校验 / 不冲突",是配置加载语义的核心。改任何一张都要慎重。
- 平台钩子签名固定 :
setPlatformDefaults/validatePlatformConfig/validatePlatformExecOpt/IsRootless/IsSwarmCompatible等,新增平台只要实现这套钩子。
- mergo Merge 的方向 :
Merge(fileConfig, flagsConfig)------ flagsConfig 覆盖 fileConfig。这符合"CLI 优先级更高"的常识,但 mergo 对 slice/map 的合并语义要小心。
- 冲突检测 fail-fast:unknown key、flag vs file 双重设置、迁移键新老共存------任何一个冲突都拒绝启动,避免歧义。
- boolValue 提前覆盖 :修复
daemon.json里"tls": false被 CLI 默认值覆盖的历史 bug(#20289),是配置加载里最微妙的细节。
- 凭证脱敏贯穿始终 :
Sanitize、MaskCredentials、conflict 报错时对 proxy 脱敏------多道防线避免 reload 日志泄漏凭证。
- Builder 配置独立 :
builder.go单独管理 BuildKit GC/Entitlements/History,自定义 UnmarshalJSON 处理字段迁移和默认值,便于跟随 BuildKit 上游演进。
- Reload 顺序是已知 TODO:thaJeztah 在源码里明说 Reload 现有顺序不对(先校验后合并),未来要重构成"合并 → 校验 → 应用"的原子流程。
- FHS 兼容的二进制查找 :
lookupBinPath按 FHS 3.0/2.3 在/usr/libexec/docker等位置找docker-init/docker-proxy,避免污染用户 PATH。
- BOM/UTF-16 兼容是 Windows 友好性的关键:PowerShell 5.1 / 老 Notepad 默认带 BOM,dockerd 兼容这些编码以减少 Windows 用户的踩坑。
- 校验允许 0 :
MTU=0/MaxConcurrentDownloads=0被接受为"未设置"。这是个历史决定,源码里有 TODO 想改成更严格的语义。
14. 学习路径建议
按以下顺序读,效率最高:
config.go::Config/CommonConfig------先看数据模型,理解有哪些字段;同时浏览顶部常量。
config.go::New()+config_linux.go::setPlatformDefaults------默认值从哪来。
config.go::getConflictFreeConfiguration------配置加载核心,重点理解三张白名单 map 和 4 项冲突检测。
config.go::MergeDaemonConfigurations------理解 mergo 合并的方向和坑。
config.go::Validate+config_linux.go::validatePlatformConfig------校验体系。
config.go::Reload+command/daemon.go::reloadConfig------热重载链路。
builder.go------BuildKit 子配置,自定义 UnmarshalJSON 的范本。
config_test.go(~855 行)------大量边界用例,看完后基本能应对任何 daemon.json 配置问题。
读这套代码时建议备着一份 daemon.json 示例对照字段名(参考官方文档 https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file),文档与本仓源码的字段名一一对应。