「Docker」- 构建镜像

  CREATED BY JENKINSBOT

构建镜像的两种方法

方法一、使用 docker commit 命令(不推荐)

类似于版本控制系统的提交命令,比如 git commit 命令。

创建容器 -> 修改 -> 提交

使用docker commit命令提交修改过的容器。该命令只提交与之前差异的部分。

还可以使用 -a 选项指定作者,选项 -m 指定提交信息。

使用 docker inspect 命令查看镜像信息。

访问 docker commit 查看 docker commit 命令的所有选项。

方法二、使用 Dockerfile 构建镜像(推荐)

因为通过 Dockerfile 构建镜像具有可重复性、透明性、幂等性,因此推荐使用 Dockerfile 来构建镜像。

新镜像是基于已有进行构建的。

Dockerfile 使用 DSL(Domain Specific Lanaguage)

如何使用 Dockerfile 构建镜像

第一步、编写 Dockerfile 文件

简单 Dockerfile 示例:

FROM ubuntu:14.04
MAINTAINER James Turnbull "jam@example.com"
RUN apt-get update && apt-get install -y nginx
RUN echo "Hi, I am in your container. " > /usr/share/nginx/html/index.html
EXPOSE 80

Dockerfile 由指令和参数组成,每个指令为大写字母,后跟参数,按照从上到下的顺序执行。

每个指令都会创建一个新的镜像层,并进行提交。Dockerfile 的执行流程如下:
1)从基础镜像(使用 FROM 指定)运行一个容器;
2)执行 Dockerfile 中的一条指令,对容器进行修改;
3)在指令结束后,执行提交动作,提交一个新的镜像层;
4)基于刚才提交的镜像层,运行一个新的容器;
5)回到 2) 直到所有指令执行完成;

如果Dockerfile执行失败了,我们也会得到一个可以的镜像,然后可以在这个镜像的基础上进行调试。

默认情况下,命令在/bin/sh -c中执行,如果在不支持shell的平台上、或希望使用exec执行,则可以使用RUN命令的另一种格式:

RUN [ "apt-get", "install", "-y", "nginx" ]

使用数组来传递参数。

指令EXPOSE用于告诉Docker该”容器内的应用程序将会使用哪些端口“,但这并不意味着可以自动访问任意容器运行中服务的端口(这里是80)。出于安全原因,需要用户使用docker run运行容器时来指定打开的端口。

第二步、执行构建命令

需要创建一个目录,这个目录称为构建环境,在 Docker 中称之为 上下文,构建时该目录的内容会上传到 Docker 守护进程,这样 Docker 守护进程就能直接访问用户想在镜像中存储的任何代码、文件、数据。

在上下文目录中执行 docker build 命令后,docker 或读取 Dockerfile 文件,然后执行其中的指令。可以使用-t选项指定仓库、名称、TAG信息。

命令类似于:docker build -t "jam/static_web:v1" .

最后的点(.)指出当前目录为上下文目录。如果未指定标签(这里是v1),则默认的为latest标签。

也可以使用Git仓库的源地址来指定Dockerfile的位置:docker build -t "jam/static_web:v1" git@github.com:jam/static_web

还可以使用-f选项指定Dockerfile的位置,用于处理Dockerfile文件名非Dockerfile的情况。但是-f选项指定的文件必须位于「构建上下文目录」中:docker build -t "jam/static_web:v1" -f /path/to/dockerfile-v1 .

构建的每一步都会提交,在构建结束后,最终会返回一个镜像 ID。

第三步、查看新镜像的信息(可选)

再次使用 docker image 命令来查看镜像。

如果想深入查看镜像是如何构建的,可以使用 docker history 命令查看镜像的构建历史。

常见问题汇总

在指令失败时,我们需要如何处理?

通常,在构建失败时,会产生错误日志,我们根据日志进行调试修改 Dockerfile 即可。

此外,我们还可以,基于上个成功执行的命令所产生的镜像 ID,来创建一个容器,然后运行出错的命令进行调试。在找到错误原因后,进行处理,然后重新构建。

构建产生的镜像非常大,如何排查原因?

可以使用 docker history 命令查看镜像个构建历史。如下示例,我们发现导致镜像增大的指令:

# docker history 0xa0000/proxy_exporter:0.0.5
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
6e5fb0db3577        4 hours ago         /bin/sh -c #(nop)  CMD ["/bin/sh" "-c" "\"py…   0B
40a3b4bd0dc9        4 hours ago         /bin/sh -c #(nop)  ENV PORT=1923                0B
f926695ca75f        4 hours ago         /bin/sh -c pip3 install -r requirements.txt     7.08MB
006c03ee97e5        4 hours ago         /bin/sh -c #(nop) ADD multi:d3ed25191265c52e…   19.1kB
a9c8c4ce9fcf        4 hours ago         /bin/sh -c #(nop) ADD file:6eade3ef219d74a8b…   14.2MB
8a7453e4653c        4 hours ago         /bin/sh -c apt-get update && apt-get install…   403MB
b96ea74726a8        4 hours ago         /bin/sh -c #(nop) WORKDIR /config               0B
56def654ec22        7 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           7 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           7 weeks ago         /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B
<missing>           7 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           7 weeks ago         /bin/sh -c #(nop) ADD file:4974bb5483c392fb5…   63.2MB

