概述

我们可以把刚才的对容器的所有操作命令都记录到一个文件里,就像写更脚本程序。

之后用 docker build 命令以此文件为基础制作一个镜像,并会自动提交到本地仓库。

这样的话镜像的构建会变的透明化,对镜像的维护起来也更加简单,只修改这个文件即可。

同时分享也更加简单快捷,因为只要分享这个文件即可。

Dokcerfile 是一个普通的文本文件,文件名一般叫 Dockerfile

其中包含了一系列的指令(Instruction), 每一条指令都会构建一层,就是描述该层是如何创建的。

小试牛刀

编辑 Dockerfile 文件

cat Dockerfile

详细内容

FROM    centos:6.7
MAINTAINER    author "author@xxxx.com"

RUN     /bin/echo 'root:123456' |chpasswd
RUN     useradd baiyp
RUN     /bin/echo 'baiyp:123456' |chpasswd
RUN     /bin/echo -e "LANG=\"en_US.UTF-8\"" >/etc/default/local
EXPOSE  22
EXPOSE  80
CMD     /usr/sbin/sshd -D
指令介绍
  • FORM 定义一个基础镜像
  • MAINTAINER 指定作者
  • LABEL 定义一些元数据信息,比如作者、版本、关于镜像的描述信息
  • RUN 行命令行的命令
  • EXPOSE 暴露容器端口
  • CMD exec模式

构建镜像

命令语法格式

docker bulid -t 仓库名/镜像名:tag .

构建镜像
docker build -t centos:test .

打印信息

Sending build context to Docker daemon  35.84kB
Step 1/9 : FROM    centos:6.7
 ---> 9f1de3c6ad53
Step 2/9 : MAINTAINER    author "author@xxxx.com"
 ---> Running in d8598710b287
Removing intermediate container d8598710b287
 ---> 29575eb94c47
Step 3/9 : RUN     /bin/echo 'root:123456' |chpasswd
 ---> Running in 14609bc56811
Removing intermediate container 14609bc56811
 ---> bf3c9f2538e2
Step 4/9 : RUN     useradd baiyp
 ---> Running in 1b8edada3749
Removing intermediate container 1b8edada3749
 ---> eec3097321fc
Step 5/9 : RUN     /bin/echo 'baiyp:123456' |chpasswd
 ---> Running in ecda90b62c1d
Removing intermediate container ecda90b62c1d
 ---> 8c50a85f9811
Step 6/9 : RUN     /bin/echo -e "LANG=\"en_US.UTF-8\"" >/etc/default/local
 ---> Running in 07420e079c63
Removing intermediate container 07420e079c63
 ---> 06cb8a6fa648
Step 7/9 : EXPOSE  22
 ---> Running in a6168fe82b15
Removing intermediate container a6168fe82b15
 ---> 0560cb59d461
Step 8/9 : EXPOSE  80
 ---> Running in 4222e011e4c7
Removing intermediate container 4222e011e4c7
 ---> 04528ff76564
Step 9/9 : CMD     /usr/sbin/sshd -D
 ---> Running in 9e4f2ffb0066
Removing intermediate container 9e4f2ffb0066
 ---> a1969a6700f2
Successfully built a1969a6700f2		# 表示构建成功
Successfully tagged centos:test		# TAG 标签
上下文(context)

这个 . 表示当前目录,这实际上是在指定上下文的目录是当前目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

最佳实战

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的

Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.qf 参数指定某个文件作为 Dockerfile

一般大家习惯性的会使用默认的文件名 Dockerfile,以及会将其置于镜像构建上下文目录中。

$ tree .
.
├── Dockerfile.qf
└── test
    ├── a.txt
    ├── b.txt
    └── test.qf
[root@localhost dockerfile_qf_ignore]# cat Dockerfile.qf
FROM alpine
COPY ./test.qf /root/test.qf
$ docker build -f ../Dockerfile.qf -t alpine:test.qf .
$ docker run -it alpine:test.qf /bin/sh

Dockerfile 详解

FROM 指令

主要作用是指定一个镜像作为构建自定义镜像的基础镜像,在这个基础镜像之上进行修改定制。

这个指令是 Dockerfile 中的必备指令,同时也必须是第一条指令。

Docker Store 上有很多高质量的官方镜像,可以直接作为我们的基础镜像。

作为服务类的,如 Nginx Mongo

用于开发的, 如 Python golang

操作系统类, 如 Centos ubuntu

除了一些现有的镜像,Docker 还有一个特殊的镜像 scratch

这个镜像是虚拟的,表示空白镜像

