Docker镜像

Docker镜像

镜像是Docker容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。

1.hello-world——最小的镜像

hello-world是Docker官方提供的一个镜像,通常用来验证Docker是否安装成功。

通过docker pull从Docker Hub下载它

fig:

用docker images命令查看镜像的信息

fig:

通过docker run运行

fig:

Dockerfile是镜像的描述文件,定义了如何构建Docker镜像。

hello-world的Dockerfile内容如下图

fig:

(1)FROM scratch镜像是从白手起家,从0开始构建。

  通常使用 Docker 镜像时会以一个已存在的镜像为基础,在其上进行定制,这个已存在的镜像就是基 础镜像。

  在 DockerFile 中必须指定基础镜像,FROM 指令就是用于指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

  Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一 个空白的镜像。在 Dockerfile 中以 scratch 为基础镜像 (FROM scratch),'

  意味着不以任何镜像为基 础,接下来所写的指令将作为镜像第一层开始存在。

  对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在 可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。

(2)COPY hello/将文件“hello”复制到镜像的根目录。

(3)CMD["/hello"]容器启动时,执行/hello。

 

镜像hello-world中就只有一个可执行文件“hello”,其功能就是打印出“Hello from Docker ......”等信息。

/hello就是文件系统的全部内容,连最基本的/bin、/usr、/lib、 /dev都没有。

hello-world虽然是一个完整的镜像,但它并没有什么实际用途。

通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件

这样的镜像我们称作base镜像

 

2.base镜像

base镜像有两层含义:

(1)不依赖其他镜像,从scratch构建;

(2)其他镜像可以以之为基础进行扩展。

所以,能称作base镜像的通常都是各种Linux发行版的Docker镜像,比如Ubuntu、Debian、CentOS等。我们以CentOS为例考察base镜像包含哪些内容。

下载镜像:

docker pull centos

查看镜像信息

fig:

镜像大小200多兆

 

为什么一个CentOS才200MB ?

这是由于Linux操作系统由内核空间和用户空间组成。

fig:

1.rootfs

内核空间是kernel, Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。

用户空间的文件系统是rootfs,包含我们熟悉的/dev、/proc、/bin等目录。

对于base镜像来说,底层直接用Host的kernel,自己只需要提供rootfs就行了

而对于一个精简的OS, rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了。

 

2.base镜像提供的是最小安装的Linux发行版

CentOS镜像的Dockerfile的内容如下图

fig:

第二行ADD指令添加到镜像的tar包就是CentOS 7的rootfs。

在制作镜像时,这个tar包会自动解压到/目录下,生成/dev、/proc、/bin等目录。

注:可在Docker Hub的镜像描述页面中查看Dockerfile。

 

3.支持运行多种Linux OS

不同Linux发行版的区别主要就是rootfs。

比如Ubuntu 14.04使用upstart管理服务,apt管理软件包;而CentOS 7使用systemd和yum。

这些都是用户空间上的区别,Linux kernel差别不大。

所以Docker可以同时支持多种Linux镜像,模拟出多种操作系统环境,如下图所示。

fig:

上图Debian和BusyBox(一种嵌入式Linux)上层提供各自的rootfs,底层共用Docker Host的kernel。

 

这里需要说明的是:

(1)base镜像只是在用户空间与发行版一致,kernel版本与发行版是不同的。

例如CentOS 7使用3.x.x的kernel,那么在CentOS容器中使用的实际上也是Host 3.x.x的kernel,如下图

fig:

① Host kernel为3.10.0。

② 启动并进入CentOS容器。

③ 验证容器是CentOS 8。

④ 容器的kernel版本与Host一致。

 

(2)容器只能使用Host的kernel,并且不能修改。

所有容器都共用host的kernel,在容器中没办法对kernel升级

如果容器对kernel版本有要求(比如应用只能在某个kernel版本下运行),则不建议用容器,这种场景虚拟机可能更合适。

 

3.镜像的分层结构

