[转]Docker中的镜像

引言


  这篇文章中我们主要来探讨下Docker镜像,它是用来启动容器的构建基石,本文的所用到的Dcoker版本是17.1,API版本是1.33,Go的版本是1.9.2,OS是基于Arch Linux的Manjaro。

Docker镜像的概念


  总的来说,Docker镜像是由文件系统叠加而成的。

bootfs

  最底端是一个引导文件系统,即bootfs,这很像典型的Linux/Unix的引导文件系统。Docker用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动后,它将会被移到内存中,而引导文件系统则会被卸载(unmount),以留出更多的内存供initrd磁盘镜像使用。

rootfs

  Docker看起来很像一个典型的Linux虚拟化栈。实际上,Docker镜像的第二层是root文件系统rootfs,它位于引导文件系统之上。rootfs可以是一种或多种操作系统(如Centos或者Ubuntu系统)。

只读与联合加载

  在传统的Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查之后,它才会被切换为读写模式。但是在Docker里,root文件系统永远只能是只读状态,并且Docker利用联合加载(union mount)技术又会在root文件系统层上加载更多的只读文件系统。联合加载指的是一次同时加载多个文件系统,但是在外面看起来只能看到一个文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。

镜像

  Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像(parent image),可以依次类推,直到镜像栈的最底部,最底部的镜像称为基础镜像(base image)。最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统。我们想在Docker中运行的程序就是在这个读写层中执行的。下图是一张示意图:
The docker filesystem layers

写时复制

  当Docker第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,如果想修改一个文件,这个文件首先会从该读写层下面的只读层复制到该读写层。该文件的只读版本依然存在,但是已经被读写层中的该文件副本所隐藏。   通常这种机制被称为写时复制(copy on write),这也是使Docker如此强大的技术之一。每个只读镜像层都是只读的,并且以后永远不会变化。当创建一个新容器时,Docker会构建出一个镜像栈,并在栈的最顶端添加一个读写层。这个读写层再加上其下面的镜像层以及一些配置数据,就构成了一个容器。在上一章我们已经知道,容器是可以修改的,它们都有自己的状态,并且是可以启动和停止的。容器的这种特点加上镜像分层框架(image-layering framework),使我们可以快速构建镜像并运行包含我们自己的应用程序和服务的容器。

列出镜像


  我们可以使用docker images命令来列出所有镜像。

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB

 

  本地镜像都保存在Docker宿主机的/var/lib/docker目录下。每个镜像都保存在Docker所采用的存储驱动目录下面,如aufs或者devicemapper。也可以在/var/lib/docker/containers目录下面看到所有的容器。
  镜像从仓库下载下来。镜像保存在仓库中,而仓库存在于Registry中。默认的Registry是由Docker公司运营的公共Registry服务,即Docker Hub,就如GitHub一样。
  在Docker Hub(或者用户自己运营的Registry)中,镜像是保存在仓库中的。可以将镜像仓库想象为类似Git仓库的东西。它包括镜像、层以及关于镜像的元数据(metadata)。
  每个镜像仓库都可以存放很多镜像(比如,ubuntu仓库包含了 Ubuntu 12.04、12.10、13.04、13.10和14.04的镜像)。
  我们可以用docker pull来拉取ubuntu仓库中的Ubuntu12.04的镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:12.04
12.04: Pulling from library/ubuntu
d8868e50ac4c: Pull complete
83251ac64627: Pull complete
589bba2f1b36: Pull complete
d62ecaceda39: Pull complete
6d93b41cfc6b: Pull complete
Digest: sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
Status: Downloaded newer image for ubuntu:12.04

hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB
ubuntu 12.04 5b117edd0b76 7 months ago 104MB

 

  我们再用docker images命令来看一下,发现已经得到了Ubuntu的latest镜像和12.04镜像。这表明ubuntu镜像实际上是聚集在一个仓库下的一系列镜像。
  我们看到Docker提供了TAG来区分同一个仓库中的不同镜像,如12.04、12.10、quantal或者precise等。每个标签对组成特定镜像的一些镜像层进行标记,比如,标签12.04就是对所有Ubuntu 12.04镜像的层的标记。这种机制使得在同一个仓库中可以存储多个镜像。

1
2
3
4
5
6
7
8
9
10
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:precise
precise: Pulling from library/ubuntu
Digest: sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
Status: Downloaded newer image for ubuntu:precise

hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB
ubuntu 12.04 5b117edd0b76 7 months ago 104MB
ubuntu precise 5b117edd0b76 7 months ago 104MB

 

  我们看一下上面的例子,我们可以通过在仓库名后面加上一个冒号和标签名来指定该仓库中的某一镜像,在我们的docker images输出中新的12.04镜像以相同的镜像ID出现了两次,这是因为一个镜像可以有多个标签。这使我们可以方便地对镜像进行打标签并且很容易查找镜像。在这个例子中,ID 5b117edd0b76的镜像实际上被打上了12.04和precise两个标签,分别代表该Ubuntu发布版的版本号和代号。
  Docker Hub中有两种类型的仓库:用户仓库(user repository)和顶层仓库(top-level repository)。用户仓库的镜像都是由Docker用户创建的,而顶层仓库则是由Docker内部的人来管理的。
  用户仓库的命名由用户名和仓库名两部分组成,如hazzacheng/ubuntu,用户名:hazzacheng,仓库名:ubuntu。
  与之相对,顶层仓库只包含仓库名部分,如ubuntu仓库。顶层仓库由Docker公司和由选定的能提供优质基础镜像的厂商(如Fedora团队提供了fedora镜像)管理,用户可以基于这些基础镜像构建自己的镜像。同时顶层仓库也代表了各厂商和Docker公司的一种承诺,即顶层仓库中的镜像是架构良好、安全且最新的。

拉取镜像


  用docker run命令从镜像启动一个容器时,如果该镜像不在本地,Docker会先从Docker Hub下载该镜像。如果没有指定具体的镜像标签,那么Docker会自动下载latest标签的镜像。
  我们也可以向前面做的那样,通过docker pull来事先将该镜像拉取到本地。我们来拉取一个fedora的镜像:

1
2
3
4
5
hazzacheng@hazzacheng-pc ~> sudo docker pull fedora:20
20: Pulling from library/fedora
4abd98c7489c: Pull complete
Digest: sha256:5d5a02b873d298da9bca4b84440c5cd698b0832560c850d92cf389cef58bc549
Status: Downloaded newer image for fedora:20

 

  我们也可以通过docker images命令来只查看fedora的镜像:

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker images fedora
REPOSITORY TAG IMAGE ID CREATED SIZE
fedora 20 ba74bddb630e 15 months ago 291MB

 

  因为Docker Hub实在太慢了,还经常被墙,所以我们一般从国内的镜像源pull,例如网易蜂巢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo docker pull hub.c.163.com/public/centos:7.2.1511
7.2.1511: Pulling from public/centos
a3ed95caeb02: Pull complete
545d7a0a5d69: Pull complete
391b1a24f697: Pull complete
7925fb64b42b: Pull complete
66edcdbd7fd5: Pull complete
0046acf831b2: Pull complete
b9bc9910c14f: Pull complete
3965c1da099d: Pull complete
2192a5a11821: Pull complete
38535f20ee00: Pull complete
d74d10b0ba21: Pull complete
1da8ab1a04c1: Pull complete
Digest: sha256:0bc43868ac6553fce2c2af378dc370ccab92bf68edc45224216447f58f651fd4
Status: Downloaded newer image for hub.c.163.com/public/centos:7.2.1511

 

  我们也可以用阿里云的镜像加速来进行加速,具体做法请Google。

查找镜像


  可以通过docker search查找所有Docker Hub上的公共的可用镜像,例如我们搜索下mysql:

1
2
3
4
5
6
hazzacheng@hazzacheng-pc ~> sudo docker search mysql
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relati... 5327 [OK]
mariadb MariaDB is a community-developed fork of M... 1657 [OK]
mysql/mysql-server Optimized MySQL Server Docker images. Crea... 370 [OK]
...

 

  它返回了如下信息:

  • NAME:仓库名
  • DESCRIPTION:镜像描述
  • STARS:用户评价,类似与GiyHub里的stars
  • OFFICIAL:是否官方
  • AUTOMATED:表示这个镜像是由Docker Hub的自动构建(Automated Build)流程创建的。

构建镜像


  构建Docker镜像有以下两种方法:

  • 使用docker commit命令。
  • 使用docker build命令和Dockerfile文件。

登录到Docker

  注册完帐号之后,我们可以通过docker login登录到Docker Hub,因为Docker Hub对于国内实在是太慢了,所以我们登录到网易的Register,这条命令将会完成登录到网易蜂巢的工作,并将认证信息保存起来以供后面使用。可以使用docker logout命令从一个Registry服务器退出。

1
sudo docker login -u xxx -p xxx hub.c.163.com

 

  用户的个人认证信息将会保存到HOME/.docker/config.jsonHOME/.docker/config.json中,这里的HOME指的应该是/root文件夹下。
  因为Docker Hub实在太慢了,所以接下来的操作我们都使用网易蜂巢的服务。

用Dockerfile构建镜像

  不推荐使用docker commit的方法来构建镜像,所以我们这里也不介绍那种方法了。推荐使用被称为Dockerfile的定义文件和docker build命令来构建镜像。Dockerfile使用基本的基于DSL(Domain Specific Language))语法的指令来构建一个Docker镜像,我们推荐使用Dockerfile方法来代替docker commit,因为通过前者来构建镜像更具备可重复性、透明性以及幂等性。
  一旦有了Dockerfile,我们就可以使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。
  我们创建一个包含简单Web服务器的Docker镜像。
  我们先创建一个目录,里面保存初始的Dockerfile:

