Docker入门(二)

学习资料:公众号cloudman

Docker组件如何协作?

当我们执行docker run -d -p 80:80 httpd

如果是第一次执行,则会经历以下步骤:

  1. Docker客户端搜索本地镜像:发现没有httpd镜像
  2. Docker daemon从Docker Hub下载镜像
  3. 下载的镜像于是被保存在本地
  4. Docker daemon启动容器

docker images:查看本地的镜像

在这里插入图片描述

docker ps:显示容器

在这里插入图片描述

镜像的内部

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

通过docker pull下载

运行:docker run hello-world

在这里插入图片描述

Dockerfile是镜像的描述文件,定义了如何构建Docker镜像。Dockerfile的语法简洁且可读性强。

hello-world的Dockerfile内容:

FROM scratch
COPY hello /
CMD ["/hello"]

只有短短三条指令。

  1. FROM scratch :此镜像从0开始构建
  2. COPY hello / :将文件"hello"复制到镜像的根目录
  3. CMD [“/hello”]:容器启动时,执行 /hello

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

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

base镜像

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

base镜像有两层含义:

  1. 不依赖于其他镜像,从scratch构建
  2. 其他镜像可以之为基础进行扩展。

所以,能称作base镜像的通常都是各种Linux发行版的Docker镜像,比如Ubuntu,Debian,CentOS

我们看一下Ubuntu的镜像信息:

docker images ubuntu

在这里插入图片描述

为什么这么小???

首先,Linux操作系统由内核空间和用户空间组成。如下图:

在这里插入图片描述

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

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

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

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

下面是ubuntu镜像的Dockerfile的内容

FROM scratch
ADD ubuntu-jammy-oci-amd64-root.tar.gz /
CMD ["bash"]

第二行ADD指令添加到镜像的tar包就是Ubuntu的rootfs。在制作镜像时,这个tar包会自动解压到/目录下,生成/dev, /porc, /bin等目录。

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

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

比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。

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

需要说明的是:

  1. base镜像只是在用户空间与发行版一致,kernel版本与发行版是不同的。
  2. 容器只能使用Host的kernel,并且不能修改

Docker的分层结构

实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile如下:

FROM debian
RUN apt-get install emacs
RUN apt-get install apache2
CMD ["/bin/bash"]

在这里,

  1. 新镜像不是从scratch开始,而是直接在debian base镜像上构建
  2. 安装emacs编辑器
  3. 安装apache2
  4. 容器启动时运行bash

实际构建过程如下图所示:
在这里插入图片描述

可以看到,新镜像是从base镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

这种分层结构最大的好处就在于共享资源

问:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如/etc下的文件,这时其他容器的/etc是否也会被修改?

答:不会,修改会被限制在单个容器内。这就是我们接下来要学习的容器Copy-on-Write特性。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常叫做“容器层”,“容器层”之下的都叫“镜像层”

在这里插入图片描述

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

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

下面我们深入讨论容器层的细节。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如/a,上层的/a会覆盖下层的/a,也就是说用户只能访问到上层中的文件/a。在容器层中,用户看到的是一个叠加之后的文件系统。

  1. 添加文件

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

  2. 读取文件

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

  3. 修改文件

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

  4. 删除文件

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

只有当修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。所以镜像可以被多个容器共享

构建镜像

某些情况下我们不得不自己构建镜像,比如:

  1. 找不到现成的镜像,比如自己开发的应用程序
  2. 需要在镜像中加入特定功能,比如官方镜像几乎都不提供ssh

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

  1. docker commit命令
  2. Dockerfile 构建文件

docker commit

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

  1. 运行容器
  2. 修改容器
  3. 将容器保存为新的镜像

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

  1. 运行容器
    在这里插入图片描述

    -it的作用是以交互模式进入容器,并打开终端

  2. 安装vi

在这里插入图片描述

  1. 保存为新镜像

​ 执行 docker commit <ID/NAMES> <NEW_NAME> 命令将容器保存为镜像。

然鹅,Docker并不建议用户通过这种方式构建镜像,原因如下:

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

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

既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?

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

Dockerfile构建镜像

FROM ubuntu
RUN apt-get update && apt-get install -y vim

当执行docker build构建镜像:

qiqi_dell@dell-2:~/df$ docker build -t ubuntu-with-vi-dockerfile .
[+] Building 235.2s (6/6) FINISHED
 => [internal] load build definition from Dockerfile                                                   0.0s
 => => transferring dockerfile: 100B                                                                   0.0s
 => [internal] load .dockerignore                                                                      0.0s
 => => transferring context: 2B                                                                        0.0s
 => [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                                               234.5s
 => exporting to image                                                                                 0.5s
 => => exporting layers                                                                                0.5s
 => => writing image sha256:eff9c63cbadf264c41d96d27da3f37d23c7cb8eff65315080b5bb0566f06f08b           0.0s
 => => naming to docker.io/library/ubuntu-with-vi-dockerfile                                           0.0s

当前目录为df,之下 有一个Dockerfile文件

-t将新镜像命名为ubuntu-with-vi-dockerfile

.指明build context为当前目录,Docker默认会从build context中查找Dockerfile文件,也可以通过-f参数指定Dockerfile的位置。

[1/2] 执行第一层:将ubuntu作为base镜像

[2/2]执行RUN,安装vim

查看镜像:
在这里插入图片描述

查看镜像分层结构:

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

在这里插入图片描述

镜像分层结构:

在这里插入图片描述

镜像的缓存特性

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

qiqi_dell@dell-2:~/df2$ docker build -t ubuntu-with-vi-dockerfile-2 .
[+] Building 0.2s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                                                              0.0s
 => => transferring dockerfile: 118B                                                                              0.0s
 => [internal] load .dockerignore                                                                                 0.0s
 => => transferring context: 2B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                  0.0s
 => [1/3] FROM docker.io/library/ubuntu                                                                           0.0s
 => [internal] load build context                                                                                 0.0s
 => => transferring context: 55B                                                                                  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:a7dc8a695e28d29f6fc066df5083003a2ebed6756dc716d5dbca8bd0ff07ec66                      0.0s
 => => naming to docker.io/library/ubuntu-with-vi-dockerfile-2                                                    0.0s

CACHED表明已经运行过相同的RUN指令,这次直接使用缓存中的镜像层。

镜像分层结构:

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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

除了构建时使用缓存,Docker 在下载镜像时也会使用。

调试Dockerfile

所有脚本和程序都有可能出错,有错不可怕,但必须有办法排查。

回顾Dockerfile构建镜像的过程:

  1. 从base镜像运行一个容器
  2. 执行一条指令,对容器做修改
  3. 执行类似docker commit的操作,生成一个新的镜像层
  4. Docker再基于刚刚提交的镜像运行一个新容器。
  5. 重复2-4步,直到Dockerfile中的所有指令执行完毕。

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

FROM busybox
RUN touch tmpfile
RUN /bin/bash -c echo "continue to build..."
COPY testfile /

Dockerfile在执行第三步RUN指令时失败,并且告诉我们原因:

qiqi_dell@dell-2:~/debug$ docker build -t image-debug .
[+] Building 6.8s (7/8)
 => [internal] load build definition from Dockerfile                                                              0.0s
 => => transferring dockerfile: 137B                                                                              0.0s
 => [internal] load .dockerignore                                                                                 0.1s
 => => transferring context: 2B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                                                 4.4s
 => [internal] load build context                                                                                 0.0s
 => => transferring context: 55B                                                                                  0.0s
 => [1/4] FROM docker.io/library/busybox@sha256:6bdd92bf5240be1b5f3bf71324f5e371fe59f0e153b27fa1f1620f78ba16963c  1.1s
 => => resolve docker.io/library/busybox@sha256:6bdd92bf5240be1b5f3bf71324f5e371fe59f0e153b27fa1f1620f78ba16963c  0.0s
 => => sha256:6bdd92bf5240be1b5f3bf71324f5e371fe59f0e153b27fa1f1620f78ba16963c 2.29kB / 2.29kB                    0.0s
 => => sha256:dacd1aa51e0b27c0e36c4981a7a8d9d8ec2c4a74bf125c0a44d0709497a522e9 527B / 527B                        0.0s
 => => sha256:bc01a3326866eedd68525a4d2d91d2cf86f9893db054601d6be524d5c9d03981 1.46kB / 1.46kB                    0.0s
 => => sha256:22b70bddd3acadc892fca4c2af4260629bfda5dfd11ebc106a93ce24e752b5ed 772.99kB / 772.99kB                0.9s
 => => extracting sha256:22b70bddd3acadc892fca4c2af4260629bfda5dfd11ebc106a93ce24e752b5ed                         0.0s
 => [2/4] RUN touch tmpfile                                                                                       0.5s
 => ERROR [3/4] RUN /bin/bash -c echo "continue to build..."                                                      0.7s
------
 > [3/4] RUN /bin/bash -c echo "continue to build...":
#6 0.629 /bin/sh: /bin/bash: not found
------

Dockerfile 常用指令

FROM:指定base镜像

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

COPY:将文件从build context复制到镜像

COPY 支持两种形式:

  1. COPY src dest
  2. COPY [“src”,“dest”]

注意:src只能指定build context中的文件或目录

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

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

...
ENV MY_VERSION 1.3
RUN apt-get install -y mypackage=$MY_VERSION
...

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

  1. RUN执行命令并创建新的镜像层,RUN经常用于安装软件包
  2. CMD设置容器启动后默认执行的命令及参数,但CMD能够被docker run后面的命令行参数替换
  3. ENTRYPOINT配置**容器启动时运行的命令****。

Shell 和Exec格式

我们可用两种方式指定RUN、CMD、ENTRYPOINT要运行的命令:Shell格式和Exec格式,二者在使用上有细微的区别。

  • Shell格式:
<instruction> <command>

例如:

RUN apt-get install python3
CMD echo "Hello world"
ENTRYPOINT echo "Hello world"

当执行指令时,shell格式底层会调用/bin/sh -c

例如下面的Dockerfile片段:

ENV name Jqq
ENTRYPOINT echo "Hello, $name"

执行 docker run 将输出:

Hello, Jqq

这时环境变量已被值替换。

  • Exec格式:
<instruction> ["exectutable","param1","param2",...]

例如:

RUN ["apt-get","install","python3"]
CMD ["/bin/echo","Hello world"]
ENTRYPOINT ["/bin/echo", "Hello world"]

当指令执行时,会直接调用,不会被shell解析,例如下面的Dockerfile片段:

ENV name Jqq
ENTRYPOINT ["/bin/echo","Hello, $name"]

运行容器将输出:

Hello, $name

注意环境变量"name"没有被替换。

如果希望使用环境变量,要显式指定参数

ENV name Jqq
ENTRYPOINT ["/bin/sh","-c","echo Hello $name"]

运行容器将输出:

Hello, Jqq

CMD和ENTRYPOINT都推荐使用Exec格式,因为指令可读性更强,更容易理解,RUN则两种格式都可以。

CMD

允许用户指定容器的默认执行的命令,此命令会在容器启动且docker run没有指定其他命令时运行。

  1. 如果docker run指定了其他命令,CMD指定的默认命令将被忽略
  2. 如果Dockerfile中有多个CMD指令,只有最后一个CMD有效

CMD有三种格式:

  1. Exec格式: [“exectutable”,“param1”,“param2”,…]

    这是CMD的推荐格式

  2. CMD [“param1”,“param2”]为ENTRYPOINT提供额外的参数,此时ENTRYPOINT必须使用Exec格式

  3. Shell格式:CMD command param1 param2

第二种格式要与Exec格式的ENTRYPOINT指令配合使用,其用途是为ENTRYPOINT设置默认的参数。

ENTRYPOINT

让容器以应用程序或者服务的形式运行。

ENTRYPOINT与CMD很小,不同之处在于ENTRYPOINT一定会被执行,即使运行docker run时指定了其他命令

使用ENTRYPOINT的两种格式——Shell和Exec(推荐格式)时需小心,因为这两种格式的效果差别很大。

Exec格式

ENTRYPOINT中的参数始终会被使用,而CMD的额外参数可以在容器启动时动态替换掉。

例如:

ENTRYPOINT ["/bin/echo", "Hello"]
CMD ["world"]

当容器通过docker run -it [image]启动时,输出为:

Hello world

而如果通过docker run -it [image] Jqq启动,输出为:

Hello Jqq
Shell 格式

ENTRYPOINT会忽略任何CMD或docker run提供的参数

建议

  1. 使用RUN指令安装应用和软件包,构建镜像
  2. 如果Docker镜像的用途是运行程序或服务,比如运行一个MySQL,应该优先使用Exec格式的ENTRYPOINT指令。CMD可为ENTRYPOINT提供额外的默认参数,同时可利用docker run 命令行替换默认参数。
  3. 如果想为容器设置默认的启动命令,可使用CMD指令。用户可在docker run命令行中替换此默认命令。

镜像命名的最佳实践

镜像的名字由两部分组成:

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

默认tag为latest

借鉴软件版本命名方式能够让用户很好地使用镜像

当发布一个镜像myimage,版本为v1.9.1,我们可以给镜像打上四个tag:1.9.1、1.9、1和latest

docker tag myimage-v1.9.1 myimage:1
docker tag myimage-v1.9.1 myimage:1.9
docker tag myimage-v1.9.1 myimage:1.9.1
docker tag myimage-v1.9.1 myimage:latest

而当我们发布v1.9.2时,我们可以为其打上1.9.2的tag,并将1.9、1和latest标签从v1.9.1移到v1.9.2

docker tag myimage-v1.9.2 myimage:1
docker tag myimage-v1.9.2 myimage:1.9
docker tag myimage-v1.9.2 myimage:1.9.2
docker tag myimage-v1.9.2 myimage:latest

之后的版本以此类推

在这里插入图片描述

这种tag方案使镜像的版本很直观,使用户灵活选择。

使用公共Registry

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

如何在Docker Hub上存取镜像:

  1. 在Docker Hub上注册一个账号

  2. 在Docker Host上登录

    docker login -u captain_j
    

    输入密码

  3. 修改镜像的repository

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

    可以通过docker tag <image> [username]/xxx:tag重命名镜像

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

    Docker会上传镜像的每一层。但如果我们的镜像是基于base镜像的,也只有新增加的镜像层会被上传。

    如果想上传统一repository中的所有镜像,忽略tag部分就可以了

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

搭建本地Registry

Docker已经将Registry开源了,同时在Docker Hub上也有官方的镜像registry。下面我们就在docker中运行自己的registry

  1. 启动registry容器
docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:2

使用的镜像是registry:2

-d是后台启动容器

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

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

  1. 通过docker tag重命名镜像,使之与registry匹配
docker tag <imageName>:<tag> registry.example.net:5000/<username>/<imageName>:<tag>

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

前面已经讨论了镜像名称由repository和tag两部分组成。而repository的完整格式为:[registry-host]:[port]/[username]/xxx

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

  1. 通过docker push上传镜像

  2. 通过docker pull从本地registry下载镜像

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

Docker 镜像小结

常用操作:

命令解释
images显示镜像列表
history显示镜像构建历史
commit从容器创建新镜像
build从Dockerfile构建镜像
tag给镜像打标签
pull从registry下载镜像
push将镜像上传到registry
rmi删除Docker host中的镜像
search搜索Docker Hub中的镜像
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值