FORM scratch
...

这以为着这将不以任何镜像为基础镜像。

可以把可执行的二进制文件复制到镜像中直接执行,容器本身就是和宿主机共享 Linux内核的。

使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

制作 Hello world
安装 gcc

在任意一台 Linux 机器上,安装 gcc

查看有没有安装

rpm -qa gcc glibc-static

没有的话,进行安装即可

yum install gcc glibc-static
编辑 C 源代码文件
cat hello.c

查看文件内容

#include <stdio.h>

int main()
{
   printf("Hello, Yangge! \n");
   return 0;
}
编译源码
gcc --static hello.c -o hello

编译好后,测试一下

$ ls
hello  hello.c
# hello   是我们编译好的二进制文件
# hello.c 是 C 的源码文件
$ ./hello
Hello Yangge     # 输出结果,说明编译成功
编辑 Dockerfile

在有 hello 二进制的文件目录下,编译 Dockerfile 文件,内容如下:

$ ls
Dockerfile  hello  hello.c
$ cat Dockerfile
FROM scratch
ADD hello /
CMD ["/hello"]
  • ADD 是把当前目录下的 hello 文档拷贝到 容器中的根目录下
  • CMD 执行根目录下的 hello 文件
构建新的镜像

注意命令的最后有个 .

$ docker build -t xiguatian/hello-yangge .
Sending build context to Docker daemon  868.9kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : ADD hello /
 ---> 63ed3c13b7fd
Step 3/3 : CMD ["/hello"]
 ---> Running in a26622affa68
Removing intermediate container a26622affa68
 ---> dfadd4a86525
Successfully built dfadd4a86525
Successfully tagged xiguatian/hello-yange:latest
查看本地仓库验证
$ docker image ls xiguatian/hello-yange
REPOSITORY             TAG    IMAGE ID     CREATED         SIZE
xiguatian/hello-yange latest dfadd4a86525  3 minutes ago   865kB

可以看到镜像很小

利用新的镜像运行一个容器
$ docker run --rm qf/hello-yange
Hello Yangge
关于 Alpine

Alpine Linux是一款独立的非商业性通用Linux发行版,专为那些了解安全性,简单性和资源效率的高级用户而设计。

Alpine Linux围绕musl libc和busybox构建。这使得它比传统的GNU / Linux发行版更小,更节省资源。一个容器需要不超过8 MB的空间,而对磁盘的最小安装需要大约130 MB的存储空间。您不仅可以获得完整的Linux环境,还可以从存储库中选择大量的软件包。

二进制软件包被缩减和拆分,使您可以更好地控制安装的内容,从而使您的环境尽可能地小巧高效。

简单

Alpine Linux是一个非常简单的发行版,它会尽量避免使用。它使用自己的包管理器,称为apk,OpenRC init系统,脚本驱动的设置,就是这样!这为您提供了一个简单,清晰的Linux环境,没有任何噪音。然后,您可以添加项目所需的软件包,因此无论是构建家用PVR还是iSCSI存储控制器,薄型邮件服务器容器或坚如磐石的嵌入式交换机,其他都不会挡道。

安全

Alpine Linux的设计考虑到了安全性。内核修补了一个非官方的grsecurity / PaX端口,并且所有的用户级二进制文件被编译为位置独立可执行文件(PIE)和堆栈粉碎保护。这些主动安全功能可防止利用整个类别的零日等漏洞。

LABEL 指令

LABEL 指令用于指定一个镜像的描述信息

LABEL指令将元数据添加到镜像中。

LABEL是一个键值对。

要在LABEL值中包含空格,请像在命令行解析中一样使用引号和续行符\

示例

几个用法示例

LABEL maintainer="yangge@qf.com"
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

一个镜像可以有多个LABEL标签。您可以在一行中指定多个标签。并且目前的版本不再会影响到镜像的大小了。

但是仍然可以把他们写在一行或用反斜线进行续航

LABEL multi.label1="value1" multi.label2="value2" other="value3"
--------------------------------------------------------------------------------------
LABEL multi.label1="value1" \
      multi.label2="value2" \
      other="value3"

有继承关系的镜像,标签也会有面向对象编程中继承的关系和特性

要查看镜像的 LABEL 信息,请使用该docker inspect命令。

ENV 指令

用于设置环境变量

语法

语法格式有两种

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...
示例

推荐的方式,易读

ENV VERSION=1.0 DEBUG=on \
 NAME="Happy Feet"

不推荐都方式,不易读

ENV NODE_VERSION 7.2.0
其他指令使用
RUN echo $NODE_VERSION
...