Docker支持通过扩展现有镜像,创建新的镜像。

实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。

比如我们现在构建一个新的镜像,Dockerfile如下图所示。

fig:

① 新镜像不再是从scratch开始,而是直接在Debian base镜像上构建。

② 安装emacs编辑器。

③ 安装apache2。

④ 容器启动时运行bash。

fig:

可以看到,新镜像是从base镜像一层一层叠加生成的。

每安装一个软件,就在现有镜像的基础上增加一层。

 

为什么Docker镜像要采用这种分层结构呢?

最大的一个好处就是:共享资源。

比如:有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;

同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。

 

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。

这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”

fig:

所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中

只有容器层是可写的,容器层下面的所有镜像层都是只读的。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统

如果不同层中有一个相同路径的文件,比如 /a,上层的 /a会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。

在容器层中,用户看到的是一个叠加之后的文件系统

(1)添加文件。在容器中创建文件时,新文件被添加到容器层中。

(2)读取文件。在容器中读取某个文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,打开并读入内存。

(3)修改文件。在容器中修改已存在的文件时,Docker会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。

(4)删除文件。在容器中删除文件时,Docker也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。

只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。

可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

 

构建镜像

在一些场景里面我们才需要自己去构造镜像,如:

(1)找不到现成的镜像,比如自己开发的应用程序。

(2)需要在镜像中加入特定的功能,比如官方镜像几乎都不提供ssh。

Docker提供了两种构建镜像的办法:

docker commit命令与Dockerfile构建文件。

 

1.docker commit

写在前面,Docker并不建议用户通过这种方式构建镜像。原因如下:

(1)这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在debian base镜像中也加入vi,还得重复前面的所有步骤。

(2)更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。

虽然docker commit不是推荐的方法,但是即便是用Dockerfile(推荐方法)构建镜像,底层也是docker commit一层一层构建新镜像的。学习docker commit能够帮助我们更加深入地理解构建过程和镜像的分层结构。

 

docker commit命令是创建新镜像最直观的方法,其过程包含三个步骤:

● 运行容器。

● 修改容器。

● 将容器保存为新的镜像。

举个例子:在Ubuntu base镜像中安装vi并保存为新镜像。

1.拉取最新版的 Ubuntu 镜像

docker pull ubuntu

或者:

docker pull ubuntu:latest

fig:

 

2.运行容器

fig:

-it参数的作用是以交互模式进入容器,并打开终端。a1fea830529c是容器的内部ID。

 

3.安装vi

确认vi没有安装

fig:

安装vim

fig: fig:

 

4.保存为新镜像

在新窗口查看当前运行的容器

cool_matsumoto是Docker为我们的容器随机分配的名字。

执行docker commit命令将容器保存为镜像

 

查看新镜像的属性

fig:

从新镜像启动容器,验证vi已经可以使用

fig:

 

2.Dockerfile

Dockerfile是一个文本文件,记录了镜像构建的所有步骤。

2.1 第一个Dockerfile

用Dockerfile创建上节的ubuntu-with-vi

vim Dockerfile

fig:

#当前目录为 /root。
[root@0x1e61 ~]# pwd
/root
#Dockerfile准备就绪
[root@0x1e61 ~]# ls
Dockerfile tool
# 运行docker build命令,-t将新镜像命名为ubuntu-with-vi-dockerfile,命令末尾的.指明build context为当前目录。Docker默认会从build context中查找Dockerfile文件,我们也可以通过-f参数指定Dockerfile的位置。
[root@0x1e61 ~]# docker build -t ubuntu-with-vi-dockerfile .
[+] Building 48.4s (6/6) FINISHED
#从Dockerfile中加载构建定义
=> [internal] load build definition from Dockerfile 0.0s
#转移dockerfile: 95B
=> => transferring dockerfile: 95B 0.0s
#[内部]加载 .dockerignore
=> [internal] load .dockerignore 0.0s
#转移 context(上下文): 2B
=> => transferring context: 2B 0.0s
# 为docker.io/library/ubuntu:最新版本加载元数据
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> [1/2] FROM docker.io/library/ubuntu 0.0s
=> [2/2] RUN apt-get update && apt-get install -y vim 47.6s
# 输出为镜像
=> exporting to image 0.8s
# 输出为容器层
=> => exporting layers 0.7s
# 写入镜像
=> => writing image sha256:fbe0283029b16890d780748928bf263715dd486352140601c0b30cb3ec34ad 0.0s
# 命名为docker.io/library/ubuntu-with-vi-dockerfile
=> => naming to docker.io/library/ubuntu-with-vi-dockerfile

 

