Docker
基本概念
镜像Image
操作系统分为内核和用户空间,内核启动后,会挂载root文件系统为其提供用户空间支持。docker镜像就相当于是一个root文件系统。
是一个特殊文件系统,除了提供容器运行所需的程序、库、资源、配置等文件外,还包含一些为运行准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不会包含任何动态数据,其内容在构建之后也不会被改变
分层存储
镜像包含操作系统完整的root文件系统,体积庞大,在设计docker时,将其设计为分层存储架构。严格来说,镜像并非像一个ISO那要的打包文件,镜像只是一个虚拟概念,实际体现并非由一个文件组成,而是由一组文件系统组成,或说是多层文件系统联合组成。
镜像构建时,会一层层构建,前一层是后一层的基础,每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己的这一层。例如删除前一层文件的操作并不是真的删除前一层文件,而是仅在当前层标记为该文件已珊瑚虫。在最终容器运行时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉
分层存储的特性还使得镜像的复用、定制变得更加容易,甚至可以使用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像
容器Container
镜像和容器的关系,就像是面向对象程序中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容易可以被创建、启动、停止、删除、暂停等。
容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行属于自己的独立的命名空间。因此容器可以拥有自己的root文件系统、自己的网络配置、自己的进程空间,设置自己的用户ID空间。容器内的进程是运行在一个隔离的环境里面,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。因为这种隔离特性,常常会混淆容器和虚拟机。
容易也是分层存储,每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,称这个为容器运行时读写而装备的存储层为容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层随之消亡。任何保存于容器存储层的信息都会随容器删除而丢失。
按照最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有文件写入操作,都应该使用数据卷、或者绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。
数据卷的生存周期独立容器,使用数据卷后,容器删除或者重新运行之后,数据不会丢失。
仓库Repository
镜像构建后,容易在当前宿主机上运行,但是如果需要在其他服务器上使用这个镜像,就需要一个集中的存储、分发镜像的服务,docker register就是这样的服务。一个docker regisert中可以包含多个仓库,仓库可以包含多个标签Tag,每个标签对应一个镜像。
通常一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。可以通过<仓库名>:<标签>
的格式指定具体这个软件哪个版本的镜像。如果不给出标签,将以lasest作为默认版本。
使用镜像
获取镜像
docker pull [选项] [Docker Register 地址[:端口号]/]仓库名[:标签]
可通过docker pull --help
命令看到。镜像名称格式:
- Docker 镜像仓库地址:地址的名称一般是
<域名/IP>[:端口号]
默认地址是Docker Hub - 仓库名:仓库名是两段式名称,即
<用户名>/<软件名>
。对于Docker Hub不给出用户名,默认就是 library,也就是官方镜像
例如:
1 | [1;32mdocker@default[0m:[1;34m~[0m |
上面命令没有给出docker镜像仓库地址,会从Docker Hub获取镜像,镜像名称是ubuntu:16.04
,因此获取官方镜像library/ubuntu
,仓库中标签为 16.04的镜像
下载过程中可以看出分层存储概念,镜像由多层存储所构成,下载也是一层层的去下载,并非单一文件。下载过程中给出每一层ID的前12位,并且下载结束后,给出该镜像sha256的完整摘要,以确保下载一致性
运行
以镜像为基础启动并运行一个容器,启动里面的bash并镜像交互操作
1 | [1;32mdocker@default[0m:[1;34m~[0m$ [Kdocker run -it --rm ubuntu:16.04 bash |
docker run
是容器运行的命令:
-it
:这是两个参数,也给是-i
,交互式操作,一个是-t
终端。进入bash
执行一些命令并查看返回结果,因此需要交互式终端--rm
:容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动docker rm
。这里使用--rm
可以避免浪费空间,只是随便执行个命令,查看结果,不需要排障和保留结果ubuntu:16.64
:这是用这个镜像为基础来启动容器bash
:在镜像后的是命令,希望有个交互式的shell,使用的是bash
列出镜像
docker image ls
1 | [1;32mdocker@default[0m:[1;34m~[0m$ docker image ls |
列表包含了仓库名、标签、镜像ID、创建时间以及所占空间。
镜像ID是镜像的唯一标识,一个镜像可以对应多个标签。
镜像体积
这里标识所占的空间和在Docker Hub上看到的镜像大小不一样,这是因为Docker Hub中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像总是保持着压缩状态,因此Docker Hub中显示的大小是网络传输中更关心的流量大小,而docker image ls
显示的是镜像下载到本地后展开的大小,是展开后的各层所占空间的总和,因此镜像到本地后,查看空间的时候,更关心的是本地磁盘空间所用的大小。
列表中的镜像体积总和并非是所有镜像实际硬盘消耗,由于docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于Docker使用UnionFS,相同层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
使用 docker system df
查看镜像、容器、数据卷所占用的空间
1 | [1;32mdocker@default[0m:[1;34m~[0m$ docker system df |
虚悬镜像
有时候会特殊镜像,既没有仓库名,也没有标签,为none
1 | <none> <none> 09089dhs989 5 days ago 23MB |
这些镜像原来是镜像名和标签的,随着官方镜像维护发布新版本,重新pull这个镜像,镜像名被转移到了新下载的镜像上,而旧的镜像上的名称则被取消,从而成为了<none>
。docker build也可能导致这样的问题。由于新旧镜像同名,旧镜像名称被取消,从而仓库名、标签均为<none>
。这类无标签镜像也被成为虚悬镜像(dangling image),可以用docker image ls -f dangling=true
显示这些镜像
虚悬镜像也是失去了存在价值,可以随意删除,用docker image prune
删除
中间层镜像
为了加速镜像构建、重复利用资源,Docker会利用中间镜像,在使用一段时间后,会看到一些依赖的中间镜像。默认的docker image ls
列表只会显示顶层镜像,如果希望显示中间层镜像在内的所有镜像的话,用docker image ls -a
出现无标签镜像,与虚悬镜像不同,这些镜像都是中间层镜像,是其他镜像所依赖的镜像,删除这些镜像会导致上层镜像因为依赖丢失出错。删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除
列出部分镜像
1 | 根据仓库名 |
特定格式显示
1 | 将所有镜像的ID列出 |
删除本地镜像
1 | docker image rm [选项] <镜像1>[<镜像2>...] |
ID、镜像名、摘要删除镜像
镜像可以是短id,长id,镜像名或者镜像摘要,短id取前3个字符以上,能区分别的镜像就可以
1 | docker image rm 502 |
Untagged和Deleted
删除行为分为两种,一种是untagged,一种是deleted。镜像的唯一标识是其ID和摘要,而一个惊喜可以有多个标签。当我们删除指定标签后,可能还有别的标签指向这个镜像,那么delete行为不会发生,会显示untagged信息。
当该镜像所有标签都被取消了,改镜像可能就失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,在删除的时候也是从上层向基础层方法依次进行判断删除。镜像的多层结构使得复用变动镜像容易,因此很有可能某个其他镜像正依赖当前镜像的某一层。这种情况不会触发删除该层行为。直到没有任何层依赖当前层时,才会真实删除当前层。
除了镜像依赖还有容器对镜像的依赖。如果有用这个镜像启动的容器存在,即使容器没有运行,那么同样不可以删除这个镜像。容器是以经为基础,再加一层容器存储层,组成这样的多层存储结构去运行。因此该镜像如果被这个容器所依赖,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后来删除镜像。
利用commit理解镜像构成
镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行的基础。当Docker Hub的镜像无法满足需求时,我们就需要定制镜像。
定制Web服务器为例子
1 | nginx镜像启用一个容器,命名为webserver,映射80端口,在浏览器可以访问这个nginx服务器 |
修改了容器文件,也就是改动了容器的存储层,通过 docker diff
可以看到具体的改变
定制好变化后,保存下来形成镜像。当我们运行一个容器(不使用卷),我们做的任何文件修改都会被记录于容器存储层里。而docker提供了docker commit命令,可以将容器的存储层保存下来成为镜像。在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原来容器最后的文件变化
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]
1 | 将容器保存为镜像 |
接着
1 | 查看新定制的镜像并使用,也可以使用docker history具体查看镜像内的历史记录 |
使用 docker commit 命令可以直观帮助理解镜像分层存储概念,但是实际环境不会这么做。
仔细观察docker diff webserver的结果,会发现除了修改的文件外,由于命令的执行,还有很多文件被改动或者添加了,这仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像尤为臃肿。
此外,使用docker commit都是黑箱操作。只有制作者才知道做了什么,docker diff可以看出一些线索,但不能保证生成一致的镜像。
Dockerfile定制镜像
dockerfile是一个文本文件,其内包含了一天天的指令(Instruction),每一条指令构建一层,因此每一条指令的内容就是描述该层应当如何构建
在一个空白目录,建立一个文件夹,并命名为Dockerfile
1 | mkdir myngix |
FROM指定基础镜像
定制镜像是以一个镜像为基础,在其上进行定制,FROM就是指定基础镜像,因此FROM是Dockerfile文件的第一条指令,除了docker hub现有的镜像,还有一个特殊的镜像 scratch
,虚拟镜像,表示一个空白的镜像
FROM scratch
以它为基础的话,意味着不以任何镜像为基础,接下来写的指令作为镜像的第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用GO语言开发的引用会使用这种方式,这也是为什么说Go是特别适合容器微服务架构的语言的原因之一
RUN指定命令
RUN指令是定制镜像最常见的指令之一。格式有两种:
shell格式:RUN<命令>
,就像直接在命令行输入的命令一样
exec格式:RUN["可执行命令","参数1","参数2"]
,这更像是函数调用中格式
1 | FROM debian:jessie |
dockerfile中每个指令都会建立一层,RUN也不例外,每一个RUN的行为都会新建一层,在其上执行结果后,commit一层的修改,构成新的镜像,上面这种写法创建了7个镜像,这是完全没有意义的,运行时很多不需要的东西都被装进了镜像,比如编译、更新的软件包等,结果就是产生臃肿、非常多层的镜像,不仅增加了构建时间,也容易出错。UnionFS是有最大层数限制的,比如AUFS,曾经是最大不超过42层,现在是不超过127层。
上面的Dockerfile正确写法应该是:
1 | FROM debian:jessie |
之前所有命令的目的只有一个,就是编译、安装redis可执行文件。因此没有必要建立多层,这里仅使用一个RUN,并使用 &&
将各个需要命令串联起来,将之前的7层变成1层。并且为了格式化还进行了换行。Dockerfile支持Shell类的行尾添加 \
的命令行,以及行首 #
进行注释的格式。良好的格式会让排障更加容易。
此外,上面的命令还进行了清理工作,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件,在镜像构建过程要确保每一层只添加真正需要添加的东西,任何无关的东西都应该被清理掉。
构建镜像
在Dockerfile文件所在目录执行:
1 | docker@default:~/mynginx$ docker build -t nginx:v3 . |
docker build
格式:
docker build [选型] <上下文路径/URL/->
镜像构建上下文(Context)
docker build 命令最后一个 .
,代表当前目录,而Dockerfile就在当前目录,所以会误以为这个路径是指定Dockerfile所在路径,这样的理解不准确,其实这是在指定上下文路径。
docker build的工作原理:Docker在运行时分为Docker引擎(也就是服务端守护进程)和客户端工具。Docker的引擎提供了一组REST API,被称为Docker Remote API,而docker命令这样的客户端工具,则是通过这组API与Dokcer引擎交互,从而完成各种功能。因此我们表面上好像在本机执行各种docker功能,实际上,一切都是使用的远程调用形式在服务端(Docker引擎)完成。也因为这种C/S设计,让我们操作远程服务器的Docker引擎变得轻而易举。
当我们进行镜像构建时,并非所有的定制都会通过RUN指令完成,经常会需要将一些本地文件复制进镜像,比如通过COPY指令,ADD指令。而docker build命令构建镜像,并非在本地构建,而是在服务端,也就是Docker引擎中构建。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文概念,当构建的时候,用户会指定构建镜像上下文路径,docker build命令得知这个路径后,会将路径下的所有内容打包,然后上传给Docker引擎,这样Docker引擎收到这个上下文之后,展开就会获得构建镜像所需要的一切文件。
例如在Dockerfile这样写:
COPY ./package.json /app/
这并不是要复制执行docker build命令所在的目录下的package.json,也不是复制Dockerfile所在目录下的package.json,而是复制上下文context目录下的package.json
因此,COPY这类指令中的源文件的路径都是相对路径,如果真的需要那些文件,应该将它们复制到上下文目录中去。观察docker build输出,可以看到这个发送上下文的过程:
1 | docker@default:~/mynginx$ docker build -t nginx:v3 . |
理解构建上下文对于镜像构建很重要,避免犯一些不应该的错误,比如初学者在发现 Copy/opt/xxxx /app
不工作后,干脆把Dockerfile放到了硬盘跟目录去构建,结果发现docker build执行后,在发送一个几十GB的东西,极为缓慢而且容易构建失败,那是因为这种做法是让docker build打包整个硬盘,这显然是使用错误。
一般来说,应该会将Docker置于一个空目录,或者根目录下面,如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给Docker引擎,那么可以用.dockerignore
剔除不需要作为上下文传递给Docker引擎的。
在默认情况下,如果不额外指定Dockerfile的话,会将上下文目录下的名为Dockerfile的文件作为Dockerfile,实际上不要求名字必须为Dockerfile,并且不要求必须位于上下文目录中,比如用 -f ../Dockerfile.php
参数指定某个文件作为 Dockerfile
其他Docker build用法
直接用 git repo进行构建
docker build还支持从URL构建,比如从 Git repo
1 | 指定构建所需要的git repo,并且指定默认的master分支,构建目录为/814/,然后docker就会自己去git clone这个项目、切换到指定分支,并进入到指定目录后开始构建 |
用tar压缩包构建
1 | docker build http://server/context.tar.gz |
如果给出的URL不是Git repo,而是个tar压缩包,那么Docker引擎会下载这个包,并自动解压,以其作为上下文,并且构建。
从标准输入中读取Dockerfile进行构建
docker build - < Dockerfile
或者
cat Dockerfile | docker build -
如果标准输入传入的是文本文件,则将其视为Dockerfile,并开始构建。这种形式由于直接从标准输入中读取Dockerfile的内容,没有上下文,因此不可以像其他方法那样可以将本地文件COPY进镜像之类的事情
从标准输入中读取上下文压缩包镜像构建
docker build - < context.tar.gz
如果发现标准输入的文件格式是 gzip/bzip2/xz的话,将会使其成为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。
指令详解
COPY复制文件
格式:
COPY <源路径>...<目标路径>
COPY["<源路径1>",..."<目标路径>"]
和RUN指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用
COPY指令将从构建上下文目录中<源路径>
的文件/目录复制到新的一层的镜像内的<目标路径>
位置,比如:
1 | COPY package.json /usr/src/app/ |
源路径可以是多个,甚至是通配符,其通配符规则要满足GO的filepath.Match规则,如:
1 | COPY hom* /mydir/ |
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作路径可以用WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还有一点使用COPY指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用,特别是构建相关文件都在使用Git进行管理时。
ADD更高级的复制文件
ADD指令和COPY的格式和性质基本一致,但是在COPY基础上增加了一些功能。
比如 <源路径>
可以是一个URL,这种情况下Docker引擎会视图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为600,如果这并不是想要的权限,还要增加额外的一层RUN进行权限调整。另外,如果下载但是个压缩包,需要解压缩,也一样需要额外的一层RUN指令进行解压缩。所以不如直接使用RUN,然后用wget或者curl工具下载,处理权限、解压缩、然后清理无用文件更合理。因此这个功能其实并不好用,而且不推荐使用
如果<源路径>
为一个tar压缩文件,压缩格式为gzip,bzip2以及xz的情况下,ADD指令将会自动解压缩这个压缩文件到 <目标路径>
去。
在某些情况下,这个自动解压功能非常有用,比如官方镜像ubuntu中
1 | FROM scratch |
在某些情况下,希望复制压缩文件而不解压就不用ADD命令了,在Dockerfile最佳实践文档中要求,尽可能使用COPY,因为它的语义很明确,就是复制文件而已,而ADD则包含了更复杂的功能,其行为也不一定很清晰。最适合用ADD的场合就是所提及的需要自动解压缩的场合。ADD指令会令镜像构建缓存失败,从而可能会令镜像构建变得比较缓慢。
因此COPY与ADD指令选择可以遵循这样的原则,所有文件复制均使用COPY指令,仅在需要自动解压缩的时候使用ADD
CMD容器启动命令
CMD 指令格式和 RUN相似,也是两种格式:
- shell格式:CMD
<命令>
- exec格式:CMD[“可执行文件”,“参数1”,”参数2”…]
- 参数列表格式:CMD[“参数1”,”参数2”]。在指定了ENTRYPOINT后,用CMD指定具体参数
Docker不是虚拟机,容器就是进程,在启动容器的时候,需要指定所运行的程序及参数。CMD指令就是用指定默认的容器主进程的启动命令。
在运行时可以指定新的命令来提点镜像设置中的这个默认命令,比如ubuntu镜像默认的CMD是/bin/bash
,如果我们直接docker run -it ubuntu的话,会直接进入bash,也可以在运行的时候指定运行别的命令,如 docker run -it ubuntn cat /etc/os-release
,这里就是使用cat /etc/os-release
命令替换了默认的 /bin/bash
命令,输出了系统版本信息。
在指令格式上,一般推荐 exec格式,这类格式在解析时会被解析为JSON数组,因此一定要使用双引号,而不是单引号。使用shell的话,实际的命令会被包装为sh-c的参数的形式进行执行,比如:
CMD echo \$HOME =》 CMD[“sh”,”-c”,”echo \$HOME”]
这就是为什么可以使用环境变量的原因,因此这些环境变量会被shell进行解析处理。
Docker不是虚拟机,容器的应用都应该是以前台运行,而不是像虚拟机、物理机用upstart/systemd
去启动后台服务,容器没有后台服务概念。一些人将CMD写为:
CMD service nginx start
然后发现容器执行后立即退出了,甚至在容器内去使用systemtcl命令结果却发现根本执行不了。
对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就是去了生存的意义,从而退出,其他辅助进程不是它需要关心的东西。
而是用 service nginx start命令,则是希望upstart来以后台守护进程形式启动nginx,CMD service nginx start实际上是 CMD["sh","-C","service nginx start"]
,因此主进程实际上是sh,当 service nginx start命令结束后,sh也就结束了,sh作为主进程退出,自然容器退出。
正确的做法是直接执行nginx可执行文件,并且要求前台形式运行:
CMD [“nginx”,”-g”,”daemon off”;]
ENTRYPOINT入口点
与RUN指令一样,分为exec和shell格式。目的和CMD一样,都是在指定容器启动程序及参数。ENTRYPOINT在运行时也可以替代,不过比CMD要略繁琐,需要通过 docker run –entrypoint指定。
指定ENTRYPOINT后,CMD的含义就发生了变化,不再是直接的运行其命令,而是将CMD的内容作为参数传给ENTRYPOINT指令:
“ “
有了CMD后,为什么还要有ENTRYPOINT?
场景一:让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网IP的镜像,可以使用CMD实现
1 | FROM ubuntu:16.04 |
使用 docker build -t myip .
来构建镜像的话,如果查询当前公网的ip,只需要执行
1 | docker run myip |
现在把镜像当做命令使用,如果命令有参数要怎么加上?CMD中实际的命令是curl,如果希望展示HTTP头部信息,就需要加上 -i
参数
1 | docker run myip -i |
可以看到执行文件找不到的报错,executable file not found
。在镜像后面的是 command,运行时会替换CMD的默认值,这里的-i
替换了CMD,而不是加在 curl -s http://ip.cn
后面。而 -i
不是命令,所以自然找不到。
如果希望加入 -i
这参数,就必须重新完整的输入这个命令
1 | docker run myip curl -s http://ipinfo.cn -i |
这显然不是很好的解决方案,使用ENTRYPOINT可以解决这个问题:
1 | FROM ubuntu:16.04 |
运行 docker run myip -i
1 | docker@default:~/myip$ docker run myip |
当存在ENTRYPOINT后,CMD的内容将会作为参数传给ENTRYPOINT,而 -i
就是新的CMD,因此会作为参数传给curl。
场景二:应用运行前的准备工作
启动容器就是启动主进程,有时候在启动主进程前,需要一些准备工作。
例如mysql类的数据库,需要一些数据库配置、初始化工作,这些工作要在mysql服务器运行前解决。
另外,希望避免使用root用户去启动服务,从而提高安全性,而在启动前还需要以root身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其他命令依旧可以使用root身份执行,方便调试。
这些准备工作与容器CMD无关的,无论CMD为什么,都需要事先进行一个预处理的工作,这种情况可以写一个脚本,放入到ENTRYPOINT中去执行,而这个脚本也会将接收到的参数也就是CMD的命令作为命令,在脚本最后执行,官方镜像redis就是这样做的
1 | FROM alpine:3.4 |
可以看到为了redis服务创建redis用户,并在最后指定了ENTRYPOINT为docker-entrypoint.sh脚本
1 | !/bin/sh |
脚本的内容根据CMD的内容来判断如果是redis-server就切换到redis用户身份去启动服务器,否则使用root身份执行
1 | docker run -it redis id |
ENV设置环境变量
格式:
- ENV
<key><value>
- ENV
<key1>=<value> <key2>=<value2>
设置环境变量,无论是后面的其他指令RUN,还是运行时的引用,都可以直接使用这里定义的环境变量
1 | ENV VERSION=1.0 DEBUG=on \ |
定义了环境变量,在后续的指令里面就可以使用这个环境变量,比如官方node镜像Dockerfile中妈就有类似这样的代码
1 | ENV NODE_VERSION 7.2.0 |
定义了NODE_VERSION环境变量,其后RUN这层,多次使用$NODE_VERSION
来进行操作定制。可以看到,升级镜像构建版本的时候,只要更新7.2.0既可,构建维护变得更轻松了
下列的指令可以支持环境变量展开:
ADD/COPY/ENV/EXPOSE/LABEL/USER/WORKDIR/VOLUME/STOPSIGNAL/ONBUILD
ARG构建参数
格式:ARG<参数名>[=<默认值>]
构建参数和ENV的效果一样,都是设置环境变量,不同的是,ARG所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用ARG保存密码之类的信息,因为docker history还是可以看到所有值的。
Dockerfile中ARG指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令docker build中用 --build-arg<参数名>=<值>
来覆盖。
在1.13之前的版本,要求 --build-arg
中的参数名,必须在Dockerfile中用ARG定义过了,如果没有则会报错退出构建。在1.13之后的版本,只是显示警告信息,并继续构建。这对于使用CI系统,用同样的构建流程构建不同的Dockerfile的时候比较有帮助,避免构建命令必须根据每个Dockerfile的内容修改
VOLUME定义匿名卷
格式为:
- VOLUME
["<路径1>","路径2"...]
- VOLUME
<路径>
容器运行时应该尽量保持容器存储层不发生写操作,对于数据库需要保存动态数据的应用,其数据库文件应该保存于卷中,为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在Dockerfile中,我们实现指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据
VOLUME /data
这里的 /data
目录就会在运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。运行时也可以覆盖这个挂载设置:
docker run -d -v mydata:/data xxx
使用了 mydata这个命令卷挂载到了 /data
这个位置,替代了Dockerfile中定义的匿名卷的挂载配置
EXPOSE声明端口
格式为:EXPOSE<端口1>[<端口2>...]
EXPOSE指令是声明运行时容器提供服务端口,在运行时并不会因为这个声明应用就会开启这个端口的服务,在Dockerfile中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个好处就是运行时使用随机端口映射时,也就是docker run -P时,会自动随机映射EXPOSE端口
在早期Docker版本中还有一个特殊用处,以前所有的容器都是运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个Docker引擎参数 --icc=false
,当指定该参数后,容器间将默认无法互访,除非互相使用了--links
参数的容器才可以互通,并且只有镜像中EXPOSE所声明的端口才可以被访问。这个 -icc=false
的用法在引入了docker network后已经基本不用了,通过自定义网络可以轻松实现容器间的互联与隔离。
要将 EXPOSE和在运行时使用 -p <宿主端口>:<容器端口>
区分开,-p
是映射宿主端口和容器端口,也就是将容器的对应端口服务公开给外界访问,而EXPOSE仅仅是声明容器打算使用什么端口而已,并不会自动在宿舍进行映射。
WORKDIR指定工作目录
格式:WORKDIR<工作目录路径>
使用WORKDIR指令可以来指定工作目录(或者称为当前目录),以后各层的目录就被改成指定目录,如该目录不存在,WORKDIR就会帮你建立目录
错误例子:
RUN cd /app
RUN echo “hello” > world.txt
把Dockerfile当做Shell脚本来书写,构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是hello.在shell中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响下一个命令;而在Dockerfile中,这两行RUN命令的执行环境根本不同,是两个完全不同的容器,这是对Dockerfile构建分层存储的概念不了解所导致的错误。
如果需要改变各层的工作目录位置,应该使用WORKDIR指令
USER指定当前用户
格式:USER<用户名>
USER指令和WORKDIR相似,都是改变环境并影响以后的层,WORKDIR是改变工作目录,USER则是改变之后层的执行RUN,CMD以及ENTRYPOINT这类命令的身份
和WORKDIR一样,USER只是帮助切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换
1 | RUN groupadd -r redis && useradd -r -g redis redis |
如果以root执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程。不要使用su或者sudo,这些都需要比较麻烦的配置,而且在TTY缺失的环境下经常出错,建议使用 gosu
1 | # 建立 redis用户,并使用 gosu换另一个用户执行命令 |
HEALTHCHECK健康检查
格式:
- HEALTHCHECK
[选项] CMD <命令>
,设置检查容器健康状态的命令 - HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽其健康检查指令。
在没有HEALTHCHECK指令前,Docker引擎只可以通过容器内主进程是否退出来判断容器是否状态异常,很多情况下没问题,但是如果程序进入死锁状态,或者死循环,应用程序不会退出,但是该容器已经无法提供服务了。在1.12前,Docker不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务却还在接受用户请求。
在1.12之后,Docker提供了这个指令,通过改指令指定一行命令,用这行命令来判断容器主进程服务状态是否还正常,从而比较真实的反应容器实际状态。
当在一个镜像指定了HEALTHCHECK指令后,用其启动容器,初始状态为starting,在HEALTHCHECK指令检查成功后变为healthy,如果连续一定次数失败,则会变为unhealthy
支持系列选项:
–interval=<间隔>:两次健康检查的间隔,默认为30s
–timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查视为失败,默认30s
–retries=<次数>:当连续失败指定次数后,则容器状态为unhealthy,默认3次
和CMD/ENTRYPOINT一样,HEALTHYCHECK只出现一次,如果写了多个,只有最后一个生效。
在HEALTHYCHECK [选项] CMD
后面命令,格式和ENTRYPOINT一样,分为shell,和exec。命令的返回值决定了该次健康检查的成功与否:0为成功1是失败,2保留,不要使用这个值
假设我们有个镜像简单的Web服务,希望增加健康检查判断Web服务是否正常工作,用curl判断,dockerfile可以这么写:
1 | FROM nginx |
设置了每5秒检查一次,如果健康检查超过3秒没响应就是视为失败。构建并启动容器
1 | docker build -t myweb:v1 |
ONBUILD为他人做嫁衣裳
格式:ONBUILD<其他指令>
ONBUILD是一个特殊的指令,后面跟着的是其他指令,例如RUN/COPY等,而这些指令,在当前镜像构建时不会被执行,只有当以当前镜像为基础,去构建下一个镜像才会执行
Dockerfile中的其他指令都是为了定制当前镜像而准备的,ONBUILD是为了帮助别人定制自己准备的。Nodejs使用npm管理,拿到程序后,一般会先安装依赖,然后启动,一般会这么写Dockerfile
1 | FROM node:slim |
把这个Dockerfile放到Nodejs的根目录,构建好镜像后,就可以直接拿来启动容器运行,但是如果有第二个第三个差不多的项目,随着文件附件越多,版本控制就越难。
如果在第一个Nodejs项目发现了Dockerfile存在的问题,比如敲错字,或者需要安装额外的包,然后开发人员修复了这个Docker,再次构建,问题解决。但是其他项目呢?虽然最初Dockerfile是复制、粘贴自第一个项目,但是并不会因为第一个项目修复了Dockerfile就会自动修复
那么我们可以做一个基础镜像,然后各个项目使用这个基础镜像。镜像更新后就不用同步Dockerfile的变化,重新构建后就继续了基础镜像的更新:
1 | FROM node:slim |
这里把项目相关的构建指令拿出来,放在子项目去。假设这个基础镜像为my-node,各个项目内的Dockerfile就变成:
1 | FROM my-node |
基础镜像变化后,各个项目都用这个Dockerfile重新构建镜像,会基础基础镜像的更新。
但是问题并没有完全解决,这个Dockerfile如果有东西要修改,又要重新一个一个修改。这样制作镜像只解决了原来Dockerfile的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理
ONBUILD可以解决这个问题:
1 | FROM node:slim |
在构建镜像的时候,这三行并不会被执行,然后各个项目的Dockerfile就变成
1 | FROM my-node |
当在各个项目目录中,用这个只有一行的Dockerfile构建镜像时,之前基础镜像的那三行ONBUILD就会执行,成功的将当前项目的代码复制进镜像,并且针对本项目进行npm install,生成对应镜像
多阶段构建
在17.05之前,构建Docker镜像通常会采用两种方式
全部放入一个Dockerfile
一种方式是将所有的构建过程编包含在一个Dockerfile中,包括项目以及依赖库的编译、测试、打包等流程,这里可能会有一些问题:
- Dockerfile特别长,可维护性降低
- 镜像层次多,镜像体积较大,部署时间变长
- 源代码存在泄漏的问题
例如:编写app.go文件,输出Hello World!
1 | package main |
编写Dockerfile.one文件
1 | FROM golang:1.9-alpine |
构建镜像
1 | docker build -t go/helloworld:1 -f Dockerfile.one |
分割到多个Dockerfile
另一种方式,就是事先在一个Dockerfile将项目以及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要编写两个Dockerfile和一些编译脚本才能将两个阶段自动整合起来,这种方式虽然可以很好规避第一种方式存在的风险,但明显部署过程比较复杂
Dockerfile.build文件
1 | FROM golang:1.9-alpine |
Dockerfile.copy文件
1 | FROM alpine:latest |
新建build.sh
1 | !/bin/sh |
运行脚本即可构建镜像
1 | chmod +x build.sh |
对比两种生成的镜像大小
1 | docker image ls |
使用多阶段构建
为了解决以上的问题,17.05后开始支持多阶段构建(multistage builds),使用多阶段构建可以解决上面的问题,并只需要编写一个Dockerfile:
例如:
编写Dockerfile文件:
1 | FROM golang:1.9-alpine as builder |
构建镜像
1 | docker build -t go/helloworld:3 . |
对比三个镜像
1 | docker image ls |
很明显使用多阶段构建的镜像体积小,同时也解决了上边的问题
只构建一阶段的镜像
使用as为某一阶段命令,例如:
1 | FROM golang:1.9-alpine as builder |
只构建builder阶段的镜像,可以在使用 docker build的时候加上--target
1 | docker build --target builder -t username/imagename:tag . |
构建时从其他镜像复制文件
上面的例子使用 COPY --from=0 /go/src/github.com/go/helloworld/app .
从上一阶段的镜像中复制文件,我们可以复制任意镜像中的文件
1 | COPY --from=nginx:latest /ect/nginx/nginx.conf /nginx.conf |
其他制作镜像的方式
从rootfs压缩包导入
格式:docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]
压缩包可以是本地文件,远程Web文件,甚至是从标准输入中得出的,压缩包就会在镜像 /
目录展开,并直接作为镜像第一层提交
1 | 创建一个OpenVZ的Ubuntu14.04模板的镜像: |
这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz
文件,并且作为根文件系统展开导入,并保存为镜像 openvz/ubuntu:14.04
导入成功后,使用 docker image ls可以看到这个导入的镜像
看到历史会看到描述中有导入的文件链接
1 | docker history openvz/ubuntu:14.04 |
docker save和docker load
将镜像保存为一个tar文件,然后传输到另外一个位置,再加载进来。这是在没有Docker Registry的做法,现在已不推荐。镜像迁移直接使用Docker Registry,无论是直接使用Docker Hub还是使用内网私有Registry都可以
保存镜像
docker save将镜像保存为归档文件
1 | docker save alpine | gzip > alpine-latest.tar.gz |
然后将它复制到另外一个机器上,用下面的命令加载镜像:
1 | docker load -i alpine-latest.tar.gz |
结合这两个命令和ssh甚至是pv,利用Linux管道可以写一个命令完成从一个机器将镜像迁移到另外一个机器,并且带进度条的功能:
1 | docker save <镜像名> | bzip2 | pv |ssh <用户名>@<主机名> 'cat|docker load' |
镜像实现的原理
Docker镜像是怎么实现增量的修改和维护的?
每个镜像都由多层次构成,Docker使用Union FS将这些不同层结合到一个镜像中去。
通常Union FS有两个用途,一方面可以实现不借助LVM,RAID将多个disk挂在同一个目录下,另一个更常用的就是讲一个只读的分支和一个可写的分支联合在一起。
操作容器
简单来说,容器似乎运行的一个或一组应用,以及它们的运行态环境,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用
启动容器
启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。因为Docker的容器很轻级,用户可以随时删除和新建容器。
新建并启动
1 | 输出Hello World后终止容器 |
当利用 docker run来创建容器时,Docker在后台运行的标准操作包括:
- 检查本地是否存在指定的镜像,不存在就从共有仓库下载
- 利用镜像创建并启动一个容器
- 分配一个文件系统,并在只读的镜像层外面挂载一层可读层
- 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
- 从地址池配置一个ip地址给容器
- 执行用户指定的应用程序
- 执行完毕后容器被终止
启动已终止的容器
docker container start命令可以将一个已经终止的容器启动运行。
容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的,除此之外没有其他资源。可以在伪终端中利用ps或top来查进程信息
守护态运行
更多时候,需要让Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。可以通过添加 -d
参数实现。
不用 -d
参数运行容器:
1 | docker run ubuntu:17.10 /bin/sh -c "while true;do echo hello world;sleep 1;done" |
容器会把输出结果stdout打印到宿主主机上面
如果使用了 -d
参数运行容器
1 | docker run -d ubuntu:17.10 /bin/sh -c "while true;do echo hello world; sleep 1;done" |
此时容器会在后台运行并不会把输出结果打印到宿主机上面(输出结果可以用docker logs查看)。
注:容器是否会长久运行,和 docker run 指令的命令有关,和 -d
参数无关。
使用 -d
参数启动后返回一个唯一的id,可以通过 docker container ls命令来查看容器信息
1 | docker container ls |
终止
使用 docker container stop 来终止一个运行中的容器
当Docker 容器中指定的应用终结时,容器也会自动终止。上面只启动了一个容器,用户可以通过exit或者ctrl+d来推出终端,所创建的容器立刻终止。
终止状态的容器可以使用 docker containers ls -a命令看到例如:
1 | docker container ls -a |
然后通过 docker run restart命令会让一个运行态的容器终止,然后再重新启动它。
进入容器
使用 -d
参数时,容器启动后会进入后台。某些时候需要进入容器操作,包括docker attch命令或docker exec命令,推荐使用docker exec
attch命令
docker attch是Docker自带命令
1 | docker run -dit ubuntu |
exec命令
-i -t参数
docker exec可以跟多个参数
只用 -i
参数时,由于没有分配伪终端,界面没有Linux命令符,但命令执行结果仍然可以返回
当 -i
/-t
参数一起使用时,可以看到Linux命令提示符
1 | docker run -dit ubuntu |
如果从这个stdin中exit,不会导致容器的停止。这就是为什么推荐使用 docker exec的原因
导出和导入容器
导出容器
1 | docker container ls -a |
导入容器快照
1 | docker import 从容器快照文件中再导入为镜像 |
删除
1 | docker container rm haha |
删除一个运行中的容器,可以添加 -f
,Docker会发送 SIGKILL
信号给容器
清理所有处于终止状态的容器:
1 | docker container prune |
访问仓库
仓库Repository是集中存放镜像的地方。
一个容易混淆的概念是注册服务器(Registry)。实际上注册服务器是管理仓库的具体服务器,每个服务器可以有多个仓库,而每个仓库下面可以有多个镜像。从这方面来说,仓库被认为是一个具体的项目或者目录。例如对于 仓库地址dl.dockerpool.com/ubuntu
来说,dl.dockerpool.com
是注册服务器地址,ubuntu
是仓库名。大部分时候并不需要严格区分这两者的概念
Docker Hub
Docker官方维护了一个公共仓库Docker Hub,大部分需求可以在Docker Hub中直接下载镜像实现
在 https://cloud.docker.com 免费注册一个 Docker 账号,通过docker login命令交互式输入用户名和密码完成在命令行登录,docker logout退出登录。
docker search命令查找官方仓库镜像,利用 docker pull命令将它下载到本地
1 | centos为关键词 |
查找的时候通过 --filter=stars=N
参数可以指定仅显示收藏数量为N以上的镜像
推送镜像
用户登录后通过docker push推送自己的镜像到Docker Hub。
1 | username替换为Docker 账户用户名 |
自动创建
Automated Builds功能对于需要经常升级镜像内程序来说,十分方便。
用户创建了镜像,安装了某个软件,如果软件发布新版本则需要手动更新镜像。而自动创建运行用户通过Docker Hub指定一个跟踪网站上的项目,Github/BitBucket等,一旦项目发生新的提交或者创建新的标签,Docker Hub会自动构建镜像并推送到 Docker Hub中
配置自动创建,有几个步骤:
- 创建并登陆Docker Hub,以及目标网站
- 在目标网站中连接账户到Docker Hub
- 在Docker Hub中配置一个自动创建
- 选取一个目标网站的项目(需含有Dockerfile)和分支
- 指定Dockerfile的位置,并提交创建
之后可以在Docker Hub的自动创建页面中跟踪每次创建的状态
私有仓库
用户可以创建一个本地仓库供私人使用
docker-registry是官方提供的工具,用户构建私有镜像库
安装docker-registry
通过官方registry镜像来运行
1 | docker run -d -p 5000:5000 --restart=always --name registry registry |
这将使用官方的registry镜像来启动私有仓库,默认情况下,仓库会被创建在容器的 /var/lib/registry
目录下,可以通过-v
参数指定镜像文件存在本地的指定路径。例如下面的例子将上传的镜像放到本地的/opt/data/registry
1 | docker run -d \ |
上传、搜索、下载
创建好私有仓库后,可以使用docker tag来标记一个镜像,然后将它推送到仓库,例如私有仓库地址为 127.0.0.1:5000
,现在本机已有的镜像
1 | docker image ls |
docker tag将 ubuntu:latest镜像标记为 127.0.0.1:5000/ubuntu/:latest
,格式为:
docker tag IMAGE[:TAG] [REGISTRT_HOST[:REGISTRY_PROT]/]REGISTORY[:TAG]
1 | docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest |
docker push上传标记的镜像
1 | docker push 127.0.0.1:5000/ubtuntu:latest |
curl查看仓库中的镜像
1 | curl 127.0.0.1:5000/v2/_catalog |
删除已有的镜像从私有仓库下载镜像
1 | docker image rm 127.0.0.1:5000/ubuntu:latest |
注意事项
如果不想使用 127.0.0.1:5000
作为仓库地址,比如想让本网段的其他主机也能把镜像推送到私有仓库。得把例如 192.168.199.100:5000
这样的内网地址作为私有仓库地址,这时会发现无法成功推送镜像。
这是因为 Docker 默认不允许非 HTTPS
方式推送镜像。通过 Docker 的配置选项来取消这个限制.
Ubuntu 14.04, Debian 7 Wheezy
对于使用 upstart
的系统而言,编辑 /etc/default/docker
文件,在其中的 DOCKER_OPTS
中增加如下内容:
1 | DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com --insecure-registries=192.168.199.100:5000" |
重新启动服务。
1 | $ sudo service docker restart |
Ubuntu 16.04+, Debian 8+, centos 7
对于使用 systemd
的系统,请在 /etc/docker/daemon.json
中写入如下内容(如果文件不存在新建该文件)
1 | { |
注意:该文件必须符合
json
规范,否则 Docker 将不能启动。
其他
对于 Docker for Windows 、 Docker for Mac 在设置中编辑 daemon.json
增加和上边一样的字符串即可。
私有仓库高级配置
利用Docker Compose 搭建一个拥有权限认证、TLS的私有仓库。新建一个文件夹,以下步骤在该文件夹镜像
准备站点证书
国内各大云服务商提供免费的站点证书,也可以使用openssl自行签发证书。假设要搭建的私有仓库地址为docker.domain.com
,使用openssl自行签发docker.domain.com的站点SSL证书
第一步创建CA私钥
openssl genrsa -out “root-ca.key” 4096
第二步利用私钥创建CA根证书请求文件
1 | openssl req \ |
第三步创建CA根证书新建root-ca.cnf
1 | [root_ca] |
第四步签发根证书
1 | openssl x509 -req -days 3650 -in "root-ca.csr" \ |
第五步生成站点SSL私钥
1 | openssl genrsa -out "docker.domain.com.key" 4096 |
第六步使用私钥生成证书请求文件
1 | openssl req -new -key "docker.domain.docm.key" -out "site.csr" -sha256 \ |
第七步配置证书,新建site.cnf文件
1 | [server] |
第八步签署站点SSL证书
1 | openssl x509 -req -days 750 -in "site.csr" -sha256 \ |
这样就拥有了docker.domain.com
的网站SSL私钥docker.domain.com.key
和SSL证书docker.domain.com.crt
以及CA根证书root-ca.crt
新建ssl文件夹将docker.domain.com.key
、docker.domain.com.crt
、root-ca.crt
这三个文件移入,删除其他文件
配置私有仓库
私有仓库默认的配置文件位于 /etc/docker/registry/config.yml
,现在本地编辑 config.yml
,之后挂载到容器中
1 | version: 0.1 |
生成http认证文件
1 | mkdir auth |
编辑 docker-compose.yml
1 | version: '3' |
修改hosts
编辑 /etc/hosts
1 | 127.0.0.1 docker.domain.com |
启动
1 | docker-compose up -d |
这样就搭建好了一个具有权限认证、TLS的私有仓库。
由于自行签发的CA根证书不被系统信任,需要将CA根证书 ssl/root-ca.crt
移入 etc/docker/certs.d/docker.domain.com
文件夹中
1 | sudo mkdir -p /etc/docker/certs.d/docker.domain.com |
关键在于退出登录,尝试推送镜像
1 | docker logout docker.domain.com |
数据管理
数据卷Volumes
数据卷是一个可供一个或者多个容器使用的特殊目录,它绕过UFS,可以提供很多有用的特征:
- 数据卷可以在容器之间共享和重用
- 对数据卷的修改会立马生效
- 对数据卷的更新不会影响镜像
- 数据卷默认一直存在,即使容器被删除
数据的使用,类似于Linux下对目录或者文件进行mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷
创建一个数据卷
1 | docker volume create my-vol |
启动一个挂载数据卷的容器
1 | 用 docker run命令的时候,使用 --mount标记将数据卷挂载容器里。在一次docker run 中可以挂载多个数据卷 |
查看数据卷的具体信息
主机里面使用以下命令可以查看web容器的信息
1 | docker inspect web |
删除数据卷
1 | docker volume rm my-vol |
数据卷是被设计用来持久化数据的,生命周期独立于容器,Docker不会在容器被删除后自动删除容器,并且也不会存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷,可以再删除容器的时候使用 docker rm -v
的命令
无主的数据卷可能会占据很多空间,清理可以使用:
1 | docker volume prune |
挂载主机目录Bind mounts
挂载一个主机目录作为数据卷
1 | 使用 --mount标记可以指定挂载一个本地主机的目录到容器中去 |
上面的命令加载主机的 /src/webapp
目录到容器的 /opt/webapp目录
,这个功能在进行测试比较方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,使用 -v
参数时如果本地目录不存在Docker会自动创建一个文件夹,使用 --mount
采纳数如果本地目录不存在,Docker会报错
Docker挂载主机目录默认权限是读写,用户可以通过增加 readonly指定为只读
1 | docker run -d -p \ |
查看数据卷的具体信息
在主机里查看web容器的信息
1 | docker inspect web |
挂载一个本地主机文件作为数据卷
--mount
标记也可以从主机挂载单个文件到容器中
1 | docker run --rm -it \ |
使用网络
外部访问容器
容器中可以运行一些网络应用,让外部也可以访问,可以通过 -P/p
参数来指定端口映射
当使用-P
,Docker会随机映射一个 49000~49900
的端口到内部容器开放的端口
1 | docker run -d -p training/webapp python app.py |
-p
则可以指定要映射的端口,并且在一个指定端口上只可以绑定一个容器。支持的格式为:
ip:hostPort:containerPort|ip::containerPort|hostPort:containerPort
1 | 映射所有接口地址:使用 hostPort:containerPort格式本地的5000端口映射到容器的5000端口,此时会默认绑定本地所有接口上的所有地址 |
容器互联
随着Docker网络的完善,将容器加入自定义Docker网络来连接多个容器的做法优于使用 --link
参数
新建网络
1 | 创建一个新的Docker网络,-d参数指定Docker网络类型,有bridge overlay,其中overlay网络类型适用于Swarm mode |
链接容器
1 | 运行一个容器并连接到新建的my net网络 |
如果有多个容器之间需要互相连接,推荐使用Docker compose
配置DNS
Docker利用虚拟文件来挂载容器的3个相关配置文件
在容器中使用 mount命令可以看到挂载信息。
这种机制可以让宿主主机DNS信息发生更新后,所有Docker容器的DNS配置通过 /etc/resolv.conf
文件立刻得到更新。
配置全部容器的DNS,可以在/etc/docker/daemon.json
文件中增加以下内容来设置
1 | { |
这样每次启动容器DNS自动配置为114.114.114.114
和 8.8.8.8
。证明已经生效:
1 | docker run -it --rm ubuntu:18.04 cat etc/resolv.conf |
如果用户想要手动指定容器的配置,可以在使用docker run的时候加入参数:
-h HOSTNAME
/--hostname=HOSTNAME
设定容器的主机名,它会被写到容器内的etc/hostname
和/etc/hosts
,在容器外部看不到,既不会在docker container ls中显示,也不会在其他容器 /etc/hosts看到。--dns=IP_ADDRESS
添加DNS服务器到容器的/etc/resolv.conf
中,让容器用这个服务器来解析所有不在/etc/hosts
中的主机名--dns-search=DOMAIN
设定容器的搜索域,当设定搜索域为.example.com
时,在搜索一个名为host的主机,DNS不仅搜索host,还会搜索host.example.com
如果在容器启动时没有指定最后两个参数,Docker会默认用主机上的 resolve.conf
配置容器
Docker Compose项目
Docker Compose是Docker 官方编排(Orchestration)项目之一,负责快速的部署分布式应用。
Compose定位是定义和运行多个Docker容器的应用。
使用一个Dockerfile模板文件可以很方便定义一个单独的容器,但经常会碰到需要多个容器互相配合来完成某项任务的情况,要实现一个Web项目,除了Web服务容器本身,玩玩还要加上后端的数据库服务容器,甚至负载均衡容器等。
Compose运行用户通过一个单独的docker-cmpose.yml模板文件来定义一组相关联应用容器为一个项目
Compose中有两个重要的概念:
- 服务service:一个应用的容器,实际上可以包括若干运行相同镜像的容器实例
- 项目project:由一组关联的应用容器组成一个完整业务单元,在docker-compose.yml文件中定义
Compose的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷的生命周期管理
Compose项目由Python编写,实际上调用了Docker服务提供的API来对容器进行管理,因此只要所操作的平台支持Docker Api,就可以在其上利用Compose进行编排管理
使用
场景
常见的Web网络,该项目包含web应用和缓存,下面使用Python记录页面次数的web网站
app.py
1 | # dockerTest/app.py |
Dockerfile
1 | FROM python:3.6-alpine |
docker-compose.yml
1 | version: '3' |
运行
1 | docker-compose up |
此时访问本地5000端口,每次刷新页面,计数就会增加1.
如果想要后台运行加 -d
命令说明
命令对象与格式
对于Compose来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有服务都会受到命令影响。
docker-compose [COMMAND] --help
或者 docker-compose help [COMMAND]
可以查看具体某个命令的使用格式,基本的使用格式是:
1 | docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...] |
命令选项
-f,--file FILE
指定使用的Compose模板文件,默认是docker-compose.yml
,可以多次指定-p,--project-name NAME
指定项目名称,默认将使用所在目录名称作为项目名--x-networking
使用Docker的可拔插网络后端特性--x-network-driver DRIVER
指定网络后端的驱动,默认为bridge
--verbose
输出更多调试信息-v,--version
打印版本并退出
命令使用说明
build
格式:
docker-compose build [options] [SERVICE…]
构建(重新构建)项目中的服务容器。服务容器一旦构建后,将会带上一个标记名,对于web项目中的一个db容器,可能是web_db
可以随时在项目目录下运行 docker-compose build
来重新构建服务
选项包括:
--force-rm
删除构建过程中的临时容器--no-cache
构建镜像过程中不使用cache(将加大构建过程)--pull
始终尝试通过pull来获取更新版本的镜像
config
验证Compose 文件格式是否正确,若格式错误显示原因
down
此命令会停止up命令所启动的容器,并移除网络
exec
进入指定容器
help
获得命令的帮助
images
列出Compose文件中包含的镜像
kill
格式:
docker-compose kill [options] [SERVICE…]
通过发送SIGKILL信号来强制停止服务容器。支持通过-s
参数来指定发送信号:
1 | docker-compose kill -s SIGINT |
logs
格式:
docker-compose logs [options] [SERVICE…]
查看服务容器的输出。默认情况下,docker-compose将对不同的服务输出使用不同的颜色来区分,可以通过 --no-color
来关闭颜色
pause
格式:
docker-compose pause [SERVICE…]
暂停一个服务容器
port
格式:
docker-compose port [option] SERVICE PRIVIATE_PORT
打印某个容器端口所映射的公共端口
选项:
--protocol=proto
指定端口协议,tcp默认值或者udp--index=index
如果同一服务存在多个容器,指定命令对象容器的序号(默认为1)
ps
格式:
docker-compose ps [options] [SERVICE…]
列出项目中目前的所有容器
选项:
-q
只打印容器的ID信息
pull
格式:
docker-compose pull [options] [SERVICE…]
拉取服务依赖的镜像
选项:
--ignore-pull-failures
忽略拉取镜像过程中的错误
push
推送服务依赖的镜像到Docker镜像仓库
restart
格式:
docker-compose restart [options] [SERVICE…]
重启项目中的服务。
选项:
-t,--timeout TIMEOUT
指定重启前停止容器的超时(默认为10秒)
rm
格式
docker-compose rm [options] [SERVICE…]
删除所有停止状态服务容器,推荐先指定docker-compose stop命令来停止容器
选项:
-f,--force
强制直接删除,包括非停止状态的容器,一般尽量不要使用该选项-v
删除容器所挂载的数据卷
run
格式:
docker-compose run [options] [-p PORT…] [-e KEY=VAL…] SERVICE [COMMAND] [ARGS…]
在指定服务上执行一个命令
1 | docker-compose run ubuntu ping docker.com |
将会启动一个ubuntu服务容器,并执行ping docker.com命令
默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中
该命令类似启动容器后执行指定的命令,相关卷、链接等等都会按照配置自动创建
两个不同点:给定命令将会覆盖原有的自动运行命令,不会自动创建端口,以避免冲突
如果不希望自动启动关联的容器,可以使用 --no-deps
选项,例如
1 | docker-compose run --no-deps web python manage.py shell |
将不会启动web容器所关联的其他容器
选项:
-d
后台运行容器--name NAME
为容器指定一个名字--entrypoint CMD
覆盖默认的容器启动指令-e KEY=VAL
设置环境变量值,可多次使用选项来设置多个环境变量-u,--user=""
指定运行容器的用户名或者uid--no-deps
不自动启动关联的服务容器--rm
运行命令后自动删除容器,d
默认下将忽略-p,--publish=[]
映射容器端口到本地主机--service-ports
配置服务端口并映射到本地主机-T
不分配伪tty,意味着依赖tty的指令将无法运行
scale
格式:
docker-compose scale [options] [SERVICE=NUM…]
设置指定服务运行的容器个数
通过service-num的参数来设置数量。例如
1 | docker-compose scale web=3 db=2 |
start
格式:
docker-compose start [SERVIC…]
启动已经存在的服务器
stop
格式:
docker-compose stop [options] [SERVICE…]
停止已经处于运行状态的容器,但不删除它,通过 docker-compose start可以再次启动这些容器
top
查看各个服务容器内运行的内存
unpause
格式:
docker-compose unpause [SERVICE…]
恢复处于暂停状态的服务
up
格式:
docker-compose up [options] [SERVICE…]
尝试自动完成包括构建镜像,重新创建服务,启动服务,并关联服务相关容器的一系列操作。链接的服务器都会被自动启动,除非已经处于运行状态
大部分时候都可以通过该命令来启动一个项目
默认情况下,docker-compose up启动的容器在前台,控制台将会同时打印所有容器的信息,可以方便调试,使用 ctrl-c停止命令时,所有容器将会停止
生产环境推荐使用 docker-compose up -d,这将会在后台启动并运行所有容器。
默认情况下,如果服务容器已经存在,docker-compose up将会尝试停止容器,然后重新创建(保持使用volume-from
挂载的卷),以保证新的服务匹配docker-compose.yml文件的最新内容。如果不希望容器被停止并重新创建,可以使用 docker-compose up –no-recreate。这样只会启动处于停止状态的容器,而忽略已经运行的服务,如果用户只想重新部署某个服务,可以使用 docker-compose up --no-deps -d <SERVICE_NAME>
来重新创建服务并后台停止旧服务,启动新服务,并不会影响到期所依赖的服务。
选项:
-d
在后台运行服务容器--no-color
不使用颜色来区分不同的服务的控制台输出--no-deps
不启动服务所链接的容器--force-recreate
强制重新创建容器,不能与--no-recreate
同时使用--no-buld
不自动构建缺失的服务镜像-t,timeout TIMEOUT`停止同期时候的超时(默认为10秒)
version
打印版本信息
模板文件
模板文件就是使用compose的核心,大部分指令跟docker run相关参数的含义类似
默认的模板文件名称为 docker-compose.yml
,格式为YAML格式
1 | version: "3" |
每个服务都必须通过image指令指定镜像或build(需要Dockerfile)指令等来自动构建生成镜像
如果使用build命令,在Dockerfile中设置的选项,例如CMD/EXPOSE/VOLUME/ENV等将会被自动获取,无需在docker-compose.yml中再次设置。
build
指定Dockerfile所在文件夹的路径(可以是绝对路径,或者相对docker-compose.yml文件路径),Compose将会利用它自动构建这个镜像,然后使用这个镜像
1 | version: '3' |
也可以使用context指令指定Dockerfile所在文件夹的路径,dockerfile指令指定Dockerfile文件名,arg指令指定构建镜像时的变量
1 | version: '3' |
使用cache_from指定构建镜像的缓存
1 | build: |
cap_add,cap_drop
指定容器的内核能力(capacity)分配
1 | # 让容器拥有所有能力 |
configs、deploy
仅用于Swarm mode
cgroup_parent
指定父cgroup组,意味着将继承改组的资源限制
1 | # 创建一个cgroup组名称为cgroups_1 |
container_name
指定容器名称。默认使用项目名称_服务名称_序号
1 | container_name:docker-web-container |
devices
指定设备映射关系
1 | devices: |
depends_on
解决容器的依赖、启动先后问题。
1 | # 先启动redis db 再启动web |
dns
自定义DNS服务器,可以是一个值,也可以是一个列表
1 | dns: 8.8.8.8 |
dns_search
配置DNS搜索域,可以是一个值,也可以是一个列表
1 | dns_search:example.com |
tmpfs
挂载一个tmpfs文件系统到容器
1 | tmpfs: /run |
env_file
从文件中获取环境变量,可以为单独的文件路径或列表
通过 docker-compose -f FILE
方式来指定Compose模板文件,则 env_file
中变量的路径会基于模板文件路径。如果有变量名称与environment指令冲突,则按照惯例,以后者为准
1 | env_file: .env |
环境变量文件中每一行必须符合格式,支持#
开头的注释行
1 | # common.env: Set development environment |
environment
设置环境变量,可以使用数组和字典两种格式
只给定名称的变量会自动获取运行Compose主机上对应变量的值,可以用阿里防止泄露不必要的数据
1 | environment: |
如果变量名称或者值中用到true|false,yse|no
等表示布尔含义的词汇,最好放到引号中,避免YAML自动解析某些内容为对应的布尔语义,这些特定词汇,包括
1 | y|Y|Yes|YES|n|N|no|NO|true|True|TRUE|false|False|FALSE|on|ON|off|Off|OFF |
expose
暴露端口,但不映射到宿主机,只被连接的服务访问。仅可以指定内部端口为参数
1 | expose: |
external_links
链接到docker-compose.yml外部的容器,甚至并非Compose管理的外部容器,不建议使用
1 | external_links: |
extra_hosts
类似Docker中的--add-host
参数,指定额外的host名称映射信息
1 | extra_hosts: |
会在启动后的服务容器中 /etc/hosts
文件中添加如下两条条目
1 | 8.8.8.8 googledns |
healthcheck
通过命令检查容器是否健康运行
1 | healthcheck: |
image
指定为镜像名称或者镜像ID,如果镜像在本地不存在,Compose将会尝试拉取这个镜像
1 | image: ubuntu |
labels
为容器添加Docker元数据(metadata)信息,可以为容器添加辅助说明信息
1 | labels: |
links
不推荐使用该指令
logging
配置日志选项
1 | loggin: |
network_mode
设置网络模式,使用和 docker run 的 network参数一样的值
1 | network_mode: "bridge" |
networks
配置容器连接的网络
1 | version: "3" |
pid
跟主机系统共享进程命名空间,打开该选项的容器之间,以及容器和宿主机系统之间可以通过进程ID来互相访问和操作
1 | pid: "host" |
ports
暴露端口信息
使用宿主端口:容器端口(HOST:CONTAINER)格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以
1 | ports: |
secrets
存储敏感数据,例如mysql服务密码
1 | version: "3.1" |
security_opt
指定容器模板标签label机制的默认属性(用户、角色、类型、级别等)。例如配置标签的用户名和角色名
1 | security_opt: |
stop_signal
设置另一个信号来停止容器。在默认情况下使用的是SIGTERM停止容器
1 | stop_signal: SIGUSR1 |
sysctls
配置容器内核参数
1 | sysctls: |
ulimits
指定容器的ulimits限制值
例如,指定最大容器的进程数为65535.指定文件句柄数为20000(软限制,应用可以随时修改,不能超过硬限制)和40000(系统硬限制,只能root用户提高)
1 | ulimits: |
volumes
数据卷所挂载路径设置,可以设置宿主主机路径(HOST:CONTAINER)或加上访问模式(HOST:CONTAINER:ro)
该指令中路径支持相对路径
1 | volumes: |
其他指令
domainname,entrypoint,hostname,ipc,mac_address,privileged,read_only,shm_size,restart,stdin_open,tty,user,wor等指令,基本跟 docker run中对应参数的功能一致
1 | # 指定服务容器启动后执行的入口文件 |
读取变量
Compose模板文件支持动态读取主机的系统环境变量和当前目录下的 .env文件中的变量
例如,下面的Compose文件将从运行它的环境中读取变量 ${MONGO_VERSION}
的值,并写入执行的指令中
1 | verison: '3' |
如果执行 MONGO_VERSION=3.2 docker-compose up
则会启动一个 mongo:3.2
镜像的容器;如果执行 MONGO_VERSION=2.8 docker-compose up
则会启动一个 mongo:2.8
镜像的容器。
若当前目录存在 .env
文件,执行 docker-compose
命令时将从该文件中读取变量。
在当前目录新建 .env
文件并写入以下内容。
1 | # 支持 # 号注释 |
执行 docker-compose up
则会启动一个 mongo:3.6
镜像的容器。