---- 接上篇 ----
什么是Docker镜像?
Docker镜像是由文件系统叠加而成。最低端是一个引导文件系统,即bootfs,这很像典型的Linux/Unix的引导文件系统。当一个容器启动后,它会被移到内存中,而引导文件系统则会被卸载,以留出更多的内存供initrd磁盘镜像使用。第二层是root文件系统rootfs,它位于引导文件系统之上。
在传统的Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束后并完成完整性检查之后,它才会被切换为读写模式。但是在Docker里,root文件系统永远只能是只读状态,并且Docker利用联合加载技术又会在root文件系统层上加载更多的只读文件系统(联合文件系统指的是一次性加载多个文件系统,但是在完成看起来只能看见一个文件系统)。联合加载会将各层文件系统叠加在一起,这样最终的文件系统会包含所有的底层文件和目录。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像,可以依次类推,直到镜像栈的最底部,最底部的镜像栈称为基础镜像。
当一个镜像启动时,Docker会在该镜像的最顶层加载一个读写文件系统。我们想在Docker中运行的程序就是在这个读写层中执行的。
当Docker第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,如果想修改一个文件,这个文件首先会从该读写层下面的只读层复制到改读写层。改文件只读版本依然存在,但是已经被读写层中的改文件副本所隐藏。(通常这种机制被称为写时复制:每个只读镜像都是只读的,并且以后永远不会变化。当创建一个容器时,Docker会构建出一个镜像栈,并在栈顶添加一个读写层。这个读写层再加上其下面的镜像层以及一些配置数据,就构成了一个容器。)
查看Docker主机上的可用镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/ubuntu latest 113a43faa138 6 weeks ago 81.2 MB
docker.io/ubuntu 12.04 5b117edd0b76 15 months ago 104 MB
该指令会列出宿主机上已下载的所有镜像(保存在/var/lib/docker目录下)。通过TAG来区分同一仓库的不同镜像,我们可以通过仓库名后面加上一个冒号和标签名来指定该仓库中的某一镜像,如下:
$ docker run -t -i --name new_container ubuntu:12.04 /bin/bash
该命令可以在后面加上镜像的名称来查看指定的镜像,如下:
$ docker images ubuntu:12.04
REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/ubuntu 12.04 5b117edd0b76 15 months ago 104 MB
拉取镜像:
通过docker run命令从镜像启动一个容器时,如果发现镜像不在本地,docker会先从Docker Hub下载该镜像。如果没有指定具体的标签,那么docker会自动下载latest标签的镜像。
$ docker pull ubuntu:12.04
也可以通过docker pull先将镜像拉取到本地,可以节省从一个新镜像启动一个容器所需的时间。
查找镜像:
$ docker search nginx
INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
docker.io docker.io/nginx Official build of Nginx. 9058 [OK]
docker.io docker.io/jwilder/nginx-proxy Automated Nginx reverse proxy for docker c... 1363 [OK]
docker.io docker.io/richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable ... 591 [OK]
改命令会在Docker Hub上所有带有nginx的镜像
构建自己的镜像:
构建镜像有两种方法:
- 使用docker commit命令(不推荐)
- 使用docker build命令和Dockerfile文件
一般来说,我们不是真正的创建新镜像,而是基于一个已有的基础镜像,构建出新镜像而已。
首先:
登录到Docker hub:
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: zhagnhao
Password:
Login Succeeded
也可以使用docker logout退出登录。
(1.)docker commit
- 先基于一个镜像创建一个容器 docker run -i -t --name my_container ubuntu /bin/bash
- 容器中进行相应操作。如:安装包apt-get update && apt-get -y install apache2
- 退出容器。执行docker commit container_id zhagnhao/my_container
需要注意的是,docker commit提交的只是创建容器的镜像与容器的当前状态之间的差异部分,这使得该更新非常轻量。
(2.)docker build && Dockerfile
首先,创建一个Dockerfile文件:
$ mkdir my_web && cd my_web && touch Dockerfile
我们创建了一个my_web目录用来保存Dockerfile,这个目录就是我们的构建环境,Docker则称此为环境上下文或者构建上下文。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到docker守护进程。这样docker守护进程就能直接访问用户想在镜像中存储的任何代码、文件或者其它数据。
接着,我们创建了一个空的Dockerfile,编辑Dockerfile:
# version 1.0.0
FROM ubuntu:14.04
MAINTAINER zhanghao
RUN apt-get update && apt-get install -y nginx
RUN echo 'Hi success'> /usr/share/nginx/html/index.html
EXPOSE 80
Docker运行Dockerfile的大体步骤:
- Docker从基础镜像运行一个容器
- 执行一条命令,对容器做出修改
- 执行类似的docker commit操作,提交一个新的镜像层
- Docker再基于刚提交的镜像运行一个新的容器
- 执行Dockerfile的下一条指令,直到所有指令都执行完成
从上面可以看出,如果用户的Dockerfile由于某些原因导致没有正常结束,那么用户将得到一个可以使用的镜像,但是指定的仓库和镜像名不会生效。(这对调试非常有帮助)
- 每一个Dockerfile的第一条指令必须是FROM。并且FROM指定的镜像必须是一个已经存在的镜像,后续的指令都将基于该镜像进行。
- MAINTAINER会告诉Docker作者是谁。这有助于标示镜像的所有者和联系方式
- RUN会在当前镜像中运行指定的命令。每条RUN指令都会创建一个新的镜像层,如果该指令执行成功,就会将该镜像层提交,之后继续执行Dockerfile的下一条指令
- EXPOSE指令,这条指令告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不是意味着可以自动访问任意容器运行中服务的端口。出于安全的原因,Docker并不会自动打开该端口,而是需要用户在使用docker run运行容器时来指定需要打开哪些端口。
构建镜像:
$ cd my_web
$ docker build -t="my_web/nginx" .
通过-t选项为新镜像设置仓库和名称。上面告诉Docker在当前目录查找Dockerfile文件。
构建缓存:
为了提高镜像构建的速度,Docker会缓存构建过程中的中间镜像。当从一个已在缓存中的基础镜像开始构建新镜像时,会将dockerfile中的下一条指令和基础镜像的所有子镜像做比较,如果有一个子镜像是由相同的指令生成的,则命中缓存,直接使用该镜像。这里我们需要注意一点,镜像制作过程中缓存命中与否检查的是dockerfile中的指令,并不会检查镜像是否一致;当然COPY,ADD两个指令例外,不仅检测会指令字符集,还会检测对比需要添加的文件是否一致(校验不考虑修改访问时间,考虑元数据以及内容)如我们在镜像中使用了
RUN apt-get -y upgrade
指令,docker daemon在校验的时候,会判断指令集相同,从而使用缓存并不会更新。解决这个问题的其中一种方法就是在docker build时添加参数 --no-cache=true
从新镜像启动容器:
$ docker run -d -p 80 --name my_web my_web/nginx nginx -g "daemon off;"
这里我们通过刚刚构建的镜像启动了一个新的容器,通过添加-d参数,告诉docker在后台运行。我们也指定了需要在容器中运行的命令nginx -g "daemo off;"(注意要有;号)。这将以前台运行的方式启动Nginx,来作为我们的Web服务器。
通过添加-p标志来控制Docker在运行时应该公开哪些端口给外部(宿主机),运行一个容器时,Docker可以通过两种方法来在宿主机上分配端口。
- docker可以在宿主机上随机选择位于32768~61000的一个比较大的端口号来映射到容器中的80端口上
- 可以在Docker宿主机中指定一个具体的端口号来映射容器到容器中的80端口上
docker run命令将在Docker宿主机上随机打开一个端口,这个端口会连接到容器中的80端口上。
可以通过docker ps查看端口的使用情况:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
278998b7b8cb zhanghao/nginx "nginx -g 'daemon ..." 3 seconds ago Up 2 seconds 0.0.0.0:32769->80/tcp my_web
可以看到,容器中的80端口映射到了宿主机的32769端口上。
我们也可以通过docker port来查看容器的端口映射情况:
$ docker port my_web
80/tcp -> 0.0.0.0:32769
-p选项还为我们在将容器端口想宿主机公开时提供一个灵活性。比如,可以指定容器中的端口映射到Docker宿主机的某一特定端口上,如下:
$ docker run -d -p 8080:80 --name my_web zhanghao/nginx nginx -g "daemon off;"
上面的命令会将容器内的80端口绑定到本地宿主机的8080端口上。
Docker还提供一个更简单的方式,即-P(注意这里是大写P)参数,该参数可以用来对外公开在Dockerfile中通过EXPOSE指令公开的所有端口,如下:
$ docker run -d -P --name my_web2 zhanghao/nginx nginx -g "daemon off;"
该命令会将容器内的80端口对本地宿主机公开,并且绑定到宿主机的一个随机端口上。该命令会将用来狗将该镜像的Dockerfile文件中EXPOSE指令指定的其它端口也一并公开。
Dockerfile中的指令:
(1)CMD:用于指定一个容器启动时要运行的命令。这里有点类似于RUN指令,只是RUN指令是指定构建时要运行的命令,而CMD是指定容器启动时要运行的命令。这和使用docker run命令启动容器时指定要运行的命令非常类似。
- 需要注意的是,要运行的命令存放在一个数组结构中(如:CMD ["/bin/bash","-l"],这里的-l标志传递给了/bin/bash)
- 使用doker run命令可以覆盖CMD指令:如果我们在Dockerfile里指定了CMD指令,而同时在docker run命令行中也指定了要运行的命令,命令行的指定的命令会覆盖Dockerfile中的CMD指令。
- 在Dockerfile中只能指定一条CMD指令。如果指定了多条,也只有最后一条会被执行
(2)ENTRYPOINT:与CMD非常类似,也很容易和CMD指令弄混。ENTRYPOINT指令提供的命令不容易在启动容器时被覆盖。实际上,docker run命令行中指定的任何参数都会被当做参数再次传递给ENTRYPOINT指令中指定的命令。
(3)WORKDIR:用来在从镜像创建一个新的容器时,在容器内部设定一个工作目录。ENTRYPOINT和/或CMD指定的程序会在这个目录下执行。我们可以使用该指令为Dockerfile中后续的一系列指令设置工作目录,也可以为最终的容器设置工作目录。
WORDKDIR /opt/webapp/db
RUN bundle install
WORKDIR /opt/webapp
ENTRYPOINT ["rackup"]
这里,我们将工作目录切换为/opt/webapp/db后运行了bundle install命令,之后又将工作目录设置为/opt/webapp,最后设置了ENTRYPOINT指令来启动rackup命令。
可以通过-w标志在运行时覆盖工作目录,如下:
$ docker run -ti -w /var/log ubuntu pwd /var/log
该命令将容器内的工作目录设置为/var/log
(4)ENV:用来在构建过程中设置环境变量
ENV MY_PATH /home
WORDDIR $MY_PATH
这个环境变量可以在后续的任何RUN指令中使用。也可以通过-e参数来传递环境变量。
$ docker run -ti -e "PORT=8080" ubuntu env
(5)USER:用来指定该镜像会以什么样的用户去执行。
USER nginx
表示基于该镜像启动的容器会以nginx用户的身份来执行。这里可以指定用户名或UID以及组或GID。也可以通过添加-u标志来覆盖指令指定的值。(如果不通过USER指令指定用户,默认用户为root)
(6)VOLUME:用来向基于镜像创建的容器添加卷。一个卷可以存在一个或多个容器内特定的目录,这个目录可以绕过联合文件系统,并提供如下共享数据或者对数据进行持久化功能。
- 卷可以在容器共享和重用
- 一个容器可以不是必须和其他容器共享卷
- 对卷的修改是立即生效的
- 对卷的修改不会对更新镜像产生影响
- 卷会一直存在直到没有任何容器在使用它
卷功能让我们可以将数据、数据库或者其它内容添加到镜像中而不是将这些内容提交到镜像中,并且允许我们在多容器之间共享这些内容。(可以指定多个卷)
VOLUME ["/opt/project"]
这条指令将会为基于此镜像创建的任何容器创建一个名为/opt/project的挂载点。
docker cp是和VOLUME指令相关并且也是很实用的指令。该命令允许从容器复制文件和复制文件到容器上。
(7)ADD:用来将构建环境下的文件和目录复制到镜像中。
ADD myweb.war /opt/my/tomcat/webapp/
这里将构建目录下打好的war包复制到镜像中的/opt/my/tomcat/webapp/下。指向的源文件可以是一个URL,或者构建上下文环境中的文件名或目录。不能对构建目录或者上下文环境之外的文件进行ADD操作。
docker会根据源后缀判断是文件还是目录。如源文件为归档文件(gzip、xz、bzip2),Docker会自动将归档文件解开。
ADD会使得构建缓存变得无效:如果通过ADD指令向镜像中添加一个文件或目录,那么将使得Dockerfile中的后续指令都不能使用之前的构建缓存。
(8)COPY:改指令非常类似于ADD,它们根本的不同是COPY只关心在构建上下文中复制本地文件,而不会去做文件提取和解压工作。
COPY myweb.war /opt/
文件源路径必须是一个与当前构建环境相对的文件或目录,本地文件都放到和Dockerfile同一个目录下。不能复制该目录之外的任何文件,因为构建环境将会上传到Docker守护进程,而复制是docker守护进程中进行的。任何位于构建环境之外的东西都是不可用的。COPY指令的目的位置则是容器内部的一个绝对路径。
(9)LABEL:用户Docker镜像添加元数据。元数据以键值对的形式展现出来。
LABEL version="1.0" type="Data Center"
可通过docker inspect查看
(10)STOPSIGNAL:用来设置停止容器时发送什么系统调用信号给容器。
(11)AVG:用来定义可以在docker build命令运行时传递给构建运行时的变量,我们只需要在构建时使用--build-arg标志即可。用户只能在构建时指定在Dockerfile文件中定义过的参数
ARG build
ARG webapp_user=user
上面的第二条AVG指令设置了一个默认值,如果构建时没有为改参数指定值,就会使用这个默认值。
(12)ONBUILD:能为镜像添加触发器。当一个镜像被用做其它镜像的基础镜像时,该镜像中的触发器将会被执行。触发器会在构建过程中插入新的指令,我们可以认为这些指令是紧跟在FROM之后指定的。触发器可以是任何构建命令。
ONBUILD ADD ../app/src
ONBUILD RUN cd /app/src && make
ONBUILD指令可以通过docker inspect命令来查看。
将镜像推送到Docker Hub:
$ docker push my_web
删除镜像:
$ docker rmi zhanghao/nginx
运行自己的Docker Registry:
从容器运行Registry:
$ docker run -p 5000:5000 registry:2
该命令会启动一个运行Registry应用2.0版本的容器,并将5000端口绑定到本地宿主机。
使用Registry:
(1)使用新Registry为镜像打上标签
$ docker tag 22d343feef docker.example.com:5000/zhanghao/nginx
(2)打完标签后,就能通过docker push命令将它推送到新的Registry中去了
$ docker push docker.example.com:5000/zhanghao/nginx
从本地Registry构建新容器:
$ docker run -it docker.example.coom:5000/zhanghao/nginx /bin/sh