Docker镜像
Docker镜像
镜像是Docker容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。
1.hello-world——最小的镜像
hello-world是Docker官方提供的一个镜像,通常用来验证Docker是否安装成功。
通过docker pull从Docker Hub下载它
用docker images命令查看镜像的信息
通过docker run运行
Dockerfile是镜像的描述文件,定义了如何构建Docker镜像。
hello-world的Dockerfile内容如下图
(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
查看镜像信息
镜像大小200多兆
为什么一个CentOS才200MB ?
这是由于Linux操作系统由内核空间和用户空间组成。
1.rootfs
内核空间是kernel, Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。
用户空间的文件系统是rootfs,包含我们熟悉的/dev、/proc、/bin等目录。
对于base镜像来说,底层直接用Host的kernel,自己只需要提供rootfs就行了。
而对于一个精简的OS, rootfs可以很小,只需要包括最基本的命令、工具和程序库就可以了。
2.base镜像提供的是最小安装的Linux发行版
CentOS镜像的Dockerfile的内容如下图
第二行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镜像,模拟出多种操作系统环境,如下图所示。
上图Debian和BusyBox(一种嵌入式Linux)上层提供各自的rootfs,底层共用Docker Host的kernel。
这里需要说明的是:
(1)base镜像只是在用户空间与发行版一致,kernel版本与发行版是不同的。
例如CentOS 7使用3.x.x的kernel,那么在CentOS容器中使用的实际上也是Host 3.x.x的kernel,如下图
① 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如下图所示。
① 新镜像不再是从scratch开始,而是直接在Debian base镜像上构建。
② 安装emacs编辑器。
③ 安装apache2。
④ 容器启动时运行bash。
可以看到,新镜像是从base镜像一层一层叠加生成的。
每安装一个软件,就在现有镜像的基础上增加一层。
为什么Docker镜像要采用这种分层结构呢?
最大的一个好处就是:共享资源。
比如:有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;
同时内存中也只需加载一份base镜像,就可以为所有容器服务了,而且镜像的每一层都可以被共享。
可写的容器层
当容器启动时,一个新的可写层被加载到镜像的顶部。
这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”
所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。
只有容器层是可写的,容器层下面的所有镜像层都是只读的。
镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。
如果不同层中有一个相同路径的文件,比如 /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
2.运行容器
-it参数的作用是以交互模式进入容器,并打开终端。a1fea830529c是容器的内部ID。
3.安装vi
确认vi没有安装
安装vim
4.保存为新镜像
在新窗口查看当前运行的容器
cool_matsumoto是Docker为我们的容器随机分配的名字。
执行docker commit命令将容器保存为镜像
查看新镜像的属性
从新镜像启动容器,验证vi已经可以使用
2.Dockerfile
Dockerfile是一个文本文件,记录了镜像构建的所有步骤。
2.1 第一个Dockerfile
用Dockerfile创建上节的ubuntu-with-vi
vim Dockerfile
#当前目录为 /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镜像的顶部添加一个新的镜像层而得到的
这个新镜像层的内容由RUN apt-get update && apt-get install -y vim生成。
这一点我们可以通过docker history命令验证。
docker history会显示镜像的构建历史,也就是Dockerfile的执行过程。
ubuntu-with-vi-dockerfile与Ubuntu镜像相比,确实只是多了顶部的一层fbe0283029b1,由apt-get命令创建,大小为110MB。
docker history也向我们展示了镜像的分层结构,每一层由上至下排列。
注:missing表示无法获取IMAGE ID,通常从Docker Hub下载的镜像会有这个问题。
2.3 镜像的缓存特性
我们接下来看Docker镜像的缓存特性。
Docker会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无须重新创建。
下面举例说明。在前面的Dockerfile中添加一点新内容,往镜像中复制一个文件。
[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
如果我们希望在构建镜像时不使用缓存,可以在docker build命令中加上--no-cache参数。
Dockerfile中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。
也就是说,如果我们改变Dockerfile指令的执行顺序,或者修改或添加指令,都会使缓存失效。
举例说明,比如交换前面RUN和COPY的顺序。
虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker必须重建受影响的镜像层。
可以看到又重新下载了。缓存失效了
除了构建时使用缓存,Docker在下载镜像时也会使用。例如我们下载httpd镜像
已经存在,无需下载
2.4 调试Dockerfile
总结一下通过Dockerfile构建镜像的过程:
(1)从base镜像运行一个容器。
(2)执行一条指令,对容器做修改。
(3)执行类似docker commit的操作,生成一个新的镜像层。
(4)Docker再基于刚刚提交的镜像运行一个新容器。
(5)重复2~4步,直到Dockerfile中的所有指令执行完毕。
从这个过程可以看出,如果Dockerfile由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试Dockerfile非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。
我们来看一个调试的例子。Dockerfile内容如图
执行docker build
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可以查看镜像的信息
实际上一个特定镜像的名字由两部分组成: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。
注:Docker官方自己维护的镜像没有用户名,比如httpd。
通过docker push将镜像上传到Docker Hub
Docker会上传镜像的每一层。因为cloudman6/httpd:v1这个镜像实际上跟官方的httpd镜像一模一样,Docker Hub上已经有了全部的镜像层,所以真正上传的数据很少。同样的,如果我们的镜像是基于base镜像的,也只有新增加的镜像层会被上传。如果想上传同一repository中所有镜像,省略tag部分就可以了
(1)登录https://hub.docker.com,在Public Repository中就可以看到上传的镜像
如果要删除上传的镜像,只能在Docker Hub界面上操作。
(2)这个镜像可被其他Docker host下载使用了
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
现在已经可通过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。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix