阅读 58

Dockerfile 最佳实践

工作中经常遇到要打容器镜像的任务,今天的文章介绍了在打容器镜像方面的最佳实践。

Dockerfile:Docker 特有的镜像构建定义文件。

一、Dockerfile 简介

Dockerfile 是 Docker 中用于定义镜像自动化构建流程的配置文件,在 Dockerfile 中,包含了构建镜像过程中需要执行的命令和其他操作。通过 Dockerfile 可以更加清晰、明确的给定 Docker 镜像的制作过程,由于仅是简单、小体积的文件,在网络等介质中传递的速度快,能够更快的实现容器迁移和集群部署。

1.jpg

通常来说,对 Dockerfile 的定义就是针对一个名为 Dockerfile 的文件,其虽然没有扩展名,但本质就是一个文本文件,可以通过常见的文本编辑器或者 IDE 创建和编辑它。

Dockerfile 的内容很简单,主要以两种形式呈现,一种是注释行,另一种是指令行。Dockerfile 中,拥有一套独立的指令语法,用于给出镜像构建过程中所要执行的过程。Dockerfile 里的指令行,就是由指令与其相应的参数所组成。

用开发中的常见流程来类比 Dockerfile。

在一个完整的开发、测试、部署过程中,程序运行环境的定义通常是由开发人员来进行的,因为开发更熟悉程序运转的各个细节,更适合搭建适合程序的运行环境。

以此为前提,为了方便测试和运维搭建相同的程序运行环境,常用的做法是由开发人员编写一套环境搭建手册,帮助测试人员和运维人员了解环境搭建的流程。

Dockerfile 就像这样一个环境搭建手册,因为其中包含的就是一个构建容器的过程。

而比环境搭建手册更好的是,Dockerfile 在容器体系下能够完成自动构建,既不需要测试和运维人员深入理解环境中各个软件的具体细节,也不需要人工执行每一个搭建流程。

相对于提交容器修改再进行镜像迁移的方式相比,使用 Dockerfile 有很多优势:

  • Dockerfile 的体积远小于镜像包,更容易进行快速迁移和部署
  • 环境构建流程记录在 Dockerfile 中,能够直观的看到镜像构建的顺序和逻辑
  • 使用 Dockerfile 构建镜像能够更轻松的实现自动部署等自动化流程
  • 在修改环境搭建细节时,修改 Dockerfile 文件更简单

实际开发使用中很少会选择容器提交这种方法来构建镜像,而是几乎采用 Dockerfile 来制作镜像。

二、环境搭建与镜像构建

A、Dockerfile 编写

以下为完整的 Dockerfile,用于构建 Docker 官方所提供的 Redis 镜像。

FROM debian:stretch-slim

RUN groupadd -r redis && useradd -r -g redis redis

