hualinux 进阶 1.3: centos8 docker CE 入门及安装(二) 构建docker镜像

目录

一、相关docker镜像构建

二、使用docker commit构建(不推荐)

2.1 准备工作

2.2 运行容器并进行修改

2.3 构建新镜像

2.3.1 查看commit帮助

2.3.2 将容器保存为新的镜像

2.3.3 测试

 2.4 慎用docker commit

三、使用 Dockerfile 定制镜像(推荐)

3.1 说明

3.2 使用Dockerfile制作镜像

FROM表示指定基础镜像

RUN执行命令

其它Dockerfile指令

3.3 用docker build命令构建镜像

四、其它生成镜像的方法

4.1 从rootfs压缩包导入

4.2 docker save和docker load

附录一、Dockerfile指令详解

copy复制文件

ADD 更高级的复制文件

CMD 容器启动命令

ENTRYPOINT 入口点

ENV 设置环境变量

ARG 构建参数

VOLUME 定义匿名卷

EXPOSE 声明端口

WORKDIR 指定工作目录

USER 指定当前用户

HEALTHCHECK 健康检查

ONBUILD 为他人做嫁衣裳

参考文档


上一章《hualinux 进阶 1.2: centos8 docker CE 入门及安装(一)》我对docker进行了介绍、安装、一些基础操作,篇开始讲docker镜像制作

Docker 提供了两种构建镜像的方法:

  1. docker commit命令(不推荐)
  2. Dockerfile构建文件(推荐)

一、相关docker镜像构建

对于 Docker 用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的 Docker 官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。

使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为 Docker 的工程师知道如何更好的在容器中运行软件。

当然,某些情况下我们也不得不自己构建镜像,比如:

1.找不到现成的镜像,比如自己开发的应用程序。

2.需要在镜像中加入特定的功能,比如官方镜像几乎都不提供 ssh。

Docker 提供了两种构建镜像的方法:

  1. docker commit命令
  2. Dockerfile构建文件

 

二、使用docker commit构建(不推荐)

docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:

  1. 运行容器
  2. 修改容器
  3. 将容器保存为新的镜像(docker commit命令)

2.1 准备工作

我这里以上一章《hualinux 进阶 1.2: centos8 docker CE 入门及安装(一)》下载的nginx1.18镜像为例子,让主机的目录和配置文件映射到docker中,也有不少公司是直接把源代码打包进镜像中的,这样下载镜像就把源代码一起下载了,多省事!

为了方便操作,我在宿主机子上也安装一个nginx,这样方便修改

#1 nginx安装

#写入nginx配置文件
cat>/etc/yum.repos.d/nginx.repo<<EOF
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/\$releasever/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/\$releasever/\$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF

#安装nginx
cat /etc/yum.repos.d/nginx.repo 
dnf install nginx -y

刚才是1.18的 

#2 建立相关的文件和目录

宿主机上建立目录和创建index.html文件

#建立相关目录
mkdir -pv /disk1/www/hualinux.com

#写入index.html文件
cat>/disk1/www/hualinux.com/index.html<<EOF
<html>
<head>
  <title>首页</title>
  <meta charset="UTF-8">
</head>
<body>
<h3>欢迎来到 hualinux</h3>
</body>
</html>
EOF
#查看
cat /disk1/www/hualinux.com/index.html

#建立日志文件
mkdir -p /disk1/logs/nginx
chown nginx.nginx -R /disk1/logs/nginx
 

#3. 修改配置文件

cd /etc/nginx/conf.d/
cp default.conf default.conf.orig

修改default.conf为如下内容

