目录

学习 Docker(4)-构建镜像

镜像是生成容器的模版,镜像应该是无状态的,不包含具体的配置。

构建镜像有两种方式:从已有的容器构建,从 Dockerfile 构建。

从容器构建

基于容器创建一个新镜像。

1
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

该操作会将容器的文件更改或设置提交到新镜像中,但不包含容器中挂载的数据卷。

  • —-author -a 指定镜像作者,例如 “name [email protected]”;
  • -—message -m commit 信息;
  • —-change -c 创建镜像时,应用 Dockerfile 指令;
  • --pause -p 构建镜像时停止容器运行。
1
docker commit -c "EXPOSE 80" c3f279d17e0a  svendowideit/testimage:version4

从 Dockerfile 构建

构建命令

1
docker build [OPTIONS] PATH | URL | -

docker build 命令从 Dockerfile 和 “上下文” 构建 Docker 镜像。上下文提供构建过程中引用的文件。

选项:

  • -t 指定镜像名称和 tag,可以有多个 -t 参数;
  • -f 指定 Dockerfile,默认为 上下文根目录下的 Dockerfile 文件。
  • --build-arg 指定构建时的参数,比如设置代理 --build-arg HTTP_PROXY=http://10.20.30.2:1234

如下是一个使用参数的例子

1
docker build -f Dockerfile.debug -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 .

使用 Dockerfile.debug 文件作为 Dockerfile 文件,使用当前目录作为上下文目录,生成两个镜像,名称为 whenry/fedora-jboss,tag 分别为 latestv2.1

传参:

  • 传入 URLPATH 时,构建的上下文是位于 PATHURL 中的文件集。
  • 传入 - 表示从 STDIN 中读取 Dockerfile,此时没有上下文。

如果传入的参数是 URL,该 URL 可以指向三种资源:Git 仓库、tarball 打包文件和纯文本文件。

Git 仓库

URL 参数指向 Git 仓库时,仓库为构建上下文。系统递归地获取仓库和子模块内容,但不会保留提交历史记录。

如果 URL 中包含片段(fragment),则会使用 git pull -—recursive 获取仓库的所有分支和子模块。

在 fragment 中,还可以指定上下文的配置,用 : 分隔。: 之前为分支、tag 或 remote ref,: 之后为仓库的一个子目录,该子目录将用作构建上下文。

例如下面的命令将使用 container 分支的 docker 目录作为上下文:

1
docker build https://github.com/docker/rootfs.git#container:docker

参考如下:

构建语法后缀使用的提交使用构建上下文
myrepo.gitrefs/heads/master/
myrepo.git#mytagrefs/tags/mytag/
myrepo.git#mybranchrefs/heads/mybranch/
myrepo.git#pull/42/headrefs/pull/42/head/
myrepo.git#:myfolderrefs/heads/master/myfolder
myrepo.git#master:myfolderrefs/heads/master/myfolder
myrepo.git#mytag:myfolderrefs/tags/mytag/myfolder
myrepo.git#mybranch:myfolderrefs/heads/mybranch/myfolder

Tarball 打包文件

如果将 URL 指向远程的 tarball 压缩文件,文件将会在运行 Docker 守护程序的主机被下载(该主机不一定是发出构建命令的主机),解压缩后作为构建时的上下文。

Tarball 上下文必须是符合标准 tar UNIX 格式的 tar 档案,并且可以使用 xzbzip2gzipidentity(无压缩)格式中的任何一种进行压缩。

如下,Docker 守护程序将获取 context.tar.gz 并将其用作构建上下文。

1
docker build http://server/context.tar.gz

或者通过 - 从 STDIN 读取:

1
docker build - < context.tar.gz

文本文件

通过 URL 指定一个远端的 Dockerfile 文件:

1
docker build http://server/Dockerfile

或者通过 - 和 STDIN 指定一个本地的 Dockerfile 文件:

1
docker build - < Dockerfile

这两种情况下,没有上下文可以使用,-f--file 选项会被忽略。

Dockerfile 指令

Dockerfile 中的注释以 # 开头。

总览

  • FROM 从哪个基础镜像派生,必须是 Dockerfile 文件的第一条非注释指令;
  • LABEL 生成镜像的一些信息,如 namedescriptionmaintainer 等等;
  • RUN 构建时执行的命令,用于生成环境、复制文件等,RUN 命令可以有多条;
  • EXPOSE 随机映射端口;
  • CMDENTRYPOINT 容器运行时执行的命令,只能有一条;
  • ADDCOPY 复制文件或目录到 Docker 镜像中;
  • VOLUME 随机映射数据卷;
  • WORKDIR 指定运行时工作目录,通常使用绝对路径;
  • ENV 设置执行时环境变量;
  • USER 指定运行时用户,格式为 <name|uid>[:<group|gid>]
  • ONBUILD 构建触发器,当该镜像被作为基础镜像构建其他镜像时执行;
  • MAINTAINER 不推荐使用,请使用 LABEL 替代;

