Docker images

2017/11/20 docker

Docker 镜像

镜像是多层存储,每一层是在前一层的基础上运行的修改; 容器也是多层存储,是在以镜像位基础层,在其基础上加一层作为容器运行时的存储层

获取镜像

从Docker Registry获取镜像命令 docker pull

docker pull [选项] [docker registry地址]<仓库名>:<标签>

docker pull --help 查看选项

如:

$ docker pull ubuntu:14.04
14.04: Pulling from library/ubuntu
bae382666908: Pull complete
f1ddd5e846a8: Pull complete
90d12f864ab9: Pull complete
a57ea72e3176: Pull complete
783a14252520: Pull complete
Digest: sha256:f6eed4def93a3b54da920737f0abf1a8cae2e480bb368280c898265fcaf910a3
Status: Downloaded newer image for ubuntu:14.04

运行

进行交互式操作

$ docker run -it --rm \
ubuntu:14.04 \
bash

root@c077b083df00:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

root@c077b083df00:/# exit
exit

docker run 就是运行容器的命令

  • -it : 两个参数 -i:交互式操作 -t 终端 需要进入bash执行操作时需要这两个参数
  • --rm : 容器退出后随之将其删除。 默认情况下,为了排障需求,退出的容器不会立即删除,除非手动docker rm
  • ubuntu:14.04 : 指用 ubuntu:14.04 镜像为基础启动容器
  • bash : 放在镜像名后的是命令,这里我们希望有个交互式的shell

exit 退出容器

列出镜像

列出镜像使用 docker images 命令

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
lei/centos          6.8                 fdc17b1e4c05        5 days ago          195MB
runoob/ubuntu       v2                  a4b2c4829c56        5 days ago          161MB
nginx               latest              40960efd7b8f        10 days ago         108MB
ubuntu              latest              dd6f76d9cc90        10 days ago         122MB
ubuntu              14.04               3aa18c7568fc        10 days ago         188MB
hello-world         latest              725dcfab7d63        11 days ago         1.84kB
centos              6.8                 6704d778b3ba        11 days ago         195MB
centos              latest              d123f4e55e12        11 days ago         197MB
ubuntu              15.10               9b9cb95443b5        16 months ago       137MB

中间层镜像

为了加速镜像构建,重复利用资源,Docker会利用中间层镜像。所以在使用一段时间后,会看到一些依赖的中间层镜像,默认的docker images 列表中只会显示顶层镜像,希望显示中间层镜像需要加-a参数

$ docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
lei/centos          6.8                 fdc17b1e4c05        5 days ago          195MB
<none>              <none>              e835bfc50674        5 days ago          195MB
<none>              <none>              c6ddb3497ae1        5 days ago          195MB
<none>              <none>              31cc5698655f        5 days ago          195MB
<none>              <none>              c098a2284a97        5 days ago          195MB
<none>              <none>              a90cab8fdcea        5 days ago          195MB
runoob/ubuntu       v2                  a4b2c4829c56        5 days ago          161MB
nginx               latest              40960efd7b8f        10 days ago         108MB
ubuntu              latest              dd6f76d9cc90        10 days ago         122MB
ubuntu              14.04               3aa18c7568fc        10 days ago         188MB
hello-world         latest              725dcfab7d63        11 days ago         1.84kB
centos              6.8                 6704d778b3ba        11 days ago         195MB
centos              latest              d123f4e55e12        11 days ago         197MB
ubuntu              15.10               9b9cb95443b5        16 months ago       137MB

这些无标签的镜像就是中间层镜像

docker images 其他命令

  1. 列出部分镜像

     $ docker images ubuntu
     REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
     ubuntu              latest              dd6f76d9cc90        10 days ago         122MB
     ubuntu              14.04               3aa18c7568fc        10 days ago         188MB
     ubuntu              15.10               9b9cb95443b5        16 months ago       137MB
    
  2. 列出特定某个镜像,指定仓库名和标签

     $ docker images ubuntu:16.04
    
  3. 过滤器 -f 列出mongo:3.2以后的镜像

     $ docker images -f since=mogo:3.2  ```since``` 以后 ```before``` 以前
    
  4. 特定格式显示

     #列出所有镜像ID
     $ docker images -q
     fdc17b1e4c05
     a4b2c4829c56
     40960efd7b8f
     dd6f76d9cc90
     3aa18c7568fc
     725dcfab7d63
     6704d778b3ba
     d123f4e55e12
     9b9cb95443b5
    
     $ docker images --format ": "
     fdc17b1e4c05: lei/centos
     a4b2c4829c56: runoob/ubuntu
     40960efd7b8f: nginx
     dd6f76d9cc90: ubuntu
     3aa18c7568fc: ubuntu
     725dcfab7d63: hello-world
     6704d778b3ba: centos
     d123f4e55e12: centos
     9b9cb95443b5: ubuntu
    
     $ docker images --format "table \t\t"
     IMAGE ID            REPOSITORY          TAG
     fdc17b1e4c05        lei/centos          6.8
     a4b2c4829c56        runoob/ubuntu       v2
     40960efd7b8f        nginx               latest
     dd6f76d9cc90        ubuntu              latest
     3aa18c7568fc        ubuntu              14.04
     725dcfab7d63        hello-world         latest
     6704d778b3ba        centos              6.8
     d123f4e55e12        centos              latest
     9b9cb95443b5        ubuntu              15.10
    

