docker-Builder镜像构建

|-------------------|-------------------------------------------------|------------------------------|
| 章节 | 关键文件 | 适合解答 |
| 1. 定位与历史 | daemon/builder/builder.go 注释 | Builder 包为什么这么设计? |
| 2. 整体架构 | daemon/builder/... 目录 | Builder 由哪些子模块组成? |
| 3. 接口契约 | daemon/builder/builder.go | Daemon 应该给 Builder 提供什么能力? |
| 4. 构建入口 | dockerfile/builder.go | BuildManager.Build 干了什么? |
| 5. 上下文检测 | remotecontext/detect.go | tar/git/URL 三种 context 怎么分流? |
| 6. 评估器与分发 | dockerfile/evaluator.godispatchers.go | Dockerfile 指令如何被分派? |
| 7. 状态与生命周期 | evaluator.go::dispatchState | 一个 Stage 的状态如何演化? |
| 8. 容器与提交 | dockerfile/internals.gocontainerbackend.go | RUN/COMMIT 的真实流程? |
| 9. 镜像缓存 | dockerfile/imageprobe.go | 缓存探测如何判定命中? |
| 10. COPY/ADD | dockerfile/copy.go | 文件复制 + chown 怎么做? |
| 11. BuildArgs | dockerfile/buildargs.go | --build-argARG 的语义? |
| 12. 与 BuildKit 关系 | daemon/builder/backend/backend.go | 谁是 v1 谁是 v2? |
| 13. 调用链总览 | --- | 全图速查 |


1. 模块定位与历史

daemon/builder/builder.go 的包注释开门见山:

Historically, only server-side Dockerfile interpreters existed.

This package allows for other implementations of Docker builders.

也就是说,daemon/builder 这一层只定义 构建器的接口(contracts),具体实现可以有多种:

  • 经典构建器 (Classic Builder / v1):daemon/builder/dockerfile/ 包,Dockerfile 解析 → 逐条指令用临时容器执行 → 每步 commit 出一层镜像。
  • BuildKit 构建器 (v2):daemon/internal/builder-next/,更现代、并行、支持密钥挂载/SSH/多阶段并发等。

⚠️ 生产默认走 BuildKit;本模块保留是为了向后兼容 DOCKER_BUILDKIT=0 场景以及少量仍依赖 commit 模型的代码路径(例如 docker commit --change)。

daemon/builder/dockerfile/builder.go 内部的 Builder 注释也明确:

Builder is a Dockerfile builder.

It implements the builder.Backend interface.

注意区分:builder.Backend 是 "Daemon 提供给 Builder 的能力",而 dockerfile.Builder 是 "Builder 本身"。


2. 整体架构

Go 复制代码
            HTTP / API  ─ POST /build
                          │
                          ▼
        daemon/builder/backend/backend.go::Backend.Build
                          │
              ┌───────────┴────────────┐
              │ Version==BuilderBuildKit│
              ▼                         ▼
        builder-next (BuildKit)    dockerfile.BuildManager
                                          │ (本文重点)
                                          ▼
                              ┌─────────────────────────┐
                              │ dockerfile.Builder       │
                              │  - dispatchDockerfile... │
                              │  - dispatch() switch     │
                              │  - commitContainer()     │
                              └──────────┬──────────────┘
                                         │ 通过 builder.Backend 回调
                                         ▼
                              ┌─────────────────────────┐
                              │ Daemon (ImageService /  │
                              │  ExecBackend / Cache)   │
                              └─────────────────────────┘

关键目录与职责:

|-------------------------------------------------|--------------------------------------------------------------------|
| 目录/文件 | 角色 |
| daemon/builder/builder.go | 接口契约:SourceBackendImageROLayerRWLayerImageCache 等 |
| daemon/builder/dockerfile/ | 经典构建器的实现核心(解析、评估、分发、提交、复制) |
| daemon/builder/dockerfile/builder.go | BuildManagerBuilderBuild() 主流程 |
| daemon/builder/dockerfile/evaluator.go | 评估器:dispatch() 大 switch、dispatchState 状态对象 |
| daemon/builder/dockerfile/dispatchers.go | 每条 Dockerfile 指令的执行体(ENV/RUN/COPY/...) |
| daemon/builder/dockerfile/internals.go | 内部工具:commit、create、probeCache、performCopy 的辅助 |
| daemon/builder/dockerfile/containerbackend.go | 临时容器生命周期管理(Create/Run/Wait/Remove) |
| daemon/builder/dockerfile/buildargs.go | --build-arg / ARG / meta-arg 三种参数 |
| daemon/builder/dockerfile/imageprobe.go | 缓存探测器接口与命中/失效逻辑 |
| daemon/builder/dockerfile/imagecontext.go | 镜像挂载缓存(imageSources/imageMount) |
| daemon/builder/dockerfile/copy.go | COPY/ADD 的源解析、通配符、远程下载、tarsum |
| daemon/builder/remotecontext/ | 构建上下文来源(archive/git/url/lazycontext) |
| daemon/builder/backend/backend.go | 面向 API 路由的薄壳:选择 v1/v2,处理 squash/tag |


