目录

隐藏
  1. docker pull 下来的镜像文件都在哪?
  2. Dockerfile 怎么写?
  3. docker images 命令显示的镜像占了好大的空间,怎么办?每次都是下载这么大的镜像?
  4. 拿到一个镜像,如何获得镜像的 Dockerfile ?
  5. 为什么说不要使用 import, export, save, load, commit 来构建镜像?
  6. docker images -a 后显示了好多 <none> 的镜像?都是什么呀?能删么?
  7. Dockerfile 就是 shell 脚本吧?那我懂,一行行把需要装的东西都写进去不就行了。
  8. docker commit 怎么用啊?
  9. 可以看到镜像各层的依赖关系么?
  10. 应用代码是应该挂载宿主目录还是放入镜像内?
docker pull 下来的镜像文件都在哪?
初学 Docker 要反复告诫自己,Docker 不是虚拟机。

Docker不是虚拟机,Docker 镜像也不是虚拟机的 ISO 文件。Docker 的镜像是分层存储,每一个镜像都是由很多层,很多个文件组成。而不同的镜像是共享相同的层的,所以这是一个树形结构,不存在具体哪个文件是 pull 下来的镜像的问题。

具体镜像保存位置取决于系统,一般Linux系统下,在 /var/lib/docker 里。对于使用 Union FS 的系统(Ubuntu),如 aufs, overlay2 等,可以直接在 /var/lib/docker/{aufs,overlay2} 下看到找到各个镜像的层、容器的层,以及其中的内容。

但是,对于CentOS这类没有Union FS的系统,会使用如devicemapper这类东西的一些特殊功能(如snapshot)模拟,镜像会存储于块设备里,因此无法看到具体每层信息以及每层里面的内容。

需要注意的是,默认情况下,CentOS/RHEL 使用 lvm-loop,也就是本地稀疏文件模拟块设备,这个文件会位于 /var/lib/docker/devicemapper/devicemapper/data 的位置。这是非常不推荐的,如果发现这个文件很大,那就说明你在用 devicemapper + loop 的方式,不要这么做,去参照官方文档,换 direct-lvm,也就是分配真正的块设备给 devicemapper 去用。

Dockerfile 怎么写?
最直接也是最简单的办法是看官方文档。

这篇文章讲述具体 Dockerfile 的命令语法: https://docs.docker.com/engine/reference/builder/

然后,学习一下官方的 Dockerfile 最佳实践: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/

最后,去 Docker Hub 学习那些 官方(Official)镜像 Dockerfile 咋写的。

docker images 命令显示的镜像占了好大的空间,怎么办?每次都是下载这么大的镜像?
这个显示的大小是计算后的大小,要知道 docker image 是分层存储的,在1.10之前,不同镜像无法共享同一层,所以基本上确实是下载大小。但是从1.10之后,已有的层(通过SHA256来判断),不需要再下载。只需要下载变化的层。所以实际下载大小比这个数值要小。而且本地硬盘空间占用,也比docker images列出来的东西加起来小很多,很多重复的部分共享了。
拿到一个镜像,如何获得镜像的 Dockerfile ?
  • 直接去 Docker Hub 上看:大多数 Docker Hub 上的镜像都会有 Dockerfile,直接在 Docker Hub 的镜像页面就可以看到 Dockerfile 的链接;
  • 如果是自己公司做的,最简单的办法就是打个电话、发个消息问一下。别看这个说法看起来很傻,不少人都宁可自己琢磨也不去问;
  • 如果没有 Dockerfile,一般这类镜像就不应该考虑使用了,这类黑箱似的镜像很容有有问题。如果是什么特殊原因,那继续往下看;
  • docker history 可以看到镜像每一层的信息,包括命令,当然黑箱镜像的 commit 看不见操作;
  • docker inspect 可以分析镜像很多细节。
  • 直接运行镜像,进入shell,然后根据上面的分析结果去进一步分析日志、文件内容及变化。
  • 经过分析后,自己写 Dockerfile 还原操作。

为什么说不要使用 import, export, save, load, commit 来构建镜像?
commit 命令在前一个问答已经说过,这是制作黑箱镜像,无法维护,不应该被使用。

import 和 export 的做法,实际上是将一个容器来保存为 tar 文件,然后在导入为镜像。这样制作的镜像同样是黑箱镜像,不应该使用。而且这类导入导出会导致原有分层丢失,合并为一层,而且会丢失很多相关镜像元数据或者配置,比如 CMD 命令就可能丢失,导致镜像无法直接启动。

save 和 load 确实是镜像保存和加载,但是这是在没有 registry 的情况下,手动把镜像考来考去,这是回到了十多年的 U 盘时代。这同样是不推荐的,镜像的发布、更新维护应该使用 registry。无论是自己架设私有 registry 服务,还是使用公有 registry 服务,如 Docker Hub