利用 commit 理解镜像构成

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

镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行,之前的例子是使用Docker Hub的镜像,直接使用这些镜像可以满足一定的需求,而当这些镜像无法直接满足需求时,就需要定制这些镜像

定制一个web服务器为例子,来讲解镜像是如何构建的。

	#启动一个名字为webserver的nginx容器,将容器80端口映射到本机80端口
	$ docker run --name webserver -d -p 80:80 nginx

这是访问本机80端口 http://localhost 可以查看页面

修改这个首页面可以使用docker exec命令进入容器,修改其内容。

	$ docker exec -it webserver bash
	root@6f48338a0bbb:/# echo "<h1>Hellow, Docker</h1>"  > /usr/share/nginx/html/index.html
	root@6f48338a0bbb:/# exit
	exit

这样就修改了首页内容,我们修改了容器的文件,也就是改动了容器的存储层。我们可以通过docker diff命令看到具体的改动。

	docker diff webserver
	C /root
	A /root/.bash_history
	C /usr
	C /usr/share
	C /usr/share/nginx
	C /usr/share/nginx/html
	C /usr/share/nginx/html/index.html
	C /var
	C /var/cache
	C /var/cache/nginx
	A /var/cache/nginx/uwsgi_temp
	A /var/cache/nginx/client_temp
	A /var/cache/nginx/fastcgi_temp
	A /var/cache/nginx/proxy_temp
	A /var/cache/nginx/scgi_temp
	C /run
	A /run/nginx.pid

现在我们定制好了变化,希望能将其保存下来形成镜像。

当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层。而Docker提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最好的文件变化。 docker commit的语法格式为:

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们可以用下面的命令将容器保存为镜像:

$ docker commit \
	--author "zhanglei <zhanglei@gmail.com" \
	--message "修改默认网页" \
	webserver \
	nginx:v2
sha256:ad87b05c7593391270a505d1029eee609b46d0b838ca4711c71917c3bd148e4e

其中 –author 是指修改的作者,–message是记录本次修改的内容

可以用docker history具体查看镜像内的历史记录

docker history nginx:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
ad87b05c7593        2 minutes ago       nginx -g daemon off;                            351B                修改默认网页
40960efd7b8f        10 days ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B
<missing>           10 days ago         /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B
<missing>           10 days ago         /bin/sh -c #(nop)  EXPOSE 80/tcp                0B
<missing>           10 days ago         /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   0B
<missing>           10 days ago         /bin/sh -c set -x  && apt-get update  && a...   53.1MB
<missing>           10 days ago         /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.6....   0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13....   0B
<missing>           10 days ago         /bin/sh -c #(nop)  LABEL maintainer=NGINX ...   0B
<missing>           11 days ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           11 days ago         /bin/sh -c #(nop) ADD file:45233d6b5c9b91e...   55.2MB

定制好后可以运行这个镜像

#启动一个新的容器 web2,将80端口映射到本机81端口
docker run --name web2 -d -p 81:80 nginx:v2

使用docker commit命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。 首先,如果仔细观察之前的docker diff webserver 的结果,你会发现除了真正想要修改的/usr/share/nginx/html/index.html文件外,由于命令的执行,还有很多文件被改动或添加了,这还仅仅是最简单的操作,如果是安装软件包,编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像极为臃肿。 此外,使用docker commit意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令,怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间也无法记清具体操作。


使用Dockerfile定制镜像

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

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

以之前定制nginx镜像为例,这次我们使用Dockerfile来定制。 在一个空白目录中,建立一个文本文件,并命名为Dockerfile

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其内容为:

FROM nginx
RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html