3. 接口契约:daemon/builder/builder.go

这是整个 Builder 模块的"对外协议",只有 ~115 行,但是阅读优先级最高的文件之一。

3.1 常量与 Source

Go 复制代码
const DefaultDockerfileName = "Dockerfile"

type Source interface {
    Root() string              // 构建上下文根目录
    Close() error              // 释放临时目录等资源
    Hash(path string) (string, error)  // 计算文件指纹,用于缓存键
}

Source 是 ADD/COPY 的统一来源:可能是 tar 解压后的目录、git clone 后的目录、或者镜像内层(用于多阶段 COPY --from)。

3.2 Backend 接口:Daemon 给 Builder 的能力

Go 复制代码
type Backend interface {
    ImageBackend        // 镜像/层操作
    ExecBackend         // 容器执行操作

    CommitBuildStep(context.Context, backend.CommitConfig) (image.ID, error)
    ContainerCreateWorkdir(containerID string) error
    CreateImage(ctx context.Context, config []byte, parent string,
                contentStoreDigest digest.Digest) (Image, error)

    ImageCacheBuilder   // 构建一个有状态的缓存查询器
}

拆开看:

  • ImageBackend.GetImageAndReleasableLayer:根据 ref/id 拉镜像 + 拿到一个只读层(ROLayer)。是否拉取由 PullOption 控制(NoPull / ForcePull / PreferLocal)。
  • ExecBackend:4 个回调------ContainerAttachRawContainerCreateIgnoreImagesArgsEscapedContainerRmContainerStartContainerWait,覆盖了"运行临时容器执行 RUN"所需的全部能力。
  • CommitBuildStep:把容器快照成新镜像(不触发事件、不更新指标,专门给 Builder 用)。
  • CreateImage:用配置 JSON 直接生成镜像(用于 COPY/ADD 这种"非容器路径"产生的层)。
  • ImageCacheBuilder.MakeImageCache(...)ImageCache.GetCache(parentID, cfg, platform):核心缓存查询。

设计要点:Builder 不直接调用 containerd,也不直接操作镜像存储 ,所有副作用都通过 Backend 接口回调到 Daemon。这种"控制反转"让 Builder 可以独立测试(参见 mockbackend_test.go),也让 BuildKit 能用不同的 Backend 实现。

3.3 Image / ROLayer / RWLayer

Go 复制代码
type Image interface {
    ImageID() string
    RunConfig() *container.Config
    MarshalJSON() ([]byte, error)
    OperatingSystem() string
}

type ROLayer interface {
    Release() error
    NewRWLayer() (RWLayer, error)
    DiffID() layer.DiffID
    ContentStoreDigest() digest.Digest
}

type RWLayer interface {
    Release() error
    Root() string           // 挂载点(容器根 / 临时根)
    Commit() (ROLayer, error)  // 把可写层固化成新的只读层
}

这组接口把"镜像 = 配置 JSON + 一组只读层"的模型抽象出来:

  • RUN 走 ROLayer → RWLayer → 容器跑命令 → CommitBuildStep(容器路径)。
  • COPY/ADD 走 ROLayer → RWLayer → 直接写文件 → RWLayer.Commit() → CreateImage(非容器路径)。

4. 构建入口:dockerfile/builder.go

4.1 BuildManager:单例

Go 复制代码
type BuildManager struct {
    idMapping user.IdentityMapping  // 用户/UID 映射(user namespace)
    backend   builder.Backend
    pathCache pathCache              // COPY 路径指纹缓存,避免重复算 hash
}

func (bm *BuildManager) Build(ctx, config) (*builder.Result, error) {
    buildsTriggered.Inc()                            // 指标 +1
    if config.Options.Dockerfile == "" {
        config.Options.Dockerfile = "Dockerfile"     // 默认文件名兜底
    }

    source, dockerfile, err := remotecontext.Detect(config)  // ① 解析上下文
    defer source.Close()

    b, err := newBuilder(ctx, builderOptions{...})            // ② 装配 Builder
    return b.build(ctx, source, dockerfile)                   // ③ 执行
}

BuildManagerdaemon/command/daemon.go:603 通过 dockerfile.NewBuildManager(d.BuilderBackend(), d.IdentityMapping()) 创建,整个 Daemon 生命周期内只有一份pathCachesyncmap.Map 实现,跨构建共享路径指纹。

4.2 Builder:每构建一个产物一份

BuilderdispatchDockerfileWithCancellation 期间的临时对象,关键状态:

Go 复制代码
type Builder struct {
    options          *buildbackend.BuildOptions
    Stdout, Stderr   io.Writer
    Aux              buildbackend.AuxEmitter
    Output           io.Writer
    docker           builder.Backend
    idMapping        user.IdentityMapping
    disableCommit    bool                 // docker commit --change 时为 true
    imageSources     *imageSources         // 镜像挂载缓存
    pathCache        pathCache
    containerManager *containerManager     // 临时容器池
    imageProber      ImageProber
    platform         *ocispec.Platform
}

newBuilder 做三件事:创建 ImageProber、装配 imageSources 和 containerManager、解析 --platform

4.3 Builder.build():解析 → 评估

Go 复制代码
func (b *Builder) build(ctx, source, dockerfile) (*builder.Result, error) {
    defer b.imageSources.Unmount()                      // 收尾:卸载所有挂载

    stages, metaArgs, err := instructions.Parse(dockerfile.AST, nil)  // ④ 语法树 → 指令对象
    if b.options.Target != "" {                          // --target 只保留前 N 个 stage
        stages = stages[:targetIx+1]
    }

    buildLabelOptions(b.options.Labels, stages)          // --label 注入到末段

    dockerfile.PrintWarnings(b.Stderr)
    state, err := b.dispatchDockerfileWithCancellation(ctx, stages, metaArgs, dockerfile.EscapeToken, source)
    if state.imageID == "" {
        return nil, errors.New("No image was generated. Is your Dockerfile empty?")
    }
    return &builder.Result{ImageID: state.imageID, FromImage: state.baseImage}, nil
}