shell 模式和 exec 模式

RUN、CMD 和 ENTRYPOINT 命令都有两种格式。以 RUN 举例:

shell 模式,形如 RUN <command>,实际在构建过程中执行时命令为 /bin/sh -c <command>,例如:

1
RUN echo "hello"

exec 模式,形如 RUN ["executable", "param1", "param2"]

1
RUN ["/bin/bash", "-c", "echo", "hello"]

区别在于:

  1. 运行时的环境变量(CMD 和 ENTRYPOINT),shell 模式能够访问,exec 模式不能访问,因为 exec 模式直接执行命令不通过 shell;
  2. 在 Dockerfile 中使用 ENV 定义的环境变量,shell 模式可以使用 ${ENV_NAME} 或者 $ENV_NAME 的形式访问,exec 模式不能访问;
  3. 运行时(CMD 和 ENTRYPOINT),shell 模式命令不是 PID1 进程,exec 模式命令是 PID1 进程。

另外,exec 模式的指令会作为 JSON 列表被读取,因此要使用双引号而不是单引号。

RUN、CMD 和 ENTRYPOINT

RUN 命令在构建过程中执行。

CMD 和 ENTRYPOINT 指定了容器运行时的默认指令。

CMD 和 ENTRYPOINT 命令单独使用时:

  • CMD 和 ENTRYPOINT 单独使用时只能有一条(如果有多条指令,只有最后一条会生效)。
  • 如果容器运行时指定了运行指令,CMD 指令会被覆盖,但 ENTRYPOINT 指令会被执行;
  • 如果容器运行时指定了 --entrypoint 参数,ENTRYPOINT 指令会被覆盖;

CMD 和 ENTRYPOINT 命令配合使用时(都要在 exec 模式下):

  • ENTRYPOINT 指令作为运行指令,CMD 指令作为运行指令的参数;
  • 容器运行时可以替换 CMD 中指定的运行参数;

举个例子,以下 Dockerfile 文件构建的镜像

1
2
3
FROM ubuntu:latest
ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]

执行时

1
2
3
4
$ docker run [image]
Hello world
$ docker run [image] docker
Hello docker

ADD 和 COPY

都可以复制文件或目录到 Docker 镜像中。

共同点:

  • 源路径最好是相对路径(相对构建上下文目录),只能是上下文目录的子目录或文件
  • 目标路径可以是 Docker 镜像中的绝对路径,或相对于 WORKDIR 的相对路径;

不同点:

  • ADD 可以接受本地的 tar 压缩文档并解压复制,或者远程的目录。

如果只是单纯复制文件,使用 COPY 就可以;

VOLUME

指定数据卷挂载点,容器运行时可以在该挂载点上挂载数据卷。

格式可以是 JSON 字符串数组,或者以空格分隔的普通字符串,以下是等价的:

1
2
VOLUME ["/var/log/", "/var/db"]
VOLUME /var/log /var/db

构建过程

使用 Dockerfile 的构建过程

  1. 从基础镜像运行一个容器;
  2. 执行 Dockerfile 中的下一条指令,对容器作出修改;
  3. 执行类似 commit 的操作,提交一个新的镜像;
  4. 基于刚提交的镜像运行一个新容器;
  5. 重复步骤 2~4,直到 Dockerfile 中的所有指令执行完成;
  6. 删除中间层容器。

从以上过程可以看出,Dockerfile 构建过程中会生成中间层镜像并被保留。

使用 docker history <image name> 可以查看一个镜像的构建过程:

1
2
3
4
5
6
7
8
$ docker history ubuntu
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
735f80812f90        2 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           2 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           2 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$…   2.76kB
<missing>           2 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           2 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           2 weeks ago         /bin/sh -c #(nop) ADD file:4bb62bb0587406855…   83.5MB

构建缓存

Docker 在构建时,如果曾经执行过相同的操作,会调用之前构建时生成的镜像缓存,从而加快构建速度。

但一些情况下,我们不希望使用构建缓存,比如构建时执行 apt update 等指令,我们希望获取的是最新的软件包版本。此时有两种方法可以避免使用缓存:

  • build 时使用 —-no-cache 参数;
  • 在 Dockerfile 中使用 ENV REFRESH_DATE XXX 来标识缓存刷新时间,在该时间后进行构建不会使用上次的缓存。

提高构建效率

  1. 合理使用构建缓存;
  2. 仅向上下文目录中添加 Dockerfile 文件和构建所需的文件;
  3. 使用 .dockerignore 排除上下文目录中的文件和目录。