Dockerfile 详解
目录
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