Docker镜像

镜像的分层结构

base镜像
  • base 镜像简单来说就是不依赖其他任何镜像,完全从0开始建起,其他镜像都是建立在他的之上,可以比喻为大楼的地基,docker镜像的鼻祖。
  • base 镜像有两层含义:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。
  • 所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
分层结构概述

1.共享宿主机的kernel
2.base镜像提供的是最小的Linux发行版
3.同一docker主机支持运行多种Linux发行版
4.采用分层结构的最大好处是:共享资源
5.Copy-on-Write 可写容器层
6.容器层以下所有镜像层都是只读的
7.docker从上往下依次查找文件
8.容器层保存镜像变化的部分,并不会对镜像本身进行任何修改
9.一个镜像最多127层

  • 内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。 对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。 而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。
    我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就不足为奇了。)
  • base镜像提供的是最小的Linux发行版
  • 同一docker主机支持运行多种Linux发行版( bootfs (boot file system) 主要包含 bootloader 和 kernel, bootloader主要是引导加载kernel, 当boot成功后 kernel 被加载到内存中后 bootfs就被umount了
    rootfs (root file system) 包含的就是典型 Linux 系统中的 /dev, /proc, /bin, /etc 等标准目录和文件。
    由此可见对于不同的linux发行版, bootfs基本是一致的, rootfs会有差别, 因此不同的发行版可以公用bootfs。比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。所以 Docker 可以同时支持多种 Linux镜像,模拟出多种操作系统环境。)
    在这里插入图片描述
  • 可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。
为什么 Docker 镜像要采用这种分层结构呢?
  • 最大的一个好处就是 ——共享资源。
    比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
    这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?答案是不会!
    修改会被限制在单个容器内。
    这就是我们接下来要说的容器 Copy-on-Write 特性
  • 新数据会直接存放在最上面的容器层。
  • 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。
  • 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。
可写的容器层
  • 当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
  • 典型的Linux在启动后,首先将 rootfs(root file system根文件系统) 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录, 每一个被称作一个Layer。
    在这里插入图片描述
  • 所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的
  • 下面我们深入讨论容器层的细节。
  • 镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
  • 添加文件:在容器中创建文件时,新文件被添加到容器层中。
  • 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存
  • 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
  • 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
  • 只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

镜像的构建

docker commit 构建新镜像三部曲
  • 运行容器
  • 修改容器
  • 将容器保存为新的镜像
    缺点:
    效率低、可重复性弱、容易出错
    使用者无法对镜像进行审计,存在安全隐患
构建镜像的命令
运行容器 # docker run -it --name test busybox
修改容器 (以下命令在容器内运行) # echo helloworld > testfile
将容器保存为新的镜像 # docker commit  test test:v1
查看镜像 # docker images test:v1

举例:

[root@server1 ~]# docker load -i ubuntu.tar
[root@server1 ~]# docker images
[root@server1 ~]# docker run -it --name vm1 ubuntu
root@11aea71d877a:/# uname -r   #查看内核版本,与素主机内核版本相同,内核不在镜像分层结构内。
root@11aea71d877a:/# touch file{1..100}   #新建100个文件,容器层,都不会影响到镜像层
root@11aea71d877a:/# ls
[root@server1 ~]# docker ps -a   #查看所有进程

在这里插入图片描述

[root@server1 ~]# docker commit vm1 ubuntu:v1   #有100个文件,把vm1容器的数据提交到镜像,并且给镜像起个名字
[root@server1 ~]# docker history ubuntu:v1    #查看历史,最上边是bash
[root@server1 ~]# docker history ubuntu:latest   #查看历史,除最上边之外和v1相同
[root@server1 ~]# docker rm -f vm3   #强制删除vm3容器

在这里插入图片描述

[root@server1 ~]# docker run -it --name vm2 ubuntu:v1  #有100个文件
[root@server1 ~]# docker rm -f vm3
[root@server1 ~]# docker run -it --name vm3 ubuntu  #没有,镜像是ubuntu:latest
[root@server1 ~]# docker run -it --name vm4 ubuntu:v1   #有100个文件

在这里插入图片描述

[root@server1 ~]# docker rmi ubuntu:v1   #删除镜像
[root@server1 ~]# docker rmi -f  ubuntu:v1   #强制删除
[root@server1 ~]# docker history ubuntu:latest   #查看镜像构建历史
[root@server1 ~]# docker history game2048:latest   #查看端口为80,是镜像封装时就加进去的。

在这里插入图片描述

[root@server1 ~]# docker ps  活跃的
[root@server1 ~]# docker start vm2 #开启容器vm2
[root@server1 ~]# docker container attach vm2   #进入vm2
exit 退出容器时会释放掉 但是提交之后数据都还在 。镜像层不会变。 