ENV GOSU_VERSION 1.10
RUN set -ex; \
	\
	fetchDeps=" \
		ca-certificates \
		dirmngr \
		gnupg \
		wget \
	"; \
	apt-get update; \
	apt-get install -y --no-install-recommends $fetchDeps; \
	rm -rf /var/lib/apt/lists/*; \
	\
	dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
	wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
	wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
	export GNUPGHOME="$(mktemp -d)"; \
	gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
	gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
	gpgconf --kill all; \
	rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
	chmod +x /usr/local/bin/gosu; \
	gosu nobody true; \
	\
	apt-get purge -y --auto-remove $fetchDeps

ENV REDIS_VERSION 3.2.12
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-3.2.12.tar.gz
ENV REDIS_DOWNLOAD_SHA 98c4254ae1be4e452aa7884245471501c9aa657993e0318d88f048093e7f88fd

RUN set -ex; \
	\
	buildDeps=' \
		wget \
		\
		gcc \
		libc6-dev \
		make \
	'; \
	apt-get update; \
	apt-get install -y $buildDeps --no-install-recommends; \
	rm -rf /var/lib/apt/lists/*; \
	\
	wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
	echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
	mkdir -p /usr/src/redis; \
	tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
	rm redis.tar.gz; \
	\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
	sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
	grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
	\
	make -C /usr/src/redis -j "$(nproc)"; \
	make -C /usr/src/redis install; \
	\
	rm -r /usr/src/redis; \
	\
	apt-get purge -y --auto-remove $buildDeps

RUN mkdir /data && chown redis:redis /data
VOLUME /data
WORKDIR /data

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]
复制代码

B、Dockerfile 结构

当调用构建命令让 Docker 通过 Dockerfile 构建镜像时,Docker 会逐一按顺序解析 Dockerfile 中的指令,并根据它们不同的含义执行不同的操作。

可以将 Dockerfile 的指令简单分为五大类:

  • 基础指令 - 用于定义新镜像的基础和性质
  • 控制指令 - 指导镜像构建的核心部分,用于描述镜像在构建过程中需要执行的命令
  • 引入指令 - 用于将外部文件直接引入到构建镜像内部
  • 执行指令 - 为基于镜像所创建的容器,指定在启动时需要执行的脚本或命令
  • 配置指令 - 通过配置指令对其网络、用户等内容进行配置\

三、常见 Dockerfile 指令

以下常见的 Dockerfile 指令,基本包含常用的 90%功能。

A、FROM

通常来说,不会从零开始搭建一个镜像,而是会选择一个已经存在的镜像作为新镜像的基础。

在 Dockerfile 里,通过 FROM 指令指定一个基础镜像,之后所有的指令都是基于这个镜像所展开的。在镜像构建的过程中,Docker 也会先获取到这个给出的基础镜像,再从这个镜像上进行构建操作。

FROM 指令支持三种形式:

FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]
复制代码

选择一个基础镜像是构建新镜像的根本,则 Dockerfile 中的第一条指令必须是 FROM 指令,因为没有了基础镜像,一切构建过程都无法开展。在 Dockerfile 中可以多次出现 FROM 指令,当 FROM 第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的内容合并到此刻构建镜像的内容里。

B、RUN

镜像的构建虽然是按照指令执行的,但指令只是引导,最终大部分内容还是控制台中对程序发出的命令,而 RUN 指令就是用于向控制台发送命令的指令。

在 RUN 指令之后,直接拼接上需要执行的命令,在构建时,Docker 就会执行这些命令,并将它们对文件系统的修改记录下来,形成镜像的变化。

RUN <command>
RUN ["executable", "param1", "param2"]
复制代码

RUN 指令支持****换行,如果单行的长度过长,可以对内容进行切割,方便阅读。

C、ENTRYPOINT 和 CMD

基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINT 和 CMD 实现的。

ENTRYPOINT ["executable", "param1", "param2"]
ENTRYPOINT command param1 param2

CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
复制代码

ENTRYPOINT 指令和 CMD 指令的用法近似,都是给出需要执行的命令,并且它们都可以为空,或者说是不在 Dockerfile 里指出。

当 ENTRYPOINT 与 CMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。

D、EXPOSE

由于构建镜像时更了解镜像中应用程序的逻辑,也更加清楚它需要接收和处理来自哪些端口的请求,所以在镜像中定义端口暴露显然是更合理的做法。

通过 EXPOSE 指令就可以为镜像指定要暴露的端口。

EXPOSE <port> [<port>/<protocol>...]
复制代码

通过 EXPOSE 指令配置了镜像的端口暴露定义,基于这个镜像所创建的容器,在被其他容器通过**--link **选项连接时,就能够直接允许来自其他容器对这些端口的访问了。

E、VOLUME

在一些程序里,需要持久化一些数据,比如数据库中存储数据的文件夹就需要单独处理,可以通过数据卷来处理这些问题。

使用数据卷需要在创建容器时通过**-v **选项来定义,而有时候由于镜像的使用者对镜像了解程度不高,会漏掉数据卷的创建,从而引起不必要的麻烦。

制作镜像的人是最清楚镜像中程序工作的各项流程的,所以制作人来定义数据卷也是最合适的。在 Dockerfile 里,通过了 VOLUME 指令来定义基于此镜像的容器所自动建立的数据卷。

VOLUME ["/data"]
复制代码

在 VOLUME 指令中定义的目录,在基于新镜像创建容器时,会自动建立为数据卷,不需要再单独使用**-v **选项来配置。

F、COPY 和 ADD

制作新的镜像的时候,可能需要将一些软件配置、程序代码、执行脚本等直接导入到镜像内的文件系统里,使用 COPY 或 ADD 指令能够直接从宿主机的文件系统里拷贝内容到镜像里的文件系统中。

COPY [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] <src>... <dest>

COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
复制代码

COPY 与 ADD 指令的定义方式完全一样,两者的区别主要在于 ADD 能够支持使用网络端的 URL 地址作为 src 源,并且在源文件被识别为压缩包时,自动进行解压,而 COPY 没有这两个能力。

虽然看上去 COPY 能力稍弱,但对于那些不希望源文件被解压或没有网络请求的场景,COPY 更为简单。

四、构建镜像

编写 Dockerfile 后,通过** docker build **命令构建镜像。

docker build ./webapp
复制代码

docker build 的参数为目录路径(本地路径或 URL 路径),该目录会作为构建的环境目录,例如,使用 COPY 或是 ADD 拷贝文件到构建的新镜像时,会以这个目录作为基础目录。

默认情况下,docker build 也会从这个目录下寻找名为 Dockerfile 的文件,将它作为 Dockerfile 内容的来源。如果 Dockerfile 文件路径不在这个目录下,或者有另外的文件名,可以通过**-f **选项单独给出 Dockerfile 文件的路径。

docker build -t webapp:latest -f ./webapp/a.Dockerfile ./webapp
复制代码

在构建时带上**-t **选项,用它来指定新生成镜像的名称。

docker build -t webapp:latest ./webapp
复制代码

A、构建中使用变量

在实际编写 Dockerfile 时,与搭建环境相关的指令占有大部分比例的指令。搭建程序所需运行环境时,难免涉及到一些可变量,例如依赖软件的版本,编译的参数等等。

可以直接将这些数据写入到 Dockerfile 中,但是这些可变量会经常调整,在 Dockerfile 里,可以用 ARG 指令来建立一个参数变量,可以在构建时通过构建指令传入这个参数变量,并且在 Dockerfile 里使用它。

例如,希望通过参数变量控制 Dockerfile 中某个程序的版本,在构建时安装我们指定版本的软件,可以通过 ARG 定义的参数作为占位符,替换版本定义的部分。

FROM debian:stretch-slim

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
复制代码

以上例子中,将 Tomcat 的版本号通过 ARG 指令定义为参数变量,在调用下载 Tomcat 包时,使用变量替换掉下载地址中的版本号。通过这样的定义,就可以在不对 Dockerfile 进行大幅修改的前提下,轻松实现对 Tomcat 版本的切换并重新构建镜像了。

如果需要通过这个 Dockerfile 文件构建 Tomcat 镜像,可以在构建时通过** docker build --build-arg **选项来设置参数变量。

docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat
复制代码

B、环境变量

环境变量也是用来定义参数的东西,与 ARG 指令相类似,环境变量的定义是通过 ENV 指令来完成的。

FROM debian:stretch-slim

ENV TOMCAT_MAJOR 8
ENV TOMCAT_VERSION 8.0.53

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"
复制代码

环境变量的使用方法与参数变量一样,都是能够直接替换指令参数中的内容。

与参数变量只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器。环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。

另一个不同点是,环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写的,所以如果要修改环境变量的值,需要到 Dockerfile 修改。不过即使这样,只要我们将 ENV 定义放在 Dockerfile 前部容易查找的地方,其依然可以很快的帮助切换镜像环境中的一些内容。

由于环境变量在容器运行时依然有效,所以运行容器时还可以对其进行覆盖,在创建容器时使用**-e 或是--env **选项,可以对环境变量的值进行修改或定义新的环境变量。

docker run -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7
复制代码

这种用法在开发中非常常见,也正是因为这种允许运行时配置的方法存在,环境变量和定义它的 ENV 指令,是更常使用的指令,会优先选择它们来实现对变量的操作。

通过 ENV 指令和 ARG 指令所定义的参数,在使用时都是采用**$ + NAME **这种形式来占位的,所以它们之间的定义就存在冲突的可能性。对于这种场景,ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量,即使它们定时的顺序是相反的。

C、合并命令

上文中 Redis 镜像的 Dockerfile 中,RUN 指令里聚合了大量的代码。

事实上,以下两种写法对于搭建的环境来说是没有太大区别的。

RUN apt-get update; \
    apt-get install -y --no-install-recommends $fetchDeps; \
    rm -rf /var/lib/apt/lists/*;

RUN apt-get update
RUN apt-get install -y --no-install-recommends $fetchDeps
RUN rm -rf /var/lib/apt/lists/*
复制代码

看似连续的镜像构建过程,其实是由多个小段组成。每当一条能够形成对文件系统改动的指令在被执行前,Docker 先会基于上条命令的结果启动一个容器,在容器中运行这条指令的内容,之后将结果打包成一个镜像层,如此反复,最终形成镜像。

2.jpg

基于这个原理,绝大多数镜像会将命令合并到一条指令中,这种做法不但减少了镜像层的数量,也减少了镜像构建过程中反复创建容器的次数,提高了镜像构建的速度。

D、构建缓存

Docker 在镜像构建的过程中,还支持缓存策略来提高镜像的构建速度。

由于镜像是多个指令所创建的镜像层组合而得,如果判断新编译的镜像层与已经存在的镜像层未发生变化,则完全可以直接利用之前构建的结果,而不需要再执行这条构建指令,这就是镜像构建缓存的原理。

基于这个原则,在条件允许的前提下,更建议将不容易发生变化的搭建过程放到 Dockerfile 的前部,充分利用构建缓存提高镜像构建的速度。另外,指令的合并也不宜过度,而是将易变和不易变的过程拆分,分别放到不同的指令里。

在另外一些时候,不希望 Docker 在构建镜像时使用构建缓存,这时可以通过**--no-cache **选项来禁用它。

docker build --no-cache ./webapp
复制代码

五、临摹案例

编写 Dockerfile,阅读和思考前人的作品是必不可少的。

Docker官方提供的 Docker Hub 是 Docker 镜像的中央仓库,它除了镜像丰富之外,带来的另一项好处就是其大部分镜像都是能够直接提供 Dockerfile 文件,可以进行学习。

最后给大家分享一份Docker入门到实战的学习文档,免费分享,需要的点链接领取zhuanlan.zhihu.com/p/396653935

文章转自: zhuanlan.zhihu.com/p/57335983

文章分类
后端
文章标签