Dockerfile 详解

目录Dockerfile 概念与用途1. 构建上下文(Build Context)2. 缓存机制(Build Cache)缓存机制实例详解3. 多阶段构建(Multi-Stage Build)4. 常用指令详解5. 编写 Dockerfile 的 10 条最佳实践6. 常见问题排查7. 参考链接

Dockerfile 概念与用途

Dockerfile 是用于描述 容器镜像 构建流程的声明式脚本文件,每条指令依次说明"从哪个基础镜像开始 → 复制/下载哪些文件 → 运行哪些命令 → 镜像启动时执行什么"。

主要优势:

可移植:一次编写,任何支持 OCI 标准的容器运行时(Docker、Podman、containerd、CRI-O 等)都能运行生成的镜像

可重复:构建步骤显式记录,便于在 CI/CD 中自动化执行

可追溯:镜像的每一层都对应一条指令,排错与版本回退更直观

借助 Dockerfile,开发者可以把应用以及依赖(系统库、配置文件、启动命令)封装进不可变镜像,实现"像复制文件一样部署软件"。

1. 构建上下文(Build Context)

docker build <上下文路径> 中的路径即 构建上下文,其下所有文件都会被打包发送给 Docker Daemon。

使用 .dockerignore 排除无关文件(如 node_modules/、*.log、.git 等),可显著减少传输体积与构建时间。

也可以通过 docker buildx build https://github.com/your/repo.git#branch 直接使用远程 Git 仓库作为上下文。

# 示例:在独立目录中放置 Dockerfile

mkdir build && cp Dockerfile build/

cd build && docker build -t myimage:latest .

2. 缓存机制(Build Cache)

Docker 逐行解析 Dockerfile 并为每条指令创建镜像层。若 同一指令文本 + 同一上一层 ID 已构建过,Docker 将复用缓存。COPY/ADD 还会比较源文件校验和;在 BuildKit 模式下,ARG、ENV 等变化同样会触发失效。

失效规则:某层失效后,其后的所有层均需重新构建。

缓存机制实例详解

以 Golang 应用为例,演示缓存机制的工作原理:

❌ 低效的写法(缓存命中率低):

FROM golang:1.21-alpine

WORKDIR /src

COPY . . # 第3步:复制所有文件

RUN go mod download # 第4步:下载依赖

RUN go build -o app . # 第5步:编译应用

CMD ["./app"] # 第6步:启动命令

构建过程分析:

# 第一次构建

$ docker build -t myapp .

Step 1/6 : FROM golang:1.21-alpine

---> abc123def456 (缓存命中)

Step 2/6 : WORKDIR /src

---> Running in xyz789

---> def456ghi789 (新层)

Step 3/6 : COPY . .

---> hij789klm012 (新层,复制所有文件)

Step 4/6 : RUN go mod download

---> Running in mno345pqr678

---> pqr678stu901 (新层,下载依赖,耗时45秒)

Step 5/6 : RUN go build -o app .

---> Running in stu901vwx234

---> vwx234yza567 (新层,编译应用,耗时30秒)

Step 6/6 : CMD ["./app"]

---> yza567bcd890 (新层)

# 修改源码后重新构建

$ echo 'fmt.Println("updated")' >> main.go

$ docker build -t myapp .

Step 1/6 : FROM golang:1.21-alpine

---> abc123def456 (缓存命中)

Step 2/6 : WORKDIR /src

---> def456ghi789 (缓存命中)

Step 3/6 : COPY . .

---> efg123hij456 (文件变化,缓存失效!)

Step 4/6 : RUN go mod download

---> Running in hij456klm789

---> klm789nop012 (重新执行,又要45秒!)

Step 5/6 : RUN go build -o app .

---> Running in nop012pqr345

---> pqr345stu678 (重新执行,又要30秒!)

Step 6/6 : CMD ["./app"]

---> stu678vwx901 (重新执行)

✅ 高效的写法(缓存命中率高):

FROM golang:1.21-alpine

WORKDIR /src

COPY go.mod go.sum ./ # 第3步:只复制依赖文件

RUN go mod download # 第4步:下载依赖

COPY . . # 第5步:复制源代码

RUN go build -o app . # 第6步:编译应用

CMD ["./app"] # 第7步:启动命令

优化后的构建过程:

# 第一次构建

$ docker build -t myapp .

Step 1/7 : FROM golang:1.21-alpine

---> abc123def456 (缓存命中)

Step 2/7 : WORKDIR /src

---> def456ghi789 (新层)

Step 3/7 : COPY go.mod go.sum ./

---> ghi789jkl012 (新层,仅依赖文件)

Step 4/7 : RUN go mod download

---> jkl012mno345 (新层,下载依赖,耗时45秒)

Step 5/7 : COPY . .

---> mno345pqr678 (新层,复制源码)

Step 6/7 : RUN go build -o app .

---> pqr678stu901 (新层,编译应用,耗时30秒)

Step 7/7 : CMD ["./app"]

---> stu901vwx234 (新层)

# 修改源码后重新构建(go.mod未变)

$ echo 'fmt.Println("updated")' >> main.go

$ docker build -t myapp .

Step 1/7 : FROM golang:1.21-alpine

---> abc123def456 (缓存命中)

Step 2/7 : WORKDIR /src

---> def456ghi789 (缓存命中)

Step 3/7 : COPY go.mod go.sum ./

---> ghi789jkl012 (缓存命中,go.mod未变)

Step 4/7 : RUN go mod download