2.2 查看镜像分层结构

ubuntu-with-vi-dockerfile是通过在base镜像的顶部添加一个新的镜像层而得到的

fig:

这个新镜像层的内容由RUN apt-get update && apt-get install -y vim生成。

这一点我们可以通过docker history命令验证。

fig:

docker history会显示镜像的构建历史,也就是Dockerfile的执行过程。

ubuntu-with-vi-dockerfile与Ubuntu镜像相比,确实只是多了顶部的一层fbe0283029b1,由apt-get命令创建,大小为110MB。

docker history也向我们展示了镜像的分层结构,每一层由上至下排列。

注:missing表示无法获取IMAGE ID,通常从Docker Hub下载的镜像会有这个问题。

 

2.3 镜像的缓存特性

我们接下来看Docker镜像的缓存特性。

Docker会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无须重新创建。

下面举例说明。在前面的Dockerfile中添加一点新内容,往镜像中复制一个文件。

fig:

[root@0x1e61 ~]# ls
Dockerfile testfile tool
[root@0x1e61 ~]# docker build -t unbuntu-with-vi-dockerfile-2 .
[+] Building 0.1s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 110B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 30B 0.0s
=> [1/3] FROM docker.io/library/ubuntu 0.0s
=> CACHED [2/3] RUN apt-get update && apt-get install -y vim 0.0s
=> [3/3] COPY testfile / 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:bdb5843dd6a7026601db9a2e40243ef9fc472de1ef3585f8acf95cc19b6c5a 0.0s
=> => naming to docker.io/library/unbuntu-with-vi-dockerfile-2 0.0s
#可以看到都是0.0s 说明有缓存,直接使用缓存就行



在ubuntu-with-vi-dockerfile镜像上直接添加一层就得到了新的镜像ubuntu-with-vi-dockerfile-2

fig:

fig:

如果我们希望在构建镜像时不使用缓存,可以在docker build命令中加上--no-cache参数

Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。

也就是说,如果我们改变Dockerfile指令的执行顺序,或者修改或添加指令,都会使缓存失效。

 

举例说明,比如交换前面RUN和COPY的顺序。

fig:

虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker必须重建受影响的镜像层。

可以看到又重新下载了。缓存失效了

除了构建时使用缓存,Docker在下载镜像时也会使用。例如我们下载httpd镜像

fig:

已经存在,无需下载

 

2.4 调试Dockerfile

总结一下通过Dockerfile构建镜像的过程:

(1)从base镜像运行一个容器。

(2)执行一条指令,对容器做修改。

(3)执行类似docker commit的操作,生成一个新的镜像层。

(4)Docker再基于刚刚提交的镜像运行一个新容器。

(5)重复2~4步,直到Dockerfile中的所有指令执行完毕。

从这个过程可以看出,如果Dockerfile由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试Dockerfile非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。

 

我们来看一个调试的例子。Dockerfile内容如图

fig:

执行docker build

fig:

Dockerfile在执行第三步RUN指令时失败。可以清楚的看到busybox镜像中没有bash导致了失败

 

2.5 Dockerfile常用指令

● FROM指定base镜像。

● MAINTAINER设置镜像的作者,可以是任意字符串。

● COPY将文件从build context复制到镜像。
COPY支持两种形式: COPY src dest与COPY ["src", "dest"]。
注意:src只能指定build context中的文件或目录。