1
2
3
hazzacheng@hazzacheng-pc ~> mkdir static_web
hazzacheng@hazzacheng-pc ~> cd static_web/
hazzacheng@hazzacheng-pc ~/static_web> touch Dockerfile

 

  我们创建了一个名为static_web的目录用来保存Dockerfile,这个目录就是我们的构建环境(build environment),Docker则称此环境为上下文(context)或者构建上下文(build context)。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样Docker守护进程就能直接访问用户想在镜像中存储的任何代码、文件或者其他数据。
  输入Dockerfile的内容:

1
2
3
4
5
6
# Version: 0.0.1  
FROM ubuntu:latest
MAINTAINER Hazza Cheng "hazzacheng@gmail.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由一系列指令和参数组成。每条指令,如FROM,都必须为大写字母,我们分别介绍一下它们。

FROM

  每个Dockerfile的第一条指令必须是FROM。FROM指令指定一个已经存在的镜像,后续指令都将基于该镜像进行,这个镜像被称为基础镜像(base iamge)。

MAINTAINER

  接着指定了MAINTAINER指令,这条指令会告诉Docker该镜像的作者是谁,以及作者的电子邮件地址。这有助于标识镜像的所有者和联系方式。

RUN

  然后就是run指令,RUN指令会在当前镜像中运行指定的命令,Dockerfile中的指令会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。每条指令都会创建一个新的镜像层并对镜像进行提交。Docker大体上按照如下流程执行Dockerfile中的指令:

  • Docker从基础镜像运行一个容器。
  • 执行一条指令,对容器做出修改。
  • 执行类似docker commit的操作,提交一个新的镜像层。
  • Docker再基于刚提交的镜像运行一个新容器。
  • 执行Dockerfile中的下一条指令,直到所有指令都执行完毕。

  因为每执行一条指令,就会提交一个新的镜像层,如果用户的Dockerfile由于某些原因(如某条指令失败了)没有正常结束,那么用户将得到了一个可以使用的镜像。这对调试非常有帮助:可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么用户的指令会失败进行调试。
  默认情况下,RUN指令会在shell里使用命令包装器/bin/sh -c来执行。如果是在一个不支持shell的平台上运行或者不希望在shell中运行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令:

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

 

EXPOSE

  最后设置了EXPOSE指令,这条指令告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问任意容器运行中服务的端口(这里是80)。出于安全的原因,Docker并不会自动打开该端口,而是需要用户在使用docker run运行容器时来指定需要打开哪些端口。一会儿我们将会看到如何从这一镜像创建一个新容器。可以指定多个EXPOSE指令来向外部公开多个端口。

基于Dockerfile构建新镜像

  执行docker build命令时,Dockerfile中的所有指令都会被执行并且提交,并且在该命令成功结束后返回一个新镜像,我们来操作一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
hazzacheng@hazzacheng-pc ~/static_web> sudo docker build -t="hazzacheng/static_web:v1" .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu:latest
16.04: Pulling from library/ubuntu
Digest: sha256:7c67a2206d3c04703e5c23518707bdd4916c057562dd51c74b99b2ba26af0f79
Status: Downloaded newer image for ubuntu:16.04
---> 20c44cd7596f
Step 2/5 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 35ead1a7e3eb
---> a3c3c3fa91b7
Step 3/5 : RUN apt-get update && apt-get install -y nginx
---> Running in 1b386b6b18e2
...
hazzacheng@hazzacheng-pc ~/static_web> sudo docker build -t="hazzacheng/static_web:v1" .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu:latest
16.04: Pulling from library/ubuntu
Digest: sha256:7c67a2206d3c04703e5c23518707bdd4916c057562dd51c74b99b2ba26af0f79
Status: Downloaded newer image for ubuntu:latest
---> 20c44cd7596f
Step 2/5 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 35ead1a7e3eb
---> a3c3c3fa91b7
Step 3/5 : RUN apt-get update && apt-get install -y nginx
---> Running in 1b386b6b18e2
...
---> 7466d1df3456
Step 4/5 : RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
---> Running in c52e4b9e5e62
---> 95063d4e2b0e
Step 5/5 : EXPOSE 80
---> Running in 841cb52e0856
---> 165e3539bd98
Removing intermediate container 35ead1a7e3eb
Removing intermediate container 1b386b6b18e2
Removing intermediate container c52e4b9e5e62
Removing intermediate container 841cb52e0856
Successfully built 165e3539bd98
Successfully tagged hazzacheng/static_web:v1

 

  如上所示,我们通过-t选项为新镜像设置了仓库,名称和一个标签。如果没有制定任何标签,Docker将会自动为镜像设置一个latest标签。
  我们还可以通过Git仓库来构建Docker镜像:

1
2
$ sudo docker build -t="hazzacheng/static_web:v1" \
git@github.com:hazzacheng/docker-static_web

 

  这里Docker假设在这个Git仓库的根目录下存在Dockerfile文件。
  自Docker 1.5.0开始,也可以通过-f标志指定一个区别于标准Dockerfile的构建源的位置:

1
dockerbuild-t"hazzacheng/static_- web" -f path/to/file

 

  这个文件可以不必命名为Dockerfile,但是必须要位于构建上下文之中。

指令失败

  如果一个指令失败时,例如我们不小心将前面的nginx打成ngin,程序会构建到第四步时错误退出,但是我们可以用docker run命令来基于这次构建到目前为止已经成功的最后一步创建一个容器。

Dockerfile和构建缓存

  由于每一步的构建过程都会将结果提交为镜像,所以Docker的构建镜像过程就显得非常聪明。它会将之前的镜像层看作缓存。比如,在我们的调试例子里,我们不需要在第1步到第3步之间进行任何修改,因此Docker会将之前构建时创建的镜像当做缓存并作为新的开始点。实际上,当再次进行构建时,Docker会直接从第4步开始。当之前的构建步骤没有变化时,这会节省大量的时间。如果真的在第1步到第3步之间做了什么修改,Docker则会从第一条发生了变化的指令开始。   然而,有些时候需要确保构建过程不会使用缓存。比如,如果已经缓存了前面的第3步,即apt-get update,那么Docker将不会再次刷新APT包的缓存。这时用户可能需要取得每个包的最新版本。要想略过缓存功能,可以使用docker build--no-cache标志。

基于构建缓存的Dockerfile模板

  我们可以利用缓存实现简单的Dockerfile模板,比如在Dockerfile文件顶部增加包仓库或者更新包,从而尽可能确保缓存命中。我们可以在自己的Dockerfile文件顶部使用相同的指令集模板,例如对Ubuntu:

1
2
3
4
FROM ubuntu:16.04  
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV REFRESHED_AT 2018-01-16
RUN apt-get -qq update

 

  FROMMAINTAINER都与我们前面所说的一样,ENV用来在镜像中设置环境变量。在这个例子里,我通过ENV指令来设置了一个名为REFRESHED_AT的环境变量,这个环境变量用来表明该镜像模板最后的更新时间。最后,我使用了RUN指令来运行apt-get -qq update命令。该指令运行时将会刷新APT包的缓存,用来确保我们能将要安装的每个软件包都更新到最新版本。
  有了这个模板,如果想刷新一个构建,只需修改ENV指令中的日期。这使Docker在命中ENV指令时开始重置这个缓存,并运行后续指令而无须依赖该缓存。也就是说,RUN apt-get update这条指令将会被再次执行,包缓存也将会被刷新为最新内容。
  可以扩展此模板,比如适配到不同的平台或者添加额外的需求。比如,可以支持一个fedora镜像:

1
2
3
4
FROM fedora:20 
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV REFRESHED_AT 2018-01-16
RUN yum -q makecache

 

  如果我们想深入探究镜像是如何构建出来的,可以使用docker history:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hazzacheng@hazzacheng-pc ~/D/ubuntu_nginx> sudo docker history hazzacheng/static_web:v1
IMAGE CREATED CREATED BY SIZE COMMENT
19ac4c6d5a01 4 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
c559d8d9c6da 4 minutes ago /bin/sh -c echo 'Hi, I am in your container'… 27B
c5ec0eb43d2f 4 minutes ago /bin/sh -c apt-get install -y nginx 56.5MB
04a2d034d655 5 minutes ago /bin/sh -c apt-get -qq update 39.5MB
f6ccf8510418 7 minutes ago /bin/sh -c #(nop) ENV REFRESHED_AT=2018-01-… 0B
0bd1ce7aa2e5 7 minutes ago /bin/sh -c #(nop) MAINTAINER Hazza Cheng "h… 0B
2a4cca5ac898 38 hours ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 38 hours ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 38 hours ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$… 2.76kB
<missing> 38 hours ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 38 hours ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 38 hours ago /bin/sh -c #(nop) ADD file:affda766655e01cbd… 111MB

 

  从上面的结果可以看到新构建镜像的每一层,以及创建这些层的Dockerfile指令。

端口映射

  我们利用守护方式从镜像启动一个容器:

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker run -d -p 80 --name static_web hazzacheng/static_web:v1 \
nginx -g "daemon off;"
c3ae5b033e273353291c97791f482d005715cdb7a7a7ea8918ae320a11113a84

 

  -p用来控制ocker在运行时应该公开哪些网络端口给外部(宿主机)。运行一个容器时,Docker可以通过两种方法来在宿主机上分配端口。

  • Docker可以在宿主机上随机选择一个位于32768~61000的一个比较大的端口号来映射到容器中的80端口上。
  • 可以在Docker宿主机中指定一个具体的端口号来映射到容器中的80端口上。

  docker run命令将在Docker宿主机上随机打开一个端口,这个端口会连接到容器中的80端口上。我们来看一下:

1
2
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                   NAMES
c3ae5b033e27 hazzacheng/static_web:v1 "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:32768->80/tcp static_web

 

  可以看到,容器中的80端口映射到了宿主机的32768上,我们也可以通过docker port来查看容器的端口映射:

1
2
hazzacheng@hazzacheng-pc ~> sudo docker port static_web 80
0.0.0.0:32768

 

  -p选项还为我们在将容器端口向宿主机公开时提供了一定的灵活性。比如,可以指定将容器中的端口映射到Docker宿主机的某一特定端口上,例如-p 8080:80会将容器内的80端口绑定到本地宿主机的8080端口上,我们需要小心使用,如果要运行多个容器,只有一个容器能成功地将端口绑定到本地宿主机上,这将会限制Docker的灵活性。   我们也可以将端口绑定限制在特定的IP地址上,如-p 127.0.0.1:80:80,我们将容器内的80端口绑定到了本地宿主机的127.0.0.1这个IP的80端口上。我们也可以使用类似的方式将容器内的80端口绑定到一个宿主机的随机端口上,如-p 127.0.0.1::80。也可以通过在端口绑定时使用/udp后缀来指定UDP端口。
  Docker还提供了一个更简单的方式,即-P参数,该参数可以用来对外公开在Dockerfile中通过EXPOSE指令公开的所有端口:

1
2
sudo docker run -d -P --name static_web hazzacheng/static_web:v1 \
nginx -g "daemon off;"

 

  该命令会将容器内的80端口对本地宿主机公开,并且绑定到宿主机的一个随机端口上。该命令会将用来构建该镜像的Dockerfile文件中EXPOSE指令指定的其他端口也一并公开。

Dockerfile指令

  我们介绍一些常用的Dockerfile指令,你也可以通过官方文档来学习。

CMD

  CMD指令用于指定一个容器启动时要运行的命令。这有点儿类似于RUN指令,只是RUN指令是指定镜像被构建时要运行的命令,而CMD是指定容器被启动时要运行的命令。需要注意的是,要运行的命令是存放在一个数组结构中的。这将告诉Docker按指定的原样来运行该命令。当然也可以不使用数组而是指定CMD指令,这时候Docker会在指定的命令前加上/bin/sh -c。这在执行该命令的时候可能会导致意料之外的行为,所以Docker推荐一直使用以数组语法来设置要执行的命令。例如下面的用法:

1
2
CMD ["/bin/bash"]
CMD ["/bin/bash", "-l"]

 

  使用docker run命令可以覆盖CMD指令。如果我们在Dockerfile里指定了CMD指令,而同时在docker run命令行中也指定了要运行的命令,命令行中指定的命令会覆盖Dockerfile中的CMD指令。
  在Dockerfile中只能指定一条CMD指令。如果指定了多条CMD指令,也只有最后一条CMD指令会被使用。如果想在启动容器时运行多个进程或者多条命令,可以考虑使用类似Supervisor这样的服务管理工具。

ENTRYPOINT

  ENTRYPOINT指令与CMD指令非常类似,也很容易和CMD指令弄混。这两个指令的区别是,我们可以在docker run命令行中覆盖CMD指令。有时候,我们希望容器会按照我们想象的那样去工作,这时候CMD就不太合适了。而ENTRYPOINT指令提供的命令则不容易在启动容器时被覆盖。实际上,docker run命令行中指定的任何参数都会被当做参数再次传递给ENTRYPOINT指令中指定的命令。例如:

1
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"]

 

  ENTRYPOINT也可以和CMD一起使用,例如:

1
2
ENTRYPOINT ["/usr/sbin/nginx"]  
CMD ["-h"]

 

  如果在启动容器时不指定任何参数,则在CMD指令中指定的-h参数会被传递给Nginx守护进程,即Nginx服务器会以/usr/sbin/nginx -h的方式启动,该命令用来显示Nginx的帮助信息。
  这使我们可以构建一个镜像,该镜像既可以运行一个默认的命令,同时它也支持通过docker run命令行为该命令指定可覆盖的选项或者标志。
  如果确实需要,用户也可以在运行时通过docker run--entrypoint标志覆盖ENTRYPOINT指令。

WORKDIR

  WORKDIR指令用来在从镜像创建一个新容器时,在容器内部设置一个工作目录,ENTRYPOINTCMD指定的程序会在这个目录下执行。我们可以使用该指令为Dockerfile中后续的一系列指令设置工作目录,也可以为特定的指令设置不同的工作目录:

1
2
3
4
WORKDIR /opt/webapp/db  
RUN bundle install 
WORKDIR /opt/webapp 
ENTRYPOINT [ "rackup" ]

 

  可以通过-w标志在运行时覆盖工作目录:

1
docker run -ti -w /var/log ubuntu pwd  /var/log

 

  该命令会将容器内的工作目录设置为/var/log

ENV

  ENV指令用来在镜像构建过程中设置环境变量。

1
ENV JAVA_PATH /home/java/

 

  这个新的环境变量可以在后续的任何RUN指令中使用,可以在ENV指令中指定单个环境变量,也可以指定多个变量。

1
ENV JAVA_PATH=/home/java JRE_PATH=/home/java/jre

 

  也可以使用docker run命令行的-e标志来传递环境变量。这些变量将只会在运行时有效:

1
docker run -ti -e "WEB_PORT=8080" ubuntu env

 

  在容器中WEB_PORT环境变量被设为了8080。

USER

  USER指令用来指定该镜像会以什么样的用户去运行,我们可以指定用户名或UID以及组或GID,甚至是两者的组合。

1
2
3
4
5
6
USER user  
USER user:group 
USER uid 
USER uid:gid 
USER user:gid 
USER uid:group

 

  也可以在docker run命令中通过-u标志来覆盖该指令指定的值。
  如果不通过USER指令指定用户,默认用户为root。

VOLUME

  VOLUME指令用来向基于镜像创建的容器添加卷。一个卷是可以存在于一个或者多个容器内的特定的目录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化的功能:

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

  卷功能让我们可以将数据(如源代码)、数据库或者其他内容添加到镜像中而不是将这些内容提交到镜像中,并且允许我们在多个容器间共享这些内容。我们可以利用此功能来测试容器和内部的应用程序代码,管理日志,或者处理容器内部的数据库。

1
VOLUME ["/opt/project"]

 

  这条指令将会为基于此镜像创建的任何容器创建一个名为/opt/project的挂载点。也可以通过指定数组的方式指定多个卷:

1
VOLUME ["/opt/project", "/data" ]

 

  docker cp是和VOLUME指令相关并且也是很实用的命令。该命令允许从容器复制文件和复制文件到容器上。

ADD

  ADD指令用来将构建环境下的文件和目录复制到镜像中。例如,在安装一个应用程序时。ADD指令需要源文件位置和目的文件位置两个参数:

1
ADD software.lic /opt/application/software.lic

 

  ADD指令将会将构建目录下的software.lic文件复制到镜像中/opt/application/software.lic指向源文件的位置参数可以是一个URL,或者构建上下文或环境中文件名或者目录。不能对构建目录或者上下文之外的文件进行ADD操作。   在ADD文件时,Docker通过目的地址参数末尾的字符来判断文件源是目录还是文件。如果目的地址以/结尾,那么Docker就认为源位置指向的是目录。如果目的地址不是以/结尾,那么Docker就认为源位置指向的是文件。   文件源也可以使用URL的格式:

1
ADD http://wordpress.org/latest.zip /root/wordpress.zip

 

  ADD在处理本地压缩文件(tar archive)时还有一些特殊处理。如果将一个压缩文件(合法的压缩文件包括gzip、bzip2、xz)指定为源文件,Docker会自动将压缩文件解开(unpack):

1
ADD latest.tar.gz /var/www/wordpress/

 

  这条命令会将压缩文件latest.tar.gz解开到/var/www/wordpress/目录下。Docker解开压缩文件的行为和使用带-x选项的tar命令一样:该指令执行后的输出是原目的目录已经存在的内容加上压缩文件中的内容。如果目的位置的目录下已经存在了和压缩文件同名的文件或者目录,那么目的位置中的文件或者目录不会被覆盖。
  如果目的位置不存在的话,Docker将会为我们创建这个全路径,包括路径中的任何目录。新创建的文件和目录的模式为0755,并且UID和GID都是0。   ADD指令会使得构建缓存变得无效,这一点也非常重要。如果通过ADD指令向镜像添加一个文件或者目录,那么这将使Dockerfile中的后续指令都不能继续使用之前的构建缓存。

COPY

  COPY指令非常类似于ADD,它们根本的不同是COPY只关心在构建上下文中复制本地文件,而不会去做文件提取(extraction)和解压(decompression)的工作