这个Dockerfile很简单,涉及到两个指令,FROM和RUN

  • FROM 指定基础镜像 所谓定制镜像,那一定是以一个镜像位基础,在其上进行定制。就像我们之前运行了一个nginx镜像的容器,再进行修改一样,基础镜像是必须指定的。而FROM就是指定基础镜像,因此一个DockerfileFROM是必备的指令,并且必须是第一条指令。 在Docker Hub上有非常多的高质量的官方镜像,有可以直接拿来使用的服务器类的镜像,如:nginxredismongomysqlhttpdphptomcat等,也有一些方便开发,构建,运行的各类语言应用的镜像,如nodeopenjdkpythonrubygolang等。可以在其中寻找一个最符合我们最终目标的镜像为基础的镜像金星定制。如果没有找到对应服务镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntudebiancentosfedoraalpine 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。 出来选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,名为scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

      FORM scratch
      ...
    

    如果你以scratch为基础镜像的话,意味着你不可以以任何镜像为基础,接下来所写的指令将作为镜像的第一层开始存在。 不以任何系统为基础,直接可执行文件复制镜像的做法并不罕见,比如swarmcoreos/etcd。对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像提交更加小巧。使用GO语言开发的应用很多会使用这种方式来制作镜像。

  • RUN执行命令 run指令是用来执行命令行命令的。由于命令行的强大能力,RUN指令在定制镜像时时最常用的指令之一。其格式有两种:

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

          RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
    
  • exec格式: RUN["可执行文件",“参数1”,“参数2”],这更像是函数调用中的格式。

    既然RUN就像Shell脚本一样可以执行命令,那么我们是否可以像Shell脚本一样把每个命令对应一个RUN呢,比如这样:

      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
    

    之前说过,Dockerfile中每一个指令都会建立一层,RUN也不例外。每一个RUN的行为,就和我们刚才手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit这一层的修改,构成新的镜像。 而上面的这种写法,创建了8层镜像。这样会使镜像非常臃肿。 Union FS 是有最大层数限制的,比如AUFS,曾经最大不得超过42层,限制是不得超过127层。 上面的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层。

构建镜像

通过之前定制的nginx镜像的Dockerfile构建镜像

$ cat Dockerfile
FROM nginx
RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html

$ docker build -t nginx:v3 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 40960efd7b8f
Step 2/2 : RUN echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 814f7278866e
 ---> 51263a0b4459
Removing intermediate container 814f7278866e
Successfully built 51263a0b4459
Successfully tagged nginx:v3

在Step 2中,RUN指令启动了容器814f7278866e,执行了所要求的命令,并最后提交了这一层 51263a0b4459 ,随后删除了所用到的这个容器 814f7278866e。 这里使用docker build命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里我们指定了最终的镜像名称 -t nginx:v3,构建成功后,我们可以运行这个镜像。

镜像构建上下文(Context)

docker build 命令最后有一个.,.表示当前目录,而Dockerfile就在当前目录,因此不少初学者以为这个路径是在指定Dockerfile所在路径,这么理解其实是不准确的,如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。

首先要理解docker build工作原理。 Docker在运行时分为Docker引擎(服务端守护进程)和客户端工具。Docker的引擎提供了一组REST API,被称为Docker Remote API,如docker 命令这样的客户端工具,则是通过这组API与Docker引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种docker功能,但实际上,一切都是使用的远程调用形式在服务端(Docker引擎)完成。也因为这种C/S设计,让我们操作远程服务器的Docker引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令,ADD指令等。而docker build命令构建镜像,其实并非在本地构建,而是在服务端,也就是Docker引起中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎。这样Docker 引擎收到这个上下文包后,战况就会获得构建镜像所需的一切文件。 如果在Dockerfile中这么写:

COPY ./package.json /app/

这并不是要复制执行docker build命令所在的目录下的package.json,也不是复制Dockerfile所在目录下的package.json,而是复制上下文(context)目录下的package.json。 因此,COPY这类指令中的源文件的路径都是相对路径。

其他docker build的用法

  • 直接用Git repo进行构建 docker build支持从url构建,比如可以直接从Git repo中构建:

      $ docker build https://github.com/twang2218/gitlab-ce-zh.git#8.14
    
  • 用给定的tar压缩包构建

      $ docker build http://server/context.tar.gz
    
  • 从标准输入中读取Dockerfile进行构建

      docker build - < Dockerfile
    

      cat Dockerfile | docker build .
    
  • 从标准输入中读取上下文压缩包进行构建

      $ docker build - < context.tar.gz
    

Dockerfile 指令详解

COPY 复制文件

格式:

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

RUN指令一样,也有两种格式,一种是类似于命令行,一种是类似于函数调用。 COPY 指令姜葱构建上下文目录中<原路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置。比如:

COPY package.json /usr/src/app/

原路径可以是多个,可以是通配符,比如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

ADD 更高级的复制文件

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