注意:

  • 真正的解析器不在本仓 ,由 github.com/moby/buildkit/frontend/dockerfile/instructions.../parser 提供。Moby 共享 BuildKit 的前端解析器(语法树节点),但不复用它的执行引擎 ------执行(评估)由 daemon/builder/dockerfile/evaluator.go 自实现。这是 v1/v2 的分水岭。
  • EscapeToken 来自 parser,决定续行符是 \(Linux 风格)还是 `(Windows 风格)。

4.4 dispatchDockerfileWithCancellation:评估主循环

伪代码(精简后):

Go 复制代码
buildArgs := NewBuildArgs(b.options.BuildArgs)
totalCommands := len(metaArgs) + len(parseResult) + Σ(stage.Commands)

// ⑤ 处理 FROM 之前的 ARG(metaArgs),影响 FROM 镜像名解析
for i := range metaArgs {
    printCommand("Step %d/%d : %v", ...)
    processMetaArg(metaArgs[i], shlex, buildArgs)
}

stagesResults := newStagesBuildResults()

for _, stage := range parseResult {
    request := newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults)

    printCommand(stage.SourceCode)
    initializeStage(ctx, request, &stage)     // ⑥ FROM ...
    request.state.updateRunConfig()
    fmt.Fprintf(b.Stdout, " ---> %s\n", truncateID(state.imageID))

    for _, cmd := range stage.Commands {
        select {                               // ⑦ 取消检查(每条指令之间)
        case <-ctx.Done(): return canceled
        default:
        }
        printCommand(cmd)
        dispatch(ctx, request, cmd)            // ⑧ 真正执行单条指令
        request.state.updateRunConfig()
        fmt.Fprintf(b.Stdout, " ---> %s\n", truncateID(state.imageID))
    }

    emitImageID(b.Aux, state)                  // ⑨ 推送 BuildKit aux 兼容消息
    buildArgs.MergeReferencedArgs(state.buildArgs)
    commitStage(state, stagesResults)          // ⑩ 注册到 stagesResults,供后续 COPY --from 使用
}

buildArgs.WarnOnUnusedBuildArgs(b.Stdout)      // ⑪ --build-arg 没被引用就告警
return request.state, nil

printCommand 输出的就是构建日志里经典的 Step 3/7 : RUN apt-get update


5. 构建上下文:remotecontext/detect.go

Detect(config) 根据 Options.RemoteContext 分流:

|------------------|-----------------------------------|-----------------------------------------------|
| 输入 | 分支 | 返回 |
| 空(默认) | newArchiveRemote | 把 config.Source(HTTP 请求 body 里的 tar)解压成临时目录 |
| client-session | 报错 | v1 builder 已不支持 |
| git URL | newGitRemoteMakeGitContext | git clone 到临时目录 → 转 tar → 走 archive 路径 |
| http(s) URL | newURLRemote | downloadRemote → 检测 Content-Type |
| | ├ text/plain | 当作单文件 Dockerfile 解析(context 为空) |
| | └ tar/octet-stream | 走 archive 路径 |

withDockerfileFromContext 还会做这几件事:

  1. Dockerfile 大小写兜底 :找不到 Dockerfile 就退而求其次找小写 dockerfile(Linux 文件系统区分大小写,但保持兼容)。
  1. .dockerignore处理 :读取忽略规则,如果规则把 Dockerfile 或 .dockerignore 本身匹配上,则把它们从上下文里删掉,避免进入镜像。

FullPathsymlink.FollowSymlinkInScope 把任意路径锁死在 Root() 之内,防止 ADD/COPY 通过软链接逃出构建上下文------这是一道重要的安全边界。


6. 评估器与分发器

6.1 dispatch():分发总开关

Go 复制代码
func dispatch(ctx, d dispatchRequest, cmd instructions.Command) (retErr error) {
    // 1) 平台检查(个别指令在 Windows 上不可用)
    if c, ok := cmd.(instructions.PlatformSpecific); ok { c.CheckPlatform(state.operatingSystem) }

    // 2) 收集环境(image ENV + 当前 stage 的 build-arg)
    envs := shell.EnvsFromSlice(append(runConfig.Env, buildArgs.FilterAllowed(runConfig.Env)...))

    // 3) 变量展开(指令实现了 SupportsSingleWordExpansion)
    if ex, ok := cmd.(instructions.SupportsSingleWordExpansion); ok {
        ex.Expand(func(word string) (string, error) {
            newword, _, err := d.shlex.ProcessWord(word, envs); return newword, err
        })
    }

    // 4) 容器清理策略(defer)
    defer func() {
        if options.ForceRemove { containerManager.RemoveAll(stdout) }
        else if options.Remove && retErr == nil { containerManager.RemoveAll(stdout) }
    }()

    // 5) 类型分发大 switch
    switch c := cmd.(type) {
    case *instructions.EnvCommand:         return dispatchEnv(ctx, d, c)
    case *instructions.MaintainerCommand:  return dispatchMaintainer(ctx, d, c)
    case *instructions.LabelCommand:       return dispatchLabel(ctx, d, c)
    case *instructions.AddCommand:         return dispatchAdd(ctx, d, c)
    case *instructions.CopyCommand:        return dispatchCopy(ctx, d, c)
    case *instructions.OnbuildCommand:     return dispatchOnbuild(ctx, d, c)
    case *instructions.WorkdirCommand:     return dispatchWorkdir(ctx, d, c)
    case *instructions.RunCommand:         return dispatchRun(ctx, d, c)
    case *instructions.CmdCommand:         return dispatchCmd(ctx, d, c)
    case *instructions.HealthCheckCommand: return dispatchHealthcheck(ctx, d, c)
    case *instructions.EntrypointCommand:  return dispatchEntrypoint(ctx, d, c)
    case *instructions.ExposeCommand:      return dispatchExpose(ctx, d, c, envs)
    case *instructions.UserCommand:        return dispatchUser(ctx, d, c)
    case *instructions.VolumeCommand:      return dispatchVolume(ctx, d, c)
    case *instructions.StopSignalCommand:  return dispatchStopSignal(ctx, d, c)
    case *instructions.ArgCommand:         return dispatchArg(ctx, d, c)
    case *instructions.ShellCommand:       return dispatchShell(ctx, d, c)
    }
    return errors.Errorf("unsupported command type: %v", reflect.TypeOf(cmd))
}

注意 dispatchRequest值类型 ,每个 stage 重建一次;它把 stateshlexbuildersourcestages 拼成一个上下文,避免 dispatcher 函数签名单个参数爆炸。

6.2 dispatchState:一个 Stage 的运行时状态

Go 复制代码
type dispatchState struct {
    runConfig       *container.Config   // 当前镜像的 container.Config(CMD/ENV/...)
    maintainer      string
    cmdSet          bool
    imageID         string              // 当前阶段最新提交出的镜像 ID
    baseImage       builder.Image       // FROM 来的镜像
    stageName       string              // AS <name>
    buildArgs       *BuildArgs          // 本阶段的 ARG 副本
    operatingSystem string
}

每条 dispatcher 几乎都会:

  1. 修改 runConfig(设置 EnvCmdEntrypointVolumeLabels 等);
  1. b.commit(ctx, state, "<comment>") 把当前 runConfig "提交成一层"。

commit 内部会构造一个 Cmd = [shell..., "#(nop) <comment>"] 的临时 config------这解释了为什么 docker history里非 RUN 的指令会显示为 #/nop)注释行

6.3 指令清单速查

|-----------------------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------|-----------|
| 指令 | dispatcher | 落点 | 是否 commit |
| FROM | initializeStage(不是 dispatch,是 stage 入口) | state.imageID/baseImage/runConfig | 否(直接拿父镜像) |
| MAINTAINER | dispatchMaintainer | state.maintainer(写入 history.Author) | 是 |
| RUN | dispatchRun | 临时容器执行 → CommitBuildStep | 是(容器路径) |
| CMD/ENTRYPOINT | dispatchCmd/dispatchEntrypoint | runConfig.Cmd/Entrypoint | 是(配置变更) |
| ENV/LABEL/EXPOSE/USER/VOLUME/WORKDIR/STOPSIGNAL/SHELL/ONBUILD/HEALTHCHECK | 对应 dispatchXxx | runConfig 对应字段 | 是 |
| ARG | dispatchArg | BuildArgs.AddArg | 是 |
| COPY | dispatchCopy | performCopy → exportImage | 是(非容器路径) |
| ADD | dispatchAdd | performCopy → exportImage,允许本地解压 + 远程下载 | 是 |

重要差异 :BuildKit 才支持 RUN --mountCOPY --chmodADD --chmod 等。v1 builder 一遇到这些 flag 就直接报 "requires BuildKit"------见 dispatchRundispatchAdd 开头的检查。


7. 容器与镜像提交:internals.go + containerbackend.go

7.1 commit:把"配置变更"提交成镜像

很多指令(ENV/LABEL/USER 等)并不真正运行容器,但仍会生成一个新层------这就是 commit 干的事:

Go 复制代码
func (b *Builder) commit(ctx, state, comment) error {
    if b.disableCommit { return nil }                       // docker commit --change 时跳过
    if !state.hasFromImage() { return errors.New("...") }

    runConfigWithCommentCmd := copyRunConfig(state.runConfig,
        withCmdComment(comment, state.operatingSystem))     // Cmd = [shell, "#(nop) ENV FOO=bar"]
    id, err := b.probeAndCreate(ctx, state, runConfigWithCommentCmd)
    return b.commitContainer(ctx, state, id, runConfigWithCommentCmd)
}

probeAndCreate = 先 probeCache,命中直接复用,没命中才创建容器。commitContainer 调 Daemon 的 CommitBuildStep

7.2 dispatchRun:RUN 指令的真实流程

Go 复制代码
func dispatchRun(ctx, d, c) error {
    // 1) OS 校验、BuildKit flag 校验
    // 2) 解析命令行(shell 形式 vs exec 形式),决定是否走 sh -c / cmd /S /C
    cmdFromArgs, argsEscaped := resolveCmdLine(...)

    // 3) 把 build-arg 拼到 cmd 前面(以 |<n> 标记,便于缓存命中判别)
    saveCmd := cmdFromArgs
    if len(buildArgs) > 0 {
        saveCmd = prependEnvOnCmd(buildArgs, buildArgs, cmdFromArgs)
    }

    // 4) 构造探针 config(带 argsEscaped,清空 entrypoint,关掉 healthcheck)
    runConfigForCacheProbe := copyRunConfig(stateRunConfig,
        withCmd(saveCmd), withArgsEscaped(...),
        withEntrypointOverride(saveCmd, nil), withoutHealthcheck())

    // 5) 命中缓存就跳过
    if hit, _ := b.probeCache(state, runConfigForCacheProbe); hit { return nil }

    // 6) 真正创建 + 启动 + 等待容器
    runConfig := copyRunConfig(stateRunConfig,
        withCmd(cmdFromArgs), withArgsEscaped(argsEscaped),
        withEnv(append(stateRunConfig.Env, buildArgs...)),
        withEntrypointOverride(saveCmd, []string{""}), withoutHealthcheck())
    cID, err := b.create(ctx, runConfig)
    if err := b.containerManager.Run(ctx, cID, stdout, stderr); err != nil {
        return &jsonstream.Error{...}        // 把 exit code 包成 API 错误
    }
    return b.commitContainer(ctx, state, cID, runConfigForCacheProbe)
}

几个常被忽略的细节:

  • prependEnvOnCmd |<n>前缀:把 build-arg 注入到 RUN 命令字符串里。这样相同的 RUN + 相同的 build-arg 才能命中缓存;改变 build-arg 会导致缓存失效------这是设计上期望的。
  • withoutHealthcheck:RUN 期间不要让父镜像的健康检查脚本干扰,临时禁掉。
  • withEntrypointOverride(..., []string{""}):清空 entrypoint,避免 RUN 被 ENTRYPOINT 干扰(ContainerCreate 会把 [""] 翻成 nil)。

7.3 containerManager.Run:临时容器执行体

Go 复制代码
func (c *containerManager) Run(ctx, cID, stdout, stderr) error {
    // ① Attach 容器 IO(stdout/stderr 接到 builder 的输出)
    // ② 起一个 goroutine 监听 ctx.Done:取消就强制删除容器
    // ③ ContainerStart
    // ④ 阻塞读 IO
    // ⑤ ContainerWait(WaitConditionNotRunning)
    // 退出码 != 0 → *statusCodeError
}

tmpContainers 是一个 map[string]struct{},记录本次构建过程中创建的所有容器;构建结束(或出错、或 ForceRemove)由 RemoveAll 统一清理。这就是为什么经典构建日志里会看到:

复制代码
 ---> Running in 1a2b3c4d5e6f
 ---> Removed intermediate container 1a2b3c4d5e6f

8. 镜像缓存:imageprobe.go

8.1 接口

Go 复制代码
type ImageProber interface {
    Reset(ctx) error
    Probe(parentID string, runConfig *container.Config,
          platform ocispec.Platform) (string, error)
}

Probe 返回非空 cacheID = 命中,返回空 = 未命中。Reset 在每个 stage 开始时调用,让缓存基于 --cache-from 重新建立。

8.2 命中与"熔断"

Go 复制代码
func (c *imageProber) Probe(parentID, runConfig, platform) (string, error) {
    if c.cacheBusted { return "", nil }            // 一旦本次构建内 miss 过,后续直接放弃
    cacheID, err := c.cache.GetCache(parentID, runConfig, platform)
    if cacheID == "" {
        c.cacheBusted = true                       // 第一次 miss 就置位
        return "", nil
    }
    return cacheID, nil
}

经典构建器的缓存语义是线性、严格顺序的:从父镜像出发,一旦某条指令的 cache miss,后面所有指令都视为 miss(因为后面的层 hash 链已经断了)。这与 BuildKit 的"按内容寻址、并行重用"完全不同。

--no-cachenewImageProber 直接返回 nopProber{},所有 Probe 永远返回空,效果就是每次都重跑。

8.3 GetCache 在 Daemon 侧的实现

ImageService.MakeImageCachedaemon/containerd/cache.go:25 委托给 daemon/internal/image/cache

  • 没传 --cache-from:用父镜像的子层 history 做匹配;
  • 传了 --cache-from:把这些镜像视作可选起点,遍历它们的 history 寻找父 ID + runConfig 完全匹配的层。

匹配 key 是 (parentID, runConfig),所以 ENV、RUN 等任何"改变 config"或"改变命令"的修改都会让 key 不同------这是缓存的基本原理。


9. COPY/ADD:copy.go + internals.go::performCopy

COPY/ADD 走的是非容器路径 :直接在 RWLayer 上写文件,然后 exportImage 创建新镜像。

Go 复制代码
func (b *Builder) performCopy(ctx, req, inst) error {
    // ① 计算源 hash(单源直取,多源用 sha256 拼接)
    srcHash := getSourceHashFromInfos(inst.infos)
    commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)

    // ② 缓存探针:把"源 hash + 目标"塞进 Cmd 当注释,参与 cache key
    runConfigWithCommentCmd := copyRunConfig(state.runConfig, withCmdCommentString(commentStr, ...))
    if hit, _ := b.probeCache(state, runConfigWithCommentCmd); hit { return nil }

    // ③ 挂载目标镜像的可写层
    imgMount, _ := b.imageSources.Get(ctx, state.imageID, true, ...)
    rwLayer, _ := imgMount.NewRWLayer()
    defer rwLayer.Release()

    // ④ 解析目标路径(相对 WORKINGDIR)+ 解析 chown
    destInfo, _ := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer)
    if inst.chownStr != "" {
        id, _ = parseChownFlag(ctx, b, state, inst.chownStr, destInfo.root, b.idMapping)
    }

    // ⑤ 真正拷贝(performCopyForInfo:考虑通配符、tar 自动解压、保持属主等)
    for _, info := range inst.infos {
        performCopyForInfo(destInfo, info, opts)
    }

    // ⑥ RWLayer.Commit() → 新只读层 → CreateImage
    return b.exportImage(ctx, state, rwLayer, imgMount.Image(), runConfigWithCommentCmd)
}

exportImage 的关键步骤:

Go 复制代码
newLayer, _ := layer.Commit()                                  // 固化可写层
newImage := image.NewChildImage(parentImage, ChildConfig{
    Author: state.maintainer,
    ContainerConfig: runConfig,
    DiffID: newLayer.DiffID(),
    Config: copyRunConfig(state.runConfig),
}, parentImage.OS)
config, _ := newImage.MarshalJSON()
exportedImage, _ := b.docker.CreateImage(ctx, config, state.imageID, newLayer.ContentStoreDigest())
state.imageID = exportedImage.ImageID()

ADD vs COPY 的差异

|--------------------|-----------------------------------|--------------------------|
| 维度 | ADD | COPY |
| 本地 tar 自动解压 | ✅(allowLocalDecompression=true) | ❌ |
| 远程 URL 下载 | ✅(newRemoteSourceDownloader) | ❌(errOnSourceDownload) |
| --from=<stage> 源 | ❌ | ✅ |
| --chown | ✅(v1 也支持) | ✅ |
| --chmod | ❌(要 BuildKit) | ❌ |

COPY --from 通过 getImageMountstages.get(nameOrIndex)imageSources.Get 链路,允许从前序 stage 的镜像里直接拷贝文件,是多阶段构建的基石。


10. BuildArgs:buildargs.go

BuildArgs 维护三类参数:

|--------------------|--------------------------|----------------------------------------|
| 字段 | 含义 | 例子 |
| argsFromOptions | --build-arg KEY=VAL | docker build --build-arg VERSION=1.2 |
| allowedMetaArgs | FROM 之前的 ARG(meta-arg) | ARG VERSION=1.0\nFROM img:${VERSION} |
| allowedBuildArgs | FROM 之后的 ARG | ARG NODE_ENV\nRUN echo $NODE_ENV |
| referencedArgs | Dockerfile 真正引用过的 ARG | 用于"未使用告警" |

builtinAllowedBuildArgsHTTP_PROXY/HTTPS_PROXY/... 等代理变量视作透明:它们可以在 RUN 里被使用,但不会污染 history。

getBuildArg 的解析优先级:--build-arg 选项 > 当前 stage 内 ARG 默认值 > meta-arg 默认值。

FilterAllowed 把 ARG 转成 KEY=VAL 形式注入到 RUN 的环境变量(如果未在父镜像 ENV 中存在)。WarnOnUnusedBuildArgs 在构建末尾打印 [Warning] One or more build-args [foo] were not consumed


11. imageSources:镜像挂载缓存

imageSources 是构建过程中所有用到的镜像挂载的注册表:

复制代码
type imageSources struct {
    byImageID map[string]*imageMount  // 按 ID 去重
    mounts    []*imageMount           // 顺序记录,便于 Unmount
    getImage  getAndMountFunc
}
  • Get(idOrRef, localOnly, platform):第一次访问时拉镜像 + 创建 ROLayer,后续命中缓存。
  • Add(im, platform):scratch 镜像(image == nil)补全为空镜像(OS=linux on Windows)。
  • Unmount():构建结束统一卸载,b.builddefer b.imageSources.Unmount() 保证收尾。

每个 imageMount 持有一个 builder.Image + builder.ROLayer;需要写文件时通过 NewRWLayer() 拿到可写层(Cow),不再用就 Release()


12. 与 BuildKit 的关系:daemon/builder/backend/backend.go

这一层是 API 路由和构建器之间的薄壳:

Go 复制代码
func (b *Backend) Build(ctx, config) (string, error) {
    useBuildKit := options.Version == build.BuilderBuildKit
    var buildResult *builder.Result
    if useBuildKit {
        buildResult, err = b.buildkit.Build(ctx, config)   // BuildKit (v2)
    } else {
        buildResult, err = b.builder.Build(ctx, config)    // Classic (v1,本文主角)
    }
    if options.Squash {
        imageID = squashBuild(...)                          // --squash 后处理
    }
    if imageID != "" && !useBuildKit {
        fmt.Fprintf(stdout, "Successfully built %s\n", truncateID(imageID))
        tagImages(...)                                      // 经典模式这里才打 tag
    }
    return imageID, err
}

要点:

  • 选择 v1/v2 的依据是 BuildOptions.Version,由客户端通过 ?version= 决定(或环境变量 DOCKER_BUILDKIT)。
  • Successfully built <id>这行日志只 v1 打,BuildKit 自己有 progress UI。
  • --squash 不论哪种 builder 都支持,但走 ImageComponent.SquashImage 做扁平化。
  • PruneCacheCancel 当前只委托给 b.buildkit------也就是说经典构建器并没有独立的 cache prune 接口,所有缓存清理都是清 BuildKit 的。

13. 调用链总览(一图速查)

复制代码
HTTP POST /build
└── api/server/router/build  →  buildbackend.Backend (Backend)
    └── daemon/builder/backend/backend.go::Backend.Build
        ├── if BuildKit:    b.buildkit.Build(ctx, config)
        └── else (v1):      b.builder.Build(ctx, config)
            └── dockerfile.BuildManager.Build
                ├── remotecontext.Detect
                │   ├── newArchiveRemote   (默认 tar)
                │   ├── newGitRemote       (git URL)
                │   └── newURLRemote       (http URL)
                ├── newBuilder
                │   ├── newImageProber     (cache/no-cache)
                │   ├── newImageSources    (镜像挂载注册表)
                │   └── newContainerManager(临时容器池)
                └── Builder.build
                    ├── instructions.Parse  (AST → 指令)
                    └── dispatchDockerfileWithCancellation
                        ├── processMetaArg    (FROM 前的 ARG)
                        └── for each stage:
                            ├── initializeStage
                            │   ├── imageProber.Reset
                            │   ├── getFromImage (含 stage 引用解析)
                            │   └── dispatchTriggeredOnBuild (父镜像 ONBUILD)
                            └── for each cmd:
                                └── dispatch
                                    ├── dispatchEnv / Label / User / ...
                                    │   └── b.commit → probeAndCreate → commitContainer
                                    │                          └── Backend.CommitBuildStep (Daemon)
                                    ├── dispatchRun
                                    │   ├── probeCache (cache hit 跳过)
                                    │   ├── b.create → ContainerCreateIgnoreImagesArgsEscaped
                                    │   ├── containerManager.Run
                                    │   │   ├── ContainerAttachRaw
                                    │   │   ├── ContainerStart
                                    │   │   └── ContainerWait
                                    │   └── commitContainer → Backend.CommitBuildStep
                                    ├── dispatchCopy / dispatchAdd
                                    │   ├── probeCache
                                    │   ├── imageSources.Get → NewRWLayer
                                    │   ├── performCopyForInfo (文件写入 + 可选解压/下载)
                                    │   └── exportImage
                                    │       ├── RWLayer.Commit
                                    │       └── Backend.CreateImage
                                    └── dispatchWorkdir
                                        ├── probeAndCreate
                                        ├── Backend.ContainerCreateWorkdir
                                        └── commitContainer

14. 学习路径建议

  1. 先读 5 个文件daemon/builder/builder.godockerfile/builder.godockerfile/evaluator.godockerfile/dispatchers.go(按需读 dispatchRun/dispatchCopy)→ dockerfile/internals.go。这五者构成 v1 builder 的主干。
  1. 挑一个指令跟一遍 :建议从 RUN 开始,因为它串起了缓存探测、临时容器、stdout 透传、错误退出码、commit 这条最复杂的链。
  1. 再看 COPY :理解"非容器路径"如何提交镜像(RWLayer.Commit + CreateImage),与 RUN 形成"两条提交路径"的对比。
  1. 最后看 BuildArgs / imageSources / remotecontext:这些是周边支撑,掌握了主干再读会非常自然。
  1. 横向对比 BuildKit :当 v1 全图清楚了,再去 daemon/internal/builder-next/ 看同样的 Dockerfile 是怎么用 LLB/DAG 并行求解的------能更深刻理解"为什么 BuildKit 是必然"。

15. 关键设计要点小结

  • 接口与实现分离daemon/builder/builder.go 只声明契约,让 Daemon 通过实现 Backend 反向注入能力;构建器本身不直接接触 containerd。
  • 解析器复用、执行器分叉 :v1 和 v2 共享 buildkit/frontend/dockerfile/parserinstructions,但评估器是 v1 独有。
  • 两套提交路径 :RUN → 容器路径(CommitBuildStep);COPY/ADD → 非容器路径(RWLayer.Commit + CreateImage)。
  • 缓存严格线性 :一旦 miss 即"熔断"(cacheBusted),不再尝试后续命中,这是 v1 缓存语义的本质。
  • 取消粒度 :在每条指令执行之间 检查 ctx.Done(),所以单条 RUN 一旦开始就不会立即响应取消,需要等它结束或被 containerManager.Run 内部的 goroutine 强删。
  • 安全边界symlink.FollowSymlinkInScope 把 ADD/COPY 路径锁在上下文根下;FollowSymlinkInScope 同样用于容器内拷贝防止逃逸。
  • BuildKit 才能用的特性RUN --mountCOPY --chown 之外的所有 --<flag>(如 --chmod--link--parents)v1 都直接拒绝;这也是为什么社区强烈推荐切到 BuildKit。
相关推荐
易筋紫容1 小时前
为什么做了 DevOps,你还是管不好开源依赖?
运维·开源·devops
IT二叔1 小时前
Kubernetes(K8s)-08-Deployment详解
云原生·容器·kubernetes
强里秋千墙外道1 小时前
Ubuntu 开机 Kernel Panic:HWE 内核升级失败 + NVIDIA DKMS 踩坑实录
linux·运维·ubuntu
coder!mq1 小时前
什么是泛型?有什么好处?
linux·运维·服务器·面试题·八股文
日取其半万世不竭10 小时前
用 Portainer 可视化管理 Docker:容器再多也不用背命令了
运维·docker·容器
动恰客流统计13 小时前
ReID边缘计算视觉统计:餐饮店客流增长的数字化破局路径
java·大数据·运维·人工智能
张忠琳14 小时前
【NVIDIA】NVIDIA Container Toolkit v1.19.1--07-NVCDI模块:CDI Spec生成引擎深度分析之七
云原生·容器·架构·kubernetes·nvidia
沉迷学习 日益消瘦16 小时前
01-Kubernetes 入门:概念与安装
云原生·容器·kubernetes
weixin_4196583116 小时前
Docker 搭建 Jenkins 服务
java·docker·jenkins