Docker基础(二)
在之前的笔记中,我们已经简单介绍了Docker,以及Docker三个基本概念:镜像(Image
),容器(Container
)以及仓库(Repository
))的相关知识了,这里我们继续深入学习Docker。
Docker镜像深入
在之前的使用镜像笔记中,我们简单介绍了:
- 管理本地主机上的镜像
- 从仓库获取镜像
- 运行镜像
接下来我们深入学习一下:
- 镜像实现的基本原理
- Docker镜像Commit操作
镜像实现的基本原理
Docker 镜像是怎么实现增量的修改和维护的?
每个镜像都由很多层次构成,Docker 使用 Union FS 将这些不同的层结合到一个镜像中去。
通常 Union FS 有两个用途: 一方面可以实现不借助 LVM、RAID 将多个 disk 挂到同一个目录下,另一个更常用的就是将一个只读的分支和一个可写的分支联合在一起,Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。
Docker 在 OverlayFS 上构建的容器也是利用了类似的原理。
前面的笔记中也已经提到过了: 镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。
UnionFS
Docker技术三大要点:cgroup, namespace和unionFS的理解(强烈推荐)
Union文件系统(UnionFS)是一种
分层
、轻量级
并且高性能
的文件系统
,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。Union 文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
UnionFS可以把文件系统上多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
关于镜像加载原理,在上面的推荐文章中已经说的很详细了,这里就不再多说。
当用
docker run
启动这个容器时,实际上在镜像的顶部添加了一个新的可写层。这个可写层也叫容器层。容器启动后,其内的应用所有对容器的改动,文件的增删改操作都只会发生在容器层中,对容器层下面的所有只读镜像层没有影响。
(视频讲解)
Docker镜像Commit操作
注意:
docker commit
命令除了学习之外,还有一些特殊的应用场合,比如被入侵后保存现场等。但是,不要使用docker commit
定制镜像,定制镜像应该使用Dockerfile
来完成(DockerFile)
镜像是容器的基础,每次执行 docker run
的时候都会指定哪个镜像作为容器运行的基础。在之前的例子中,我们所使用的都是来自于 Docker Hub 的镜像(即docker pull
第三方的镜像)。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。
之前我们已经说到过了,镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
案例:commit一个删除了docs的tomcat镜像
通过容器提交镜像(docker commit)以及推送镜像(docker push)笔记
docker commit:根据容器创建一个新的镜像
语法:
1 | docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]] |
OPTIONS说明:
-a
:提交的镜像作者-c
:使用Dockerfile指令来创建镜像-m
:提交时的说明文字-p
:在commit时,将容器暂停
我们常用的方式如下:
1 | docker commit -m="要提交的描述信息" -a="作者" 容器ID 要创建的目标镜像名:[标签名] |
例子:首先我们docker run
一下我们之前docker pull
的tomcat镜像,输入:
1 | docker run -it -p 8888:8080 --name tomcat9 -d tomcat |
参数说明:
-p
:指定端口映射,这里的格式为:hostPort:containerPort,即主机端口:docker容器端口-P
:随机端口映射-i
:以交互模式运行容器,通常与-t
同时使用-t
:为容器重新分配一个伪输入终端,通常与-i
同时使用-d
:后台运行容器,并返回容器ID, 也即启动守护式容器
注意,这里可能是由于使用了tomcat9的缘故,直接访问
http://192.168.219.101:8888/
是这样的:可以参考这边文章解决相关问题:Docker方式启动tomcat,访问首页出现404错误
根据参考文章解决相关问题后,我们访问http://192.168.219.101:8888
,结果如下:
这个时候我们点击Documentation,即访问http://192.168.219.101:8888/docs/
,可以看到以下页面:
而为了演示本地commit的镜像效果,我们需要commit一个删除了docs的tomcat
而上面我们已经提到过了,docker commit
是根据容器创建一个新的镜像,所以我们就要根据我们现在这个容器,commit
一个新的镜像。
首先先进入tomcat中,删除docs文件:
1 | docker exec -it tomcat9 bash |
这是我们再访问http://192.168.219.101:8888/docs/
就没有页面了:
这个时候就可以根据当前的这个tomcat9容器,来commit
一个新的镜像到本地了:
1 | docker commit -a="qingbo" -m="a new tomcat without docs and webapps.dist" tomcat9 qingbo/tomcat9:9.0 |
注意:
- qingbo/tomcat9这个仓库名是采用的两段式路径,而关于两段式路径在之前的笔记中也已经提过了
commit
操作只是将镜像生成在本地,并没有push
到远程仓库
这时我们再docker run
一下我们通过docker commit
生成的镜像qingbo/tomcat9:9.0
1 | docker run -it -p 8899:8080 --name mytomcat9 -d qingbo/tomcat9:9.0 |
这时我们直接访问:http://192.168.219.101:8899/
就有以下效果:
而访问http://192.168.219.101:8899/docs/
当然也是找不到:
这就说明我们本地的镜像成功通过docker commit
生成出来了
Docker容器数据卷
数据卷(Volumes)
数据卷是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
- 数据卷可以在容器之间共享和重用
- 对数据卷的修改会立马生效
- 对数据卷的更新,不会影响镜像
- 数据卷默认会一直存在,即使容器被删除
注意:数据卷的使用,类似于 Linux 下对目录或文件进行 mount(Linux mount命令),镜像中的被指定为挂载点的目录中的文件会复制到数据卷中(仅数据卷为空时会复制)。(关于mount在上面的推荐文章**Docker技术三大要点:cgroup, namespace和unionFS的理解**中也有提到过)
数据卷概念介绍
数据卷是什么:先来看看Docker的理念:将运用与运行的环境打包形成容器运行,运行可以伴随着容器,但是我们对数据的要求是:持久化的容器之间可以共享数据 各个Docker容器产生的数据,如果不通过docker commit
生成新的镜像,使得数据做为镜像的一部分保存下来, 那么当容器删除后,数据自然也就没有了。 为了能数据的持久化存储,在docker中我们使用数据卷(Volumes)。(有点类似我们Redis里面的rdb和aof文件,Redis笔记)
数据卷能干嘛:数据卷就是目录或文件,存在于一个或多个容器中,由docker挂载到容器,但不属于联合文件系统,因此能够绕过Union FileSystem提供一些用于持续存储或共享数据的特性:卷的设计目的就是数据的持久化,完全独立于容器的生存周期,因此Docker不会在容器删除时删除其挂载的数据卷
特点:
- 数据卷可在容器之间共享或重用数据
- 卷中的更改可以直接生效
- 数据卷中的更改不会包含在镜像的更新中
- 数据卷的生命周期一直持续到没有容器使用它为止
(容器的持久化,容器间继承+共享数据)
容器内添加数据卷
- 直接命令添加
- DockerFile添加
直接命令添加数据卷
1 | docker run -it -d -v /宿主机绝对路径目录:/容器内目录 镜像名 |
本地目录的路径必须是绝对路径,如果目录不存在,Docker会自动为你创建它(注意:Dockerfile 中不支持这种用法,这是因为 Dockerfile 是为了移植和分享用的。然而,不同操作系统的路径格式不一样,所以目前还不能支持(在Dockerfile中使用VOLUME指令来给镜像添加一个或多个数据卷))
Docker 挂载数据卷的默认权限是读写,用户也可以通过
:ro
指定为只读,如:
1
2 sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro
training/webapp python app.py加了
:ro
之后,就挂载为只读了(挂载为只读后宿主机中可以写,但容器内只能读)
之前也已经介绍了docker run
的相关参数了:
- -
v /宿主机绝对路径目录:/容器内目录
:绑定一个卷,注意前面是宿主机的绝对路径,:
后面才是容器内目录
例子:我们以之前commit
的qingbo/tomcat9:9.0镜像为例,先将刚刚运行的mytomcat9这个容器删除掉:docker rm mytomcat9
(之前也已经提到过,Docker的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器)
在挂载数据卷之前,我们来看一下宿主机相应目录:
然后根据qingbo/tomcat9:9.0镜像run一个mytomcat9容器,将tomcat中的/usr/local/tomcat/logs
文件夹挂载在/home/root/dockerVolumes/mytomcat9
这个文件夹中,以完成对日志文件的持久化存储(注意数据卷要为空,因为仅数据卷为空时才会复制,我们这里都没有文件夹所以就会新建一个,当然会为空)
输入命令:
1 | docker run -d --name mytomcat9 \ |
注意:如果直接挂载一个文件,很多文件编辑工具,包括
vi
或者sed --in-place
,可能会造成文件 inode 的改变,从 Docker 1.1 .0起,这会导致报错误信息。所以最简单的办法就直接挂载文件的父目录。
结果如下:
可以发现这时宿主机已经生成了相应目录并存储了我们指定的容器内文件夹的内容(即日志信息)
宿主机和容器内的相应的目录文件是共享的,即可以相互影响
比如说我们在宿主机内的
/home/root/dockerVolumes/mytomcat9
路径下新建一个centosTest.txt 文件这时我们进入mytomcat9容器内的
/usr/local/tomcat/logs
路径下,可以发现容器内也新增了centosTest.txt 文件:同理,我们在mytomcat9容器内的
/usr/local/tomcat/logs
路径下新建dockerTest.txt 文件,在宿主机的/home/root/dockerVolumes/mytomcat9
路径下也会新增dockerTest.txt 文件:
DockerFile添加数据卷
关于DockerFile的学习后面我们会具体提到,这里我们可以先用一下DockerFile。
DockerFile可以理解为是对镜像(image)的一种源码级的解释
首先创建DockerFile文件,我们在宿主机的/home/root/myDockerFile
路径下创建一个testDockerFile文件,编辑内容如下:
1 | # volume test |
使用DockerFile时,不需要指定宿主机的路径,Docker会自动关联,可以使用
docker inspect
来查看详细信息
然后我们使用docker build
来根据testDockerFile这个DockerFile文件来创建镜像,输入命令:
1 | docker build -f /home/root/myDockerFile/testDockerFile \ |
这里后面的点**
.
是表示当前路径**(docker build [OPTIONS] PATH | URL | -
,其中PATH代表含有Dockfile的目录,当然也可以是URL中含有Dockerfile),.
号,其实是在指定镜像构建过程中的上下文环境的目录。(如果在 Dockerfile 中使用了一些 COPY 等指令来操作文件,如何让 Docker引擎 获取到这些文件呢?这里就有了一个镜像构建上下文的概念,当构建的时候,由用户指定构建镜像的上下文路径,而 docker build 会将这个路径下所有的文件都打包上传给 Docker 引擎,引擎内将这些内容展开后,就能获取到所有指定上下文中的文件了。)
这个时候我们来docker run
一下刚刚创建的镜像,输入命令:
1 | docker run -it --name testCentos qingbo/centos:1.0 |
现在这两个文件夹还是空的,我们在dataVolumeContainer1文件夹中创建文件test01.txt:
这时我们可以通过**docker inspect
命令**来查看宿主机路径,输入命令:
1 | docker inspect testCentos |
注意:这里不要犯糊涂,将容器名
testCentos
写成镜像名qingbo/centos:1.0
!不然会找不到Mounts相关信息的(想想当然是这样)(千万注意这里是容器名)
结果如下:
其实以后我们可以通过输入
docker inspect testCentos |grep Mounts -A 25
命令来查看Mounts后的25行,这样方便查看:
我们去宿主机中的/var/lib/docker/volumes/62d0da6c688a9058a0fd128643471708e213c640fc3050e944dfb6ad525d1ae8/_data
路径下,可以发现我们刚刚在容器中创建文件也存在与宿主机中了:
数据卷容器
- 如果你有一些持续更新的数据需要在容器之间共享,最好创建数据卷容器
- 数据卷容器,其实就是一个正常的容器,专门用来提供数据卷供其它容器挂载的
命名的容器挂载数据卷,其它容器通过挂载这个(父容器)实现数据共享,挂载数据卷的容器,称之为数据卷容器。
容器间传递共享:--volumes-from
例子:首先我们创建一个命名的数据卷容器qing00:
1 | docker run -it --name qing00 qingbo/centos:1.0 |
接下来在其他容器中使用 --volumes-from
参数来挂载qing00容器中的数据卷:
1 | docker run -it --name qing01 --volumes-from qing00 qingbo/centos:1.0 |
- 还可以使用多个
--volumes-from
参数来从多个容器挂载多个数据卷,也可以从其他已经挂载了数据卷的容器来挂载数据卷- 使用
--volumes-from
参数所挂载数据卷的容器自己并不需要保持在运行状态
这时我们在数据卷容器qing00中的dataVolumeContainer1数据卷下创建text00.txt文件:
在qing01和qing02容器中查看:
在qing01容器的dataVolumeContainer2数据卷下创建text02.txt文件:
在数据卷容器qing00和另一个容器qing02中:
这样就实现了这三个容器之间的数据共享
注意:
- 如果删除了挂载的容器(包括 qing00、qing01和 qing02),数据卷并不会被自动删除,比如说如果我们删除了qing00容器,但qing01和qing02这两个容器之间依然可以共享数据
- 如果要删除一个数据卷,必须在删除最后一个还挂载着它的容器时使用
docker rm -v
命令来指定同时删除关联的容器这样的设定,可以让用户在容器之间升级和移动数据卷
利用数据卷容器来备份、恢复、迁移数据卷
可以利用数据卷对其中的数据进行进行备份、恢复和迁移。
备份:
首先使用
--volumes-from
标记来创建一个加载 dbdata 容器卷的容器,并从本地主机挂载当前到容器的 /backup 目录。命令如下:
1 $ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata容器启动后,使用了
tar
命令来将 dbdata 卷备份为本地的/backup/backup.tar
。恢复:
如果要恢复数据到一个容器,首先创建一个带有数据卷的容器 dbdata2。
1 $ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash然后创建另一个容器,挂载 dbdata2 的容器,并使用
untar
解压备份文件到挂载的容器卷中。
1
2 $ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf
/backup/backup.tar
DockerFile
简言之,Dockerfile是生成镜像的配置文件,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 # 指定tomcat版本
FROM tomcat:8.5.32-jre8
# 指定工作目录
WORKDIR /app
# 将打包后的 server.jar 拷贝到镜像中
# 可以使用脉冲云的编译构建服务,在线将源码打包成 jar
ADD server.jar /app/server.jar
# 设置镜像的启动命令
CMD java -jar /app/server.jar
# 声明需要监听的端口
EXPOSE 8080该文件中首先声明了镜像的基础镜像,一般情况下,你构建的镜像需要依赖一个基础镜像,就像你在一个电脑上安装软件的前提是这个电脑已经有了一个操作系统。
然后Dockerfile中记录着生成新镜像的一个个步骤,包括拷贝文件、执行命令等。
Dockerfile中还包含着一些其他信息的声明,比如环境变量、标注需要开放的端口等。
使用**
docker build
** 命令,可以依照Dockerfile中记录的步骤,一步步生成新的镜像。注意:在正在运行的容器内执行一个个命令,安装一个个软件,然后运行**
docker commit
也能生成一个新的镜像,但是请不要这样操作。因为,使用Dockerfile可以记录下来镜像的生成过程,并且能够随时调整其中的步骤,重新生成镜像。这就是传说中的基础设施即代码,将基础环境的配置当做软件编程来进行。**
Dockerfile是用来构建Docker镜像的构建文件,由一系列命令和参数构成的脚本。
构建步骤:
- 编写Dockerfile文件
docker build
在本地生成镜像docker run
根据镜像生成容器
从刚才的 docker commit 的学习中,我们可以了解到,镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
DockerFile基础知识&构建过程解析
Dockerfile基础知识:
- 每条保留字指令都必须为大写字母且后面要跟随至少一个参数
- 指令按照从上到下,顺序执行
- 注释用
#
- 每条指令都会创建一个新的镜像层,并对镜像进行提交
Docker执行Dockerfile的大致流程:
- docker根据基础镜像运行一个容器
- 执行一条指令并对容器作出修改
- 执行类似
docker commit
的操作向本地提交一个新的镜像层 - docker再基于刚提交的镜像运行一个新容器
- 执行dockerfile中的下一条指令直到所有指令都执行完成
Dockerfile指令
DockerFile的指令有如下:
FROM
MAINTAINER
RUN
CMD
EXPOSE
ENV
COPY
ADD
ENTRYPOINT
VOLUME
USER
WORKDIR
ONBUILD
FROM
FROM
:指定基础镜像
格式: FROM image
或FROM image:tag
例子:
1 | FROM debian:buster-slim |
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx
镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM
就是指定 基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
在 Docker Hub 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如
nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便开发、构建、运行各种语言应用的镜像,如node
、openjdk
、python
、ruby
、golang
等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如
ubuntu
、debian
、centos
、fedora
、alpine
等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
1 | FROM scratch |
如果以 scratch
为基础镜像的话,意味着不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接
FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
MAINTAINER
MAINTAINER
:指定维护者信息
格式为 MAINTAINER name
例子:
1 | # Maintainer: docker_user <docker_user at email.com> (@docker_user) |
MAINTAINER
不是必须的指令
RUN
RUN
:在镜像内运行命令(注意,这是在镜像打包过程中运行的命令,不是启动容器后的命令。RUN指令常常用来在镜像打包过程中安装软件)
格式为:RUN command
或 RUN ["executable", "param1", "param2"]
例子:
1 | RUN set -eux; \ |
RUN command
将在 shell 终端中运行命令,即/bin/sh -c
;RUN ["executable", "param1", "param2"]
则使用exec
执行。指定使用其它终端可以通过第二种方式实现,例如RUN ["/bin/bash", "-c", "echo hello"]
。
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
-
shell格式:
RUN <命令>
,就像直接在命令行中输入的命令一样例子:
1
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec格式:
RUN ["可执行文件", "参数1", "参数2"]
,这更像是函数调用中的格式
既然 RUN
就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
1 | FROM debian:stretch |
之前说过,Dockerfile 中每一个指令都会建立一层,RUN
也不例外。每一个 RUN
的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
上面的 Dockerfile
正确的写法应该是这样:
1 | FROM debian:stretch |
- 可以使用转移符
\
书写多行指令RUN
其实调用的是标准的shell,所以可以通过&&
连接执行多个命令
分析:之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN
去对应不同的命令,而是仅仅使用一个 RUN
指令,并使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。*在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。*并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \
的命令换行方式,以及行首 #
进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的**最后添加了清理工作的命令**,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。**因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。**很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
CMD
CMD
:指定容器启动时执行的命令(注意,和RUN
的区别是:RUN
是在打包过程中执行的命令,而CMD
是容器启动时执行的命令。镜像中只能有一条CMD
指令,如果有多个CMD指令,则以最后一条为准,所以我们可以覆盖基础镜像中定义的CMD指令)
支持三种格式:
CMD ["executable","param1","param2"]
:使用exec
执行(推荐方式)CMD command param1 param2
:在/bin/sh
中执行,提供给需要交互的应用CMD ["param1","param2"]
:提供给ENTRYPOINT
的默认参数
- shell 格式:
CMD <命令>
- exec 格式:
CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:
CMD ["参数1", "参数2"...]
,在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数
DockerFile文件里的CMD
会被docker run
之后的COMMAND替换(如docker run -it -P tomcat ls -l
,其中的ls -l
就会替换掉原来的 CMD ["catalina.sh", "run"]
)
例子:
1 | CMD ["mysqld"] |
在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如**,ubuntu
镜像默认的 CMD
是 /bin/bash
,如果我们直接 docker run -it ubuntu
的话,会直接进入 bash
。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release
。这就是用 cat /etc/os-release
命令替换了默认的 /bin/bash
命令了,输出了系统版本信息。**
在指令格式上,一般推荐使用 CMD ["executable","param1","param2"]
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
到
CMD
就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用
systemd
去启动后台服务,容器内没有后台服务的概念。一些初学者将
CMD
写为:
1 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
可执行文件,并且要求以前台形式运行。如:
1 CMD ["nginx", "-g", "daemon off;"]
EXPOSE
EXPOSE
:声明容器需要暴露的端口号
格式: EXPOSE <端口1> [<端口2>...]
例子:
1 | EXPOSE 3306 33060 |
EXPOSE
指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P
时,会自动随机映射 EXPOSE
的端口。
要将
EXPOSE
和在运行时使用-p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
ENV
ENV
:声明一个环境变量,可为后续的RUN、CMD、ENTRYPOINT程序所使用,并在容器运行时保持
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
例子:
1 | ENV PG_MAJOR 9.3 |
这里的例子采用的是
ENV key value
格式
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN
,还是运行时的应用,都可以直接使用这里定义的环境变量。
DockerFile中换行使用\
,对含有空格的值用双引号括起来,这和 Shell 下的行为是一致的,如:
1 | ENV VERSION=1.0 DEBUG=on \ |
定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方
node
镜像Dockerfile
中,就有类似这样的代码:
1
2
3
4
5
6
7
8
9 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
、FROM
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
、RUN
(环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份Dockerfile
制作更多的镜像,只需使用不同的环境变量即可。)
COPY
COPY
:复制本地文件到容器中(和ADD
区别是,不会自动解压tar包)
格式: COPY <src> <dest>
作用:复制本地主机的 <src>
(为 Dockerfile 所在目录的相对路径)到容器中的 <dest>
例子:
1 | COPY server.jar /app/server.jar |
和
ADD
类似,区别在于:COPY
不会自动解压tra包
注意:如果源路径为文件夹,复制的时候不是直接复制该文件夹,而是将文件夹中的内容复制到目标路径
ADD
ADD
:将Dockerfile所在目录中的文件拷贝到镜像中(如果 src
是一个tar包,那么会自动解压,并且src
支持网络路径)
格式: ADD <src> <dest>
作用:复制指定的 <src>
到容器中的 <dest>
。 其中 <src>
可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)
例子:
1 | ADD server.jar /app/server.jar |
ENTRYPOINT
ENTRYPOINT
:容器启动入口,即容器启动后执行的命令(不会被CMD指令覆盖,如果存在ENTRYPOINT
,那么CMD
指令会充当ENTRYPOINT的参数)
当指定了
ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将CMD
的内容作为参数传给ENTRYPOINT
指令,换句话说实际执行时,将变为:
1 ENTRYPOINT "<CMD>"
有两种格式:
ENTRYPOINT ["executable", "param1", "param2"]
(exec
格式)ENTRYPOINT command param1 param2
(shell
格式)
例子:
1 | ENTRYPOINT /app/entrypoint.sh # shell格式 |
注意:
ENTRYPOINT
:配置容器启动后执行的命令,并且不可被docker run
提供的参数覆盖- 每个 Dockerfile 中只能有一个
ENTRYPOINT
,当指定多个时,只有最后一个起效
VOLUME
关于VOLUME
指令在上面的笔记中已经使用过了,这里简单记录一下
VOLUME
:声明容器运行时的数据卷挂载点,将主机目录挂载到容器中,用来持久化保存容器生成的数据
格式有两种:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
例子:
1 | VOLUME ["/dataVolumeContainer1","/dataVolumeContainer2"] # 第一种 |
在 Dockerfile
中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
例如:
1 VOLUME /data这里的
/data
目录就会在容器运行时自动挂载为匿名卷,任何向/data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置,如:
1 docker run -d -v mydata:/data xxxx在这行命令中,就使用了
mydata
这个命名卷挂载到了/data
这个位置,替代了Dockerfile
中定义的匿名卷的挂载配置。
USER
USER
:指定运行容器时的用户名或 UID,后续的 RUN
也会使用指定用户
格式:USER <用户名>[:<用户组>]
例子:
1 | USER nginx |
USER
指令和WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行RUN
,CMD
以及ENTRYPOINT
这类命令的身份。
注意:USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换
示例:
1 | RUN groupadd -r redis && useradd -r -g redis redis |
如果以
root
执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用su
或者sudo
,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用gosu
。
WORKDIR
WORKDIR
:指定后续RUN
、CMD
、ENTRYPOINT
程序的工作目录,可以多次执行,就像Linux的 cd
命令
格式:WORKDIR <工作目录路径>
可以使用多个
WORKDIR
指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径
例子:
1 | WORKDIR /a |
使用
WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
一些初学者常犯的错误是把 Dockerfile
等同于 Shell 脚本来书写,如:
1 | RUN cd /app |
如果将这个 Dockerfile
进行构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是 hello
。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile
中,这两行 RUN
命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile
构建分层存储的概念不了解所导致的错误。
之前说过每一个
RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。第一层RUN cd /app
的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR
指令:
1 | WORKDIR /app |
ONBUILD
ONBUILD
:配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令
格式:ONBUILD <其它指令>
例子:
1 | ONBUILD ADD . /app/src |
ONBUILD
是一个特殊的指令,它后面跟的是其它指令,比如RUN
,COPY
等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
Dockerfile
中的其它指令都是为了定制当前镜像而准备的,唯有ONBUILD
是为了帮助别人定制自己而准备的。
例如,Dockerfile 使用如下的内容创建了镜像 image-A
:
1 | [...] |
如果基于镜像image-A
创建新的镜像时,新的Dockerfile中使用 FROM image-A
指定基础镜像时,会自动执行ONBUILD
指令内容,等价于在后面添加了两条指令,即:
1 | FROM image-A |
使用 ONBUILD
指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild
DockerFile案例
自定义镜像mycentos
之前我们也已经运行过精简版的centos容器了,可以发现有以下问题:
于是我们要通过DockerFile来定制一个mycentos镜像,要求:
- 登录后的默认路径为:
/home
- 可以使用
vim
- 可以通过
ifconfig
查看网络配置
构建步骤:
- 编写Dockerfile文件
docker build
在本地生成镜像docker run
根据镜像生成容器
首先编写DockerFile文件如下:
1 | # 指定基础镜像 |
接下来我们通过docker build
在本地构建镜像:
docker build
:用于使用 Dockerfile 创建镜像语法:
1 docker build [OPTIONS] PATH | URL | -常用:
-f
:指定要使用的Dockerfile路径--quiet
,-q
:安静模式,成功后只输出镜像 ID-rm
:设置镜像成功后删除中间容器--tag
,-t
:镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签
输入命令:
1 | docker build -f /home/root/myDockerFile/mycentosDockerFile -t mycentos:1.1 . |
这里后面的点**
.
是表示当前路径**(docker build [OPTIONS] PATH | URL | -
,其中PATH代表含有Dockfile的目录,当然也可以是URL中含有Dockerfile),.
号,其实是在指定镜像构建过程中的上下文环境的目录。官方文档中是这么说的:The
.
at the end of thedocker build
command tells that Docker should look for theDockerfile
in the current directory.(如果在 Dockerfile 中使用了一些 COPY 等指令来操作文件,如何让 Docker引擎 获取到这些文件呢?这里就有了一个镜像构建上下文的概念,当构建的时候,由用户指定构建镜像的上下文路径,而 docker build 会将这个路径下所有的文件都打包上传给 Docker 引擎,引擎内将这些内容展开后,就能获取到所有指定上下文中的文件了。)
最后我们通过docker run
来生成容器,输入命令:
1 | docker run -it --name mycentos mycentos:1.1 |
结果如下:
可以看到上面提到的3个需求都实现了。
CMD&ENTRYPOINT指令案例
ONBUILD指令案例
自定义镜像tomcat9
环境准备:
首先在宿主机/home/root/myDockerFile/mytomcat9
路径下准备好:
- 一个copy.txt文件(用于测试COPY指令)
jdk-8u171-linux-x64.tar.gz
和apache-tomcat-9.0.45.tar.gz
文件
然后就可以编写DockerFile了,在当前目录下创建DockerFile文件(注意文件命名就为DockerFile),内容如下:
1 | # 指定基础镜像 |
接着我们通过docker build
来构建镜像:
1 | docker build -t mytomcat9:1.0 . |
这里因为我们当前所在路径就在DockerFile文件(注意文件命名就为Dockerfile)所在路径,所以没有用
-f /home/root/myDockerFile/mytomcat9/tomcat9DockerFile
这一参数来指定Dockerfile文件所在路径在以后的大多数情况,我们构建一个Docker镜像往往是以下步骤:
跳到Dockerfile文件所在目录
执行
docker build
构建命令
1 docker build -t <imageName:imageTag> .关于
docker build
指令的几点重要的说明:
- 如果构建镜像时没有明确指定 Dockerfile,那么Docker客户端默认在构建镜像时指定的上下文路径下找名字为 Dockerfile 的构建文件
- Dockerfile 可以不在构建上下文路径下,此时需要构建时通过 -f 参数明确指定使用哪个构建文件,并且名称可以自己任意命名
理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app
不工作后,于是干脆将 Dockerfile
放到了硬盘根目录去构建,结果发现 docker build
执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build
打包整个硬盘,这显然是使用错误。
一般来说,应该会将 Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore
一样的语法写一个 .dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
在默认情况下,如果不额外指定 Dockerfile
的话,会将上下文目录下的名为 Dockerfile
的文件作为 Dockerfile。(这就是为什么上面强调了命名为Dockerfile)
这只是默认行为,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile
参数指定某个文件作为 Dockerfile
。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
我们输入docker build -t mytomcat9:1.0 .
后的结果如下:
最后我们docker run来生成一个tomcat9镜像来验证一下,输入命令:
1 | docker run -d --name mytomcat9 \ |
可以发现成功运行:
可以发现ADD
和COPY
指令是生效的:
且我们在docker run
命令中指定的数据卷也生效了:
自定义tomcat9部署
在上面的docker run
时我们已经配置了数据卷:
所以我们可以直接在宿主机的/home/root/dockerVolumes/mytomcat/test
路径下放我们需要发布的内容即可:
创建hello.html,代码如下:
1 |
|
然后我们访问:http://192.168.219.101:8888/test/hello.html
,结果如下:
本地镜像推送到阿里云
docker push
:将本地的镜像上传到镜像仓库(但是要先登陆到镜像仓库**docker login
**)
语法:
1 | docker push [OPTIONS] NAME[:TAG] |
OPTIONS说明:
--disable-content-trust
:忽略镜像的校验(默认开启)
步骤:
docker login
:登录到镜像仓库docker commit
:根据正在运行的容器推送到本地(这一步可以省略,因为也有其他方法生成镜像如DockerFile)docker push
:将镜像推送到远程仓库
我们在这里点击管理即可看到相关命令
要拉取镜像的话只需要:docker pull registry.cn-zhangjiakou.aliyuncs.com/qingbo1011/node1:[镜像版本号]
即可。
通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过
<仓库名>:<标签>
的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以latest
作为默认标签。以 Ubuntu 镜像 为例,
ubuntu
是仓库的名字,其内包含有不同的版本标签,如,16.04
,18.04
。我们可以通过ubuntu:16.04
,或者ubuntu:18.04
来具体指定所需哪个版本的镜像。如果忽略了标签,比如ubuntu
,那将视为ubuntu:latest
。