CMD 容器启动命令

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

  • shell 格式: CMD<命令>
  • exec 格式: CMD ["可执行文件","参数1","参数2"...]
  • 参数列表格式:CMD["参数1“,”参数2“...] 在指定了 ENTRYPOINT*指令后,用CMD```指定具体的参数

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

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu镜像默认的CMD/bin/bash,如果我们之间docker run -it uuntu 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 来指定。

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网IP的镜像,那么可以先用CMD来实现:

FROM ubuntu:16.04
RUN apt-get update \
	&& apt-get install -y curl \
	&& rm -rf /var/lib/apt/lists/*
CMD ["curl","-s","http://ip.cn"]

假如我们使用docker build -t myip .来构建镜像的话,如果我们需要查询当前公网IP,只需要执行:

$ docker run myip
当前 IP:210.12.81.4 来自:北京市 联通

这么看来可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢,比如从上面的cmd中可以看到实质的命令是curl,那么如果我们希望现实HTTP头信息,就需要加上-i参数,那么我们可以直接加-i参数给docker run myip么?

$ docker run myip -i
container_linux.go:265: starting container process caused "exec: \"-i\": executable file not found in $PATH"
docker: Error response from daemon: oci runtime error: container_linux.go:265: starting container process caused "exec: \"-i\": executable file not found in $PATH".
ERRO[0003] error waiting for container: context canceled

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

那么如果我们希望加入-i参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s http://ip.cn -i

这显然不是很好的解决方案,而使用ENTRYPOINT就可以解决这个问题。现在我们重新用ENTRYPOINT来实现这个镜像:

FROM ubuntu:16.04
RUN apt-get update \
	&& apt-get install -y curl \
	&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl","-s","http://ip.cn" ]

这次我们再来尝试直接使用docker run myip -i

$ docker run myip
当前 IP:210.12.81.4 来自:北京市 联通

$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 17 Nov 2017 02:30:19 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

当前 IP:210.12.81.4 来自:北京市 联通

可以看到,这次成功了,因为当存在ENTRYPOINT后,CMD的内容将会作为参数传给ENTRYPOINT,而这里-i就是新的CMD,因此会作为参数传给curl

场景二:应用运行前的准备工作

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

比如mysql类的数据库,可能需要一些数据库配置、初始化工作,这些工作要在最终的mysql服务器运行之前解决。

此外,可能希望避免使用root用户启动服务,从而提高安全性,而在启动服务前还需要以root身份执行一些必要的准备工作,最好切换到服务用户身份启动服务。或者除了服务外,其他名利依旧使用root身份执行,方便调试等。

这些准备工作是和容器CMD无关的,无论CMD为什么,都需要事先进行一个预处理的工资。这种情况下,可以写一个脚本,然后放入ENTRYPOINT中去执行,而这个脚本会将接到的参数(也就是<CMD>)作为命令,在脚本最后执行。比如官方镜像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-entrypint.sh脚本。

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

exec "$@"

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

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

设置环境变量

格式有两种:

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

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

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

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方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 | sha256sum -c - \
	&& 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 <参数名>=<值>来覆盖。

VOLUME 定义匿名卷

格式为:

  • VOOLE ["<路径1>","<路径2>"..]
  • VOOLE <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷中,为了防止运行时用户忘记将动态文件所保存的目录挂载为卷,在Dockerfile中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用可以正常运行。

VOLUME /data

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

docker run -d -v mydata:/data xxx

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

EXPOSE 声明端口

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

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

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

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径> 使用WORKDIR 指令可以来指定工作目录,以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR会帮你建立目录。

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

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

如果将这个Dockerfile金星构建镜像运行后,会发现找不到/app/world.txt文件,或者其内容不是hello,原因其实很简单,在Shell中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令,而在Dockerfile中,这两行RUN命令的执行环节根本不同,是两个完全不同的容器。

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/release/download/1.7/gosu-amd64" \
	&& chmod +x /usr/local/bin/gosu \
	&& gosu nobody true
#设置CMD ,并以另外的用户执行
CMD ["exec","gosu","redis","redis-server"]

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] <命令>:设置检查容器健康状况的命令

  • HEALTHCHECK NONE: 如果基础镜像有健康检查治疗,使用这行可以屏蔽掉其健康检查指令。

    HEALTHCHECK指令是告诉Docker应该如何进行判断容器的状态是否正常,假设我们有个镜像是个最简单的Web服务,我们希望增加健康检查判断其Web服务是否正常工作,我们可以curl来帮助判断,其DockerfileHEALTHCHECK可以这么写:

      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
    

这里我们设置每5秒检查一次,如果健康检查命令超过3秒没有响应就视为失败,并使用curl -fs http://localhost/||exit 1作为健康检查的命令。

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

$ docker build -t myweb:v1 .

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

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

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

删除本地镜像

如果要删除本地的镜像,可以使用docker rmi命令,其格式为:

docker rmi [选项] <镜像1> [<镜像2>...]

注意docker rm命令是删除容器。

用户docker images命令配合删除

$ docker rmi $(docker images -q -f dangling=true)

$ docker rmi $(docker images -q redis)

$ docker rmi $(docker images -q -f before=mongo:3.2)

Search

    Table of Contents