【Docker】进阶之路:(六)Docker镜像
- 理解镜像构成
- 获取镜像
- 列出镜像
- 删除本地镜像
- 定制镜像
- 使用Dockerfile定制镜像
- 使用docker build命令+Dockerfile文件定制镜像
理解镜像构成
镜像由多个层组成,每层叠加之后,从外部看来就如一个独立的对象。镜像内部是一个精简的操作系统(OS),同时还包含应用运行所必须的文件和依赖包。因为容器的设计初衷就是快速和小巧,所以镜像通常都比较小。前面多次提到镜像就像停止运行的容器(类)。实际上,可以停止某个容器的运行,并从中创建新的镜像。因此,镜像可以理解为一种构建时(build-time)结构,而容器可以理解为一种运行时(run-time)结构。
获取镜像
当用户在创建容器的时候,可以自己创建所需要的镜像。但是,在绝大多数情况下,用户选择的是从 Docker 镜像仓库中查找所需要的镜像。找到之后,将其从镜像仓库中下载到本地使用。
首先需要先从镜像仓库服务中拉取镜像。常见的镜像仓库服务是 Docker Hub,但是也存在其他镜像仓库服务。拉取操作会将镜像下载到本地 Docker 主机,可以使用该镜像启动一个或者多个容器。
Docker提供了docker search命令来查找远程镜像仓库上面的镜像。该命令的使用方法非常简单,其语法如下
docker search keyword
列出镜像
如果用户想要查看已经下载到本地的镜像,可以使用dockerimages命令。该命令可以直接使用,不加任何参数。
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
circle/ubuntu 1.0 34035f46d2c6 6 seconds ago 72.8MB
circledba/kingbase v008r006c007b0024 df101ff5974d 41 hours ago 12.4GB
tomcat latest fb5657adc892 23 months ago 680MB
ubuntu latest ba6acccedd29 2 years ago 72.8MB
centos 7 eeb6ee3f44bd 2 years ago 204MB
其中第一列为镜像名称,第二列为镜像标签,默认标签为latest:最后一个标签。第三列为镜像ID,第四列为镜像创建时间,第五列为镜像的大小。
删除本地镜像
对于当前系统中已经不需要的镜像,为了节省存储空间,管理员可以将其删除。删除镜像使用docker rmi命令,其中,rmi中的字母i表示镜像。该命令的基本语法如下:
docker rmi image ...
其中image为镜像名称。用户可以同时删除多个镜像,多个镜像名称之间用空格隔开。例如,下面的命令删除名称为tomcat的本地镜像:
[root@docker ~]# docker rmi tomcat
Untagged: tomcat:latest
Untagged: tomcat@sha256:9dee185c3b161cdfede1f5e35e8b56ebc9de88ed3a79526939701f3537a52324
Deleted: sha256:fb5657adc892ed15910445588404c798b57f741e9921ff3c1f1abe01dbb56906
Deleted: sha256:2b4d03a9ce5e200223e5c398d4739d23dd19ad0d6e692cfc65ba3a8fae838444
Deleted: sha256:35c5ea12be1face90896b3a52afc28433885c4448a6c5cfe07561f82365cd18e
Deleted: sha256:6830091c111746b7534960d17f6c156be45d8dcfe0defb06bd427ef38bf49aae
Deleted: sha256:ea82d4efcdfa1c039d722a5a9613c18d3c3a84fbba8efae5e7f13cb3b4ec379f
Deleted: sha256:79a6c362c6b1a580d2d8d33f6d860d45c530f34ff7c0441d36b61aceefdfd656
Deleted: sha256:1788a74c5c86e769f61cd615269eba11c3d7648eac4a85a1ffd2840427820a2f
Deleted: sha256:cbce712ed17923285239f9d9c0528984aef065b7413d68a0290e2c8eecc98f4a
Deleted: sha256:aa56d037ee5925ebf11127c3e1f617874c4ce8bae6b6af7d132b7f7a4a606e6f
Deleted: sha256:97e5f44efb543d466c5847602654a8cb22c9466b61d04988d47ec44b197ea874
Deleted: sha256:11936051f93baf5a4fb090a8fa0999309b8173556f7826598e235e8a82127bce
[root@docker ~]#
定制镜像
使用Dockerfile定制镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会迎刃而解。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件,其内容包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
以下举例说明如何使用Dockerfile来定制 nginx 镜像。
[root@docker ~]# docker run --name webserver -d -p 80:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
186b1aaa4aa6: Pull complete
b4df32aa5a72: Pull complete
a0bcbecc962e: Pull complete
Digest: sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
Status: Downloaded newer image for nginx:latest
8dddf86d7a44cb2d19c81fec18c6ea6f4f82626a56133da0723b1a8484f0ec78
[root@docker ~]#
这条命令会用nginx镜像启动一个容器,容器命名为webserver,并且映射了80端口,这样就可以用浏览器访问nginx服务器。如果是在本机运行的Docker,那么可以直接访问http://localhost;如果是在虚拟机、云服务器上安装的Docker,则需要将localhost转换为虚拟机地址或者实际云服务器地址。
假设需要修改这个页面,改成“Hello,Docker”的文字页面,可以使用docker exec命令进入容器,修改其内容。
[root@docker ~]# docker exec -it webserver bash
root@8dddf86d7a44:/# echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
root@8dddf86d7a44:/# exit
exit
[root@docker ~]#
我们以交互式终端方式进入webserver容器,并执行了bash命令,也就是获得一个可操作的Shell。
然后,用
Hello,Docker!
覆盖了/usr/share/nginx/html/index.html的内容。现在再刷新浏览器的话,会发现页面内容被改变了。我们修改了容器的文件,也就是改动了容器的存储层。可以通过docker diff命令查看具体的改动。
[root@docker ~]# docker diff webserver
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /root
A /root/.bash_history
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
C /run
A /run/nginx.pid
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
[root@docker ~]#
我们定制好了变化,希望能将它保存下来形成镜像。当我们运行一个容器的时候,我们做的任何文件修改都会被记录于容器存储层里。而Docker提供了一个docker commit命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit的语法格式如下:
docker commit[选项]<容器ID或容器名>[<仓库名>[:<标签>]]
可以用下面的命令将容器保存为镜像:
[root@docker ~]# docker commit --author "Circle-DBA <circledba@qq.com>" --message="修改了默认网页" webserver nginx:v2
sha256:fc43cf21d8a7bb25afe7a07f0fa14f106bc3c210346f5e423bb9c71eef8d3a71
[root@docker ~]#
其中–author是指定修改的作者,而–message则是记录本次修改的内容。这一点和Git版本控制相似,不过这里的这些信息可以留空省略。
可以在docker image ls中查看这个新定制的镜像:
[root@docker ~]# docker image ls nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v2 fc43cf21d8a7 47 seconds ago 141MB
nginx latest 605c77e624dd 23 months ago 141MB
[root@docker ~]#
还可以用docker history具体查看镜像内的历史记录,如果比较nginx:latest的历史记录,会发现新增了刚刚提交的这一层。
[root@docker ~]# docker history nginx:v2
IMAGE CREATED CREATED BY SIZE COMMENT
fc43cf21d8a7 About a minute ago nginx -g daemon off; 1.19kB 修改了默认网页
605c77e624dd 23 months ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 23 months ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
<missing> 23 months ago /bin/sh -c #(nop) EXPOSE 80 0B
<missing> 23 months ago /bin/sh -c #(nop) ENTRYPOINT ["/docker-entr… 0B
<missing> 23 months ago /bin/sh -c #(nop) COPY file:09a214a3e07c919a… 4.61kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:0fd5fca330dcd6a7… 1.04kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:0b866ff3fc1ef5b0… 1.96kB
<missing> 23 months ago /bin/sh -c #(nop) COPY file:65504f71f5855ca0… 1.2kB
<missing> 23 months ago /bin/sh -c set -x && addgroup --system -… 61.1MB
<missing> 23 months ago /bin/sh -c #(nop) ENV PKG_RELEASE=1~bullseye 0B
<missing> 23 months ago /bin/sh -c #(nop) ENV NJS_VERSION=0.7.1 0B
<missing> 23 months ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.21.5 0B
<missing> 23 months ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 23 months ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 23 months ago /bin/sh -c #(nop) ADD file:09675d11695f65c55… 80.4MB
[root@docker ~]#
新的镜像定制好后,可以运行这个镜像。
[root@docker ~]# docker run --name web2 -d -p 81:80 nginx:v2
4ceaadbc86154c3a4a04b87b1c43ab4f6fbdc1fd9b3419f75b689aed37ef66d4
这里我们命名新的服务为web2,并且映射到81端口。访问http://localhost:81查看页面结果,其内容应该和之前修改后的webserver一样。
至此,我们第一次完成了定制镜像,使用的是docker commit命令,手动操作给旧的镜像添加了新的一层,形成新的镜像。现在,我们对镜像多层存储应该有了更直观的感受。
但是,docker commit仍然需要慎用。使用docker commit命令虽然可以比较直观地帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的docker diff webserver的结果,会发现除了真正想要修改的/usr/share/nginx/html/index.html文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那么会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
此外,使用docker commit意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像外,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。
另外,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的。换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用docker commit制作镜像,后期修改的话,每一次修改都会让镜像更加臃肿,所删除的上一层的东西并不会丢失,会一直如影随形地跟着这个镜像,即使根本无法访问到,也会让镜像更加臃肿。
使用docker build命令+Dockerfile文件定制镜像
镜像的定制实际上就是定制每一层所要添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都迎刃而解了。这个脚本就是Dockerfile。
Dockerfile是一个文本文件,其内容包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容就是描述该层应当如何构建。
以下举例说明如何使用Dockerfile来定制nginx镜像。首先在一个空白目录中建立一个文本文件,并命名为Dockerfile:
[root@docker ~]# mkdir mynginx
[root@docker ~]# cd mynginx/
[root@docker mynginx]# touch Dockerfile
[root@docker mynginx]#
其内容为:
FROM nginx
RUN echo ¹<h1>Hello,Docker!</hl>¹>/usr/share/nginx/html/index.html
这个Dockerfile很简单,一共就两行,涉及两条指令:FROM和RUN。FROM指令是最重要的一个指令,且必须为Dockerfile文件开篇的第一个非注释行,用于为镜像文件构建过程指定基础镜像,后续的指令运行于此基础镜像提供的运行环境中。定制的镜像都是基于FROM的镜像,这里的nginx就是定制需要的基础镜像。后续的操作都是基于nginx。RUN指令用于执行后面跟着的命令行命令。
编写完Dockerfile文件后,开始构建镜像。在Dockerfile文件的存放目录下使用docker build命令执行构建命令。例如:
[root@docker mynginx]# docker build -t nginx:v3 .
[+] Building 0.6s (6/6) FINISHED docker:default=> [internal] load .dockerignore 0.0s=> => transferring context: 2B 0.0s=> [internal] load build definition from Dockerfile 0.0s=> => transferring dockerfile: 177B 0.0s=> [internal] load metadata for docker.io/library/nginx:latest 0.0s=> [1/2] FROM docker.io/library/nginx 0.0s=> [2/2] RUN echo '<h1>Hello,Docker!</hl>' > /usr/share/nginx/html/index.html 0.4s=> exporting to image 0.0s=> => exporting layers 0.0s=> => writing image sha256:cc54c98400c7db4200160baf22e838488239e8a463b3c925e74f97a4c6f1a88e 0.0s=> => naming to docker.io/library/nginx:v3 0.0s
[root@docker mynginx]#
最后的“.”代表本次执行的上下文路径。上下文路径是指Docker在构建镜像时要使用本机的文件(比如复制),docker build命令得知这个路径后,会将路径下的所有内容打包。
除了标准地使用Dockerfile生成镜像的方法外,由于各种特殊需求和历史原因,还提供了一些其他方法用以生成镜像。例如从rootfs压缩包导入,命令格式如下:
docker import [选项] <文件>|<URL>|-[<仓库名>[:<标签>]]
压缩包可以是本地文件、远程Web文件,甚至是从标准输入中得到的。压缩包将会在镜像/目录展开,并直接作为镜像第一层提交。
使用docker save命令可以将镜像保存为归档文件,保存镜像的命令为:
[root@docker mynginx]# docker save nginx:v3 -o nginxv3
[root@docker mynginx]# file nginxv3
nginxv3: POSIX tar archive
[root@docker mynginx]#
注意,如果同名则会覆盖(没有警告)。
使用gzip压缩:
[root@docker mynginx]# docker save nginx:v3 | gzip> nginx-v3.tar.gz
[root@docker mynginx]# ls -l |grep 'nginx'
-rw-------. 1 root root 145911808 12月 9 16:17 nginxv3
-rw-r--r--. 1 root root 54826265 12月 9 16:18 nginx-v3.tar.gz
[root@docker mynginx]#
然后将nginx-v3.tar.gz文件复制到另一个机器上,用下面这个命令加载镜像:
s docker load -i nginx-v3.tar.gz
Loaded image:nginx:v3
如果结合这两个命令以及ssh甚至pv的话,利用Linux强大的管道,我们可以写一个命令完成将镜像从一个机器迁移到另一个机器,并且带进度条功能:
docker save <镜像名> | bzip2 | pv |ssh <用户名>@<主机名> 'cat | docker load'