docker images -a 后显示了好多 <none> 的镜像?都是什么呀?能删么?
简单来说,就是说该镜像没有打标签。而没有打标签镜像一般分为两类,一类是依赖镜像,一类是丢了标签的镜像。

依赖镜像

Docker的镜像、容器的存储层是Union FS,分层存储结构。所以任何镜像除了最上面一层打上标签(tag)外,其它下面依赖的一层层存储也是存在的。这些镜像没有打上任何标签,所以在 docker images -a 的时候会以的形式显示。注意观察一下 docker pull 的每一层的sha256的校验值,然后对比一下中的相同校验值的镜像,它们就是依赖镜像。这些镜像不应当被删除,因为有标签镜像在依赖它们。

丢了标签的镜像

这类镜像可能本来有标签,后来丢了。原因可能很多,比如:

docker pull 了一个同样标签但是新版本的镜像,于是该标签从旧版本的镜像转移到了新版本镜像上,那么旧版本的镜像上的标签就丢了;
docker build 时指定的标签都是一样的,那么新构建的镜像拥有该标签,而之前构建的镜像就丢失了标签。
这类镜像被称为 dangling - 虚悬镜像,这些镜像可以删除,使用 dangling=true 过滤条件即可。

手动删除 dangling 镜像

docker rmi $(docker images -aq -f "dangling=true")
对于频繁构建的机器,比如 Jenkins 之类的环境。手动清理显然不是好的办法,应该定期执行固定脚本来清理这些无用的镜像。很幸运,Spotify 也面临了同样的问题,他们已经写了一个开源工具来做这件事情: https://github.com/spotify/docker-gc

Dockerfile 就是 shell 脚本吧?那我懂,一行行把需要装的东西都写进去不就行了。
不是这样的。

Dockerfile 不等于 .sh 脚本

Dockerfile 确实是描述如何构建镜像的,其中也提供了 RUN 这样的命令,可以运行 shell 命令。但是和普通 shell 脚本还有很大的不同。

Dockerfile 描述的实际上是镜像的每一层要如何构建,所以每一个RUN是一个独立的一层。所以一定要理解“分层存储”的概念。上一层的东西不会被物理删除,而是会保留给下一层,下一层中可以指定删除这部分内容,但实际上只是这一层做的某个标记,说这个路径的东西删了。但实际上并不会去修改上一层的东西。每一层都是静态的,这也是容器本身的 immutable 特性,要保持自身的静态特性。

所以很多新手会常犯下面这样的错误,把 Dockerfile 当做 shell 脚本来写了:

RUN yum update
RUN yum -y install gcc
RUN yum -y install python
ADD jdk-xxxx.tar.gz /tmp
RUN cd xxxx && install
RUN xxx && configure && make && make install
这是相当错误的。除了无畏的增加了很多层,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。

正确的写法应该是把同一个任务的命令放到一个 RUN 下,多条命令应该用 && 连接,并且在最后要打扫干净所使用的环境。比如下面这段摘自官方 redis 镜像 Dockerfile 的部分:

RUN buildDeps='gcc libc6-dev make' \
&& set -x \
&& apt-get update && apt-get install -y $buildDeps --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL" \
&& echo "$REDIS_DOWNLOAD_SHA1 *redis.tar.gz" | sha1sum -c - \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
&& rm redis.tar.gz \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install \
&& rm -r /usr/src/redis \
&& apt-get purge -y --auto-remove $buildDeps

docker commit 怎么用啊?
简单的回答就是,不要用 commit,去写 Dockerfile。

Docker 不是虚拟机。这句话要在学习 Docker 的过程中反复提醒自己。所以不要把虚拟机中的一些概念带过来。

Docker 提供了很好的 Dockerfile 的机制来帮助定制镜像,可以直接使用 Shell 命令,非常方便。而且,这样制作的镜像更加透明,也容易维护,在基础镜像升级后,可以简单地重新构建一下,就可以继承基础镜像的安全维护操作。

使用 docker commit 制作的镜像被称为黑箱镜像,换句话说,就是里面进行的是黑箱操作,除本人外无人知晓。即使这个制作镜像的人,过一段时间后也不会完整的记起里面的操作。那么当有些东西需要改变时,或者因基础镜像更新而需要重新制作镜像时,会让一切变得异常困难,就如同重新安装调试配置服务器一样,失去了 Docker 的优势了。