[root@vm82 conf.d]# cat default.conf
server {
    listen       80;
    server_name  localhost hualinux.com www.hualinux.com;

    #charset koi8-r;
    access_log  /disk1/logs/nginx/hualinux.access.log  main;
    error_log  /disk1/logs/nginx/hualinux.err.log ;

    location / {
        root   /disk1/www/hualinux.com;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

#检查语法
nginx -t

cd ~

 

2.2 运行容器并进行修改

我这里模拟把公司生成的项目代码直接打进docker中,并保存为新镜像

#停止之前的容器
docker stop nginx
#新建立一个容器
docker run -p 80:80 --name nginx18 -d nginx:1.18
docker ps

#复制刚才上面弄好的配置文件

#查看docker cp命令的使用

[root@vm82 ~]# docker cp --help

Usage:	docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|-
	docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH

Copy files/folders between a container and the local filesystem

Use '-' as the source to read a tar archive from stdin
and extract it to a directory destination in a container.
Use '-' as the destination to stream a tar archive of a
container source to stdout.

Options:
  -a, --archive       Archive mode (copy all uid/gid information)
  -L, --follow-link   Always follow symbol link in SRC_PATH
[root@vm82 ~]# 
#复制代码目录进docker中
docker exec nginx18 mkdir -p /disk1/www
docker cp /disk1/www/hualinux.com nginx18:/disk1/www/

#建立日志目录
docker exec nginx18 mkdir -p /disk1/logs/nginx

#查看docker中的nginx配置文件是否与自己安装的一样
docker exec nginx18 cat /etc/nginx/conf.d/default.conf
#备份一下原default.conf配置文件
docker exec nginx18 cp /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.orig

#复制本地的配置文件进docker中
docker cp /etc/nginx/conf.d/default.conf nginx18:/etc/nginx/conf.d/default.conf
#查看是否生效
docker exec nginx18 cat /etc/nginx/conf.d/default.conf

#停止再开启,让它生效
#停止
docker stop nginx18
#开启
docker start nginx18

访问一下这个IP地址,发现改变了,下是我们需要的代码

 

2.3 构建新镜像

2.3.1 查看commit帮助

[root@vm82 ~]# docker commit -h
Flag shorthand -h has been deprecated, please use --help

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

Create a new image from a container's changes

Options:
  -a, --author string    Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")
  -c, --change list      Apply Dockerfile instruction to the created image
  -m, --message string   Commit message
  -p, --pause            Pause container during commit (default true)

2.3.2 将容器保存为新的镜像

[root@vm82 ~]# docker commit -m "nginx1.18 www.hualnux.com" nginx18  hua:nginx
sha256:0f8aa5f9b316ed32595b432b42ac92b4147571a396ebc030c1663869d5f76e63
[root@vm82 ~]# docker images hua
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
hua                 nginx               0f8aa5f9b316        About a minute ago   132MB

注解:

-m:为描述

nginx18:使用的容器名字

hua:nginx:仓库名为hua,tag为nginx,如果不写默认latest

2.3.3 测试

#停止刚才的nginx18

docker stop nginx18

#运行自己新制作的把代码放进去的镜像
docker run  --name hua-ng -p 82:80 -d hua:nginx
docker ps

打开浏览器,输入 http://IP:82/,我这里是http://192.168.3.82:82/

 2.4 慎用docker commit

使用 docker commit 命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。

我们刚才只是创建了一个很小的html文件,所以镜像大小变化小,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。

此外,使用 docker commit 意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体在操作的。

 镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit 制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。

docker commit 命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用docker commit定制镜像,定制行为应该使用Dockerfile来完成。

 

三、使用 Dockerfile 定制镜像(推荐)

3.1 说明

从刚才的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

还以之前定制nginx18 镜像为例,这次我们使用 Dockerfile 来定制

#删除hua-ng容器及刚刚建立的hua:nginx镜像
docker stop hua-ng
docker rm hua-ng
docker rmi hua:nginx
#查看nginx18容器对应的镜像,在这里是第一行的那个
[root@vm82 ~]# docker images nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               1.18                9fc56f7e4c11        3 weeks ago         132MB
nginx               latest              2622e6cca7eb        3 weeks ago         132MB

3.2 使用Dockerfile制作镜像

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile :

mkdir /disk1/hua-docker
cd /disk1/hua-docker/
cat>Dockerfile<<EOF
FROM nginx:1.18
RUN echo 'welcome to hua docker nginx' > /usr/share/nginx/html/index.html
EOF
cat Dockerfile

其中FROM接镜基础镜像,可以是官方远程仓库中的,也可以位于本地仓库。

格式为:  FROM <仓库>:<标签>

FROM表示指定基础镜像

         所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

 

RUN执行命令

         RUN 指令是用来执行命令行命令的。由于命令行的强大能力, RUN 指令在定制

镜像时是最常用的指令之一。其格式有两种:

1.shell 格式:

RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的Dockrfile 中的 RUN 指令就是这种格式。

RUN echo 'welcome to hua docker nginx' > /usr/share/nginx/html/index.html

2.exec 格式:

RUN ["可执行文件", "参数1", "参数2"] ,这更像是函数调用中的格式。

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

FROM debian:jessie

RUN apt-get update

RUN apt-get install -y gcc libc6-dev make

RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"

RUN mkdir -p /usr/src/redis

RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1

RUN make -C /usr/src/redis

RUN make -C /usr/src/redis install

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

上面的 Dockerfile 正确的写法应该是这样:

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \

&& apt-get update \

&& apt-get install -y $buildDeps \

&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \

&& mkdir -p /usr/src/redis \

&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \

&& make -C /usr/src/redis \

&& make -C /usr/src/redis install \

&& rm -rf /var/lib/apt/lists/* \

&& rm redis.tar.gz \

&& rm -r /usr/src/redis \

&& apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令删除了为了编译构建

所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

 

其它Dockerfile指令

         请看附录一

 

3.3 用docker build命令构建镜像

#查看帮助
docker build --help
#构造一个仓库为hua,镜像tab为nginx,
cd /disk1/hua-docker/
docker build -t hua:nginx .
#如果上面命令没问题,查看一下是否在本地生成镜像
docker images hua
#返回到home目录
cd ~

注:最后有一个小圆点,不要忘记了!点不能用Dockerfile代替,也不能用/disk1/hua-docker/Dockerfile,小圆点是上下文路径(都是相对路径)

否则运行会报错,结果如下:

#运行hua:nignx镜像看一下效果

[root@vm82 ~]# docker run --name hua-ng -p 81:80 -d hua:nginx
002bd0df417b76128b5fcc377cb391080851403ac4d6c9a15a09a12897c47680
[root@vm82 ~]# docker ps|grep hua-ng
002bd0df417b        hua:nginx           "nginx -g 'daemon of…"   3 seconds ago       Up Less than a second   0.0.0.0:81->80/tcp   hua-ng

#打开浏览器查看一下效果,

PS:

        上面中“.” 实际上是在指定上下文的目录, docker build 命令会将该目录下的内容打包交给Docker 引擎以帮助构建镜像。

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

         那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为Dockerfile 的文件作为 Dockerfile。

         这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为Dockerfile ,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile 。

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

 

四、其它生成镜像的方法

除了标准的使用 Dockerfile 生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其它方法用以生成镜像。

4.1 从rootfs压缩包导入

格式: docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]

压缩包可以是本地文件、远程 Web 文件,甚至是从标准输入中得到。压缩包将会在镜像 / 目录展开,并直接作为镜像第一层提交。比如我们想要创建一个 OpenVZ 的 Ubuntu 14.04 模板的镜像:

$ docker import \
http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \
openvz/ubuntu:14.04
Downloading from http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz
sha256:f477a6e18e989839d25223f301ef738b69621c4877600ae6467c4e528
9822a79B/78.42 MB

这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz 文件,并且作为根文件系统展开导入,并保存为镜像 openvz/ubuntu:14.04。如果我们查看其历史的话,会看到描述中有导入的文件链接:

$ docker history openvz/ubuntu:14.04
IMAGE CREATED  CREATED BY 		 SIZE  	  COMMENT
f477a6e18e98   About a minute ago  214.9 MB  Imported from http://download.openvz.org/templa
te/precreated/ubuntu-14.04-x86_64-minimal.tar.gz

4.2 docker save和docker load

Docker还提供了docker load和docker save命令,用以将镜像保存为一个tar文件,然后传输到另一个位置上,再加载进来。这是在没有DockerRegistry时的做法,现在已经不推荐,镜像迁移应该直接使用Docker Registry,无论是直接使用Docker Hub还是使用内网私有Registry都可以。

保存镜像

使用 docker save 命令可以将镜像保存为归档文件。比如我们希望保存这个nginx镜像。

docker images nginx:latest

docker save nginx:latest | gzip > nginx-latest.tar.gz

然后我们将nginx-latest.tar.gz文件复制到了到了另一个机器上,可以用下

面这个命令加载镜像:

docker load -I nginx-latest.tar.gz

如果我们结合这两个命令以及 ssh 甚至 pv 的话,利用 Linux 强大的管道,我们可以写一个命令完成从一个机器将镜像迁移到另一个机器,并且带进度条的功能:

docker save <镜像名> | bzip2 | pv | ssh <用户名>@<主机名> 'cat | docker load'

 

附录一、Dockerfile指令详解

官网链接:https://docs.docker.com/engine/reference/builder/#dockerfile-examples

copy复制文件

格式:

COPY <源路径>... <目标路径>

COPY ["<源路径1>",... "<目标路径>"]

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

 

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

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的filepath.Match 规则,如:

COPY hom* /mydir/

COPY hom?.txt /mydir/

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

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

ADD 更高级的复制文件

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

在 Docker 官方的最佳实践文档中要求,尽可能的使用 COPY ,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是, ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD 。

CMD 容器启动命令

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

shell 格式: CMD <命令>

exec 格式: CMD ["可执行文件", "参数1", "参数2"...]

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

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。 CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu镜像默认的 CMD 是 /bin/bash ,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash 。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release 。这就是用 cat /etc/os-release命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON数组,因此一定要使用双引号 " ,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念

一些初学者将 CMD 写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

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

而使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD ["sh", "-c", "service nginx start"] ,因此主进程实际上是 sh 。那么当service nginx start 命令结束后, sh 也就结束了, sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

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

 

ENTRYPOINT 入口点

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

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。 ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过docker run 的参数 --entrypoint 来指定。

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

<ENTRYPOINT> "<CMD>"

 

ENV 设置环境变量

格式有两种:

ENV <key> <value>

ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN ,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \

NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和Shell 下的行为是一致的定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方node 镜像 Dockerfile 中,就有类似这样的代码:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \

&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \

&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \

&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \

&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \

&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \

&& ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION ,其后的 RUN 这层里,多次使用$NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可, Dockerfile 构建维护变得更轻松了。

 

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

ADD 、 COPY 、 ENV 、 EXPOSE 、 LABEL 、 USER 、 WORKDIR 、 VOLUME 、STOPSIGNAL 、 ONBUILD 。

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG 构建参数

格式: ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是, ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。

 

VOLUME 定义匿名卷

格式为:

VOLUME ["<路径1>", "<路径2>"...]

VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

 

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...] 

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P时,会自动随机映射 EXPOSE 的端口。

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个Docker 引擎参数 --icc=false ,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。 -p ,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

 

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径> 。

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,该目录需要已经存在, WORKDIR 并不会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app

RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello 。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dokerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

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

 

USER 指定当前用户

格式: USER <用户名>

USER指令和WORKDIR相似,都是改变环境状态并影响以后的层。WORKDIR是改变工作目录,USER则是改变之后层的执行RUN , CMD以及ENTRYPOINT这类命令的身份。

当然,和WORKDIR一样,USER只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis

USER redis

RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo ,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu ,可以从其项目网站看到进一步的信息:https://github.com/tianon/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 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

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

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为starting ,在 HEALTHCHECK 指令检查成功后变为 healthy ,如果连续一定次数失败,则会变为 unhealthy。

HEALTHCHECK 支持下列选项:

--interval=<间隔> :两次健康检查的间隔,默认为 30 秒;

--timeout=<时长> :健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;

--retries=<次数> :当连续失败指定次数后,则将容器状态视为unhealthy ,默认 3 次。

 

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

在 HEALTHCHECK [选项] CMD后面的命令,格式和 ENTRYPOINT 一样,分为shell 格式,和exec格式。命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2 :保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的HEALTHCHECK 可以这么写:

FROM nginx

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=5s --timeout=3s \

CMD curl -fs http://localhost/ || exit 1

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

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker ps 看到最初的状态为 (health:starting) :

$ docker ps

CONTAINER ID IMAGE    COMMAND               REATED         STATUS                         PORTS        NAMES

03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web

在等待几秒钟后,再次 docker ps ,就会看到健康状态变化为了 (healthy) :

$ docker ps

CONTAINER ID  IMAGE     COMMAND                 CREATED         STATUS                    PORTS      NAMES

03e28eb00bd0  myweb:v1  "nginx -g 'daemon off"  18 seconds ago   Up 16 seconds (healthy)  80/tcp, 443/tcp web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy) 。

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr )都会被存储于健康状态里,可以用 docker inspect 来查看。

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

{

"FailingStreak": 0,

"Log": [

{

"End": "2016-11-25T14:35:37.940957051Z",

"ExitCode": 0,

"Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",

"Start": "2016-11-25T14:35:37.780192565Z"

}

],

"Status": "healthy"

}

 

 

ONBUILD 为他人做嫁衣裳

格式: ONBUILD <其它指令> 。

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN , COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile :

FROM node:slim

RUN "mkdir /app"

WORKDIR /app

COPY ./package.json /app

RUN [ "npm", "install" ]

COPY . /app/

CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile ,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile ,而第二个项目的 Dockerfile 就会被自动修复。

那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile就会变为:

FROM node:slim

RUN "mkdir /app"

WORKDIR /app

CMD [ "npm", "start" ]

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node

COPY ./package.json /app

RUN [ "npm", "install" ]

COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的./package.json ,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的Dockerfile :

FROM node:slim

RUN "mkdir /app"

WORKDIR /app

ONBUILD COPY ./package.json /app

ONBUILD RUN [ "npm", "install" ]

ONBUILD COPY . /app/

CMD [ "npm", "start" ]

这次我们回到原始的 Dockerfile ,但是这次将项目相关的指令加上ONBUILD ,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install ,生成应用镜像。

参考文档

Dockerfie 官方文档:https://docs.docker.com/engine/reference/builder/

Dockerfile 最佳实践文档:https://docs.docker.com/engine/userguide/engimage/dockerfile_best-practices/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值