Dockerfile 最佳实践

Dockerfile 常见概念

构建上下文(Build Context)

  • 当运行 docker build 命令时,当前工作目录被称为构建上下文。
  • docker build 默认查找当前目录的 Dockerfile 作为构建输入,也可以通过 –f 指定 Dockerfile。
    • docker build –f ./Dockerfile
  • 当 docker build 运行时,首先会把构建上下文传输给 docker daemon,把没用的文件包含在构建上下文时,会导致传输时间长,构建需要的资源多。
  • 可以通过.dockerignore文件从编译上下文排除某些文件。
  • 因此需要确保构建上下文清晰,比如创建一个专门的目录放置 Dockerfile,并在目录中运行 docker build。

Build Cache

构建容器镜像时,Docker 依次读取 Dockerfile 中的指令,并按顺序依次执行构建指令。

Docker 读取指令后,会先判断缓存中是否有可用的已存镜像,只有已存镜像不存在时才会重新构建。

  • 通常 Docker 简单判断 Dockerfile 中的指令与镜像。
  • 针对 ADD和 COPY指令,Docker 判断该镜像层每一个文件的内容并生成一个 checksum,与现存镜 像比较时,Docker 比较的是二者的 checksum。
  • 其他指令,比如 RUN apt-get -y update,Docker 简单比较与现存镜像中的指令字串是否一致。
  • 一旦某一层的缓存无效,所有后续的Dockerfile命令将生成新的镜像,并且高速缓存将不被使用。
  • 如果不想使用本地缓存的镜像,也可以通过--cache-from指定缓存。指定后将不再使用本地生成的镜像链,而是从镜像仓库中下载。

多段构建(Multi-stage build)

  • 使用多段构建可以避免将不必要的文件和工具打包进最终的镜像中,从而减小镜像的体积,提高部署和运行效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 第一段, 用来构建二进制
FROM golang:1.16-alpine AS build

RUN apk add --no-cache git

RUN go get github.com/golang/dep/cmd/dep

COPY Gopkg.lock Gopkg.toml /go/src/project/ WORKDIR /go/src/project/

RUN dep ensure -vendor-only

COPY . /go/src/project/

RUN go build -o /bin/project(只有这个二进制文件是产线需要的,其他都是waste)
1
2
3
4
5
6
7
8
# 构建runtime
FROM scratch

COPY --from=build /bin/project /bin/project

ENTRYPOINT ["/bin/project"]

CMD ["--help"]

Dockerfile 常用指令

  • FROM:选择基础镜像,推荐 alpine

    FROM [–platform=] [@] [AS ]

  • LABELS:按标签组织项目

    LABEL multi.label1=”value1” multi.label2=”value2” other=”value3” 配合 label filter 可过滤镜像查询结果

    docker images -f label=multi.label1=”value1”

  • RUN:执行命令

    最常见的用法是 RUN apt-get update && apt-get install,这两条命令应该永远用&&连接,如果分开执行,RUN apt-get update 构建层被缓存,可能会导致新 package 无法安装

  • EXPOSE:发布端口

    EXPOSE [/…]

    • 是镜像创建者和使用者的约定
    • 在 docker run –P 时,docker 会自动映射 expose 的端口到主机大端口,如0.0.0.0:32768->80/tcp
  • ENV

    设置环境变量 ENV =

  • ADD:从源地址(文件,目录或者 URL)复制文件到目标路径

    ADD [–chown=:]

    ADD [–chown=:] [“”,… “”] (路径中有空格时使用)

    • ADD支持 Go 风格的通配符,如 ADD check* /testdir/
    • src 如果是文件,则必须包含在编译上下文中,ADD 指令无法添加编译上下文之外的文件
    • src 如果是 URL
      • 如果 dest 结尾没有/,那么 dest 是目标文件名,如果 dest 结尾有/,那么 dest 是目标目录名
    • 如果 src 是一个目录,则所有文件都会被复制至 dest
    • 如果 src 是一个本地压缩文件,则在 ADD 的同时完整解压操作
    • 如果 dest 不存在,则 ADD 指令会创建目标目录
    • 应尽量减少通过 ADDURL 添加 remote 文件,建议使用 curl 或者 wget && untar
  • COPY:从源地址(文件,目录或者URL)复制文件到目标路径

    COPY [–chown=:]

    COPY [–chown=:] [““,… ““]// 路径中有空格时使用

    • COPY的使用与 ADD类似,但有如下区别
    • COPY 只支持本地文件的复制,不支持 URL
    • COPY 不解压文件
    • COPY可以用于多阶段编译场景,可以用前一个临时镜像中拷贝文件
      • COPY –from=build /bin/project /bin/project

    COPY 语义上更直白,复制本地文件时,优先使用 COPY

  • CMD:容器镜像中包含应用的运行命令,需要带参数 CMD [“executable”, “param1”, “param2”…], 会被RUN后参数覆盖

  • ENTRYPOINT:定义可以执行的容器镜像入口命令

    ENTRYPOINT [“executable”, “param1”, “param2”] // docker run参数追加到后面

    ENTRYPOINT command param1 param2 // docker run 参数替换整条命令

    • docker run –entrypoint 可替换 Dockerfile 中定义的 ENTRYPOINT
    • ENTRYPOINT的最佳实践是用 ENTRYPOINT定义镜像主命令,并通过 CMD定义主要参数,当传递run参数后, 会覆盖CMD设置的主要参数,如下所示
      • ENTRYPOINT [“s3cmd”]
      • CMD [“–help”]
  • VOLUME: 将指定目录定义为外挂存储卷,等价于 docker run –v /data

  • USER:切换运行镜像的用户和用户组,因安全性要求,越来越多的场景要求容器应用要以

    USER [:]

  • WORKDIR:等价于 cd,切换工作目录

    WORKDIR /path/to/workdir

  • 其他非常用指令

    • ARG
    • ONBUILD
    • STOPSIGNAL
    • HEALTHCHECK
    • SHELL

Dockerfile 最佳实践

核心目标

  • 易管理
  • 少漏洞
  • 镜像小
  • 层级少
  • 利用缓存

具体实践

  • 不要安装无效软件包。
  • 应简化镜像中同时运行的进程数,理想状况下,每个镜像应该只有一个进程。
  • 当无法避免同一镜像运行多进程时,应选择合理的初始化进程(init process)。
  • 最小化层级数
    • 最新的 docker 只有 RUN, COPY,ADD创建新层,其他指令创建临时层,不会增加镜像大小。
      • 比如 EXPOSE指令就不会生成新层。
    • 多条 RUN命令可通过连接符连接成一条指令集以减少层数。
    • 通过多段构建减少镜像层数。
  • 把多行参数按字母排序,可以减少可能出现的重复参数,并且提高可读性。
  • 编写 dockerfile 的时候,应该把变更频率低的编译指令优先构建以便放在镜像底层以有效利用 build cache。
  • 复制文件时,每个文件应独立复制,这确保某个文件变更时,只影响该文件对应的缓存。

目标:易管理、少漏洞、镜像小、层级少、利用缓存。

刘小恺(Kyle) wechat
如有疑问可联系博主