● ADD与COPY类似,从build context复制文件到镜像。
不同的是,如果src是归档文件(tar、zip、tgz、xz等),文件会被自动解压到dest。

● ENV设置环境变量,环境变量可被后面的指令使用。

● EXPOSE指定容器中的进程会监听某个端口,Docker可以将该端口暴露出来。

● VOLUME将文件或目录声明为volume。

● WORKDIR为后面的RUN、CMD、ENTRYPOINT、ADD或COPY指令设置镜像中的当前工作目录。

● RUN在容器中运行指定的命令。

● CMD容器启动时运行指定的命令。
Dockerfile中可以有多个CMD指令,但只有最后一个生效。CMD可以被docker run之后的参数替换。

● ENTRYPOINT设置容器启动时运行的命令。
Dockerfile中可以有多个ENTRYPOINT指令,但只有最后一个生效。
CMD或docker run之后的参数会被当作参数传递给ENTRYPOINT。

 

RUN vs CMD vs ENTRYPOINT

RUN、CMD和ENTRYPOINT这三个Dockerfile指令看上去很类似,很容易混淆。

简单地说:

(1)RUN:执行命令并创建新的镜像层,RUN经常用于安装软件包。

(2)CMD:设置容器启动后默认执行的命令及其参数,但CMD能够被docker run后面跟的命令行参数替换。

(3)ENTRYPOINT:配置容器启动时运行的命令。

 

3.分发镜像

如何在多个Docker Host上使用镜像?

这里有几种可用的方法:

(1)用相同的Dockerfile在其他host构建镜像。

(2)将镜像上传到公共Registry(比如Docker Hub), Host直接下载使用。

(3)搭建私有的Registry供本地Host使用。

第一种没什么特别的,有手就行,主要放在后两种

 

3.1 为镜像命名

当我们执行docker build命令时已经为镜像取了个名字,例如前面:

docker build -t ubuntu-with-vi

这里的ubuntu-with-vi就是镜像的名字。通过docker images可以查看镜像的信息

fig:

实际上一个特定镜像的名字由两部分组成:repository和tag。

[image name] = [repository]:[tag]

如果执行docker build时没有指定tag,会使用默认值latest。其效果相当于:

docker build -t ubuntu-with-vi:latest

 

docker tag 用于给镜像打标签,语法如下:

docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]

 

① 比如我现在有一个ubuntu镜像:

[root@0x1e61 ~]# docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest ba6acccedd29 17 months ago 72.8MB

② 我对 ubuntu进行开发,开发了第一个版本,我就可以对这个版本打标签,打完标签后会生成新的镜像:

[root@0x1e61 ~]# docker tag ubuntu ubuntu:v1
[root@0x1e61 ~]# docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest ba6acccedd29 17 months ago 72.8MB
ubuntu v1 ba6acccedd29 17 months ago 72.8MB

 

③ 继续对 ubuntu 进行开发,开发了第二个版本,继续打标签:

[root@0x1e61 ~]# docker tag ubuntu ubuntu:v2
[root@0x1e61 ~]# docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest ba6acccedd29 17 months ago 72.8MB
ubuntu v1 ba6acccedd29 17 months ago 72.8MB
ubuntu v2 ba6acccedd29 17 months ago 72.8MB

④ 以此类推,每开发一个版本打一个标签,如果以后我想回滚版本,就可以使用指定标签的镜像来创建容器:

[root@0x1e61 ~]# docker run -itd ubuntu:v1
4c9f5e1ac2f20e865b70c0b6902dcc1fc630908cc4d35d8c4cd68c145c668a20

 

3.2 使用公共Registry

保存和分发镜像的最直接方法就是使用Docker Hub。

Docker Hub是Docker公司维护的公共Registry。

用户可以将自己的镜像保存到Docker Hub免费的repository中。

如果不希望别人访问自己的镜像,也可以购买私有repository。

