注意, 这是一篇摘抄文, 仅记录一些概念跟规范
原文出处Docker 从入门到实践
基本概念
Docker包括三个基本概念: 镜像(Image), 容器(Container), 仓库(Repository)
镜像
我们都知道, 操作系统分为内核和用户空间, 对于Linux而言, 内核启动后, 会挂载root文件系统为其提供用户空间支持. 而Docker镜像(Image), 就相当于一个root文件系统. 比如官方镜像ubuntu:18.04就包含了完整的一套Ubuntu 18.04 最下系统的root文件系统
Docker镜像是一个特殊的文件系统, 除了提供容器运行时所需的程序, 库, 资源, 配置等文件外, 还包含了一些为运行时准备的一些配置参数(如匿名卷, 环境变量, 用户等). 镜像不包含任何动态数据, 其内容在构建之后也不会被改变.
分层存储
因为镜像包含操作系统完整的root文件系统, 其体积往往是庞大的, 因此在Docker设计时, 就充分利用Union Fs 的技术, 将其设计为分层存储的架构. 所以严格来说, 镜像并非是像一个ISO那样的打包文件, 镜像只是一个虚拟的概念, 其实际体现并非由一个文件组成, 而是有一组文件系统组成, 或者说, 由多层问价那系统联合组成
镜像构建时, 会一层层构建, 前一层是后一层的基础. 每一层构建完就不会再发生改变, 后一层上的任何改变只发生在自己这一层. 比如删除前一层文件的操作, 实际不是真的删除前一层的文件, 而是仅在当前层标记为该文件已删除. 在最终容器运行的时候, 虽然不会看到这个文件, 但是实际上该文件会一直跟随镜像. 因此, 在构建镜像的时候, 需要额外小心, 每一层尽量只包含该层需要添加的东西, 任何额外的东西应该在该层构建结束前清理掉.
分层存储的特征还使得镜像的复用, 定制变的更为容易. 甚至可以用之前构建好的镜像作为基础层, 然后进一步添加新的层, 以定制自己所需的内容, 构建新的镜像.
Docker容器
镜像(Image)和容器(Container)的关系, 就像是面向对象程序设计的类和实例一样, 镜像是静态的定义, 容器是镜像运行时的实体. 容器可以被创建, 启动, 停止, 删除, 暂停等.
容器的实质是进程, 但与直接在宿主执行的进程不同, 容器进程运行于属于自己的独立的命名空间. 因此容器可以拥有自己的root文件系统, 自己的网络配置, 自己的进程空间, 甚至自己的用户ID空间. 容器内的进程是运行在一个隔离的环境里, 使用起来, 就好像是在一个独立于宿主的系统下操作一样. 这种特性使得容器封装的应用比直接在宿主运行更加安全.
镜像使用的是分层存储, 容器也是如此. 每一个容器运行时, 是以镜像为基础层, 在其上创建一个当前容器的存储层, 我们可以称这个为容器运行时读写而准备的存储层为容器存储层.
容器存储层的生存周期和容器一样, 容器消亡时, 容器存储层也随之消亡. 因此, 任何保存于容器存储层的信息都会随容器删除而丢失.
按照Docker最佳实践的要求, 容器不应该向其存储层内写入任何数据, 容器存储层要保存无状态化. 所有写入操作, 都应该使用数据卷(Volume), 或者绑定宿主目录, 在这些位置的读写会跳过容器存储层, 直接对宿主(或网络存储)发生读写, 其性能和稳定性更高.
数据卷的生存周期独立于容器, 容器消亡, 数据卷不会消亡. 因此, 使用数据卷后, 容器删除或者重新运行之后, 数据却不会消失.
Docker Registry
略
使用Dockerfile定制镜像
镜像的定制实际上就是定制每一层所添加的配置, 文件.
Dockerfile是一个文本文件, 其内包含了一条条的指令(Instruction), 每一条指令构建一层, 因此每一条指令的内容, 就是描述该层应当如何构建.
FROM制定基础镜像
所谓定制镜像, 那一定是以一个镜像为基础. 在其上进行定制. FROM就是制定基础镜像, 因此一个Dockerfile中FROM是必备的指令, 并且必须是第一条指令.
RUN执行命令
RUN指令是用来执行命令行命令的. 由于命令行的强大能力, RUN指令在定制镜像是是最常用的指令之一. 其格式有两种:
- shell格式:
RUN <命令>, 就像直接在命令行中输入的命令一样. 刚才写的Dockerfile中的RUN指令就是这种格式.
1 | RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html |
- exec格式:
RUN ["可执行文件", "参数1", "参数2"], 这更像是函数调用中的格式.
既然RUN就像Shell脚本一样可以执行命令, 那么我们是否就可以像Shell脚本一样把每个命令对应一个RUN呢?
比如:
1 | FROM debian:stretch |
之前说过, Dockerfile中每一个指令都会建立一层, RUN也不例外. 每一个RUN的行为, 就和刚才我们手工建立镜像的过程一样: 新建立一层, 在其上执行这些命令, 执行结束后, commit这一层的修改, 构成新的镜像.
而上面的这种写法, 创建了7层镜像. 这是完全没有意义的, 而且很多运行时不需要的东西, 都被装进了镜像里, 比如编译环境, 更新的软件包等等. 结果就是产生非常臃肿, 非常多层的镜像, 不仅仅增加了构建部署的时间, 也很容易出错.
Union FS 是有最大层数限制的, 比如AUFS, 曾经是最大不得超过42层, 现在是不得超过127层.
上面的Dockerfile正确的写法应该是这样:
1 | FROM debian:stretch |
首先, 之前所有的命令只有一个目的, 就是编译, 安装redis可执行文件. 因此没有必要建立很多层, 这只是一层的事情. 因此, 这里没有使用很多个RUN—-对应不同的命令, 而是仅仅使用一个RUN命令, 并使用&&将各个所需命令串联起来. 将之前的7层, 简化为了1层, 在撰写Dockerfile的时候, 要经常提醒自己, 这并不是在写Shell脚本, 而是在定义每一层该如何构建.
并且, 这里为了格式化还进行了换行, Dockerfile支持Shell类的行尾添加\的命令换行方式, 以及首行#进行注释的格式. 良好的格式, 比如换行, 缩进, 注释等, 会让维护, 排障更为容易, 这是一个比较好的习惯.
此外, 还可以看到这一组命令的最后添加了清理工作的命令, 删除了为了编译构建所需要的软件, 清理了所有下载, 展开的文件, 并且清理了apt缓存文件. 这是很重要的一步, 我们之前说过, 镜像是多层存储的, 每一层的东西并不会在下一层被删除, 会一直跟随着进项. 因此镜像构建时, 一定要确保每一层中添加真正需要添加的东西, 任何无关的东西都应该清理掉.
镜像构建上下文(Context)
如果注意, 会看到docker build 命令最后一个. 表示当前目录, 而Dockerfile就在当前目录, 然而, 这个路径不是制定Dockerfile所在路径. 这是在制定上下文路径. 什么是上下文呢?
首先我们要理解docker build的工作原理, Docker在运行时分为Docker引擎(也就是服务器守护进程)和客户端工具. Docker的引擎提供了一组REST API, 被称为Docker Remote API, 而如docker命令这样的客户端工作, 则是通过这组API与Docker引擎交互, 从而完成各种功能. 因此, 虽然表面上我们好像是在本机执行各种docker功能, 但实际上, 一切都是使用的远程调用形式在服务端(Docker引擎)完成. 也因为这种C/S设计, 让我们操作远程服务器的Docker引擎变得轻而易举.
当我们进项镜像构建的时候, 并非所有定制都会通过RUN指令完成, 通常会需要将一些本体文件复制进镜像, 比如通过COPY指令, ADD指令等, 而docker build命令构建镜像, 其实并非在本地构建, 而是在服务端, 也就是Docker引擎中构建的. 那么在这种客户端/服务端的架构中, 如何才能让服务端获得本地文件呢?
这就引入上下文的概念. 当构建的时候, 用户会指定构建镜像上下文的路径, docker build命令得知这个路径后, 将会路径下的所有内容打包, 然后上传给Docker引擎. 这样Docker引擎收到这个上下文后, 展开就会获得构建镜像所需的一切文件.
如果在Dockerfile中这么写:
1 | COPY ./package.json /app/ |
这并不是要复制执行docker build 命令所在的目录下的package.json, 也不是复制Dockerfile所在目录下的package.json, 而是复制**上下文(Context)**目录下的package.json.
因此, COPY这类指令中的源文件的路径都是相对路径. 这也是初学者经常会问的为什么COPY ../package.json /app或者COPY /opt/xxx /app无法工作的原因, 因为这些路径已经超出了上下文的范围, Docker引擎无法获得这些位置的文件. 如果真的需要那些文件, 应该将它们复制到上下文目录中去.
现在就可以理解刚才的命令docker build -t nginx:v3 .中的这个., 实际上是在指定上下文的目录, docker build 命令会将该目录下的内容打包交给Docker引擎以帮助构建镜像.
如果观察docker build输出, 我们其实已经看到了这个发送上下文的过程:
1 | $ docker build -t nginx:v3 . |
理解构建上下文对于镜像构建是很重要的, 避免犯一些不应该的错误. 比如有些初学者在发现COPY /opt/xxx /app不工作后, 于是干脆将Dockerfile放到了应该根路径中构建, 结构发现dockers build执行后, 在发送一个几十GB的东西, 即为缓慢并且很容易构建失败. 那是因为这种做法是在让docker build打包整个硬盘, 者显然是使用错误.
一般来说, 应该会将Dockerfile置于一个空目录下, 或者项目跟目录下. 如果该项目下没有所需文件, 那么应该把所需文件复制一份过来. 如果目录下有些东西确实不希望构建时传给Docker引擎, 那么可以用.gitignore一样的语法写一个.dockerignore, 该文件是用于剔除不需要作为上下文传递给Docker引擎的.
那么为什么会有人误认为.是指定Dockerfile所在目录? 这是因为在默认情况下, 如果不额外指定Dockerfile的话, 将会上下文目录下的名为Dockerfile的文件作为Dockerfile
这只是默认行为, 实际上Dockerfile的文件名并不要求必须为Dockerfile, 并且并不要求必须位于上下文目录中, 比如可以用-f ../Dockerfile.php参数指定某个文件作为Dockerfile
当然, 一般大家习惯性的会使用默认的文件名Dockerfile, 以及会将其置于镜像构建上下文目录中.