第一本Docker书 第二部分
第4章 使用Docker镜像和仓库
4.1 什么是Docker镜像
Docker镜像是由文件系统叠加而成,最底层是一个引导文件系统,即bootfs ,与典型的Linux/Unix的引导文件系统很像。实际上,当一个容器启动后,它将会被移到内存中,而引导文件系统则会被卸载,以留出更多的内存供initrd磁盘镜像使用。
Docker镜像的第二层是root文件系统rootfs,它位于引导文件系统之上,可以是一种或多种操作系统(如Debian或Ubuntu文件系统)。
传统Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查后,才会被切换为读写模式。但是在Docker里,root文件系统永远是只读状态,并且Docker利用联合加载(union mount,像洋葱一样是一个分层的系统)技术又会在root文件系统层上加载更多的只读文件系统。联合加载指的是一次同时加载多个文件系统,但在外面看来只能看到一个文件系统。联合加载会将多层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶层。位于下面的镜像称为父镜像,最底层的镜像称为基础镜像。最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统,我们想在Docker中运行的程序就是在这个读写层中执行的。如下图所示:
Docker第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,若想修改一个文件,这个文件首先会从该读写层下面的只读层复制到该读写层。该文件的只读版本依然存在,但是已经被读写层中的该文件副本所隐藏。该机制称为写时复制(copy on write)。
每个只读镜像层都是只读的,并且以后永远不会变化。
4.2 Docker镜像有关命令
4.2.1 列出镜像
$ sudo docker images
本地镜像都保存在Docker宿主机的/var/lib/docker
目录下。每个镜像都保存在Docker所采用的存储驱动目录下面,如aufs或devicemapper。可以在/var/lib/docker/containers
下看到所有容器。
镜像保存在仓库中,而仓库存在于Registry中。可以将镜像仓库想象为类似Git仓库的东西,包括镜像、层以及关于镜像的元数据(metadata)。
每个镜像仓库都可以存放很多镜像。
$ sudo docker pull ubuntu ## 拉取Ubuntu镜像
注:虽称其为Ubuntu操作系统,但实际上它不是一个完整的操作系统。它只是一个裁剪版,只包含最低限度的支持操作系统运行的组件。
为了区分同一仓库中的不同镜像,Docker提供了标签(tag)的功能。每个镜像在列出时都带有一个标签,每个标签对组成特定镜像的一些镜像层进行标记。
可以在仓库名后面加上一个冒号和标签名来指定该仓库中的某一镜像,如:
$ sudo docker run -ti --name test ubuntu:12.04 /bin/bash
Docker Hub 中有两种类型的仓库:用户仓库(user repository)和顶层仓库(top-level repository)。用户仓库的镜像都是由Docker用户创建的,而顶层仓库则是由Docker内部的人来管理的。
用户仓库的命名由用户名和仓库名两部分组成;而顶层仓库只包含仓库名部分,如Ubuntu仓库。
4.2.2 拉取镜像
docker run
命令从镜像启动一个容器时,如果本地没有该镜像,则会先从Docker Hub下载该镜像。如果没有指定镜像标签,则自动下载latest标签的镜像。
可以使用docker pull
命令自己来预先拉取镜像到本地。
$ sudo docker pull fedora
$ sudo docker pull fedora:20
4.2.3 查找镜像
可以通过docker search
命令来查找所有Docker Hub上公共的可用镜像。如:
$ sudo docker search puppet
该命令会在Docker Hub上查找所有带有puppet的镜像,并返回以下信息:
- 仓库名
- 镜像描述
- 用户评价(stars):反应镜像的受欢迎程度
- 是否官方(Official)
- 自动构建(Automated):表示这个镜像是由Docker Hub的自动构建(Automated Build)流程创建的。
4.2.4 构建镜像
修改、更新以及管理镜像的方法:
- 使用
docker commit
命令。 - 使用
docker build
命令和Dockerfile文件。
不推荐使用docker commit
构建Docker镜像,而应该使用更灵活、更强大的Dockerfile来构建Docker镜像。
1) Docker的commit命令创建镜像
先创建一个容器,在容器里面做出修改,最后将修改提交为一个新镜像。需要注意的是,docker commit
提交的只是创建容器的镜像与容器当前状态之间有差异的部分,这使得该更新非常轻量。
2) Dockerfile创建镜像
Dockerfile使用基本的基于DSL语法的指令来创建一个Docker镜像,之后使用docker build
命令基于该Dockerfile中的指令创建一个新的镜像。
Dockerfile示例:
# Version: 0.0.1
FROM ubuntu:14.04
MAINTAINER xxx "xxx@example.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, test container dockerfile' > /usr/share/nginx/html/index.html
EXPOSE 80
该Dockerfile由一系列指令和参数组成。每条指令必须为大写字母,且后面要跟随一个参数。Dockerfile中的指令会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。每条指令都会创建一个新的镜像层并对镜像进行提交。
Docker大体上按照如下流程执行Dockerfile中的指令:
- Docker从基础镜像运行一个容器。
- 执行一条指令,对容器做出修改。
- 执行类似
docker commit
的操作,提交一个新的镜像层。 - Docker 再基于刚提交的镜像运行一个新容器。
- 执行Dockerfile中的下一条指令,直到所有指令都执行完毕。
如果Dockerfile由于某些原因没有正常结束,那么将得到一个可以使用的镜像,可以基于该镜像进行调试。
Dockerfile常用语法说明:
- Dockerfile支持注释,以
#
开头的行会被认为是注释。 - 每个Dockerfile的第一条指令都应该是
FROM
,指定一个已经存在的镜像,后续指令都将基于该镜像进行,该镜像称为基础镜像(base image)。 -
MAINTAINER
指令:告诉Docker该镜像的作者是谁以及作者的电子邮件地址。 -
RUN
指令:会在当前镜像中运行指定的命令。每条RUN
指令都会创建一个新的镜像层,如果该指令执行成功,会将该镜像层提交,之后继续执行Dockerfile中的下一条指令。 -
EXPOSE
指令:告诉Docker该容器内的应用程序将会使用容器的指定端口。出于安全的原因,Docker并不会自动打开该端口,而是在运行容器时,需要使用docker run
来指定需要打开哪些端口。
Dockerfile 和构建缓存
由于每一步的构建过程都会将结果提交为镜像,所有在构建镜像过程中会将之前的镜像层看做缓存。
当镜像构建失败后再次重新构建时,如果失败前的步骤没有修改的话,会直接从失败前的镜像作为新的开始点,节省构建时间。
要想略过缓存功能,可以使用docker build
的--no-cache
标志。
基于构建缓存的Dockerfile模板
FROM ubuntu:14.04
MAINTAINER xxx "xxx@example.com ENV REFRESHED_AT 2018-01-01 RUN apt-get -qq update
构建缓存带来的一个好处是,可以实现简单的Dockerfile模板。ENV
用来在镜像中设置环境变量,可以通过环境变量设置该镜像的最后更新时间,从而如果想刷新一个构建只需修改ENV
指令中的日期。这使得Docker在命中ENV
指令时重置这个缓存,并运行后续指令而无需依赖该缓存。
4.2.5 查看新镜像
$ sudo docker images # 查看系统中的镜像
$ sudo docker history xxxx # 深入探究镜像是如何构建出来的
4.2.6 从新镜像启动容器
$ sudo docker run -d -p 80 --name test xxx/xxx nginx -g "daemon off"
-
-d
选项:告诉docker以分离(detached)的方式在后台运行,适合需要长时间运行的进程,比如守护进程。 -
-p
选项:控制docker在运行时应该公开哪些网络端口给外部(宿主机)。有两种方式在宿主机上分配端口:- Docker随机选择一个49153-65535间的一个比较大的端口号来映射容器的80端口,上面命令中就是这种方式。
-
sudo docker ps -l
可以查看容器中的端口被映射到宿主机的端口,也可以通过docker port 容器id 容器内端口号
来查看容器的端口映射情况。 - 可以在Docker宿主机中指定一个具体的端口号来映射到容器的80端口上,如
-p 8080:80
,容器中的80端口绑定到了本地宿主机的8080端口。
-
-P
选项:用来对外公开在Dockerfile中的EXPOSE指令中设置的所有端口。
4.2.7 Dockerfile指令
CMD
CMD指令用来指定一个容器启动时要运行的命令。
CMD ["/bin/bash", "-l"]
上面命令中将-l
标志传递给/bin/bash
命令。
注:
- 要运行的命令是存放在一个数组结构中的,这将告诉Docker按指定的原样来运行该命令。
- 使用
docker run
命令可以覆盖CMD指令。 - dockerfile中只能指定一条CMD指令。如果指定多条,只有最后一条指令会被使用。
ENTRYPOINT
与CMD指令非常相似。不过ENTRYPOINT提供的指令不容易在启动容器时被覆盖,事实上docker run
命令中指定的任何参数会被当做参数再次传递给ENTRYPOINT指令中指定的命令。可以与CMD指令组合使用完成一些巧妙的工作。例:
ENTRYPOINT ["/usr/sbin/nginx"]
CMD ["-h"]
启动容器时,任何在命令行中指定的参数都会被传递给Nginx守护进程。比如,指定-g "daemon off"
,参数让Nginx守护进程以前台方式运行。如果不指定任何参数,则CMD
指令中的-h参数会传递给Nginx守护进程,即Nginx服务器会以/usr/sbin/nginx -h
的方式启动。
注:如果确实需要,可以在运行时通过docker run
的--entrypoint
覆盖ENTRYPOINT指令。
WORKDIR
用来在从镜像创建容器时,在容器内部设置一个工作目录,ENTRYPOINT
和CMD
指定的程序会在这个目录下执行。
注:可以在docker run
时通过-w
标志在运行时覆盖工作目录。
ENV
用来在镜像构建过程中设置环境变量。该环境变量可以在后续的任何RUN
指令中使用,也会持久保存到创建的容器中。
注:docker run
中可以用-e
参数传递环境变量,这些变量只会在运行时有效。
USER
用来指定该镜像会以什么样的用户去运行,可以指定用户名或UID以及组或GID,甚至是两者的组合。可以在docker run
中通过-u
选项覆盖该指令指定的值。
VOLUME
用来向基于镜像创建的容器添加卷。一个卷可以存在一个或多个容器内的特定目录,这个目录可以绕过联合文件系统,并提供共享数据或对数据进行持久化的功能。
- 卷可以在容器间共享和重用。
- 对卷的修改是立时生效的。
- 对卷的修改不会对更新镜像产生影响。
- 卷会一直存在直到没有任何容器再使用它。
ADD
用来将构建环境中的文件和目录复制到镜像中。ADD指令需要源文件位置和目的文件位置两个参数。指向源文件的位置参数可以是一个URL,或者构建上下文或环境中文件名或者目录,不能对构建目录或者上下文之外的文件进行ADD操作。
- 如果目的位置不存在的话,Docker会为我们创建这个全路径,包括路径中的任何目录。新创建得我文件和目录模式为0755,并且UID和GID均为0。
- ADD在处理本地归档文件(压缩文件,如gzip, bzip2, xz文件),会自动将归档文件解开。
- 如果目的位置目录下已经存在和归档文件同名的文件或目录,则目的位置中的文件或目录不会被覆盖。
COPY
与ADD非常类似,COPY只关心构建上下文复制本地文件,不会去做文件提取和解压的工作。
文件源路径必须是一个与当前构建环境相对的文件或目录,不能复制该目录之外的任何文件。因为构建环境将会上传到Docker守护进程,而复制是在Docker守护进程中进行的。
文件目的位置必须是容器内部的一个绝对地址。
ONBUILD
为镜像添加触发器。当一个镜像被用作其他镜像的基础镜像时,该镜像中的触发器将会被执行。可以认为这些指令是紧跟FROM之后执行的。触发器可以是任何指令。
4.2.8 将镜像推送到Docker Hub以及删除镜像
$ sudo docker push xxx
$ sudo docker rmi xxx