1
COPY conf.d/ /etc/apache2/

 

  这条指令将会把本地conf.d目录中的文件复制到/etc/apache2/目录中。
  文件源路径必须是一个与当前构建环境相对的文件或者目录,本地文件都放到和Dockerfile同一个目录下。不能复制该目录之外的任何文件,因为构建环境将会上传到Docker守护进程,而复制是在Docker守护进程中进行的。任何位于构建环境之外的东西都是不可用的。COPY指令的目的位置则必须是容器内部的一个绝对路径
  任何由该指令创建的文件或者目录的UID和GID都会设置为0。   如果源路径是一个目录,那么这个目录将整个被复制到容器中,包括文件系统元数据;如果源文件为任何类型的文件,则该文件会随同元数据一起被复制。在这个例子里,源路径以/结尾,所以Docker会认为它是目录,并将它复制到目的目录中。
  如果目的位置不存在,Docker将会自动创建所有需要的目录结构,就像mkdir -p命令那样。

LABEL

  LABEL指令用于为Docker镜像添加元数据。元数据以键值对的形式展现。

1
2
LABEL version="1.0" 
LABEL location="New York" type="Data Center" role="Web Server"

 

  LABEL指令以label=“value”的形式出现。可以在每一条指令中指定一个元数据,或者指定多个元数据,不同的元数据之间用空格分隔。推荐将所有的元数据都放到一条LABEL指令中,以防止不同的元数据指令创建过多镜像层。可以通过docker inspect命令来查看Docker镜像中的标签信息。

STOPSIGNAL

  STOPSIGNAL指令用来设置停止容器时发送什么系统调用信号给容器。这个信号必须是内核系统调用表中合法的数,如9,或者SIGNAME格式中的信号名称,如SIGKILL。

ARG

  ARG指令用来定义可以在docker build命令运行时传递给构建运行时的变量,我们只需要在构建时使用--build-arg标志即可。用户只能在构建时指定在Dockerfile文件中定义过的参数。

1
2
ARG build 
ARG webapp_user=user

 

  上面例子中第二条ARG指令设置了一个默认值,如果构建时没有为该参数指定值,就会使用这个默认值。我们在docker build中使用这些参数:

1
$ docker build --build-arg build=1234 -t hazzacheng/webapp .

 

  这里构建hazzacheng/webapp镜像时,build变量将会设置为1234,而webapp_user变量则会继承设置的默认值user。
  请不要使用ARG来传递证书或者秘钥之类的信息,这些机密信息在构建过程中以及镜像的构建历史中会被暴露。
  Docker预定义了一组ARG变量,可以在构建时直接使用,而不必再到Dockerfile中自行定义。
  预定义ARG变量:

1
2
3
4
5
6
7
8
HTTP_PROXY 
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy

 

  要想使用这些预定义的变量,只需要给docker build命令传递--build-arg <variable>=<value>标志就可以了。