下列指令可以支持环境变量: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

RUN 指令

RUN 指令是在容器内执行 shell 命令,默认会是用 /bin/sh -c 的方式执行。

语法

语法格式有两种

  • RUN <command>shell形式,该命令在shell中运行)
  • RUN ["executable", "param1", "param2"]exec形式)

之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

注意:Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

注意事项

在使用 shell 方式,尽量多的使用续行符\

RUN /bin/bash -c 'source $HOME/.bashrc; \
 echo $HOME'

写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

错误示例

注意当使用 exec 方式时,需要明确指定 shell 路径,否则变量可能不会生效

FROM centos
ENV name="yangge"
RUN ["/bin/echo", "$name"]

img

可以看到 $name 被作为普通的字符串输出了,因为 $name 是 shell 中的用法,而这里里并没有 使用到 shell

正确做法
FROM alpine
ENV name="yangge"
RUN ["/bin/sh", "-c", "/bin/echo $name"]

注意: exec的方式下,列表中的内容会被解析为JSON数组,这意味着您必须在单词周围使用双引号(“) 而非单引号(’)。

img

CMD 指令

Dockerfile 中只能有一条CMD指令。如果列出多个,CMD 则只有最后一个CMD会生效。

CMD 主要目的是为运行容器时提供默认值

Docker 不是虚拟机,容器就是进程,CMD 指令就是用于指定默认的容器主进程的启动命令的。在启动(运行)一个容器时可以指定新的命令来替代镜像设置中的这个默认命令。

可以包含可执行文件,当然也可以省略。

语法

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可执行文件", "参数1", "参数2"...]

参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

注意事项

注意:不要混淆RUNCMDRUN实际上运行一个命令并提交结果; CMD在构建时不执行任何操作,但指定镜像的默认命令。

Docker 不是虚拟机,容器内没有后台服务的概念。

不要期望这样启动一个程序到后台:

CMD systemctl start nginx

这行被 Docker 理解为:

CMD ["sh" "-c" "systemctl start nginx"]

对于容器而言,其启动程序就是容器的应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

就像上面的示例中,主进程是 sh , 那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会使容器退出。

正确的做法是直接执行 nginx 这个可执行文件,并且关闭后台守护的方式,使程序在前台运行。

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 指令

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器的启动程序及参数。

ENTRYPOINT 在运行时也可以被替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

ENTRYPOINT 的格式和 RUN 指令格式一样,也分为 exec 格式和 shell 格式。

ENTRYPOINT和CMD区别

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,也就是实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

有了 CMD 后,为什么还要有 ENTRYPOINT 呢?

这种 <ENTRYPOINT> "<CMD>" 给我们带来了什么好处么?

示例

让我们来看几个场景。

场景一

让镜像变成像命令一样使用

FROM centos
RUN yum update \
    && yum install -y curl
CMD [ "curl", "-s", "http://ip.cn" ]

构建镜像后, 运行容器

$ docker run --rm centos-echo-ip-cmd

执行下面命令会报错

$ docker run --rm centos-echo-ip-cmd -i

我们可以看到报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 并不是添加在原来的 curl -s http://ip.cn 后面。

而是替换了原来的 CMD,变成了 CMD ["-i"],而 -i 根本不是命令,所以报了可执行文件找不到

所以应该使用 ENTRYPOINT 方式

FROM centos
RUN yum install -y curl
ENTRYPOINT ["curl", "-s", "http://ip.cn"]

再次构建镜像后

$ docker run --rm centos-echo-ip-entrypoint

运行容器

docker run --rm centos-echo-ip-entrypoint -i

这样的话, 最终的指令就变成 ENTRYPOINT ["curl", "-s", "http://ip.cn", "-i"]

场景二

应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

官方镜像 redis 中的示例:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中为 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINTdocker-entrypoint.sh 脚本。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    chown -R redis .
    exec gosu redis "$0" "$@"
fi

exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

注意: ENTRYPOINT 指令不会被 RUN 指令覆盖,而 CMD 指令会被 RUN 指令覆盖

WORKDIR 指令

用于声明当前的工作目录,以后各层的当前目录就被改为指定的目录。

语法

WORKDIR <工作目录路径>

如该目录不存在,WORKDIR 会帮你建立目录。

示例

再次强调!不要以为编写 Dockerfiel 是在写 shell 脚本。

错误示例

下面是一个错误示例

RUN cd /app
RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello

原因其实很简单,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。

两行 RUN 分别构建了并启动了各自全新的容器。

正确示例

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