Dockerfile详解

dockerfile常用指令
FROM:指定base镜像,如果本地不存在会从远程仓库下载。	

MAINTAINER:设置镜像的作者,比如用户邮箱等。

COPY:把文件从build context复制到镜像
支持两种形式:COPY src dest 和 COPY ["src", "dest"]
src必须指定build context中的文件或目录

ADD:用法与COPY类似,不同的是src可以是归档压缩文件,文件会被自动解压到dest,也可以自动下载URL并拷贝到镜像:
ADD html.tar /var/www
ADD http://ip/html.tar /var/www

ENV:设置环境变量,变量可以被后续的指令使用:
ENV HOSTNAME sevrer1.example.com

EXPOSE:如果容器中运行应用服务,可以把服务端口暴露出去:
EXPOSE 80

VOLUME:申明数据卷,通常指定的是应用的数据挂在点:
VOLUME ["/var/www/html"]

WORKDIR:为RUN、CMD、ENTRYPOINT、ADD和COPY指令设置镜像中的当前工作目录,如果目录不存在会自动创建。

RUN:在容器中运行命令并创建新的镜像层,常用于安装软件包:
RUN yum install -y vim

CMD 与 ENTRYPOINT
这两个指令都是用于设置容器启动后执行的命令,但CMD会被docker run后面的命令行覆盖,而ENTRYPOINT不会被忽略,一定会被执行。
docker run后面的参数可以传递给ENTRYPOINT指令当作参数。
Dockerfile中只能指定一个ENTRYPOINT,如果指定了很多,只有最后一个有效。

举例1:
利用dockerfile封装一个有httpd服务的镜像,且服务为开启状态。

[root@server1 ~]# ls
docker  game2048.tar  rhel7.tar  ubuntu.tar
[root@server1 ~]# docker load -i rhel7.tar    #导入镜像

[root@server1 ~]# mkdir docker    #docker引擎构建目录。尽量不要放在根目录下,否则会把根目录下所有数据发给docker引擎。
[root@server1 ~]# cd docker/
[root@server1 docker]# vim Dockerfile

FROM rhel7
COPY dvd.repo /etc/yum.repos.d/
RUN rpmdb --rebuilddb &&  yum install httpd -y
EXPOSE 80
VOLUME ["/var/www/html"]
CMD ["/usr/sbin/httpd","-D","FOREGROUND"]

[root@server1 docker]# vim dvd.repo
[dvd]
name=rhel7.3
baseurl=http://172.25.11.250/rhel7.3
gpgcheck=0

[root@server1 docker]# docker build -t rhel7:apache .    #最后的点表示从当前
[root@server1 docker]# docker run -it --name vm2 rhel7:apache bash
docker inspect vm2
cd /var/lib/docker/volumes/fb0f95afa6602832686368f9f9c35020c2cd84759010678d8d4f24fef033c361/_data/
vim index.html
cat index.html 
curl 172.17.0.3
docker volume ls 
docker volume prune

在这里插入图片描述
在这里插入图片描述

举例2:dockerfile常用指令几何

[root@server1 ~]# docker load -i nginx.tar 
[root@server1 docker]# vim Dockerfile 
FROM rhel7
COPY dvd.repo /etc/yum.repos.d/
RUN rpmdb --rebuilddb &&  yum install httpd -y
ADD nginx-1.15.8.tar.gz /mnt
EXPOSE 80
VOLUME ["/var/www/html"]
WORKDIR /mnt
ENV name world
CMD echo "hello $name"   =CMD ["/bin/sh", "-c", "echo hello $name"]   =ENTRYPOINT ["/bin/echo", "hello"] \ CMD ["world"]