除了Docker Hub, quay.io是另一个公共Registry,提供与Docker Hub类似的服务。

 

下面介绍如何用Docker Hub存取我们的镜像。

(1)首先得在Docker Hub上注册一个账号。

(2)在Docker Host上登录

docker login -u 账号 -p 密码

(3)修改镜像的repository,使之与Docker Hub账号匹配。

Docker Hub为了区分不同用户的同名镜像,镜像的registry中要包含用户名,完整格式为:[username]/xxx:tag。

fig:

注:Docker官方自己维护的镜像没有用户名,比如httpd。

通过docker push将镜像上传到Docker Hub

fig:

 

Docker会上传镜像的每一层。因为cloudman6/httpd:v1这个镜像实际上跟官方的httpd镜像一模一样,Docker Hub上已经有了全部的镜像层,所以真正上传的数据很少。同样的,如果我们的镜像是基于base镜像的,也只有新增加的镜像层会被上传。如果想上传同一repository中所有镜像,省略tag部分就可以了

(1)登录https://hub.docker.com,在Public Repository中就可以看到上传的镜像

fig:

如果要删除上传的镜像,只能在Docker Hub界面上操作。

 

(2)这个镜像可被其他Docker host下载使用了

fig:

 

3.4 搭建本地Registry

Docker Hub虽然非常方便,但还是有些限制,比如:

(1)需要Internet连接,而且下载和上传速度慢。

(2)上传到Docker Hub的镜像任何人都能够访问,虽然可以用私有repository,但不是免费的。

(3)因安全原因很多组织不允许将镜像放到外网。解决方案就是搭建本地的Registry。

Docker已经将Registry开源了,同时在Docker Hub上也有官方的镜像registry。

 

1. 启动registry容器

使用的镜像是registry:2

docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:2

● -d:后台启动容器。

● -p:将容器的5000端口映射到Host的5000端口。5000是registry服务端口。

● -v:将容器 /var/lib/registry目录映射到Host的 /myregistry,用于存放镜像数据。

通过docker tag重命名镜像,使之与registry匹配

[root@0x1e61 ~]# docker tag wesuiliye/httpd:v1 registry.Wesuiliye:5000/wesuiliye/httpd:v1

在镜像的前面加上了运行registry的主机名称和端口。

前面已经讨论了镜像名称由repository和tag两部分组成。

而repository的完整格式为:[registry-host]:[port]/[username]/xxx

只有Docker Hub上的镜像可以省略registry-host:[port]。

 

2. 通过docker push上传镜像

出现下面的错误,说明没有找到主机

去/etc/hosts查看

未更改成Weisuiliye所以找不到。要么改这里,要么改命令成localhost

[root@0x1e61 ~]# docker push registry.localhost:5000/wesuiliye/httpd:v1

fig:

现在已经可通过docker pull从本地registry下载镜像了

[root@0x1e61 ~]# docker pull registry.localhost:5000/wesuiliye/httpd:v1

除了镜像的名称长一些(包含registry host和port),使用方式完全一样。

以上是搭建本地registry的简要步骤。

当然registry也支持认证,https安全传输等特性,具体可以参考官方文档

https://docs.docker.com/registry/configuration/

 

相关命令

● images:显示镜像列表。

● history:显示镜像构建历史。

● commit:从容器创建新镜像。

● build:从Dockerfile构建镜像。

● tag:给镜像打tag。

● pull:从registry下载镜像。

● push:将镜像上传到registry。

● rmi:删除Docker host中的镜像。
rmi只能删除host上的镜像,不会删除registry的镜像。
如果一个镜像对应了多个tag,只有当最后一个 tag被删除时,镜像才被真正删除。

● search:搜索Docker Hub中的镜像。
search让我们无须打开浏览器,在命令行中就可以搜索Docker Hub中的镜像。
当然,如果想知道镜像都有哪些tag,还是得访问Docker Hub。

posted @   0x1e61  阅读(811)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示