原文地址:http://www.eussi.top/view/23
1、docker镜像简介
Docker镜像是由文件系统叠加而成。最底端是一个引导文件系统,即 bootfs,这很像典型的 Linux/unix的引导文件系统。 Docker用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动后,它将会被移到内存中,而引导文件系统则会被卸载(unmount),以留出更多的内存供initrd磁盘镜像使用。
到目前为止,Docker看起来还很像一个典型的 Linux虚拟化栈。实际上, Docker镜像的第二层是root文件系统rootfs,它位于引导文件系统之上。 rootfs可以是一种或多种操作系统(如 Debian或者 Ubuntu文件系统)。
在传统的 Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检査之后,它才会被切换为读写模式。但是在 Docker里,root文件系统永远只能是只读状态,并且 Docker利用联合加载( union mount)技术又会在root文件系统层上加载更多的只读文件系统。联合加载指的是一次同时加载多个文件系统,但是在外面看起来只能看到一个文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像( parent image),可以依次类推,直到镜像栈的最底部,最底部的镜像称为基础镜像( base image)。最后,当从一个镜像启动容器时, Docker会在该镜像的最顶层加载一个读写文件系统。我们想在 Docker中运行的程序就是在这个读写层中执行的。如图:
当 Docker第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,如果想修改一个文件,这个文件首先会从该读写层下面的只读层复制到该读写层。该文件的只读版本依然存在,但是已经被读写层中的该文件副本所隐藏。
通常这种机制被称为写时复制( copy on write),这也是使 Docker如此强大的技术之一。每个只读镜像层都是只读的,并且以后永远不会变化。当创建一个新容器时,Docker会构建出一个镜像栈,并在栈的最顶端添加一个读写层。这个读写层再加上其下面的镜像层以及些配置数据,就构成了一个容器。我们已经知道,容器是可以修改的,它们都有自己的状态,并且是可以启动和停止的。容器的这种特点加上镜像分层框架( image-layering framework),使我们可以快速构建镜像并运行包含我们自己的应用程序和服务的容器。
2、列出镜像
docker images
本地镜像都保存在 Docker宿主机的/var/lib/docker
目录下。每个镜像都保存在 Docker所采用的存储驱动目录下面,如aufs或者 devicemapper。也可以在/var/lib/docker/containers
目录下面看到所有的容器。
镜像从仓库下载下来。镜像保存在仓库中,而仓库存在于Registry中。默认的 Registry是由Docker公司运营的公共Registry服务,即 Dockerhub,地址为 https://hub.docker.com/
Docker Registry的代码是开源的,也可以运行自己私有的Registry。
在Docker Hub中,镜像是保存在仓库中的,里面存储了镜像、层以及关于镜像的元数据。每个镜像的仓库都可以存放很多镜像。查看某仓库的镜像可以通过
docker pull centos
拉取镜像(使用国外镜像拉取下载可能会非常慢,还会经常失败),再通过
docker images
查看镜像
centos 6.6 368c96d786ae 2 weeks ago 203MB
centos centos6.6 368c96d786ae 2 weeks ago 203MB
centos 6.7 9f1de3c6ad53 2 weeks ago 191MB
centos centos6.7 9f1de3c6ad53 2 weeks ago 191MB
centos 6.8 82f3b5f3c58f 2 weeks ago 195MB
centos centos6.8 82f3b5f3c58f 2 weeks ago 195MB
centos 6.9 2199b8eb8390 2 weeks ago 195MB
centos centos6.9 2199b8eb8390 2 weeks ago 195MB
centos 6.10 48650444e419 2 weeks ago 194MB
centos centos6.10 48650444e419 2 weeks ago 194MB
centos 7.0.1406 cc2cf48cc784 2 weeks ago 210MB
centos centos7.0.1406 cc2cf48cc784 2 weeks ago 210MB
centos 7.1.1503 e1430271e2f9 2 weeks ago 212MB
centos centos7.1.1503 e1430271e2f9 2 weeks ago 212MB
centos 7.2.1511 9aec5c5fe4ba 2 weeks ago 195MB
centos centos7.2.1511 9aec5c5fe4ba 2 weeks ago 195MB
......
以上是聚集在一个仓库下的一系列镜像。我们可以看好好多版本的centos,虽然我们称上面的为centos操作系统,但是需要注意的是,它实际上不是一个完整的操作系统。它只是一个裁剪版,只包含最低限度的支持系统运行的组件。
为了区分同一个仓库中的不同镜像,Docker提供了一种称为标签(tag)的功能。每个镜像在列出来时都带有一个标签,如6.6、centos6.6 等。每个标签对组成特定镜像的一些镜像层进行标记。这种机制使得在同一个仓库中可以存储多个镜像。
我们可以通过在仓库名后面加上一个冒号和标签名来指定该仓库中的某一镜像,如代码
docker run -t -i --name new_container centos:7.2.1511 /bin/bash
我们还能看到很多镜像具有相同的镜像,它们被打了很多个标签。在构建容器时指定仓库的标签也是一个很好的习惯。这样便可以准确地指定容器来源于哪里。指定镜像的标签会让我们确切知道自己使用的是什么版本,这样我们就能准确知道我们在干什么。
Docker hub中有两种类型的仓库:用户仓库( user repository)和顶层仓库(top-evel repository)。用户仓库的镜像都是由 Docker用户创建的,而顶层仓库则是由 Docker内部的人来管理的。
用户仓库的命名由用户名和仓库名两部分组成,如 eussi/test-repo
- 用户名: eussi
- 仓库名: test-repo
与之相对,顶层仓库只包含仓库名部分,如 centos仓库。顶层仓库由 Docker公司和由选定的能提供优质基础镜像的厂商管理,用户可以基于这些基础镜像构建自己的镜像。同时顶层仓库也代表了各厂商和 Docker公司的一种承诺,即顶层仓库中的镜像是架构良好、安全且最新的
注意,用户贡献的镜像都是由 Docker社区用户提供的,这些镜像并没有经过 Docker公司的确认和验证,在使用这些镜像时需要自己承担相应的风险。
3、拉取镜像
用 docker run命令从镜像启动一个容器时,如果该镜像不在本地,Docker会先从Docker Hub下载该镜像。如果没有指定具体的镜像标签,那么 Docker会自动下载 latest标签的镜像。
另外可以执行标签,拉取指定标签的镜像:
docker pull centos:centos6.6
4、查找镜像
通过 docker search命令来查找所有 Docker hub上公共的可用镜像,如
docker search java
当然也可以在 Docker Hub网站上在线查找可用镜像。
通过上面命令会完成镜像查找工作返回如下信息:
-
NAME —— 仓库名
-
DESCRIPTION —— 镜像描述
-
STARS —— 用户评价,反应出一个镜像的受欢迎程度
-
OFFICIAL —— 是否官方,由上游开发者管理的镜像(如 fedora镜像由 Fedora团队管理)
-
AUTOMATED —— 自动构建,表示这个镜像是由 Docker hub的自动构建( Automated Build)流程创建的
5、构建镜像
构建docker镜像有两种办法:
-
使用docker commit命令
-
使用docker build命令和Dockerfile文件
不推荐使用docker commit命令,推荐使用更灵活更强大的Dockerfile来构建镜像。当然这里介绍的不是真正的构建镜像,而是基于一个基础镜像如centos镜像,构建新镜像。
5.1 创建Docker Hub账号
构建镜像中很重要的一环是如何共享和发布镜像。可以将镜像推送到Docker Hub或用户自己私有的Registry中。我们可以在Docker Hub上创建一个账号,注册地址:https://hub.docker.com/signup
注册之后,就可以测试账号是否正常工作了。登录到Docker Hub,可以使用docker login
指令。
$ 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: eussi
Password:
Login Succeeded
此命令将完成登录到Docker Hub的工作,并将认证信息保存在以供后续使用。
如果登录总是超时,如:
Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
Error response from daemon: Get https://registry-1.docker.io/v2/: dial tcp: lookup registry-1.docker.io on 192.168.198.2:53: read udp 192.168.198.202:56552->192.168.198.2:53: i/o timeout
Error response from daemon: Get https://registry-1.docker.io/v2/: Get https://auth.docker.io/token?account=eussi&client_id=docker&offline_token=true&service=registry.docker.io: dial tcp: lookup auth.docker.io on 192.168.198.2:53: read udp 192.168.198.202:37243->192.168.198.2:53: i/o timeout
等等
可以通过修改/etc/hosts的方式制定IP进行访问
[root@app2 ~]# docker login -u eussi -p gw1314
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded
[root@app2 ~]# dig @114.114.114.114 registry-1.docker.io
; <<>> DiG 9.9.4-RedHat-9.9.4-18.el7 <<>> @114.114.114.114 registry-1.docker.io
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27496
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;registry-1.docker.io. IN A
;; ANSWER SECTION:
registry-1.docker.io. 38 IN A 34.232.31.24
registry-1.docker.io. 38 IN A 34.228.211.243
registry-1.docker.io. 38 IN A 52.70.175.131
registry-1.docker.io. 38 IN A 34.206.236.31
registry-1.docker.io. 38 IN A 52.22.201.61
registry-1.docker.io. 38 IN A 34.233.151.211
registry-1.docker.io. 38 IN A 52.2.186.244
registry-1.docker.io. 38 IN A 34.201.196.144
;; Query time: 20 msec
;; SERVER: 114.114.114.114#53(114.114.114.114)
;; WHEN: Wed Aug 07 13:58:38 CST 2019
;; MSG SIZE rcvd: 177
#在hosts文件中添加一个IP,保存后再进行测试
[root@app2 ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
34.201.196.144 registry-1.docker.io
5.2 使用Docker commit命令创建镜像
使用docker commit命令很简单,可以理解为在使用虚拟机创建快照一样。我们使用一个镜像运行一个容器,在容器中做一些事情,然后exit退出,我们便可以类似创建快照一样创建一个镜像。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos 7.2.1511 9aec5c5fe4ba 2 weeks ago 195MB
centos latest 9f38484d220f 2 weeks ago 202MB
ubuntu latest 94e814e2efa8 3 weeks ago 88.9MB
$ docker run -i -t centos /bin/bash
[root@1ee613ff5342 /]# touch a.txt
[root@1ee613ff5342 /]# exit
exit
$ docker ps -l -q
1ee613ff5342
$ docker commit 1ee613ff5342 eussi/test-repo
sha256:6fa3a6edc1219df2dbd59f59653c60547a598e47758bb31c6de9734847a3afff
$ docker images eussi/test-repo
REPOSITORY TAG IMAGE ID CREATED SIZE
eussi/test-repo latest 6fa3a6edc121 4 minutes ago 202MB
使用docker inspect命令查看新创建的镜像的详细信息,如:
docker inspect eussi/test-repo
也可以在提交镜像时指定更多的数据(包括标签)来详细描述所做的修改。如:
docker commit -m="A new image" --author="eussi" 1ee613ff5342 eussi/test-repo:test
此时便可以通过docker run运行我们新创建的镜像了,可以看到之前所做的修改,如:
docker run -i -t eussi/test-repo /bin/bash
5.3 使用Dockerfile命令创建镜像
Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,之后使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。
先创建了一个名为docker_context的目录用来保存 Dockerfile,这个目录就是我们的构建环境( build environment),Docker则称此环境为上下文( context)或者构建上下文( build context)。 Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样 Docker守护进程就能直接访问想在镜像中存储的仼何代码、文件或者其他数据。
我们创建Dockerfile内容如下:
# Version:0.0.1
FROM centos
MAINTAINER Wang Xmeming "wangxuemingcn@yeah.net"
RUN rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
RUN yum install -y nginx
RUN echo 'Hi, I am nginx in docker' > /usr/share/nginx/html/index.html
EXPOSE 80
该 Dockerfile由一系列指令和参数组成。每条指令,如FROM,都必须为大写字母,且后面要跟随一个参数:FROM centos,Dockerfile中的指令会按顺序从上到下执行,所以应该根据需要合理安排指令的顺序。
每条指令都会创建一个新的镜像层并对镜像进行提交。 Docker大体上按照如下流程执行 Dockerfile中的指令:
- Docker从基础镜像运行一个容器。
- 执行一条指令,对容器做出修改。
- 执行类似 docker commit的操作,提交一个新的镜像层
- Docker再基于刚提交的镜像运行一个新容器。
- 执行 Dockerfile中的下一条指令,直到所有指令都执行完毕。
从上面也可以看出,如果你的 Dockerfile由于某些原因(如某条指令失败了)没有正常结束,那么你将得到了一个可以使用的镜像。这对调试非常有帮助:可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么你的指令会失败进行调试。
Dockerfile也支持注释。以#开头的行都会被认为是注释。
每个 Dockerfile的第一条指令都应该是FROM。FROM指令指定一个已经存在的镜像,后续指令都将基于该镜像进行,这个镜像被称为基础镜像( base iamge)。在运行一个容器时,必须要指明是基于哪个基础镜像在进行构建。接着指定MAINTAINER指令,这条指令会告诉Docker该镜像的作者是谁,以及作者的电子邮件地址。这有助于标识镜像的所有者和联系方式。在这些指令之后,我们指定了RUN指令。RUN指令会在当前镜像中运行指定的命令。在这个例子里,我们通过RUN指令安装了 nginx包。
像前面说的那样,每条RUN指令都会创建一个新的镜像层,如果该指令执行成功,就会将此镜像层提交,之后继续执行Dockerfile中的下一条指令。默认情况下,RUN指令会在shel里使用命令包装器/bin/sh-c来执行。如果是在
个不支持shel平台上运行或者不希望在she运行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令:
RUN [ "yum", "install", "-y", "nginx" ]
在这种方式中,使用一个数组来指定要运行的命令和传递给该命令的每个参数。
接着设置了 EXPOSE指令,这条指令告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问任意容器运行中服务的端口(这里是80)。出于安全的原因, Docker并不会自动打开该端口,而是需要你在使用docker run运行容器时来指定需要打开哪些端口。一会儿我们将会看到如何从这一镜像创建一个新容器。可以指定多个 EXPOSE指令来向外部公开多个端口。注意,Docker也使用EXPOSE指令来帮助将多个容器链接,后续在进行介绍。
接下来便可以通过docker build
命令构建镜像了,进入上下文目录,使用-t选项为新镜像设置了仓库和名称,也可以在镜像名后面通过冒号为镜像添加标签,下面将告诉docker到本地目录下查找Dockerfile文件,执行成功后,便返回一个新镜像,所有执行流程如下:
$ mkdir docker_context
$ cd docker_context/
$ touch Dockerfile
$ vi Dockerfile
$ pwd
/root/docker_context
$ docker build -t="eussi/test-repo" .
Sending build context to Docker daemon 2.048kB
Step 1/6 : FROM centos
---> 9f38484d220f
Step 2/6 : MAINTAINER Wang Xmeming "wangxuemingcn@yeah.net"
---> Running in 3223e2024e4e
Removing intermediate container 3223e2024e4e
---> 174c4e10f20a
Step 3/6 : RUN rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
---> Running in daeb8ef7eee5
warning: /var/tmp/rpm-tmp.m1qXjl: Header V4 RSA/SHA1 Signature, key ID 7bd9bf62: NOKEY
Retrieving http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
Preparing... ########################################
Updating / installing...
nginx-release-centos-7-0.el7.ngx ########################################
Removing intermediate container daeb8ef7eee5
---> 1b5112400c14
Step 4/6 : RUN yum install -y nginx
---> Running in aa939192e03e
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
...... 中间省略 ......
Complete!
Removing intermediate container aa939192e03e
---> dfcb3b042bd0
Step 5/6 : RUN echo 'Hi, I am nginx in docker' > /usr/share/nginx/html/index.html
---> Running in bb3c514e867c
Removing intermediate container bb3c514e867c
---> ad9e92a0767b
Step 6/6 : EXPOSE 80
---> Running in 9620c8e81b12
Removing intermediate container 9620c8e81b12
---> 98ab6a8ede3a
Successfully built 98ab6a8ede3a
Successfully tagged eussi/test-repo:latest
上面命令告诉Docker到本地目录查找Dockerfile文件,也可以指定源地址来指定Dockerfile的位置,如
docker build -t="eussi/test-repo" git@github.com:eussi/docker_context
再回到docker build过程。可以看到构建上下文已经上传到了 Docker守护进程,如
Sending build context to Docker daemon 2.048kB
如果在构建上下文的根目录下存在以.dockerignore命名的文件的话,那么该文件内容会被按行进行分割,每一行都是一条文件过滤匹配模式。这非常像.gitignore文件,该文件用来设置哪些文件不会被上传到构建上下文中去。该文件中模式的匹配规则采用了
Go语言中的 filepath。
之后,可以看到 Dockerfile中的每条指令会被顺序执行,而且作为构建过程的最终结果,返回了新镜像的ID,即98ab6a8ede3a。构建的每一步及其对应指令都会独立运行,并且在输出最终镜像I之前, Docker会提交每步的构建结果。
待续…