Dockerfile 与构建缓存

Docker 的构建过程非常聪明,它会将”每一步的构建过程都会将结果提交为镜像“,它会将之前的镜像层作为缓存。

假如第 4 步构建失败,在调整 Dockerfile 重新构建之后,Docker 会直接从第 4 步开始。如果之前的步骤发生了变化,则会从发生变化的位置开始。

可以使用 docker build --no-cache 选项来跳过构建缓存。

示例:缓存 pip install -r requirement.txt 构建

在最开始时,每次修改当前目录,都会导致 pip install 重新执行:

FROM ubuntu:14.04

ENV WORKDIR="/srv/web"

COPY . ${WORKDIR}
RUN pip install -r requirement.txt

# 因为当前目录发生变化,导致 COPY 缓存失效

我们调整步骤,提前复制 requirements.txt 文件,以利用缓存:

FROM ubuntu:14.04

ENV WORKDIR="/srv/web"

COPY requirements.txt ${WORKDIR}/requirements.txt
RUN apt-get -qq update

COPY . ${WORKDIR}

#07 基于构建缓存的 Dockerfile 模板

「构建缓存」的一个好处是可以实现一个简单的「Dockerfile模板」:

FROM ubuntu:14.04
MAINTAINER James Thrnbull "jam@exampl.com"
ENV REFRESHED_AT 2014-07-01
RUN apt-get -qq update

对于上面的Dockerfile,如果我们要更新apt缓存,可以修改 REFRESHED_AT 变量,然后Docker就会从修改行开始执行,从而更新了APT缓存。实质上是,使用一个可修改的指令来控制是否重新执行该指令后面的某些指令。

#09 从新镜像启动容器

可以从新镜像中启动一个容器,检查容器是否可以正常工作。比如:

docker rn -d -p --name static_web jamtur01/static_web nginx -g "daemon off;"

在上面的命令中,我们基于该才构建的镜像,jamtur01/static_web,启动了一个名为static_web的容器。使用-d选项,告诉Docker在后台运行容器,选项适用于”需要长时间运行的守护进程“。也指定了容器要运行的nginx -g "daemon off;"命令。

上面的命令还是用到了-p选项,用于控制Docker运行时应该公开那些网络端口给外部(宿主机)。运行容器是,有两种方式在宿主机上分配端口:

	* 随机:在Docker宿主机上随机选择一个在32768-61000范围内的端口号来映射到容器的80端口上。
	* 特定:在Docker宿主机上指定一个具体的端口号来映射到容器的80端口上。

上面的命令打开了一个随机的端口,然后映射到容器的80端口上。

可以使用dokcer port命令,或者使用docker ps -l命令查看容器的端口映射情况。

选项-p可以指定”绑定到宿主机的特定端口“:-p 80:80

或者其他端口,比如-p 8080:80,将80端口绑定到宿主机的8080端口。

也可以使用类似的方式,绑定到宿主机器的特定网口或IP地址上:-p 127.0.0.1:8080:80

可以绑定到宿主机器指定IP地址的随机端口上:-p 127.0.0.1::80(可以使用docker port或docker inspect查看具体绑定到了哪个端口)

绑定端口时,使用/udp后缀来指定UDP端口。

可以使用-P参数,对外公开EXPOSE指令公开的所有端口,它们会随机绑定到宿主机上的端口。

常用 Dockerfile 指令

Dockerfile reference

CMD

Dockerfile reference | Docker Documentation / CMD

指定容器启动时运行的命令,和 docker run 命令非常相似。但是,在 docker run 中指定的命令会覆盖 CMD 指定的命令。

该指令有三种形式:
1)CMD [“executable”,”param1″,”param2″] (exec form, this is the preferred form)
2)CMD [“param1″,”param2”] (as default parameters to ENTRYPOINT)
3)CMD command param1 param2 (shell form)

该指令可以使用多次,但是只有最后一个会生效

例如CMD ["/bin/bash"],或者传递参数CMD ["/bin/bash", "-l"]

最好通过数组的形式传递命令和参数。如果直接指定命令,则Docker会在命令前添加/bin/sh -c来执行命令,容易产生意料之外的行为。

ENTRYPOINT

Dockerfile reference | Docker Documentation / ENTRYPOINT

# The exec form
ENTRYPOINT ["executable", "param1", "param2"]

# The shell form
ENTRYPOINT command param1 param2

该命令与 RUN 命令非常相似,但是该命令不会被 docker run 所覆盖,而在 docekr run 中指定的命令,会被当作参数传递给 ENTRYPOINT 指定的命令。除此之外,二者的语法、数组形式是一样的。

能够将 CMD 与 ENTRYPOINT 组合使用:

ENTRYPINT ["/usr/sbin/nginx"]
CMD ["-h"]

当执行 docker run 时,如果未指定命令,则容器启动时执行 /usr/sbin/nignx -h 命令。如果指定了命令,如 -g “daemon off;” ,则会覆盖 CMD 指令,执行指定的命令,这里是 /usr/sbin/nginx -h 命令。