ONBUILD

  ONBUILD指令能为镜像添加触发器(trigger)。当一个镜像被用做其他镜像的基础镜像时,例如用户的镜像需要从某未准备好的位置添加源代码,或者用户需要执行特定于构建镜像的环境的构建脚本,该镜像中的触发器将会被执行。
  触发器会在构建过程中插入新指令,我们可以认为这些指令是紧跟在FROM之后指定的。触发器可以是任何构建指令:

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

 

  上面的代码将会在创建的镜像中加入ONBUILD触发器,ONBUILD指令可以在镜像上运行docker inspect命令来查看。   例如,我们为Apache2镜像构建一个全新的Dockerfile,该镜像名为hazzacheng/ apache2`:

1
2
3
4
5
6
7
8
9
10
FROM ubuntu:14.04  
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
RUN apt-get -qq update && apt-get -qq install -y apache2 
ENV APACHE_RUN_USER www-data 
ENV APACHE_RUN_GROUP www-data 
ENV APACHE_LOG_DIR /var/log/apache2 
ONBUILD ADD . /var/www/ 
EXPOSE 80 
ENTRYPOINT ["/usr/sbin/apache2"] 
CMD ["-D", "FOREGROUND"]

 

  构建该镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
hazzacheng@hazzacheng-pc ~/D/apache2> sudo docker build -t="hazzacheng/apache2:1.0" .
Sending build context to Docker daemon 2.048kB
Step 1/10 : FROM ubuntu:14.04
---> 02a63d8b2bfa
Step 2/10 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Using cache
---> 5958e7d2e6ee
Step 3/10 : RUN apt-get -qq update && apt-get -qq install -y apache2
---> Running in 6602e39c1d1c
...
Removing intermediate container 6602e39c1d1c
---> 27bc656a7f72
Step 4/10 : ENV APACHE_RUN_USER www-data
---> Running in 039181b26294
Removing intermediate container 039181b26294
---> 859f7fde9c1e
Step 5/10 : ENV APACHE_RUN_GROUP www-data
---> Running in 7c8fb7c92363
Removing intermediate container 7c8fb7c92363
---> 38e48e99d7a4
Step 6/10 : ENV APACHE_LOG_DIR /var/log/apache2
---> Running in a9b51450c5f7
Removing intermediate container a9b51450c5f7
---> 13a9b236ab6c
Step 7/10 : ONBUILD ADD . /var/www/
---> Running in a13a07301846
Removing intermediate container a13a07301846
---> 9324a7ba8a37
Step 8/10 : EXPOSE 80
---> Running in fedb23f77928
Removing intermediate container fedb23f77928
---> e3c98bab1099
Step 9/10 : ENTRYPOINT ["/usr/sbin/apache2"]
---> Running in d7a1a4307afc
Removing intermediate container d7a1a4307afc
---> bb2f359867fc
Step 10/10 : CMD ["-D", "FOREGROUND"]
---> Running in 9b5a3977ab5e
Removing intermediate container 9b5a3977ab5e
---> bcf933e3caec
Successfully built bcf933e3caec
Successfully tagged hazzacheng/apache2:1.0

 

  在新构建的镜像中包含一条ONBUILD指令,该指令会使用ADD指令将构建环境所在的目录下的内容全部添加到镜像中的/var/www/目录下。我们可以轻而易举地将这个Dockerfile作为一个通用的Web应用程序的模板,可以基于这个模板来构建Web应用程序。
  我们可以通过构建一个名为webapp的镜像来看看如何使用镜像模板功能。它的Dockerfile如代码如下:

1
2
3
4
FROM hazzacheng/apache2:1.0
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV APPLICATION_NAME webapp 
ENV ENVIRONMENT development

 

  让我们看看构建这个镜像时将会发生什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker build -t="hazzacheng/webapp:1.0" .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM hazzacheng/apache2:1.0
# Executing 1 build trigger
---> 68b20313194d
Step 2/4 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 5834328436f9
Removing intermediate container 5834328436f9
---> 5ae1b436c698
Step 3/4 : ENV APPLICATION_NAME webapp
---> Running in 852154b72d50
Removing intermediate container 852154b72d50
---> 2e3b1b38b4d9
Step 4/4 : ENV ENVIRONMENT development
---> Running in 2e43a07c66a1
Removing intermediate container 2e43a07c66a1
---> 32dfacc87317
Successfully built 32dfacc87317
Successfully tagged hazzacheng/webapp:1.0

 

  可以清楚地看到,在FROM指令之后,Docker插入了一条ADD指令,这条ADD指令就是在ONBUILD触发器中指定的。执行完该ADD指令后,Docker才会继续执行构建文件中的后续指令。这种机制使我每次都会将本地源代码添加到镜像,就像上面我们做到的那样,也支持我为不同的应用程序进行一些特定的配置或者设置构建信息。这时,可以将hazzacheng/apache2当做一个镜像模板。
  ONBUILD触发器会按照在父镜像中指定的顺序执行,并且只能被继承一次,也就是说只能在子镜像中执行,而不会在孙子镜像中执行。如果我们再基于hazzacheng/apache2构建一个镜像,则新镜像是hazzacheng/apache2的孙子镜像,因此在该镜像的构建过程中,ONBUILD触发器是不会被执行的。
  这里有好几条指令是不能用在ONBUILD指令中的,包括FROMMAINTAINERONBUILD本身。之所以这么规定是为了防止在 Dockerfile构建过程中产生递归调用的问题。

提交镜像


  镜像构建完毕之后,我们也可以将它上传到Docker Hub上面去,也可以推送到其他的Registry,例如网易蜂巢,这样其他人就能使用这个镜像了。比如,我们可以在组织内共享这个镜像,或者完全公开这个镜像。
  这里我们将其推送到网易蜂巢上去:

1
2
3
4
5
6
7
8
9
10
11
12
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker tag hazzacheng/static_web:v1 hub.c.163.com/hazzacheng/static_web:v1
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker push hub.c.163.com/hazzacheng/static_web:v1
The push refers to repository [hub.c.163.com/hazzacheng/static_web]
a37eded3e1c4: Pushed
8c1b6608d69f: Pushed
71e0479ba020: Pushed
8600ee70176b: Pushed
2bbb3cec611d: Pushed
d2bb1fc88136: Pushed
a6a01ad8b53f: Pushed
833649a3e04c: Pushed
v1: digest: sha256:899e70dd39817ea95acc5ae01d335108bbd4cc5969daf4e965deeb30e8f30460 size: 8626

 

  再push之前我们必须先打上网易蜂巢的tag,即hub.c.163.com

删除镜像


  我们使用docker rmi来删除镜像,这个命令也支持删除多个镜像,我们也可以使用

1
docker rmi `docker images -a -q`

 

  来删除所有镜像。 

(原文: http://chengfeng96.com/blog/2018/01/19/Docker中的镜像/  作者: HazzaCheng)

 

同时发现其他比较好的资料:

1.  https://yeasy.gitbooks.io/docker_practice/repository/dockerhub.html

posted @ 2018-09-02 10:25  HelloSUN  阅读(1285)  评论(1编辑  收藏  举报