Dockerfile是一个文本格式的配置文件,用户可以使用Dockerfile来快速创建自定义镜像。
本章首先介绍Dockerfile典型的基本结构和它支持的众多指令,并具体讲解通过这些指令来编写定制镜像的Dockerfile,以及如何生成镜像。最后介绍使用Dockerfile的一些最佳实践经验。
8.1 基本结构
下在是Docker Hub 上两个热门镜像的Dockerfile的例子,可以帮助读者对Dockerfile结构有个基本的认识:
第一个例子是在debian:stretch基础镜像上安装Nginx环境,从而创建一个新的nginx 镜像
FROM debian:stretch-slim
LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"
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 image_creator@docker.com
该信息会写入生成镜像的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,供大家学习参考。