---> jkl012mno345 (缓存命中!跳过45秒下载)

Step 5/7 : COPY . .

---> vwx234yza567 (文件变化,但依赖已缓存)

Step 6/7 : RUN go build -o app .

---> yza567bcd890 (重新编译,耗时30秒)

Step 7/7 : CMD ["./app"]

---> bcd890efg123 (重新执行)

性能对比:

低效写法:每次代码修改都要重新下载依赖+重新编译 (~75秒)

高效写法:只有依赖变化才重新下载,代码变化只需重新编译 (~30秒)

速度提升:2.5倍!

进阶特性

Inline cache:--build-arg BUILDKIT_INLINE_CACHE=1 让生成的镜像携带缓存元数据,便于在 CI/CD 间共享。

缓存挂载:RUN --mount=type=cache,target=/root/.cargo 用于依赖缓存,避免写入镜像层。

3. 多阶段构建(Multi-Stage Build)

通过多阶段构建可显著减小最终镜像体积,并隔离编译依赖。

FROM golang:1.22 AS builder

WORKDIR /src

COPY go.* ./

RUN go mod download

COPY . .

RUN CGO_ENABLED=0 go build -o /out/app

# 轻量级运行时镜像

FROM gcr.io/distroless/base

COPY --from=builder /out/app /usr/bin/app

USER nonroot:nonroot

ENTRYPOINT ["/usr/bin/app"]

第一阶段包含完整构建链条;第二阶段仅拷贝编译产物。

FROM scratch 适用于极简场景(如静态编译的 Go 程序)。

4. 常用指令详解

指令

关键点

示例

FROM

选择安全、体积小的基础镜像,如 alpine、distroless。可用 AS 命名阶段。

FROM alpine:3.20 AS base

LABEL

添加元数据;支持 label filter。

LABEL maintainer="dev@example.com"

RUN

使用 && 链式执行并在末尾清理缓存,减少层大小;失败即终止构建。

RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*

COPY

仅本地文件;支持 --chmod / --chown;优先于 ADD。

COPY --chmod=755 app /usr/bin/app

ADD

额外支持 URL、自动解压,但因不易控制建议慎用。

ADD https://example.com/busybox.tar.gz /

ENV

设置环境变量;会影响后续缓存。

ENV TZ=Asia/Shanghai

EXPOSE

声明元数据,不会真正开放端口。

EXPOSE 80 443

USER

以非 root 身份运行提高安全性。

USER 10001:10001

WORKDIR

设置工作目录,相当于 cd。

WORKDIR /app

ENTRYPOINT

定义主命令;用 JSON 形式可避免 shell 解析。

ENTRYPOINT ["/usr/bin/app"]

CMD

默认参数;docker run 追加/覆盖。

CMD ["--help"]

VOLUME

声明匿名卷;声明后对同一路径的修改不再进入镜像层。

VOLUME ["/data"]

ENTRYPOINT 与 CMD 有何区别?

• ENTRYPOINT:定义容器启动后必定执行的"主进程",一般不会被替换;当使用 JSON 形式时,docker run 只能追加参数而不能修改主进程

• CMD:为 ENTRYPOINT 提供"默认参数";如果镜像未定义 ENTRYPOINT,则 CMD 本身可作为要执行的命令。当 docker run 明确给出新命令时会覆盖 CMD

实践范式:

ENTRYPOINT ["/usr/bin/app"]

CMD ["--port=80"]

用户可以通过 docker run image --port=8080 覆盖默认参数,但仍然执行 /usr/bin/app 这一主进程。

JSON 形式示例:

# JSON 形式(推荐)- 直接执行,不经过 shell 解析

ENTRYPOINT ["/usr/bin/app", "--config", "/etc/app.conf"]

CMD ["--port", "80", "--debug"]

# Shell 形式 - 会通过 /bin/sh -c 执行

ENTRYPOINT /usr/bin/app --config /etc/app.conf

CMD --port 80 --debug

5. 编写 Dockerfile 的 10 条最佳实践

按变更频率排序:先 COPY go.mod 等少变化文件,再 COPY 源码,提高缓存命中。

合并 RUN:将相关命令用 && 合并并清理缓存目录,减少层级。

使用 .dockerignore:排除无关文件。

只装必需依赖:过多包会带来额外漏洞与体积。

优先使用 COPY,仅在需 URL/解压时用 ADD。

启用 BuildKit:DOCKER_BUILDKIT=1 或 docker buildx build 获取新特性与更快速度。

多阶段 + distroless:减小运行时镜像并降低攻击面。

非 root 运行:USER 指定普通用户,并确保文件权限正确。

健康检查:为生产镜像添加 HEALTHCHECK,便于编排系统检测。

CI 中扫描漏洞:结合 trivy、grype 等工具发布前扫描镜像。

6. 常见问题排查

现象

可能原因

解决方案

using cache 但代码已更新

COPY 的路径不一致或文件在 .dockerignore 中

检查 COPY/ADD 路径;确认文件未被忽略

镜像过大

未清理包缓存 / 未多阶段构建

采用多阶段;在 RUN 中删除 /var/lib/apt/lists 等缓存

应用无权限访问文件

运行用户非 root

调整文件属主或使用 --chown COPY

7. 参考链接

Docker Docs – Build images with BuildKit: https://docs.docker.com/build/

官方最佳实践:https://docs.docker.com/develop/dev-best-practices/

Distroless 镜像介绍:https://github.com/GoogleContainerTools/distroless