Dockerfile 的最佳实践
创建 docker 镜像
定义 dockerfile
FROM ubuntu
# so apt-get doesn't complain
ENV DEBIAN_FRONTEND=noninteractive
RUN sed -i 's/^exit 101/exit 0/' /usr/sbin/policy-rc.d
RUN \
apt-get update && \
apt-get install -y ca-certificates && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
ADD ./bin/eic eic
ENTRYPOINT ["/eic"]
docker build
回顾 12 Factor 之进程
- 运行环境中,应用程序通常是以一个和多个进程运行的。
- 12-Factor 应用的进程必须无状态(Stateless)且无共享(Share nothing)。
- 任何需要持久化的数据都要存储在后端服务内,比如数据库。
- 应在构建阶段将源代码编译成待执行应用。
- Session Sticky 是 12-Factor 极力反对的。
- Session 中的数据应该保存在诸如 Memcached 或 Redis 这样的带有过期时间的缓存中。
Docker 遵循以上原则管理和构建应用。
理解构建上下文(Build Context)
- 当运行 docker build 命令时,当前工作目录被称为构建上下文。
- docker build 默认查找当前目录的 Dockerfile 作为构建输入,也可以通过 –f 指定 Dockerfile。
- docker build –f ./Dockerfile
- 当 docker build 运行时,首先会把构建上下文传输给 docker daemon,把没用的文件包含在构建上下文时,会导致传输时间长,构建需要的资源多,构建出的镜像大等问题。
- 试着到一个包含文件很多的目录运行下面的命令,会感受到差异;
- docker build -f $GOPATH/src/github.com/cncamp/golang/httpserver/Dockerfile ;
- docker build $GOPATH/src/github.com/cncamp/golang/httpserver/;
- 可以通过.dockerignore文件从编译上下文排除某些文件。
- 因此需要确保构建上下文清晰,比如创建一个专门的目录放置 Dockerfile,并在目录中运行 docker build。
镜像构建日志
docker build $GOPATH/src/github.com/cncamp/golang/httpserver/
Sending build context to Docker daemon 14.57MB
Step 1/4 : FROM ubuntu
---> cf0f3ca922e0
Step 2/4 : ENV MY_SERVICE_PORT=80
---> Using cache
---> a7d824f74410
Step 3/4 : ADD bin/amd64/httpserver /httpserver
---> Using cache
---> 00bb47fce704
Step 4/4 : ENTRYPOINT /httpserver
---> Using cache
---> f77ee3366d08
Successfully built f77ee3366d08
Build Cache
构建容器镜像时,Docker 依次读取 Dockerfile 中的指令,并按顺序依次执行构建指令。
Docker 读取指令后,会先判断缓存中是否有可用的已存镜像,只有已存镜像不存在时才会重新构建。
- 通常 Docker 简单判断 Dockerfile 中的指令与镜像。
- 针对 ADD 和 COPY 指令,Docker 判断该镜像层每一个文件的内容并生成一个 checksum,与现存镜像比较时,Docker 比较的是二者的 checksum。
- 其他指令,比如 RUN apt-get -y update,Docker 简单比较与现存镜像中的指令字串是否一致。
- 当某一层 cache 失效以后,所有所有层级的 cache 均一并失效,后续指令都重新构建镜像。
多段构建(Multi-stage build)
- 有效减少镜像层级的方式
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)
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
Dockerfile 常用指令
- FROM:选择基础镜像,推荐 alpine
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
docker 基础镜像:
- scratch: 空镜像,基础镜像
- scratch 是 Docker 中预留的最小的基础镜像。bosybox、Go 语言编译打包的镜像都可以基于 scratch 来构建。
- scratch镜像不在镜像仓库中,但是可以在 Dockerfile 引用。
- 运行一个包含所有依赖的二进制文件,如 Golang 程序,可以直接使用 scratch 作为基础镜像。
- busybox
- BusyBox 是一个集成了一百多个最常用 Linux 命令和工具(如 cat、echo、grep、mount、telnet 等)的精简工具箱,它只有几 MB 的大小,很方便进行各种快速验证,被誉为“Linux系统的瑞士军刀”。BusyBox 可运行于多款 POSIX 环境的操作系统中,如 Linux(包括Android)、Hurd、FreeBSD 等。
-
Alpine
- Alpine 操作系统是一个面向安全的轻型 Linux 发行版。它不同于通常的 Linux 发行版,Alpine 采用了 musl libc 和 BusyBox 以减小系统的体积和运行时资源消耗,但功能上比 BusyBox 又完善得多。在保持瘦身的同时,Alpine 还提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 查询包信息,也可以通过apk命令直接查询和安装各种软件。
- Alpine Docker 镜像也继承了 Alpine Linux 发行版的这些优势。相比于其他 Docker 镜像,它的容量非常小,仅仅只有 5MB 左右(Ubuntu系列镜像接近200MB),且拥有非常友好的包管理机制。官方镜像来自 docker-alpine 项目。
- 目前 Docker 官方已开始推荐使用 Alpine 替代之前的 Ubuntu 作为基础镜像环境。这样会带来多个好处,包括镜像下载速度加快,镜像安全性提高,主机之间的切换更方便,占用更少磁盘空间等。
-
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 无法安装
- CMD:容器镜像中包含应用的运行命令,需要带参数
CMD ["executable", "param1", "param2"…]
- EXPOSE:发布端口
EXPOSE <port> [<port>/<protocol>...]
- 是镜像创建者和使用者的约定
- 在 docker run –P 时,docker 会自动映射 expose 的端口到主机大端口,如0.0.0.0:32768->80/tcp
- ENV 设置环境变量
ENV <key>=<value> ...
- ADD:从源地址(文件,目录或者 URL)复制文件到目标路径
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] [“<src>”,... “<dest>”] (路径中有空格时使用)
- ADD 支持 Go 风格的通配符,如 ADD check* /testdir/
- src 如果是文件,则必须包含在编译上下文中,ADD 指令无法添加编译上下文之外的文件
- src 如果是 URL
- 如果 dest 结尾没有/,那么 dest 是目标文件名,如果 dest 结尾有/,那么 dest 是目标目录名
- 如果 src 是一个目录,则所有文件都会被复制至 dest
- 如果 src 是一个本地压缩文件,则在 ADD 的同时完整解压操作
- 如果 dest 不存在,则 ADD 指令会创建目标目录
- 应尽量减少通过 ADD URL 添加 remote 文件,建议使用 curl 或者 wget && untar
- COPY:从源地址(文件,目录或者URL)复制文件到目标路径
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"] // 路径中有空格时使用
- COPY 的使用与 ADD 类似,但有如下区别
- COPY 只支持本地文件的复制,不支持 URL
- COPY 不解压文件
- COPY 可以用于多阶段编译场景,可以用前一个临时镜像中拷贝文件
- COPY --from=build /bin/project /bin/project
COPY 语义上更直白,复制本地文件时,优先使用 COPY
- ENTRYPOINT:定义可以执行的容器镜像入口命令
ENTRYPOINT ["executable", "param1", "param2"] // docker run参数追加模式
ENTRYPOINT command param1 param2 // docker run 参数替换模式
- docker run –entrypoint 可替换 Dockerfile 中定义的 ENTRYPOINT
-
ENTRYPOINT 的最佳实践是用 ENTRYPOINT 定义镜像主命令,并通过 CMD 定义主要参数,如下所示
- ENTRYPOINT ["s3cmd"]
- CMD ["--help"]
-
VOLUME: 将指定目录定义为外挂存储卷,Dockerfile 中在该指令之后所有对同一目录的修改都无效
VOLUME ["/data"] 等价于 docker run –v /data,
可通过 docker inspect 查看主机的 mount point,/var/lib/docker/volumes/<containerid>/_data
- USER:切换运行镜像的用户和用户组,因安全性要求,越来越多的场景要求容器应用要以 non-root 身份运行
USER <user>[:<group>]
- WORKDIR:等价于 cd,切换工作目录
WORKDIR /path/to/workdir
- 其他非常用指令
- ARG
- ONBUILD
- STOPSIGNAL
- HEALTHCHECK
- SHELL
Dockerfile 最佳实践
- 不要安装无效软件包。
- 应简化镜像中同时运行的进程数,理想状况下,每个镜像应该只有一个进程。
- 当无法避免同一镜像运行多进程时,应选择合理的初始化进程(init process)。
- 最小化层级数
- 最新的 docker 只有 RUN, COPY,ADD 创建新层,其他指令创建临时层,不会增加镜像大小。
- 比如 EXPOSE 指令就不会生成新层。
- 多条 RUN 命令可通过连接符连接成一条指令集以减少层数。
- 通过多段构建减少镜像层数。
- 最新的 docker 只有 RUN, COPY,ADD 创建新层,其他指令创建临时层,不会增加镜像大小。
- 把多行参数按字母排序,可以减少可能出现的重复参数,并且提高可读性。
- 编写 dockerfile 的时候,应该把变更频率低的编译指令优先构建以便放在镜像底层以有效利用 build cache。
- 复制文件时,每个文件应独立复制,这确保某个文件变更时,只影响改文件对应的缓存。
目标:易管理、少漏洞、镜像小、层级少、利用缓存。
多进程的容器镜像
- 选择适当的 init 进程
- 需要捕获 SIGTERM 信号并完成子进程的优雅终止
- 负责清理退出的子进程以避免僵尸进程
开源项目: https://github.com/krallin/tini
Docker 镜像管理
docker save/load
docker tag
docker push/pull
基于 Docker 镜像的版本管理
- Docker tag
docker tag 命令可以为容器镜像添加标签
docker tag 0e5574283393 hub.docker.com/cncamp/httpserver:v1.0
- hub.docker.com: 镜像仓库地址,如果不填,则默认为 hub.docker.com
- cncamp: repositry
- httpserver:镜像名
- v1.0:tag,常用来记录版本信息
Docker tag 与 github 的版本管理合力
- 以 Kubernetes 为例
- 开发分支
- git checkout master
- Release 分支
- git checkout –b release-1.21
- 在并行期,所有的变更同时放进 master 和 release branch
- 版本发布
- 以 release branch 为基础构建镜像,并为镜像标记版本信息:docker tag 0e5574283393 k8s.io/kubernetes/apiserver:v1.21
- 在 github 中保存 release 代码快照
- git tag v1.21
- 开发分支
镜像仓库
Docker hub
- https://hub.docker.com/
创建私有镜像仓库
- sudo docker run -d -p 5000:5000 registry
Docker 优势
- 封装性:
- 不需要再启动内核,所以应用扩缩容时可以秒速启动。
- 资源利用率高,直接使用宿主机内核调度资源,性能损失小。
- 方便的 CPU、内存资源调整。
- 能实现秒级快速回滚。
- 一键启动所有依赖服务,测试不用为搭建环境犯愁,PE 也不用为建站复杂担心。
- 镜像一次编译,随处使用。
- 测试、生产环境高度一致(数据除外)。
- 隔离性:
- 应用的运行环境和宿主机环境无关,完全由镜像控制,一台物理机上部署多种环境的镜像测试。
- 多个应用版本可以并存在机器上。
- 镜像增量分发:
- 由于采用了 Union FS, 简单来说就是支持将不同的目录挂载到同一个虚拟文件系统下,并实现一种 layer 的概念,每次发布只传输变化的部分,节约带宽。
- 社区活跃:
- Docker 命令简单、易用,社区十分活跃,且周边组件丰富。
容器的劣势?
课后作业:思考并讨论容器的劣势。
课后练习 3.2
- 构建本地镜像
- 编写 Dockerfile 将练习 2.2 编写的 httpserver 容器化
- 请思考有哪些最佳实践可以引入到 Dockerfile 中来
- 将镜像推送至 Docker 官方镜像仓库
- 通过 Docker 命令本地启动 httpserver
- 通过 nsenter 进入容器查看 IP 配置