另外,Docker 不是虚拟机,其文件系统是 Union FS,分层式存储,每一次 commit 都会建立一层,上一层的文件并不会因为 rm 而删除,只是在当前层标记为删除而看不到了而已,每次 docker pull 的时候,那些不必要的文件都会如影随形,所得到的镜像也必然臃肿不堪。而且,随着文件层数的增加,不仅仅镜像更臃肿,其运行时性能也必然会受到影响。这一切都违背了 Docker 的最佳实践。

使用 commit 的场合是一些特殊环境,比如入侵后保存现场等等,这个命令不应该成为定制镜像的标准做法。所以,请用 Dockerfile 定制镜像。

可以看到镜像各层的依赖关系么?
镜像是分层存储的,镜像之间也可以依赖,因此利用 Docker 镜像很容易实现重复的部分复用。那么我们有没有办法可以可视化的看到镜像的依赖关系呢?

很早以前,Docker 有个 docker images --tree 的命令的,后来随着镜像分层平面化后,这个命令就取消了。幸运的是,Nate Jones 写了一个工具,用于可视化镜像分层依赖,叫做 dockviz: https://github.com/justone/dockviz

对于 Mac 平台的用户,可以很方便的使用 brew 来进行安装:

brew install dockviz
对于其它平台的用户,可以直接去 发布页面下载。

安装好后,直接执行 dockviz images --tree 即可:

$ dockviz images --tree
├─<missing> Virtual Size: 55.3 MB
│ └─<missing> Virtual Size: 55.3 MB
│   └─<missing> Virtual Size: 55.3 MB
│     └─<missing> Virtual Size: 55.3 MB
│       └─<missing> Virtual Size: 55.3 MB
│         └─<missing> Virtual Size: 108.3 MB
│           └─<missing> Virtual Size: 108.3 MB
│             └─<missing> Virtual Size: 108.3 MB
│               └─<missing> Virtual Size: 108.3 MB
│                 └─0b5dec81616c Virtual Size: 108.3 MB Tags: nginx:latest
└─<missing> Virtual Size: 100.1 MB
  └─<missing> Virtual Size: 100.1 MB
    └─<missing> Virtual Size: 123.9 MB
      └─<missing> Virtual Size: 131.2 MB
        ├─<missing> Virtual Size: 272.8 MB
        │ └─<missing> Virtual Size: 274.2 MB
        │   └─<missing> Virtual Size: 274.2 MB
        │     └─<missing> Virtual Size: 274.2 MB
        │       └─<missing> Virtual Size: 274.2 MB
        │         └─<missing> Virtual Size: 274.2 MB
        │           └─<missing> Virtual Size: 274.2 MB
        │             └─<missing> Virtual Size: 274.2 MB
        │               └─<missing> Virtual Size: 274.2 MB
        │                 └─<missing> Virtual Size: 737.9 MB
        │                   └─4551430cfe80 Virtual Size: 738.3 MB Tags: openjdk:latest
        └─<missing> Virtual Size: 132.4 MB
          └─<missing> Virtual Size: 132.4 MB
            └─<missing> Virtual Size: 132.4 MB
...
                            └─<missing> Virtual Size: 276.0 MB
                                └─<missing> Virtual Size: 292.4 MB
                                └─<missing> Virtual Size: 292.4 MB
                                    └─<missing> Virtual Size: 292.4 MB
                                    └─72d2be374029 Virtual Size: 292.4 MB Tags: tomcat:latest
如果觉得文本格式太繁杂,也可以生成 DOT 图),使用命令 dockviz images -d | dot -Tpng -o image_tree.png 就可以将你的镜像依赖关系绘制成图( https://imagebin.ca/v/3ZhFvSPeqAi0)。

应用代码是应该挂载宿主目录还是放入镜像内?
两种方法都可以。

如果代码变动非常频繁,比如开发阶段,代码几乎每几分钟就需要变动调试,这种情况可以使用 --volume 挂载宿主目录的办法。这样不用每次构建新镜像,直接再次运行就可以加载最新代码,甚至有些工具可以观察文件变化从而动态加载,这样可以提高开发效率。

如果代码没有那么频繁变动,比如发布阶段,这种情况,应该将构建好的应用放入镜像。一般来说是使用 CI/CD 工具,如 Jenkins, Drone.io, Gitlab CI 等,进行构建、测试、制作镜像、发布镜像、以及分步发布上线。

对于配置文件也是同样的道理,如果是频繁变更的配置,可以挂载宿主,或者动态配置文件可以使用卷。但是对于并非频繁变更的配置文件,应该将其纳入版本控制中,走 CI/CD 流程进行部署。

需要注意的一点是,绑定宿主目录虽然方便,但是不利于集群部署,因为集群部署前还需要确保集群各个节点同步存在所挂载的目录及其内容。因此集群部署更倾向于将应用打入镜像,方便部署。