[root@server1 docker]# docker build -t rhel7:v2 .
[root@server1 docker]# docker run --rm rhel7:v2    #运行完删除
hello world
两种构建方式的区别
  • 1.容器镜像的构建者可以任意修改容器的文件系统后进行发布,这种修改对于镜像使用者来说是不透明的,镜像构建者一般也不会将对容器文件系统的每一步修改,记录进文档中,供镜像使用者参考。
  • 2.容器镜像不能(更准确地说是不建议)通过修改,生成新的容器镜像。
    从镜像运行容器,实际上是在镜像顶部上加了一层可写层,所有对容器文件系统的修改,都在这一层中进行,不影响已经存在的层。比如在容器中删除一个1G的文件,从用户的角度看,容器中该文件已经没有了,但从文件系统的角度看,文件其实还在,只不过在顶层中标记该文件已被删除,当然这个标记为已删除的文件还会占用镜像空间。从容器构建镜像,实际上是把容器的顶层固化到镜像中。
  • 也就是说, 对容器镜像进行修改后,生成新的容器镜像,会多一层,而且镜像的体积只会增大,不会减小。长此以往,镜像将变得越来越臃肿。Docker提供的 export 和 import 命令可以一定程度上处理该问题,但也并不是没有缺点。
  • 3.容器镜像依赖的父镜像变化时,容器镜像必须进行重新构建。如果没有编写自动化构建脚本,而是手工构建的,那么又要重新修改容器的文件系统,再进行构建,这些重复劳动其实是没有价值的。
  • 4.Dockerfile镜像是完全透明的,所有用于构建镜像的指令都可以通过Dockerfile看到。甚至你还可以递归找到本镜像的任何父镜像的构建指令。也就是说,你可以完全了解一个镜像是如何从零开始,通过一条条指令构建出来的。
  • 5.Dockerfile镜像需要修改时,可以通过修改Dockerfile中的指令,再重新构建生成,没有任何问题
  • 6.Dockerfile可以在GitHub等源码管理网站上进行托管,DockerHub自动关联源码进行构建。当你的Dockerfile变动,或者依赖的父镜像变动,都会触发镜像的自动构建,非常方便。

镜像的优化

原则
  1. 选择最精简的基础镜像
  2. 减少镜像的层数
  3. 清理镜像构建的中间产物
  4. 注意优化网络请求
  5. 尽量去用构建缓存
  6. 使用多阶段构建镜像
实例分析

举例3:源码编译nginx
方法一:用到了减少镜像层数,清理中间产物,多阶段构建镜像

FROM rhel7 as build
COPY dvd.repo /etc/yum.repos.d/
ADD nginx-1.15.8.tar.gz /mnt
EXPOSE 80
WORKDIR /mnt/nginx-1.15.8
RUN rpmdb --rebuilddb && yum install -y gcc make zlib-devel pcre-devel && rm -fr /var/cache/yum && sed -i 's/CFLAGS="$CFLAGS -g"/#CFLAGS="$CFLAGS -g/g' auto/cc/gcc && ./configure --prefix=/usr/local/nginx && make && make install && rm -fr /mnt/nginx-*

FROM rhel7
COPY --from=build /usr/local/nginx /usr/local/nginx
EXPOSE 80
VOLUME ["/usr/local/nginx/html"]
CMD ["/usr/local/nginx/sbin/nginx","-g","daemon off;"]

[root@server1 docker]# docker build -t rhel7:nginx .
[root@server1 docker]# docker history rhel7:nginx 
[root@server1 docker]# docker run -d --name vm1 -p 80:80 rhel7:nginx   #因为容器分配到的ip跟物理机不在同一网段,所以必须作端口映射才能在物理机访问。
[root@server1 docker]# docker run -it --name vm2 rhel7:nginx bash
  • 4层。
    在这里插入图片描述

  • 浏览器中访问。
    在这里插入图片描述
    方法二:选择最精简的基础镜像。

docker load -i distroless.tar
docker load -i nginx.tar
vim Dockerfile
FROM nginx:1.16 as base

# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
ARG Asia/Shanghai

RUN mkdir -p /opt/var/cache/nginx && \
    cp -a --parents /usr/lib/nginx /opt && \
    cp -a --parents /usr/share/nginx /opt && \
    cp -a --parents /var/log/nginx /opt && \
    cp -aL --parents /var/run /opt && \
    cp -a --parents /etc/nginx /opt && \
    cp -a --parents /etc/passwd /opt && \
    cp -a --parents /etc/group /opt && \
    cp -a --parents /usr/sbin/nginx /opt && \
    cp -a --parents /usr/sbin/nginx-debug /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libpcre.so.* /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libz.so.* /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libc.so.* /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libdl.so.* /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libpthread.so.* /opt && \
    cp -a --parents /lib/x86_64-linux-gnu/libcrypt.so.* /opt && \
    cp -a --parents /usr/lib/x86_64-linux-gnu/libssl.so.* /opt && \
    cp -a --parents /usr/lib/x86_64-linux-gnu/libcrypto.so.* /opt && \
    cp /usr/share/zoneinfo/${TIME_ZON:-ROC} /opt/etc/localtime

FROM gcr.io/distroless/base
COPY --from=base /opt /
EXPOSE 80 443
ENTRYPOINT ["nginx", "-g", "daemon off;"]

docker build -t rhel7:v2 .
docker history rhel7:v2
  • 注意:ldd命令:查看可执行文件所依赖的动态链接库
    在这里插入图片描述

总结

  • 镜像常用子命令
images	显示镜像列表
history	显示镜像构建历史
commit	从容器创建镜像
build	从Dockerfile构建镜像
tag	给镜像打标签
search	搜索镜像
pull	从仓库拉取镜像
push	上传镜像到仓库
rmi	删除镜像
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值