文章目录
一、镜像的结构(分层结构)
1 什么是base镜像?
- base 镜像:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。
所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。
2 docker镜像的分层结构特点
- 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。
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 管理软件包;而 CentOS7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。所以 Docker 可以同时支持多种 Linux镜像,模拟出多种操作系统环境。)
3 关于docker镜像的资源共享
比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base
镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。
这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?答案是不会!修改会被限制在单个容器内。
这就是我们接下来要说的容器Copy-on-Write (写时复制)特性:
- 新数据会直接存放在最上面的容器层。
- 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。
- 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。
4 可写的容器层
- 当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
- 典型的Linux在启动后,首先将 rootfs 置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite” 供用户使用。在docker中,起初也是将 rootfs 以readonly方式加载并检查,然而接下来利用 union mount 的将一个 readwrite 文件系统挂载在 readonly 的rootfs之上,并且允许再次将下层的 file system设定为readonly 并且向上叠加, 这样一组readonly和一个writeable的结构构成一个container的运行目录, 每一个被称作一个Layer。
所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。
5 容器层的细节
- 上层文件覆盖:镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。
- 添加文件:在容器中创建文件时,新文件被添加到容器层中。
- 读取文件:在容器中读取某个文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后打开并读入内存。
- 修改文件:在容器中修改已存在的文件时,Docker 会从上往下依次在各镜像层中查找此文件。一旦找到,立即将其复制到容器层,然后修改之。
- 删除文件:在容器中删除文件时,Docker 也是从上往下依次在镜像层中查找此文件。找到后,会在容器层中记录下此删除操作。
- 只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。也就是说,容器层写操作时,因为最上层容器没有这个文件,所以需要从下层镜像层copy一份上来,进行修改。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。
二、镜像的构建
1 docker commit 构建新镜像
基本思路:运行容器——>修改容器——>保存容器为新的镜像
注意:ctrl+p+q
后台运行退出容器;ctrl+d
直接退出容器,这样容器会直接停止运行
[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 #查看所有进程
此时,我们对docker容器进行了修改,也就是容器层有了变化,如何保存呢?docker commit
[root@server1 ~]# docker commit vm1 ubuntu:v1 #有100个文件,把vm1容器的数据提交到镜像,并且给镜像起个名字
[root@server1 ~]# docker history ubuntu:v1 #可以查看镜像层,有多少层
[root@server1 ~]# docker history ubuntu:latest #查看历史,除最上边之外和v1相同
[root@server1 ~]# docker run -it --name vm2 ubuntu:v1 #此时,该容器有100个文件
使用commit的缺点是:效率低、可重复性弱、容易出错,使用者无法对镜像进行审计,存在安全隐患
2 Dockerfile构建镜像
Dockerfile的基本写法:
FROM:指定base镜像,如果本地不存在会从远程仓库下载。
MAINTAINER:设置镜像的作者,比如用户邮箱等。
COPY:把文件从build context复制到镜像 (都是基于dockerfile的当前目录)
支持两种形式: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,如果指定了很多,只有最后一个有效。
- 使用Dockerfile构建一个Apache应用:
- docker引擎构建目录。尽量不要放在根目录下,否则会把根目录下所有数据发给docker引擎。
[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
- 该目录下写dockerfile,以及需要copy的文件。
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
- 使用dockerfile构建apache镜像
[root@server1 docker]# docker build -t rhel7:apache . #最后的点表示从当前目录
[root@server1 docker]# docker run -d --name vm2 -p 80:80 rhel7:apache
- 查看
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 # 删除所有非活跃的容器数据卷
三、构建镜像的优化
1.减少分层(多阶段构建镜像)
尽量减少镜像层数,清理中间产物,多阶段构建镜像。
Dockerfile内容:
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跟物理机不在同一网段,所以必须作端口映射才能在物理机访问。
2. 选择最精简的基础镜像。
镜像的目的最后是封装应用,那么我们为了实现run anywhere,当然在实现功能的基础下,越小越好,方便容器迁移。
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
ok ,至此成功!