FROM alpine
WORKDIR /a/b
RUN touch a_b_f.txt
WORKDIR /a
RUN touch a_f.txt

运行容器

$ docker run -it alpine:workdir /bin/sh
/a # ls
a_f.txt  b
/a # cd b
/a/b # ls
a_b_f.txt

COPY 指令

语法格式
  • COPY <源路径>... <目标路径>
  • COPY ["<源路径1>",... "<目标路径>"]

RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

参数解释

<目标路径> 可以是容器内的绝对路径,也可以是相对于 WORKDIR 指定的工作目录的相对路径。目标路径不需要事先创建,如果目录不存在会在复制文件前先被创建。

示例

COPY 指令将会从构建的上下文目录中,把源路径的文件或目录复制到新的一层的镜像内的 <目标路径> 位置。

COPY qf.json /usr/src/app/

注意下面是错误的

COPY qf.json /usr/src/app

这样会把 qf.json 拷贝成为 /usr/src/ 目录下的 app 文件

<源路径> 可以是多个,支持通配符,如:

COPY qf* /app/
COPY q?.txt /app/

使用 COPY 指令,源文件的各种元数据都会保留。

比如读、写、执行权限、文件变更时间等。

ADD 指令

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

支持自动解压缩,压缩格式支持: gzip, bzip2 以及 xz

官方推荐使用 COPY 进行文件的复制。

ADD 指定会使构建镜像时的缓存失效,导致构建镜像的速度很慢。

COPYADD 指令中选择的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD

复制ADD qf.tar.gz  /

USER 指令

USER 则是改变执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

这个用户必须是事先在容器内存在(建立好)的,否则无法切换。

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查指令

语法
  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

运行流程

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器后的状态变化会是下面的演变过程:

初始状态会为 starting

  1. HEALTHCHECK 指令检查成功后变为 healthy

  2. 如果连续一定次数失败,则会变为 unhealthy

    HEALTHCHECK 支持下列选项:

    • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
    • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
    • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
    • --start-period=<时长>: 容器的初始化实长,默认0秒,不计入健康检测时间内。

      CMD, ENTRYPOINT 一样,HEALTHCHECK 在 Dockerfile 中只可以出现一次,如果写了多个,只有最后一个生效。

  3. 后面的命令同样支持 shell 方式和 exec 方式。

  4. 命令的返回值决定了该次健康检查的成功与否:

    0:成功;1:失败。

示例

使用 curl 命令来判断 nginx 提供的 web 服务是否正常。

DockerfileHEALTHCHECK 可以这么写

FROM centos
RUN rpm -ivh \
http://mirrors.aliyun.com/epel/epel-release-latest-7.noarch.rpm &&  yum install nginx \
                curl -y
ADD index.html /usr/share/nginx/html/index.html
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs \
            http://localhost/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80

这里设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。

构建镜像后, 启动容器,并观察容器的状态变化

$ docker build -t ali_nginx .
$ docker run -d ali_nginx
$ docker ps
CONTAINER ID  IMAGE         COMMAND               CREATED     STATUS                            PORTS        NAMES
09a8b90b0f67  ali_nginx   "nginx -g 'daemon of…"   4 seconds ago       Up 3 seconds (health: starting)   80/tcp      vigorous_jang
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
09a8b90b0f67        ali_nginx           "nginx -g 'daemon of…"   19 seconds ago      Up 18 seconds (healthy)   80/tcp              vigorous_jang

利用元数据查看容器的健康状态

docker inspect --format '{{json .State.Health}}' vigorous_jang | python -m json.tool

ONBILUD 指令

ONBILUD 指令用于当其他 Dockerfile 以自己为基础镜像时将会运行的命令。

语法

ONBUILD <其它指令>

其他指令可以是: 比如 RUN, COPY 等。

基础应用场景

假如有两个项目 A 和 B,两个项目想分别有不同的文件

A 项目下的文件
$ tree A/
A/
├── a1.txt
└── a.txt

0 directories, 2 files
B 项目下的文件
$ tree B
B
├── b1.txt
├── b2.txt
└── b.txt

0 directories, 3 files
操作示例

现在任意的空目录下创建一个 Dockerfile

文件内容:

$ cat Dockerfile
FROM alpine
ONBUILD COPY . /root/

接着用这个 Dockerfile 来构建一个所有项目都要使用的一个基础镜像

镜像名字: alpine-base

$ docker build -t alpine-base .
Sending build context to Docker daemon  6.144kB
Step 1/2 : FROM alpine
 ---> 3fd9065eaf02
Step 2/2 : ONBUILD COPY . /root/
 ---> Running in 4d6fad2809be
Removing intermediate container 4d6fad2809be
 ---> 804bfc0b47be
Successfully built 804bfc0b47be
Successfully tagged alpine-base:latest

当使用这个镜像去运行容器的时候。查看 /root 目录下,可发现并没有任何东西,

说明 COPY . /root/ 并没有此次构建镜像的过程中去执行。

$ docker run --rm alpine-base:latest ls /root/
$

现在我们在使用刚才构建的镜像为项目 A 的基础镜像,来构建 A 项目的镜像

想看看目前 A 项目下的文件:

$ cd A
$ ls
a1.txt  a.txt

在项目的 A 目录下编写 Dockerfile 文件内容如下:

$ cat Dockerfile
FROM alpine-base:latest

是的只需要这一行即可

现在让我们来构建 A 项目的镜像

$ docker build -t alpine-a .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM alpine-base:latest
# Executing 1 build trigger
 ---> 5a003e1dc65f
Successfully built 5a003e1dc65f
Successfully tagged alpine-a:latest

接着运行以这个镜像alpine-a:latest为基础镜像而运行的容器中的 /root/ 目录下会有 A 项目目录下的所有文件:

$ docker run --rm alpine-a:latest ls /root/
Dockerfile
a.txt
a1.txt

B 项目的 Dockerfile 的内容:

$ ls
b1.txt  b2.txt  b.txt
[root@docker B]# cat Dockerfile
FROM alpine-base:latest

同样构建 B 项目的 镜像,运行容器后可以看到 /root/ 目录下会有 B 项目目录下的所有文件

$ docker build -t alpine-b .
Sending build context to Docker daemon  3.584kB
Step 1/1 : FROM alpine-base:latest
# Executing 1 build trigger
 ---> e66b6ee561a9
Successfully built e66b6ee561a9
Successfully tagged alpine-b:latest
[root@docker B]# docker run --rm alpine-b:latest ls /root/
Dockerfile
b.txt
b1.txt
b2.txt

可以看出,ONBUILD 指令后面内容会在,其他镜像以此镜像为基础镜像构建的时候执行。

高级应用场景

python 项目都有自己的依赖包,通常会放在项目根目录下的一个文件,这个文件名叫:requirements.txt

此文件可以通过如下命令得到:

$  pip3 freeze > requirements.txt

内容一般为:

$  head -3 requirement.txt
Django==1.11
PyMySQL==0.8.1

可以使用如下命令来安装这些项目的依赖模块。

pip3 install -r requirement.txt

现在假设公司有多个 python3 的项目,每个项目都有自己不同的依赖模块。需要为每个项目制定一个 Dockerfile 或者镜像吗?

操作示例

比如有两个项目: CMDB 和 SUPERMAN

下面我们使用 ONBUILD 指令来构建一个基础 python镜像,

之后两个项目可以不必修改原来的 Dockerfile 就可以部署自己的环境依赖包了。

CMDB 的 Dockerfile

CMDB

$ tree CMDB/
CMDB/
├── requirments.txt
$ cat CMDB/requirmants.txt
django==1.11
$ cat CMDB/run.py
import django
print(django.VERSION)

SUPERMAN

$ tree SUPERMAN/
SUPERMAN/
├── requirments.txt
└── run.py
$ cat SUPERMAN/requirmants.txt
django==1.11
$ cat SUPERMAN/run.py
import django
print(django.VERSION)

使用 ONBUILD 指令构建 Python 基础镜像

$ cat Dockerfile
FROM python
ONBUILD COPY . /opt/
ONBUILD RUN pip3 install -r /opt/requirments.txt
ONBUILD CMD ["python3", "/opt/run.py"]
[root@docker onbulid]# docker build -t python3-base .

之后分别在各自的项目目录下创建自己的 Dockerfile

CMDB 的 Dockerfile

FROM python3-base

SUPERMAN 的 Dockerfile

FROM python3-base

这样就可以很简单的实现不同的项目只需要创建一个同样内容的镜像,而会得到自己的环境了。

另外下面的是在 shell 中的执行 python 的命令:

FROM python
ONBUILD COPY ./requirement.txt /
ONBUILD RUN pip install -r /requirement.txt
ONBUILD CMD ["python", "-c" "import django;print(django.VERSION)"]

把这个构建成所有项目的基础镜像,名字为: python-onbuild:v1.0

$ docker build -t python-onbuild:v1.0 .

其他 python 项目再使用此镜像为基础镜像时,Dockerfile 中只需一行即可:

FROM python-onbuild:v1.0