初学 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 去用。php
这个显示的大小是计算后的大小,要知道 docker image 是分层存储的,在1.10以前,不一样镜像没法共享同一层,因此基本上确实是下载大小。可是从1.10以后,已有的层(经过SHA256来判断),不须要再下载。只须要下载变化的层。因此实际下载大小比这个数值要小。并且本地硬盘空间占用,也比docker images列出来的东西加起来小不少,不少重复的部分共享了。html
简单来讲,<none> 就是说该镜像没有打标签。而没有打标签镜像通常分为两类,一类是依赖镜像,一类是丢了标签的镜像。
依赖镜像
Docker的镜像、容器的存储层是Union FS,分层存储结构。因此任何镜像除了最上面一层打上标签(tag)外,其它下面依赖的一层层存储也是存在的。这些镜像没有打上任何标签,因此在 docker images -a 的时候会以 <none> 的形式显示。注意观察一下 docker pull 的每一层的sha256的校验值,而后对比一下 <none> 中的相同校验值的镜像,它们就是依赖镜像。这些镜像不该当被删除,由于有标签镜像在依赖它们。
丢了标签的镜像
这类镜像可能原本有标签,后来丢了。缘由可能不少,好比:
docker pull 了一个一样标签可是新版本的镜像,因而该标签从旧版本的镜像转移到了新版本镜像上,那么旧版本的镜像上的标签就丢了;
docker build 时指定的标签都是同样的,那么新构建的镜像拥有该标签,而以前构建的镜像就丢失了标签。
这类镜像被称为 dangling - 虚悬镜像。这些镜像能够删除,手动删除 dangling 镜像:node
docker image prune
对于 1.13 之前的老版本,使用 dangling=true 过滤条件便可。可使用命令:docker rmi $(docker images -aq -f "dangling=true")
对于频繁构建的机器,好比 Jenkins 之类的环境。手动清理显然不是好的办法,应该按期执行固定脚原本清理这些无用的镜像。很幸运,Spotify 也面临了一样的问题,他们已经写了一个开源工具来作这件事情:https://github.com/spotify/docker-gcpython
Docker Hub上显示的是通过 gzip 压缩后的镜像大小,这个大小也是你将下载的镜像大小,通常来讲也是 Docker Hub 用户最关心的大小。
而 docker images 显示的是pull下来并解压缩后的大小,由于使用docker images的时候更关心的是本地磁盘空间占用的大小,因此这里显示的是未压缩镜像的大小。mysql
简单的回答就是,不要用 commit,去写 Dockerfile。nginx
Docker 不是虚拟机。这句话要在学习 Docker 的过程当中反复提醒本身。因此不要把虚拟机中的一些概念带过来。git
Docker 提供了很好的 Dockerfile 的机制来帮助定制镜像,能够直接使用 Shell 命令,很是方便。并且,这样制做的镜像更加透明,也容易维护,在基础镜像升级后,能够简单地从新构建一下,就能够继承基础镜像的安全维护操做。github
使用 docker commit 制做的镜像被称为黑箱镜像,换句话说,就是里面进行的是黑箱操做,除本人外无人知晓。即便这个制做镜像的人,过一段时间后也不会完整的记起里面的操做。那么当有些东西须要改变时,或者因基础镜像更新而须要从新制做镜像时,会让一切变得异常困难,就如同从新安装调试配置服务器同样,失去了 Docker 的优点了。redis
另外,Docker 不是虚拟机,其文件系统是 Union FS,分层式存储,每一次 commit 都会创建一层,上一层的文件并不会由于 rm 而删除,只是在当前层标记为删除而看不到了而已,每次 docker pull 的时候,那些没必要要的文件都会如影随形,所获得的镜像也必然臃肿不堪。并且,随着文件层数的增长,不只仅镜像更臃肿,其运行时性能也必然会受到影响。这一切都违背了 Docker 的最佳实践。sql
使用 commit 的场合是一些特殊环境,好比入侵后保存现场等等,这个命令不该该成为定制镜像的标准作法。因此,请用 Dockerfile 定制镜像。
commit 命令在前一个问答已经说过,这是制做黑箱镜像,没法维护,不该该被使用。
import 和 export 的作法,其实是将一个容器来保存为 tar 文件,而后在导入为镜像。这样制做的镜像一样是黑箱镜像,不该该使用。并且这类导入导出会致使原有分层丢失,合并为一层,并且会丢失不少相关镜像元数据或者配置,好比 CMD 命令就可能丢失,致使镜像没法直接启动。
save 和 load 确实是镜像保存和加载,可是这是在没有 registry 的状况下,手动把镜像考来考去,这是回到了十多年的 U 盘时代😭。这一样是不推荐的,镜像的发布、更新维护应该使用 registry。不管是本身架设私有 registry 服务,仍是使用公有 registry 服务,如 Docker Hub。
最直接也是最简单的办法是看官方文档。
这篇文章讲述具体 Dockerfile 的命令语法:https://docs.docker.com/engine/reference/builder/
而后,学习一下官方的 Dockerfile 最佳实践:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/
最后,去 Docker Hub 学习那些官方(Official)镜像 Dockerfile 咋写的。
也能够看个人笔记。。。http://www.cnblogs.com/syaving/p/8047183.html
不是这样的。
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
不是把全部命令都合为一个 RUN,要合理分层,以加快构建和部署。
合理分层就是将具备不一样变动频繁程度的层,进行拆分,让稳定的部分在基础,更容易变动的部分在表层,使得资源能够重复利用,以增长构建和部署的速度。
以 node.js 的应用示例镜像为例,其中的复制应用和安装依赖的部分,若是都合并一块儿,会写成这样:
COPY . /usr/src/app RUN npm install
可是,在 node.js 应用镜像示例中,则是这么写的:
COPY package.json /usr/src/app/ RUN npm install COPY . /usr/src/app
从层数上看,确实多了一层。但实际上,这三行分开是故意这样作的,其目的就是合理分层,充分利用 Docker 分层存储的概念,以增长构建、部署的效率。
在 docker build 的构建过程当中,若是某层以前构建过,并且该层未发生改变的状况下,那么 docker 就会直接使用缓存,不会重复构建。所以,合理分层,充分利用缓存,会显著加速构建速度。
第一行的目的是将 package.json 复制到应用目录,而不是整个应用代码目录。这样只有 pakcage.json 发生改变后,才会触发第二行 RUN npm install。而只要 package.json 没有变化,那么应用的代码改变就不会引起 npm install,只会引起第三行的 COPY . /usr/src/app,从而加快构建速度。
而若是按照前面所提到的,合并为两层,那么任何代码改变,都会触发 RUN npm install,从而浪费大量的带宽和时间。
合理分层除了能够加快构建外,还能够加快部署,要知道,docker pull 的时候,是分层下载的,而且已存在的层就不会重复下载。
好比,这里的 RUN npm install 这一层,每每会几百 MB 甚至上 GB。而在 package.json 未发生变动的状况下,那么只有 COPY . /usr/src/app 这一层会被从新构建,而且也只有这一层会在各个节点 docker pull 的过程当中从新下载,每每这一层的代码量只有几十 MB,甚至更小。这对于大规模的并行部署中,所节约的东西向流量是很是显著的。特别是敏捷开发环境中,代码变动的频繁度要比依赖变动的频繁度高不少,每次重复下载依赖,会致使没必要要的流量和时间上的浪费。
context,上下文,是 docker build 中很重要的一个概念。构建镜像必须指定 context:
docker build -t xxx <context路径>
或者 docker-compose.yml 中的
app: build: context: <context路径> dockerfile: dockerfile
这里都须要指定 context。
context 是工做目录,但不要和构建镜像的Dockerfile 中的 WORKDIR 弄混,context 是 docker build 命令的工做目录。
docker build 命令其实是客户端,真正构建镜像并不是由该命令直接完成。docker build 命令将 context 的目录上传给 Docker 引擎,由它负责制做镜像。
在 Dockerfile 中若是写 COPY ./package.json /app/ 这种命令,实际的意思并非指执行 docker build 所在的目录下的 package.json,也不是指 Dockerfile 所在目录下的 package.json,而是指 context 目录下的 package.json。
这就是为何有人发现 COPY ../package.json /app 或者 COPY /opt/xxxx /app 没法工做的缘由,由于它们都在 context 以外,若是真正须要,应该将它们复制到 context 目录下再操做。
话说,有一些网文甚至搞笑的说要把 Dockerfile 放到磁盘根目录,才能构建如何如何。这都是对 context 彻底不了解的表现。想象一下把整个磁盘几十个 GB当作上下文发送给 dockerd 引擎的状况,😱……
docker build -t xxx . 中的这个.,实际上就是在指定 Context 的目录,而并不是是指定 Dockerfile 所在目录。
默认状况下,若是不额外指定 Dockerfile 的话,会将 Context 下的名为 Dockerfile 的文件做为 Dockerfile。因此不少人会混淆,认为这个 . 是在说 Dockerfile 的位置,其实否则。
通常项目中,Dockerfile 可能被放置于两个位置。
一个多是放置于项目顶级目录,这样的好处是在顶级目录构建时,项目全部内容都在上下文内,方便构建;
另外一个作法是,将全部 Docker 相关的内容集中于某个目录,好比 docker 目录,里面包含全部不一样分支的 Dockerfile,以及 docker-compose.yml 类的文件、entrypoint 的脚本等等。这种状况的上下文所在目录再也不是 Dockerfile 所在目录了,所以须要注意指定上下文的位置。
此外,项目中可能会包含一些构建不须要的文件,这些文件不该该被发送给 dockerd 引擎,可是它们处于上下文目录下,这种状况,咱们须要使用 .dockerignore 文件来过滤没必要要的内容。.dockerignore 文件应该放置于上下文顶级目录下,内容格式和 .gitignore 同样。
tmp
db
这样就过滤了 tmp 和 db 目录,它们不会被做为上下文的一部分发给 dockerd 引擎。
若是你发现你的 docker build 须要发送庞大的 Context 的时候,就须要来检查是否是 .dockerignore 忘了撰写,或者忘了过滤某些东西了。
ENTRYPOINT 和 CMD 到底有什么不一样?
Dockerfile 的目的是制做镜像,换句话说,其实是准备的是主进程运行环境。那么准备好后,须要执行一个程序才能够启动主进程,而启动的办法就是调用 ENTRYPOINT,而且把 CMD 做为参数传进去运行。也就是下面的概念:
ENTRYPOINT "CMD"
假设有个 myubuntu 镜像 ENTRYPOINT 是 sh -c,而咱们 docker run -it myubuntu uname -a。那么 uname -a 就是运行时指定的 CMD,那么 Docker 实际运行的就是结合起来的结果:
sh -c "uname -a"
若是没有指定 ENTRYPOINT,那么就只执行 CMD;
若是指定了 ENTRYPOINT 而没有指定 CMD,天然执行 ENTRYPOINT;
若是 ENTRYPOINT 和 CMD 都指定了,那么就如同上面所述,执行 ENTRYPOINT "CMD";
若是没有指定 ENTRYPOINT,而 CMD 用的是上述那种 shell 命令的形式,则自动使用 sh -c 做为 ENTRYPOINT。
注意最后一点的区别,这个区别致使了一样的命令放到 CMD 和 ENTRYPOINT 下效果不一样,所以有可能放在 ENTRYPOINT 下的一样的命令,因为须要 tty 而运行时忘记了给(好比忘记了docker-compose.yml 的 tty:true)致使运行失败。
这种用法能够很灵活,好比咱们作个 git 镜像,能够把 git 命令指定为 ENTRYPOINT,这样咱们在 docker run 的时候,直接跟子命令便可。好比 docker run git log 就是显示日志。
直接去 Docker Hub 上看:大多数 Docker Hub 上的镜像都会有 Dockerfile,直接在 Docker Hub 的镜像页面就能够看到 Dockerfile 的连接;
若是是本身公司作的,最简单的办法就是打个电话、发个消息问一下。别看这个说法看起来很傻,很多人都宁肯本身琢磨也不去问;
若是没有 Dockerfile,通常这类镜像就不该该考虑使用了,这类黑箱似的镜像很容有有问题。若是是什么特殊缘由,那继续往下看;
docker history 能够看到镜像每一层的信息,包括命令,固然黑箱镜像的 commit 看不见操做;
docker inspect 能够分析镜像不少细节。
直接运行镜像,进入shell,而后根据上面的分析结果去进一步分析日志、文件内容及变化。
通过分析后,本身写 Dockerfile 还原操做。
这里所提到的是个人那个 LNMP 例子的 php 服务的 Dockerfile:https://coding.net/u/twang2218/p/docker-lnmp/git/blob/master/php/Dockerfile
FROM php:7-fpm RUN set -xe \ # "构建依赖" && buildDeps=" \ build-essential \ php5-dev \ libfreetype6-dev \ libjpeg62-turbo-dev \ libmcrypt-dev \ libpng12-dev \ " \ # "运行依赖" && runtimeDeps=" \ libfreetype6 \ libjpeg62-turbo \ libmcrypt4 \ libpng12-0 \ " \ # "安装 php 以及编译构建组件所需包" && apt-get update \ && apt-get install -y ${runtimeDeps} ${buildDeps} --no-install-recommends \ # "编译安装 php 组件" && docker-php-ext-install iconv mcrypt mysqli pdo pdo_mysql zip \ && docker-php-ext-configure gd \ --with-freetype-dir=/usr/include/ \ --with-jpeg-dir=/usr/include/ \ && docker-php-ext-install gd \ # "清理" && apt-get purge -y --auto-remove \ -o APT::AutoRemove::RecommendsImportant=false \ -o APT::AutoRemove::SuggestsImportant=false \ $buildDeps \ && rm -rf /var/cache/apt/* \ && rm -rf /var/lib/apt/lists/*
这里是针对 php 镜像进行定制,默认状况下 php:7-fpm 中没有安装所需的 mysqli, pdo_mysql, gd 等组件,因此这里须要安装,并且,部分组件还须要编译。
所以,这里涉及了两类依赖库/工具,一类是安装、编译阶段所须要的依赖;另外一类是运行时所需的依赖。要记住 Dockerfile 的最佳实践中要求最终镜像只应该保留最小的所需依赖,所以安装构建的依赖应该在安装结束后清除,这一层只保留真正须要的运行时依赖。
所以,遵循最佳实践的要求,这里区分了 buildDeps 和 runtimeDeps 后,能够在安装结束后,卸载、清理 buildDeps 的依赖。这样确保没有无关的东西还在该层中。
两种方法均可以。
若是代码变更很是频繁,好比开发阶段,代码几乎每几分钟就须要变更调试,这种状况可使用 --volume 挂载宿主目录的办法。这样不用每次构建新镜像,直接再次运行就能够加载最新代码,甚至有些工具能够观察文件变化从而动态加载,这样能够提升开发效率。
若是代码没有那么频繁变更,好比发布阶段,这种状况,应该将构建好的应用放入镜像。通常来讲是使用 CI/CD 工具,如 Jenkins, Drone.io, Gitlab CI 等,进行构建、测试、制做镜像、发布镜像、以及分步发布上线。
对于配置文件也是一样的道理,若是是频繁变动的配置,能够挂载宿主,或者动态配置文件可使用卷。可是对于并不是频繁变动的配置文件,应该将其归入版本控制中,走 CI/CD 流程进行部署。
须要注意的一点是,绑定宿主目录虽然方便,可是不利于集群部署,由于集群部署前还须要确保集群各个节点同步存在所挂载的目录及其内容。所以集群部署更倾向于将应用打入镜像,方便部署。
这是典型的对 Dockerfile 以及镜像、容器的基本概念不了解。
Dockerfile 不是 shell 脚本,而是定制 rootfs 的脚本。它并非在运行时运行的,而是在构建时运行的。
导入 .sql 文件到数据库,实际上修改的是数据库数据文件,而数据库的数据文件存储于卷,默认为匿名卷,所以当导入行为结束后,构建该层的容器中止运行,匿名卷被抛弃,全部导入行为都会丢失,所以所谓的导入 .sql 的行为在 Dockerfile 里实际上彻底没有意义。
而 service xxxx start 也彻底没有意义,这是启动后台服务,且不说 Docker 中不用后台服务,这种启动行为对文件系统根本没影响,这仅仅是让后台在构建所用的容器中运行一下,彻底没有意义。最后运行容器的时候,是另外一个进程了,该没启动的东西仍是不会启动。
可是不要所以就盲目的得出 Dockerfile 没法初始化数据库的结论。全部官方镜像都考虑到了定制的问题,去看特定官方镜像的文档,基本都会看到定制、初始化的方法。
好比官方 mysql 镜像中,能够把初始化的 .sql 脚本文件在 Dockerfile 中 COPY 至 /docker-entrypoint-initdb.d/ 目录中,在容器第一次运行的时候,若是所挂载的卷是空的,那么就会依次执行该目录中的文件,从而完成数据库初始化、导入等功能。
FROM mysql:5.7 COPY mysql-data-backup.sql /docker-entrypoint-initdb.d/
Alpine Linux 体积小是由于它所使用的基础命令来自精简的 busybox,而且它使用的是简化实现的 musl 做为库支持,而并不是完整的 glibc。musl 体积小,可是有可能有不兼容的状况,所以通常不用 Alpine 的镜像,除非空间受限,体积大小很关键时才会使用。
过去出现过兼容问题,可是随着 Docker 的使用,对 Alpine 的需求会愈来愈多,更多的兼容问题会被发现、修复,因此相信在将来这应该是个不错的选择。可是若是如今就要使用,必定要进行重复的测试,确保没有会影响到本身的 bug。
镜像是分层存储的,镜像之间也能够依赖,所以利用 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)。