如果要覆盖 ENTRYPOINT 命令,需要使用 –entrypoint 选项。

RUN

Best practices for writing Dockerfiles | Docker Documentation

当使用 apt-get 指令时,官方最佳实践建议采用如下形式:

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    && rm -rf /var/lib/apt/lists/*

3.WORKDIR

切换工作目录。在Docker中切换到指定工作目录,然后执行命令。使用-w选项覆盖工作目录。

4.ENV

在构建过程中定义环境变量。从Docker 1.4开始,一条指令可以同时定义多个环境变量。

例如:RVM RVM_PATH=/home/rvm RVM_ARCHFLAGS="-arch i386"

可以在其他指令中直接使用这些环境变量:WORKDIR $RVM_PATH

如果有必要可以使用反斜线进行转义。

这些变量会保存到容器中,即在容器中执行env时,可以看到这些环境变量。

也可以通过docker run -e选项传递变量,但是这些变量只会在运行时有效。

5.USER

指定以什么用户去运行。可以使UID和GID的组合:

	**USER user**
	**USER user:group**
	**USER uid**
	**USER uid:gid**
	**USER user:gid**
	**USER uid:gorup**

比如:USER nginx

可以使用docker run -u来覆盖。如果未设置USER指令,则默认为root用户。

6.VOLUME

向基于镜像创建的容器中添加卷。卷可以存在于一个或多个容器内的特定的目录。该目录可以绕过联合文件系统,并提供向下共享数据、数据持久化。

	卷可以在容器间共享和重用;
	一个容器可以不是必须和其他容器共享卷;
	对卷的修改立即生效;
	对卷的修改不会对更新镜像产生影响;
	卷会一个存在知道没有任何容器再使用它;

通过该功能可以将数据添加到镜像,而不是提交到镜像,并且可以在多个容器之间共享这些内容。可以通过该功能测试容器、内部程序源码,管理日志等等。

例如:VOLUME ["/opt/project", "/data"]

docker cp | Docker Documentation」「Use volumes | Docker Documentation

7.ADD

将文件复制到镜像中,文件只能位于构建上下文中。

例如:ADD foo.txt /opt/application/foo.txt

「源文件」文件、目录、URL。另外,如果「源文件」是归档文件(gzip, bzip2, xz)则会自动解压目的目录中;解压时,如果目的文件已存在,则不会复制,即不会覆盖。另外,从URL中读取的归档文件不会自动解压。

Docker通过「地址参数末尾的字符」来判断文件是「目录」还是「文件」。

如果目的目录不存在,Docker会递归创建。新文件和目录的权限默认为0755,UID=0,GID=0;

另外ADD会使构建缓存无效,之后的指令不会使用构建缓存。

8.COPY

复制构建上下文中的文件和目录到容器中,与ADD不同的是,COPY不会做额外的操作。

文件或目录的UID=0,GID=0;文件和目录的元数据也会被复制到容器中的文件和目录上。同样,如果目录不存在,Docker会递归创建。

9.LABEL

用于添加元数据,已键值对的形式存在:

	LABEL version="1.0"
	LABEL lacation="Somewhere" type="Data Center" role="Web Server"

一条指令中可以存在多个元数据,键值对形式,元数据间以空格分隔。推荐多条元数据放在一个LABEL指令中,以防止创建过多的镜像层。通过docker inspect来查看元数据。

10.STOPSIGNAL

指定docker stop时发送给容器的信号。这个信号必须是内核中合法的信号值,例如9,或者信号名SIGNAME,例如SIGKILL。

11.ARG

在Docker 1.9中引入该指令,该指令指定在docker build命令运行时,传递给构建运行时的变量,只需要在构建时指定–build-arg标志即可。用户只能在构建时指定定义过的参数。

	ARG build
	ARG webapp_user=root

上面的示例中,第二条ARG设置了默认值root。如果未使用–build-arg指定该参数,则使用默认值。

例如,docker build --build-arg build=1234 -t jamti/webapp .,设置build参数的值为1234,而webapp_user将使用默认值root。

!!!不要使用该功能传递密钥等类型的参数。

在Docker中预定义了一组ARG变量,可以在构建时直接使用,无需再定义一次。「Predefined ARGs

12.ONBUILD

为镜像添加触发器。当该镜像会用做其他镜像的基础镜像时,该镜像中触发器会被执行。

触发器在构建过程中执行当初添加的指令,可以将其视为实在FROM之后执行的

	ONBUILD ADD . /app/src
	ONBUILD RUN cd /app/src && make

在构建镜像时,这些触发器会被加入到镜像中。使用docker inspect来查看加入镜像中的触发器。

通过这种方式可以为应用程序进行一些特定的配置和设置构建信息。

!!!触发器只能被继承一次!!!有些命令不能放在触发器中,这是为了防止产生递归调用。

相关连接

编写Dockerfile的最佳实践:「Best practices for writing Dockerfiles

参考文献

Dockerfile reference
Dockerfile COPY vs ADD: key differences and best practices