Dockerfile是一个文本格式的配置文件,用户可以使用Dockerfile来快速创建自定义镜像。
本章首先介绍Dockerfile典型的基本结构和它支持的众多指令,并具体讲解通过这些指令来编写定制镜像的Dockerfile,以及如何生成镜像。最后介绍使用Dockerfile的一些最佳实践经验。
8.1 基本结构
下在是Docker Hub 上两个热门镜像的Dockerfile的例子,可以帮助读者对Dockerfile结构有个基本的认识:
第一个例子是在debian:stretch基础镜像上安装Nginx环境,从而创建一个新的nginx 镜像
FROM debian:stretch-slim LABEL maintainer="NGINX Docker Maintainers <[email protected]>" ENV NGINX_VERSION 1.13.12-1~stretch ENV NJS_VERSION 1.13.12.0.2.0-1~stretch RUN set -x \ && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \ && \ NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ found=''; \ for server in \ ha.pool.sks-keyservers.net \ hkp://keyserver.ubuntu.com:80 \ hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $NGINX_GPGKEY from $server"; \ apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \ done; \ test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ && dpkgArch="$(dpkg --print-architecture)" \ && nginxPackages=" \ nginx=${NGINX_VERSION} \ nginx-module-xslt=${NGINX_VERSION} \ nginx-module-geoip=${NGINX_VERSION} \ nginx-module-image-filter=${NGINX_VERSION} \ nginx-module-njs=${NJS_VERSION} \ " \ && case "$dpkgArch" in \ amd64|i386) \ # arches officialy built by upstream echo "deb https://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list.d/nginx.list \ && apt-get update \ ;; \ *) \ # we're on an architecture upstream doesn't officially build for # let's build binaries from the published source packages echo "deb-src https://nginx.org/packages/mainline/debian/ stretch nginx" >> /etc/apt/sources.list.d/nginx.list \ \ # new directory for storing sources and .deb files && tempDir="$(mktemp -d)" \ && chmod 777 "$tempDir" \ # (777 to ensure APT's "_apt" user can access it too) \ # save list of currently-installed packages so build dependencies can be cleanly removed later && savedAptMark="$(apt-mark showmanual)" \ \ # build .deb files from upstream's source packages (which are verified by apt-get) && apt-get update \ && apt-get build-dep -y $nginxPackages \ && ( \ cd "$tempDir" \ && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \ apt-get source --compile $nginxPackages \ ) \ # we don't remove APT lists here because they get re-downloaded and removed later \ # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies # (which is done after we install the built packages so we don't have to redownload any overlapping dependencies) && apt-mark showmanual | xargs apt-mark auto > /dev/null \ && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \ \ # create a temporary local APT repo to install from (so that dependency resolution can be handled by APT, as it should be) && ls -lAFh "$tempDir" \ && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \ && grep '^Package: ' "$tempDir/Packages" \ && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \ # work around the following APT issue by using "Acquire::GzipIndexes=false" (overriding "/etc/apt/apt.conf.d/docker-gzip-indexes") # Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) # ... # E: Failed to fetch store:/var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages Could not open file /var/lib/apt/lists/partial/_tmp_tmp.ODWljpQfkE_._Packages - open (13: Permission denied) && apt-get -o Acquire::GzipIndexes=false update \ ;; \ esac \ \ && apt-get install --no-install-recommends --no-install-suggests -y \ $nginxPackages \ gettext-base \ && apt-get remove --purge --auto-remove -y apt-transport-https ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \ \ # if we have leftovers from building, let's purge them (including extra, unnecessary build deps) && if [ -n "$tempDir" ]; then \ apt-get purge -y --auto-remove \ && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ fi # forward request and error logs to docker log collector RUN ln -sf /dev/stdout /var/log/nginx/access.log \ && ln -sf /dev/stderr /var/log/nginx/error.log EXPOSE 80 STOPSIGNAL SIGTERM CMD ["nginx", "-g", "daemon off;"]
第二个例子是基于buildpack-deps:stretch-scm基础镜像,安装Golang相关环境,制作一个GO语言的运行环境镜像:
FROM buildpack-deps:stretch-scm # gcc for cgo RUN apt-get update && apt-get install -y --no-install-recommends \ g++ \ gcc \ libc6-dev \ make \ pkg-config \ && rm -rf /var/lib/apt/lists/* ENV GOLANG_VERSION 1.10.2 RUN set -eux; \ \ # this "case" statement is generated via "update.sh" dpkgArch="$(dpkg --print-architecture)"; \ case "${dpkgArch##*-}" in \ amd64) goRelArch='linux-amd64'; goRelSha256='4b677d698c65370afa33757b6954ade60347aaca310ea92a63ed717d7cb0c2ff' ;; \ armhf) goRelArch='linux-armv6l'; goRelSha256='529a16b531d4561572db6ba9d357215b58a1953437a63e76dc0c597be9e25dd2' ;; \ arm64) goRelArch='linux-arm64'; goRelSha256='d6af66c71b12d63c754d5bf49c3007dc1c9821eb1a945118bfd5a539a327c4c8' ;; \ i386) goRelArch='linux-386'; goRelSha256='ea4caddf76b86ed5d101a61bc9a273be5b24d81f0567270bb4d5beaaded9b567' ;; \ ppc64el) goRelArch='linux-ppc64le'; goRelSha256='f0748502c90e9784b6368937f1d157913d18acdae72ac75add50e5c0c9efc85c' ;; \ s390x) goRelArch='linux-s390x'; goRelSha256='2266b7ebdbca13c21a1f6039c9f6887cd2c01617d1e2716ff4595307a0da1d46' ;; \ *) goRelArch='src'; goRelSha256='6264609c6b9cd8ed8e02ca84605d727ce1898d74efa79841660b2e3e985a98bd'; \ echo >&2; echo >&2 "warning: current architecture ($dpkgArch) does not have a corresponding Go binary release; will be building from source"; echo >&2 ;; \ esac; \ \ url="https://golang.org/dl/go${GOLANG_VERSION}.${goRelArch}.tar.gz"; \ wget -O go.tgz "$url"; \ echo "${goRelSha256} *go.tgz" | sha256sum -c -; \ tar -C /usr/local -xzf go.tgz; \ rm go.tgz; \ \ if [ "$goRelArch" = 'src' ]; then \ echo >&2; \ echo >&2 'error: UNIMPLEMENTED'; \ echo >&2 'TODO install golang-any from jessie-backports for GOROOT_BOOTSTRAP (and uninstall after build)'; \ echo >&2; \ exit 1; \ fi; \ \ export PATH="/usr/local/go/bin:$PATH"; \ go version ENV GOPATH /go ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH" WORKDIR $GOPATH
下面讲解Dockerfile中各种指令的应用。
8.2 指令说明
指令的一般格式为INSTRUCTION argument,指令包括FROM、MAINTAINER、RUn等,参见表8-1
表8-1 Dokcerfile指令说明
下面分别进行介绍。
1.FROM
指定所创建镜像的基础镜像,如果本地不存在,则默认会去Docker Hub下载指定镜像。
格式为 FROM<image>,或FROM<image><tag>,或FROM<image>@<digest>。
2. MAINTAINER
指定维护者信息,格式为MAINTAINER<name>。例如:
MAINTAINER [email protected]
该信息会写入生成镜像的Author属性域中。
3. RUN
运行指定命令。
格式为RUN<command>或RUN["executable","parame1","param2"]。注意,后一个指令会被解析为Json数组,因此必须用双引号。
前者默认将在shell终端中运行命令,即/bin/sh -c;后者则使用exec执行,不会启动shell环境。
指定使用其他终端类型可以通过第二种方式来实现,例如RUN["/bin/bash","-c","ehco hello"]。
每条RUN指令将在当前镜像的基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用\来换行。例如:
RUN set -x \ && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \ && \ NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ found=''; \
4.CMD
CMD指令用来指定启动容器时默认执行的命令。它支持三种格式:
CMD ["executable","param1","param2"]使用exec执行,是推荐使用的方式;
CMD command param1 param2 在/bin/sh中执行,提供给需要交互的应用;
CMD["param1","parma2"]提供给ENTRYPOINT的默认参数。
每个Dockerfile只能有一条CMD命令。如果指定了多条命令,只有最后一条会被执行。
如果用户启动容器时手动指定了运行的命令(作为run的参数),则会覆盖掉CMD指定的命令。
5.LABEL
LABEL指令用来指定生成镜像的元数据标签信息。
格式为LABEL <key>=<value> <key>=<value> <key>=<value>...。
例如:
LABEL version="1.0" LABEL description="This text illustrates \ that label -v alues can span multiple lines."
6. EXPOSE
声明镜像内服务所监听端口。
格式为EXPOSE <port>[<port>...]。
例如:
EXPOSE 22 80 443
注意,该指令只是起到声明作用,并不会自动完成端口映射。
在启动容器时需要使用-P,Docker主机会自动分配一个宿主主机的临时端口转发到指定端口;使用-p,则可以具体指定哪个个宿主主机的本地端口会映射过来。
7.ENV
指定环境变量,在镜像生成过程中会被后续RUN指令使用,在镜像启动的容器中也会存在。
格式为ENV<key><value> 或ENV<key>=<value>...
例如:
ENV NGINX_VERSION 1.13.12-1~stretch ENV NJS_VERSION 1.13.12.0.2.0-1~stretch
8. ADD
该命令将复制指定的<src>路径下的内容到容器中的<dest>路径下。
格式为ADD<src><dest>。
其中<src>可以是Dockerfile所在目录的一个相对路径(文件或目录),也可以是一个URL,还可以是一个tar文件(如果为tar文件,会自动解压到<dest>路径下)。<dest>可以是镜像内的绝对路径,或者相对于工作目录(WORKDIR)的相对路径。
路径支持正则格式,例如:
ADD *.c /code/
9.COPY
格式为COPY<src><dest>。
复制本地主机的<src>(为Dockerfile所在目录的相对路径、文件或目录)下的内容到镜像中的<dest>下。目标路径不存在时,会自动创建。
路径同样支持正则格式。
当使用本地目录为源目录时,推荐使用COPY。
10.ENTRYPOINT
指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有传入值作为该命令的参数。
支持两种格式:
ENTRYPOINT ["executable","param1","param2"] (exec调用执行);
ENTRYPOINT command param1 param2 (shell中执行)。
此时,CMD指令指定值将作为根命令的参数。
每个Dockerfile中只能有一个ENTRYPOINT,当指定多个时,只有最后一个有效。
在运行时,可以被--entrypoint参数覆盖掉,如docker run --entrypoint。
11.VOLUME
创建一个数据卷挂载点。
格式为VOLUME["/data"]
可以从本地主机或其它容器挂载数据卷,一般用户来存放数据库和需要保存的数据等。
12.USER
指定运行容器时的用户名或UID,后续的RUN等指令也会使用指定的用户身份。
RUN groupadd -r postgres && useradd -r -g postgres postgres
格式人USER daemon。
当服务不需要管理员权限时,可以通过命令指定运行用户,并且可以在之前创建所需要的用户。例如:
要临时获取管理员权限可以使用gosu或sudo。
13.WORKDIR
为后续的RUN、 CMD和ENTRYPOINT指令配置工作目录。
格式为WORKDIR /path /to / wrokdir。
可以使用多个WORKDIR指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如:
WORKDIR /a WORKDIR b WORKDIR c RUN pwd
则最终路径为/a/b/c。
14.ARG
指定一些镜像内使用的参数(例如版本号信息等),这些参数在执行docker build命令时才以--build-arg<varname>=<value>格式传入。
格式为ARG<name>[default valus>]。
则可以用docker build --build arg <name>=<value>,来指定参数值。
15.ONBUILD
配置当所创建的镜像作为其他镜像的基础镜像时,所执行的创建操作指令。
格式为ONBUILD[INSTRUCTION]。
例如,Dockerfile使用如下的内容创建了镜像image-A:
[...] ONBUILD ADD ./app/src ONBUILD RUN /usr/local/bin/python-build --dir /ap/src [...]
如果基于image-A 创建新的镜像时,新的Dockerfile中使用FROM image-A 指定基础镜像,会自动执行ON-BUILD指令内容,等价与在后面添加了两条指令:
FROM image-A #Automatically run the following ADD ./app/src RUN /usr/local/bin/python-build --dir /app/src
使用ONBUILD指令的镜像,推荐在标签中注明,例如ruby:1.9-onbuild。
16. STOPSIGNAL
指定所创建镜像启动的容器接收退出的信息值。例如:
STOPSIGNAL signal
17.HEALTHCHECK
配置所启动容器如何进行键康检查(如何判断健康与否),自Docker 1.12开始。
格式有两种:
HEALTHCHECK [OPTIONS] CMD command:根据所执行命令返回值是否为0来判断;
HEALTHCHECK NONE:禁止基础镜像中的健康检查。
-- interval=DURATION(默认为:30s):过多久检查一次;
--timeout=DURATION(默认为30s):每次检查等待结果的超时;
--retries=N(默认为:3):如果失败了,重试几次才最终确定失败。
18. SHELL
指定其他命令使用shell时的默认shell类型。
SHELL ["executable","parametres"]
注意:
对于Window系统,建议在Docker开头添加#escapt=`来指定转义信息。
8.3 创建镜像
编写完成Dockerfile之后,可以通过docker build命令创建镜像
基本的格式为docker build[选项][内容路径]该命令将读取指定路径下,(包括子目录)的Dockerfile,并将该路径下的所有内容发送给Docker服务端,由服务端来创建镜像。因此除非生成镜像需要,否则一般建议放置Dockerfile的目录为空目录。
例如,指定Dockerfilel所在路径为/tmp/docker_builder/,并且希望生成镜像标签为build_repo/first_image,可以使用下面的命令:
$docker build -t build_repo/first_image /tmp/docker_builder/
8.4 使用.dockerignore文件
8.5 最佳实践
精简镜像用途:
选用合适的基础镜像
提供足够清晰的命令注释和维护者信息:
正确使用版本号
减少镜像层数
及时删除临时文件和缓存文件
提高生成速度
调整合理的指令顺序
减少外部源的干扰
8.6 本章小结
本章主要介绍了围绕Dockerfile文件构建镜像的过程,包括Dockerfile的基本结构、所支持的内部指令,使用它创建镜像的基本过程,以及合理构建镜像的最佳实践。在使用Dockerfile构建镜像的过程中,读者会体会到Docker "一点修改代替大量更新"的灵活之处。
当然,编写一个高质量的Dockerfile并不是一件容易的事情,需要一定时间的学习和实践,在本书的第二部分中,笔者也给出了大量热门镜像的Dockerfile,供大家学习参考。