Docker笔记

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,也就是官方镜像

例如:

docker@default:~
$ docker pull ubuntu:16.04
16.04: Pulling from library/ubuntu

8bd67045: Pulling fs layer
e4862c05: Pulling fs layer
8949dcb1: Pulling fs layer
Digest: sha256:69bc24edd22c270431d1a9e6dbf57cfc4a77b2da199462d0251b145fdd7fa538
Status: Downloaded newer image for ubuntu:16.04

上面命令没有给出docker镜像仓库地址,会从Docker Hub获取镜像,镜像名称是ubuntu:16.04,因此获取官方镜像library/ubuntu,仓库中标签为 16.04的镜像

下载过程中可以看出分层存储概念,镜像由多层存储所构成,下载也是一层层的去下载,并非单一文件。下载过程中给出每一层ID的前12位,并且下载结束后,给出该镜像sha256的完整摘要,以确保下载一致性

运行

以镜像为基础启动并运行一个容器,启动里面的bash并镜像交互操作

docker@default:~$ docker run -it --rm ubuntu:16.04 bash
# 查看当前系统版本
]0;root@0d248af097b5: /root@0d248af097b5:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.6 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.6 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
# 退出
]0;root@0d248af097b5: /root@0d248af097b5:/# exit
exit

docker run是容器运行的命令:

  • -it:这是两个参数,也给是-i,交互式操作,一个是-t终端。进入bash执行一些命令并查看返回结果,因此需要交互式终端
  • --rm:容器退出后随之将其删除。默认情况下,为了排障需求,退出的容器并不会立即删除,除非手动docker rm。这里使用 --rm可以避免浪费空间,只是随便执行个命令,查看结果,不需要排障和保留结果
  • ubuntu:16.64:这是用这个镜像为基础来启动容器
  • bash:在镜像后的是命令,希望有个交互式的shell,使用的是bash

列出镜像

docker image ls

docker@default:~$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED        SIZE
ubuntu              16.04               c522ac0d6194        8 hours ago    126MB
nginx               latest              2622e6cca7eb        3 weeks ago    132MB
hello-world         latest              fce289e99eb9        18 months ago  1.84kB

列表包含了仓库名、标签、镜像ID、创建时间以及所占空间。

镜像ID是镜像的唯一标识,一个镜像可以对应多个标签。

镜像体积

这里标识所占的空间和在Docker Hub上看到的镜像大小不一样,这是因为Docker Hub中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像总是保持着压缩状态,因此Docker Hub中显示的大小是网络传输中更关心的流量大小,而docker image ls显示的是镜像下载到本地后展开的大小,是展开后的各层所占空间的总和,因此镜像到本地后,查看空间的时候,更关心的是本地磁盘空间所用的大小。

列表中的镜像体积总和并非是所有镜像实际硬盘消耗,由于docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于Docker使用UnionFS,相同层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。

使用 docker system df查看镜像、容器、数据卷所占用的空间

docker@default:~$ docker system df
TYPE                TOTAL               ACTIVE              SIZE      RECLAIMABLE
Images              3                   1                   258.2MB   258.2MB (99%)
Containers          1                   0                   0B        0B
Local Volumes       0                   0                   0B        0B
Build Cache         0                   0                   0B        0B

虚悬镜像

有时候会特殊镜像,既没有仓库名,也没有标签,为none

<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

出现无标签镜像,与虚悬镜像不同,这些镜像都是中间层镜像,是其他镜像所依赖的镜像,删除这些镜像会导致上层镜像因为依赖丢失出错。删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除

列出部分镜像

# 根据仓库名
docker image ls ubuntu

# 指定仓库名和标签
docker image ls ubuntu:16.04

# 过滤器参数--filter,简写-f,希望看到在mongo:3.2之后建立的镜像
docker image ls -f since=mongo:3.2

# 之前则是用before

# 如果镜像构建时,定义了LABEL,可通过LABEL过滤
docker image ls -f label=com.example.version=0.1

特定格式显示

# 将所有镜像的ID列出
docker image ls -q

# 列出镜像结果只包含镜像ID和仓库名,使用了GO模板语法
docker image ls --format "{{.ID}}: {{.Repository}}"
c522ac0d6194: ubuntu
2622e6cca7eb: nginx
fce289e99eb9: hello-world

# 以表格等距显示,并且有标题行,和默认一样,不过自己定义列
docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID            REPOSITORY          TAG
c522ac0d6194        ubuntu              16.04
2622e6cca7eb        nginx               latest
fce289e99eb9        hello-world         latest

删除本地镜像

docker image rm [选项] <镜像1>[<镜像2>...]

ID、镜像名、摘要删除镜像

镜像可以是短id,长id,镜像名或者镜像摘要,短id取前3个字符以上,能区分别的镜像就可以

docker image rm 502

# 用镜像名,<仓库名>:<标签>
docker image rm centos

# 显示镜像摘要
docker image ls --digests

# 使用镜像摘要删除镜像
docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

# 删除所有仓库名为redis的镜像
docker image rm $(docker image ls -q redis)

# 删除所在mongo:3.2之前的镜像
docker image rm$(docker image ls -q -f before=mongo:3.2)


Untagged和Deleted

删除行为分为两种,一种是untagged,一种是deleted。镜像的唯一标识是其ID和摘要,而一个惊喜可以有多个标签。当我们删除指定标签后,可能还有别的标签指向这个镜像,那么delete行为不会发生,会显示untagged信息。

当该镜像所有标签都被取消了,改镜像可能就失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,在删除的时候也是从上层向基础层方法依次进行判断删除。镜像的多层结构使得复用变动镜像容易,因此很有可能某个其他镜像正依赖当前镜像的某一层。这种情况不会触发删除该层行为。直到没有任何层依赖当前层时,才会真实删除当前层。

除了镜像依赖还有容器对镜像的依赖。如果有用这个镜像启动的容器存在,即使容器没有运行,那么同样不可以删除这个镜像。容器是以经为基础,再加一层容器存储层,组成这样的多层存储结构去运行。因此该镜像如果被这个容器所依赖,那么删除必然会导致故障。如果这些容器是不需要的,应该先将它们删除,然后来删除镜像。

利用commit理解镜像构成

镜像是容器的基础,每次执行docker run的时候都会指定哪个镜像作为容器运行的基础。当Docker Hub的镜像无法满足需求时,我们就需要定制镜像。

定制Web服务器为例子

# nginx镜像启用一个容器,命名为webserver,映射80端口,在浏览器可以访问这个nginx服务器
docker run --name webserver -d -p 80:80 nginx

# 修改欢迎页面,以交互式终端方式进入webserver容器,并执行bash命令,获得一个可操作的shell,使用新内容覆盖指定文件的内容
docker exec -it webserver bash
root@xxx: /# echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html
root@xxx: /# exit
exit

修改了容器文件,也就是改动了容器的存储层,通过 docker diff可以看到具体的改变

定制好变化后,保存下来形成镜像。当我们运行一个容器(不使用卷),我们做的任何文件修改都会被记录于容器存储层里。而docker提供了docker commit命令,可以将容器的存储层保存下来成为镜像。在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原来容器最后的文件变化

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

# 将容器保存为镜像
docker commit --author "Laibh <544289495@qq.com>" --message "修改默认页面" webserver nginx:v2 

接着

# 查看新定制的镜像并使用,也可以使用docker history具体查看镜像内的历史记录
docker image ls 

docker run --name web2 -d -p 81:80 nginx:v2

使用 docker commit 命令可以直观帮助理解镜像分层存储概念,但是实际环境不会这么做。

仔细观察docker diff webserver的结果,会发现除了修改的文件外,由于命令的执行,还有很多文件被改动或者添加了,这仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,如果不小心清理,将会导致镜像尤为臃肿。

此外,使用docker commit都是黑箱操作。只有制作者才知道做了什么,docker diff可以看出一些线索,但不能保证生成一致的镜像。

Dockerfile定制镜像

dockerfile是一个文本文件,其内包含了一天天的指令(Instruction),每一条指令构建一层,因此每一条指令的内容就是描述该层应当如何构建

在一个空白目录,建立一个文件夹,并命名为Dockerfile

mkdir myngix
cd mynginx 
touch Dockerfile
vi Dockerfile
# 内容为
FROM nginx
Run echo '<h1>Hello,Docker!</h1>' > /usr/share/nginx/html/index.html

FROM指定基础镜像

定制镜像是以一个镜像为基础,在其上进行定制,FROM就是指定基础镜像,因此FROM是Dockerfile文件的第一条指令,除了docker hub现有的镜像,还有一个特殊的镜像 scratch,虚拟镜像,表示一个空白的镜像

FROM scratch

以它为基础的话,意味着不以任何镜像为基础,接下来写的指令作为镜像的第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接FROM scratch会让镜像体积更加小巧。使用GO语言开发的引用会使用这种方式,这也是为什么说Go是特别适合容器微服务架构的语言的原因之一

RUN指定命令

RUN指令是定制镜像最常见的指令之一。格式有两种:

shell格式:RUN<命令>,就像直接在命令行输入的命令一样

exec格式:RUN["可执行命令","参数1","参数2"],这更像是函数调用中格式

FROM debian:jessie

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redios.io/release/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --script-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

dockerfile中每个指令都会建立一层,RUN也不例外,每一个RUN的行为都会新建一层,在其上执行结果后,commit一层的修改,构成新的镜像,上面这种写法创建了7个镜像,这是完全没有意义的,运行时很多不需要的东西都被装进了镜像,比如编译、更新的软件包等,结果就是产生臃肿、非常多层的镜像,不仅增加了构建时间,也容易出错。UnionFS是有最大层数限制的,比如AUFS,曾经是最大不超过42层,现在是不超过127层。

上面的Dockerfile正确写法应该是:

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \
	&& apt-get update \
	&& apt-get install -y $buildDeps \
	&& wget -O redis.tar.gz "http://download.redios.io/release/redis-3.2.5.tar.gz" \
	&& mkdir -p /usr/src/redis \ 
	&& tar -xzf redis.tar.gz -C /usr/src/redis --script-components=1 \
	&& make -C /usr/src/redis \
	&& make -C /usr/src/redis install \
	&& rm -rf /var/lib/apt/lists/* \
	&& rm redis.tar.gz \
	&& rm -r /usr/src/redis \
	&& apt-get purge -y --auto-remove $buildDeps

之前所有命令的目的只有一个,就是编译、安装redis可执行文件。因此没有必要建立多层,这里仅使用一个RUN,并使用 &&将各个需要命令串联起来,将之前的7层变成1层。并且为了格式化还进行了换行。Dockerfile支持Shell类的行尾添加 \的命令行,以及行首 #进行注释的格式。良好的格式会让排障更加容易。

此外,上面的命令还进行了清理工作,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了apt缓存文件,在镜像构建过程要确保每一层只添加真正需要添加的东西,任何无关的东西都应该被清理掉。

构建镜像

在Dockerfile文件所在目录执行:

docker@default:~/mynginx$ docker build -t nginx:v3 .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 2622e6cca7eb
Step 2/2 : RUN echo '<h1>Hello,Docker!!!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 6b7204234c09
Removing intermediate container 6b7204234c09
 ---> 4021436fa2ed
Successfully built 4021436fa2ed
Successfully tagged nginx:v3
# 在Step2中,RUN启动了一个容器6b7204234c09,执行了要求的命令,并提交了这一层4021436fa2ed,删除所用到的容器RUN启动了一个容器6b7204234c09

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输出,可以看到这个发送上下文的过程:

docker@default:~/mynginx$ docker build -t nginx:v3 .
Sending build context to Docker daemon  2.048kB

理解构建上下文对于镜像构建很重要,避免犯一些不应该的错误,比如初学者在发现 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

# 指定构建所需要的git repo,并且指定默认的master分支,构建目录为/814/,然后docker就会自己去git clone这个项目、切换到指定分支,并进入到指定目录后开始构建
docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
docker build https://github.com/twang2218/gitlab-ce-zh.git\#:8.14
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:8.14.0-ce.0
8.14.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...

用tar压缩包构建
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指令将从构建上下文目录中<源路径>的文件/目录复制到新的一层的镜像内的<目标路径>位置,比如:

COPY package.json /usr/src/app/

源路径可以是多个,甚至是通配符,其通配符规则要满足GO的filepath.Match规则,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径>可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作路径可以用WORKDIR指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还有一点使用COPY指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用,特别是构建相关文件都在使用Git进行管理时。

ADD更高级的复制文件

ADD指令和COPY的格式和性质基本一致,但是在COPY基础上增加了一些功能。

比如 <源路径>可以是一个URL,这种情况下Docker引擎会视图去下载这个链接的文件放到 <目标路径>去。下载后的文件权限自动设置为600,如果这并不是想要的权限,还要增加额外的一层RUN进行权限调整。另外,如果下载但是个压缩包,需要解压缩,也一样需要额外的一层RUN指令进行解压缩。所以不如直接使用RUN,然后用wget或者curl工具下载,处理权限、解压缩、然后清理无用文件更合理。因此这个功能其实并不好用,而且不推荐使用

如果<源路径>为一个tar压缩文件,压缩格式为gzip,bzip2以及xz的情况下,ADD指令将会自动解压缩这个压缩文件到 <目标路径>去。

在某些情况下,这个自动解压功能非常有用,比如官方镜像ubuntu中

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /

在某些情况下,希望复制压缩文件而不解压就不用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实现

FROM ubuntu:16.04
RUN apt-age update \
	&& apt-age install -y curl \
	&& rm -rf /var/lib/apt/lists/*
CMD ["curl","-s","http://ipinfo.cn"]

使用 docker build -t myip .来构建镜像的话,如果查询当前公网的ip,只需要执行

docker run myip

现在把镜像当做命令使用,如果命令有参数要怎么加上?CMD中实际的命令是curl,如果希望展示HTTP头部信息,就需要加上 -i参数

docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

可以看到执行文件找不到的报错,executable file not found。在镜像后面的是 command,运行时会替换CMD的默认值,这里的-i替换了CMD,而不是加在 curl -s http://ip.cn后面。而 -i不是命令,所以自然找不到。

如果希望加入 -i这参数,就必须重新完整的输入这个命令

docker run myip curl -s http://ipinfo.cn -i

这显然不是很好的解决方案,使用ENTRYPOINT可以解决这个问题:

FROM ubuntu:16.04
RUN apt-get update \
	&& apt-get install -y curl \
	&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["curl","-s","http://ipinfo.cn"]

运行 docker run myip -i

docker@default:~/myip$ docker run myip
{
  "ip": "113.70.180.56",
  "city": "Foshan",
  "region": "Guangdong",
  "country": "CN",
  "loc": "23.0268,113.1315",
  "org": "AS4134 CHINANET-BACKBONE",
  "timezone": "Asia/Shanghai",
  "readme": "https://ipinfo.io/missingauth"
}docker@default:~/myip$ docker run myip -i
HTTP/1.1 200 OK
Date: Thu, 09 Jul 2020 03:16:44 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 233
Vary: Accept-Encoding
Access-Control-Allow-Origin: *
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Set-Cookie: flash=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
Via: 1.1 google
Expires: Thu, 09 Jul 2020 03:16:44 GMT
Cache-Control: private

{
  "ip": "113.70.180.56",
  "city": "Foshan",
  "region": "Guangdong",
  "country": "CN",
  "loc": "23.0268,113.1315",
  "org": "AS4134 CHINANET-BACKBONE",
  "timezone": "Asia/Shanghai",
  "readme": "https://ipinfo.io/missingauth"


当存在ENTRYPOINT后,CMD的内容将会作为参数传给ENTRYPOINT,而 -i就是新的CMD,因此会作为参数传给curl。

场景二:应用运行前的准备工作

启动容器就是启动主进程,有时候在启动主进程前,需要一些准备工作。

例如mysql类的数据库,需要一些数据库配置、初始化工作,这些工作要在mysql服务器运行前解决。

另外,希望避免使用root用户去启动服务,从而提高安全性,而在启动前还需要以root身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其他命令依旧可以使用root身份执行,方便调试。

这些准备工作与容器CMD无关的,无论CMD为什么,都需要事先进行一个预处理的工作,这种情况可以写一个脚本,放入到ENTRYPOINT中去执行,而这个脚本也会将接收到的参数也就是CMD的命令作为命令,在脚本最后执行,官方镜像redis就是这样做的

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]

可以看到为了redis服务创建redis用户,并在最后指定了ENTRYPOINT为docker-entrypoint.sh脚本

# !/bin/sh
..
# allow the container to be started with '--user'
if ["$1"='redis-server' -a "$(id-u)"='0'];then
	chown -R redis .
	exec su-exec redis "$0" "$@"
fi

exec "$@"

脚本的内容根据CMD的内容来判断如果是redis-server就切换到redis用户身份去启动服务器,否则使用root身份执行

docker run -it redis id
uid=0(root) git=0(root) groups=0(root)

ENV设置环境变量

格式:

  • ENV<key><value>
  • ENV<key1>=<value> <key2>=<value2>

设置环境变量,无论是后面的其他指令RUN,还是运行时的引用,都可以直接使用这里定义的环境变量

ENV VERSION=1.0 DEBUG=on \
	NAME="Happy Feet"
# 换行符以及当有空格的值用双引号括起来

定义了环境变量,在后续的指令里面就可以使用这个环境变量,比如官方node镜像Dockerfile中妈就有类似这样的代码

ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

定义了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只是帮助切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN ["redis-server"]

如果以root执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程。不要使用su或者sudo,这些都需要比较麻烦的配置,而且在TTY缺失的环境下经常出错,建议使用 gosu

# 建立 redis用户,并使用 gosu换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "http://github.com/tianon/gosu/release/download/1.7/gosu-amd64" \
	&& chmod +x /usr/local/bin/gosu \
	&& gosu nobody true
# 设置CMD,并以另外的用户执行
CMD ["exec","gosu","redis","redis-server"]

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可以这么写:

FROM nginx 
RUN agt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHYCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1

设置了每5秒检查一次,如果健康检查超过3秒没响应就是视为失败。构建并启动容器

docker build -t myweb:v1
docker run -d --name web -p 80:80 myweb:v1
# 查看状态
docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web
# 等待几秒后再查看,发现健康状态变成了healthy,如果健康检查连续失败超过重试次数,状态就为unhealthy
# 为帮助排障,健康检查命令的输出stdout/stderr等都会被存储在健康状态,可用docker inspect查看
docker inspect --format‘{{json .State.Health}}’ web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD为他人做嫁衣裳

格式:ONBUILD<其他指令>

ONBUILD是一个特殊的指令,后面跟着的是其他指令,例如RUN/COPY等,而这些指令,在当前镜像构建时不会被执行,只有当以当前镜像为基础,去构建下一个镜像才会执行

Dockerfile中的其他指令都是为了定制当前镜像而准备的,ONBUILD是为了帮助别人定制自己准备的。Nodejs使用npm管理,拿到程序后,一般会先安装依赖,然后启动,一般会这么写Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN ["npm","install"]
COPY . /app/
CMD ["npm","start"]

把这个Dockerfile放到Nodejs的根目录,构建好镜像后,就可以直接拿来启动容器运行,但是如果有第二个第三个差不多的项目,随着文件附件越多,版本控制就越难。

如果在第一个Nodejs项目发现了Dockerfile存在的问题,比如敲错字,或者需要安装额外的包,然后开发人员修复了这个Docker,再次构建,问题解决。但是其他项目呢?虽然最初Dockerfile是复制、粘贴自第一个项目,但是并不会因为第一个项目修复了Dockerfile就会自动修复

那么我们可以做一个基础镜像,然后各个项目使用这个基础镜像。镜像更新后就不用同步Dockerfile的变化,重新构建后就继续了基础镜像的更新:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD ["npm","start"]

这里把项目相关的构建指令拿出来,放在子项目去。假设这个基础镜像为my-node,各个项目内的Dockerfile就变成:

FROM my-node
COPY ./package.json /app
RUN ["npm","install"]
COPY . /app/

基础镜像变化后,各个项目都用这个Dockerfile重新构建镜像,会基础基础镜像的更新。

但是问题并没有完全解决,这个Dockerfile如果有东西要修改,又要重新一个一个修改。这样制作镜像只解决了原来Dockerfile的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理

ONBUILD可以解决这个问题:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN ["npm","install"]
ONBULD COPY . /app/
CMD ["npm","start"]

在构建镜像的时候,这三行并不会被执行,然后各个项目的Dockerfile就变成

FROM my-node

当在各个项目目录中,用这个只有一行的Dockerfile构建镜像时,之前基础镜像的那三行ONBUILD就会执行,成功的将当前项目的代码复制进镜像,并且针对本项目进行npm install,生成对应镜像

多阶段构建

在17.05之前,构建Docker镜像通常会采用两种方式

全部放入一个Dockerfile

一种方式是将所有的构建过程编包含在一个Dockerfile中,包括项目以及依赖库的编译、测试、打包等流程,这里可能会有一些问题:

  • Dockerfile特别长,可维护性降低
  • 镜像层次多,镜像体积较大,部署时间变长
  • 源代码存在泄漏的问题

例如:编写app.go文件,输出Hello World!

package main
import "fmt"
function main(){
    fmt.Printf('Hello World!')
}

编写Dockerfile.one文件

FROM golang:1.9-alpine
RUN apk --no-cache add got ca-certificates
WORKDIR /go/src/github.com/go/helloworld/
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
	&& CGO_ENABLED=0 GOOS=linux go buld -a -installsuffix cgo -o app . \
	&& cp /go/src/github.com/go/helloworld/app/root

WORKDIR /root/
CMD ["./app"]

构建镜像

docker build -t go/helloworld:1 -f Dockerfile.one

分割到多个Dockerfile

另一种方式,就是事先在一个Dockerfile将项目以及其依赖库编译测试打包好后,再将其拷贝到运行环境中,这种方式需要编写两个Dockerfile和一些编译脚本才能将两个阶段自动整合起来,这种方式虽然可以很好规避第一种方式存在的风险,但明显部署过程比较复杂

Dockerfile.build文件

FROM golang:1.9-alpine
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld
COPY app.go .
RUN go get -d -v github.com/go-sql-driver/mysql \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

Dockerfile.copy文件

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

新建build.sh

#!/bin/sh
echo Building go/helloworld:build

docker build -t go/helloworld:build . -f Dockerfile.build

docker create --name extract go/helloworld:build
docker cp extrace:go/src/github.com/go/helloworld/app ./app
docker rm -f extract

echo Building go/helloworld:2

docker build --no-cache -t go/helloworld:2 -f Dockerfile.copy 
rm ./app

运行脚本即可构建镜像

chmod +x build.sh
./build.sh

对比两种生成的镜像大小

$ docker image ls

REPOSITORY      TAG    IMAGE ID        CREATED         SIZE
go/helloworld   2      f7cf3465432c    22 seconds ago  6.47MB
go/helloworld   1      f55d3e16affc    2 minutes ago   295MB

使用多阶段构建

为了解决以上的问题,17.05后开始支持多阶段构建(multistage builds),使用多阶段构建可以解决上面的问题,并只需要编写一个Dockerfile:

例如:

编写Dockerfile文件:

FROM golang:1.9-alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/github.com/go/helloworld/
RUN go get -d -v github.com/go-sql-driver/mysql
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a installsuffix cgo -o app
FROM alpine:latest as prod
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/go/helloworld/app .
CMD ["./APP"]

构建镜像

docker build -t go/helloworld:3 .

对比三个镜像

$ docker image ls

REPOSITORY        TAG   IMAGE ID         CREATED            SIZE
go/helloworld     3     d6911ed9c846     7 seconds ago      6.47MB
go/helloworld     2     f7cf3465432c     22 seconds ago     6.47MB
go/helloworld     1     f55d3e16affc     2 minutes ago      295MB

很明显使用多阶段构建的镜像体积小,同时也解决了上边的问题

只构建一阶段的镜像

使用as为某一阶段命令,例如:

FROM golang:1.9-alpine as builder

只构建builder阶段的镜像,可以在使用 docker build的时候加上--target

docker build --target builder -t username/imagename:tag .

构建时从其他镜像复制文件

上面的例子使用 COPY --from=0 /go/src/github.com/go/helloworld/app .从上一阶段的镜像中复制文件,我们可以复制任意镜像中的文件

COPY --from=nginx:latest /ect/nginx/nginx.conf /nginx.conf

其他制作镜像的方式

从rootfs压缩包导入

格式:docker import [选项] <文件>|<URL>|- [<仓库名>[:<标签>]]

压缩包可以是本地文件,远程Web文件,甚至是从标准输入中得出的,压缩包就会在镜像 /目录展开,并直接作为镜像第一层提交

# 创建一个OpenVZ的Ubuntu14.04模板的镜像:
docker import \
	http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz \
    openvz/ubuntu:14.04
Downloading from http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz
sha256:f477a6e18e989839d25223f301ef738b69621c4877600ae6467c4e5289822a79B/78.42 MB    

这条命令自动下载了 ubuntu-14.04-x86_64-minimal.tar.gz 文件,并且作为根文件系统展开导入,并保存为镜像 openvz/ubuntu:14.04

导入成功后,使用 docker image ls可以看到这个导入的镜像

看到历史会看到描述中有导入的文件链接

$ docker history openvz/ubuntu:14.04
IMAGE               CREATED              CREATED BY          SIZE                COMMENT
f477a6e18e98        About a minute ago                       214.9 MB            Imported from http://download.openvz.org/template/precreated/ubuntu-14.04-x86_64-minimal.tar.gz

docker save和docker load

将镜像保存为一个tar文件,然后传输到另外一个位置,再加载进来。这是在没有Docker Registry的做法,现在已不推荐。镜像迁移直接使用Docker Registry,无论是直接使用Docker Hub还是使用内网私有Registry都可以

保存镜像

docker save将镜像保存为归档文件

docker save alpine | gzip > alpine-latest.tar.gz

然后将它复制到另外一个机器上,用下面的命令加载镜像:

docker load -i alpine-latest.tar.gz
Loaded image: alpine:latest

结合这两个命令和ssh甚至是pv,利用Linux管道可以写一个命令完成从一个机器将镜像迁移到另外一个机器,并且带进度条的功能:

docker save <镜像名> | bzip2 | pv |ssh <用户名>@<主机名> 'cat|docker load'

镜像实现的原理

Docker镜像是怎么实现增量的修改和维护的?

每个镜像都由多层次构成,Docker使用Union FS将这些不同层结合到一个镜像中去。

通常Union FS有两个用途,一方面可以实现不借助LVM,RAID将多个disk挂在同一个目录下,另一个更常用的就是讲一个只读的分支和一个可写的分支联合在一起。

操作容器

简单来说,容器似乎运行的一个或一组应用,以及它们的运行态环境,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用

启动容器

启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态(stopped)的容器重新启动。因为Docker的容器很轻级,用户可以随时删除和新建容器。

新建并启动

# 输出Hello World后终止容器
docker run ubuntu:14.04 /bin/echo 'Hello world'

# 启动一个bash终端,允许用户进行交互,-t选项让Docker分配一个伪终端pseudo-tty并绑定到容器的标准输入上,-i则让容器的标准输入保持打开。在交互模式下,用户可以通过所创建的终端来输入命令
docker run -t -i ubuntu:14.04 /bin/bash

当利用 docker run来创建容器时,Docker在后台运行的标准操作包括:

  • 检查本地是否存在指定的镜像,不存在就从共有仓库下载
  • 利用镜像创建并启动一个容器
  • 分配一个文件系统,并在只读的镜像层外面挂载一层可读层
  • 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去
  • 从地址池配置一个ip地址给容器
  • 执行用户指定的应用程序
  • 执行完毕后容器被终止

启动已终止的容器

docker container start命令可以将一个已经终止的容器启动运行。

容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的,除此之外没有其他资源。可以在伪终端中利用ps或top来查进程信息

守护态运行

更多时候,需要让Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。可以通过添加 -d参数实现。

不用 -d参数运行容器:

docker run ubuntu:17.10 /bin/sh -c "while true;do echo hello world;sleep 1;done"
hello world
hello world
hello world
hello world

容器会把输出结果stdout打印到宿主主机上面

如果使用了 -d参数运行容器

docker run -d ubuntu:17.10 /bin/sh -c "while true;do echo hello world; sleep 1;done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此时容器会在后台运行并不会把输出结果打印到宿主机上面(输出结果可以用docker logs查看)。

注:容器是否会长久运行,和 docker run 指令的命令有关,和 -d参数无关。

使用 -d参数启动后返回一个唯一的id,可以通过 docker container ls命令来查看容器信息

docker container ls
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
77b2dc01fe0f  ubuntu:17.10  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

# 通过 docker icontainer logis获取容器的输出信息
docker container logs[container ID or NEWS]
hello world
hello world
hello world
hello world

终止

使用 docker container stop 来终止一个运行中的容器

当Docker 容器中指定的应用终结时,容器也会自动终止。上面只启动了一个容器,用户可以通过exit或者ctrl+d来推出终端,所创建的容器立刻终止。

终止状态的容器可以使用 docker containers ls -a命令看到例如:

docker container ls -a
CONTAINER ID        IMAGE                    COMMAND                CREATED             STATUS                          PORTS               NAMES
ba267838cc1b        ubuntu:14.04             "/bin/bash"            30 minutes ago      Exited (0) About a minute ago                       trusting_newton
98e5efa7d997        training/webapp:latest   "python app.py"        About an hour ago   Exited (0) 34 minutes ago                           backstabbing_pike

然后通过 docker run restart命令会让一个运行态的容器终止,然后再重新启动它。

进入容器

使用 -d参数时,容器启动后会进入后台。某些时候需要进入容器操作,包括docker attch命令或docker exec命令,推荐使用docker exec

attch命令

docker attch是Docker自带命令

docker run -dit ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550

docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia

docker attach 243c
root@243c32535da7:/#

# 如果从这个stdin中exit,会导致容器的停止

exec命令

-i -t参数

docker exec可以跟多个参数

只用 -i参数时,由于没有分配伪终端,界面没有Linux命令符,但命令执行结果仍然可以返回

-i/-t参数一起使用时,可以看到Linux命令提示符

docker run -dit ubuntu
69d137adef7a8a689cbcb059e94da5489d3cddd240ff675c640c8d96e84fe1f6

docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
69d137adef7a        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           zealous_swirles

docker exec -i 69d1 bash
ls 
bin
boot
dev
...

docker exec -it 69d1 bash
root@xxx:/#

如果从这个stdin中exit,不会导致容器的停止。这就是为什么推荐使用 docker exec的原因

导出和导入容器

导出容器

docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:14.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test

docker export  7691a814370e > ubuntn.tar
#这样导出容器快照到本地文件

导入容器快照

# docker import 从容器快照文件中再导入为镜像
cat ubuntu.tar | docker import - test/ubuntu:v1.0
docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

# 指定URL或者某个目录来录入
docker import http://example.com/exampleimage.tgz example/imagerepo

# 用户既可以使用 docker load 来导入镜像存储文件到本地镜像库,也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者区别在于容器快照文件将丢弃所有历史记录和元数据信息(即保存容器当时的快照状态),而镜像存储文件将保存完整记录,体积也要大,此外,从容器快照文件导入时可以重新制定标签等元数据信息

删除

docker container rm haha
haha

删除一个运行中的容器,可以添加 -f,Docker会发送 SIGKILL信号给容器

清理所有处于终止状态的容器:

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命令将它下载到本地

# centos为关键词
docker search centos
NAME                                            DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
centos                                          The official build of CentOS.                   465       [OK]
tianon/centos                                   CentOS 5 and 6, created using rinse instea...   28
blalor/centos                                   Bare-bones base CentOS 6.5 image                6                    [OK]
saltstack/centos-6-minimal                                                                      6                    [OK]
tutum/centos-6.4                                DEPRECATED. Use tutum/centos:6.4 instead. ...   5                    [OK]
# 可以看到返回包含关键字的镜像,其中包括镜像名字、描述、收藏数(表示该镜像的受关注程度)、是否官方创建、是否自动创建。官方的镜像说明是官方项目组创建和维护的,automated资源允许用户验证镜像的来源和内容。根据是否官方提供,资源镜像可以分为两种:
# 一种是类centor这样的镜像,被称为基础镜像或根镜像。这些基础镜像由Docker公司创建、验证、支持、提供。这样的镜像往往使用单个名词作为名字
# 还有一种类型,比如tianon/centos镜像,由Docker用户创建并维护的,往往带有用户名称前缀,可以通过前缀username/来指定某个用户名提供的镜像,比如tianon用户


查找的时候通过 --filter=stars=N参数可以指定仅显示收藏数量为N以上的镜像

推送镜像

用户登录后通过docker push推送自己的镜像到Docker Hub。

# username替换为Docker 账户用户名
docker tag ubuntu:17.10 username/ubuntu:17.10

docker image ls
REPOSITORY                                               TAG                    IMAGE ID            CREATED             SIZE
ubuntu                                                   17.10                  275d79972a86        6 days ago          94.6MB
username/ubuntu                                          17.10                  275d79972a86        6 days ago          94.6MB

docker push username/ubuntu:17.10

docker search username
NAME                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
username/ubuntu

自动创建

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镜像来运行

docker run -d -p 5000:5000 --restart=always --name registry registry

这将使用官方的registry镜像来启动私有仓库,默认情况下,仓库会被创建在容器的 /var/lib/registry目录下,可以通过-v参数指定镜像文件存在本地的指定路径。例如下面的例子将上传的镜像放到本地的/opt/data/registry

docker run -d \
	-p 5000:5000
	-v /opt/data/registry:/var/lib/registry \
	registry

上传、搜索、下载

创建好私有仓库后,可以使用docker tag来标记一个镜像,然后将它推送到仓库,例如私有仓库地址为 127.0.0.1:5000,现在本机已有的镜像

$ docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB

docker tag将 ubuntu:latest镜像标记为 127.0.0.1:5000/ubuntu/:latest,格式为:

docker tag IMAGE[:TAG] [REGISTRT_HOST[:REGISTRY_PROT]/]REGISTORY[:TAG]

docker tag ubuntu:latest 127.0.0.1:5000/ubuntu:latest
docker image ls
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
127.0.0.1:5000/ubuntu:latest      latest              ba5877dc9bec        6 weeks ago         192.7 MB

docker push上传标记的镜像

docker push 127.0.0.1:5000/ubtuntu:latest
The push refers to repository [127.0.0.1:5000/ubuntu]
373a30c24545: Pushed
a9148f5200b0: Pushed
cdd3de0940ab: Pushed
fc56279bbb33: Pushed
b38367233d37: Pushed
2aebd096e0e2: Pushed
latest: digest: sha256:fe4277621f10b5026266932ddf760f5a756d2facd505a94d2da12f4f52f71f5a size: 1568

curl查看仓库中的镜像

curl 127.0.0.1:5000/v2/_catalog
{"repositories":["ubuntu"]}

删除已有的镜像从私有仓库下载镜像

docker image rm 127.0.0.1:5000/ubuntu:latest

docker pull 127.0.0.1:5000/ubuntu:latest
Pulling repository 127.0.0.1:5000/ubuntu:latest
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete

$ docker image ls
REPOSITORY                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
127.0.0.1:5000/ubuntu:latest       latest              ba5877dc9bec        6 weeks ago         192.7 MB


注意事项

如果不想使用 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 中增加如下内容:

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com --insecure-registries=192.168.199.100:5000"

重新启动服务。

$ sudo service docker restart

Ubuntu 16.04+, Debian 8+, centos 7

对于使用 systemd 的系统,请在 /etc/docker/daemon.json 中写入如下内容(如果文件不存在新建该文件)

{
  "registry-mirror": [
    "https://registry.docker-cn.com"
  ],
  "insecure-registries": [
    "192.168.199.100:5000"
  ]
}

注意:该文件必须符合 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根证书请求文件

openssl req \
		-new -key "root-ca.key" \
		-out "root-ca.ssr" -sha256 \ 
		-subj '/C=CN/ST/Shanxi/L=Datong/O=Your Company Name Name/CN=Your Company Name Docker Registry CA'
		
# -subj参数里面的/C代表国家,/ST表示省,/L表示城市或者地区,/O表示组织名,/CN通用名称

第三步创建CA根证书新建root-ca.cnf

[root_ca]
basicConstrains = critical,CA:TRUE,pathlen:1
keyUsage = critical,nonRepudiation,cRLSign,keyCertSign
subjectKetIdentifier=hash

第四步签发根证书

openssl x509 -req -days 3650 -in "root-ca.csr" \
			 -signkey "root-ca.key" -sha256 -out "root-ca.crt" \
			 -extfile "root-ca.cnf" -extensions \
			 root_ca

第五步生成站点SSL私钥

openssl genrsa -out "docker.domain.com.key" 4096

第六步使用私钥生成证书请求文件

openssl req -new -key "docker.domain.docm.key" -out "site.csr" -sha256 \
			-subject '/C=CN/ST=ShanXi/L=Datong/O=Your Company Name/CN=docker.domain.com'

第七步配置证书,新建site.cnf文件

[server]
authorityKeyIdentifier=keyid,issuer
basicConstraints = critilcal,CA:FALSE
extendedKeyUsage=serverAuth
KeyUsage = critical,digitalSignature,keyEncipherment
subjectAltName = DNS:docker.domain.com,IP:127.0.0.1
subjectKeyIdentifier=hash

第八步签署站点SSL证书

openssl x509 -req -days 750 -in "site.csr" -sha256 \
		-CA "root-ca.crt" -CAkey "root-ca.key" -CAcreateserial \
		-out "docker.domain.com.crt" -extfile "site.cnf" -extensions server

这样就拥有了docker.domain.com的网站SSL私钥docker.domain.com.key和SSL证书docker.domain.com.crt以及CA根证书root-ca.crt

新建ssl文件夹将docker.domain.com.keydocker.domain.com.crtroot-ca.crt这三个文件移入,删除其他文件

配置私有仓库

私有仓库默认的配置文件位于 /etc/docker/registry/config.yml,现在本地编辑 config.yml,之后挂载到容器中

version: 0.1
log: 
	accesslog:
		disabled: true
	level: debug
	formatter: text
	fields:
		service: registry
		environment: staging
storage: 
	delete:
		enabled: true
	cache: 
		blobdescriptor: inmemory
	filesystem:
		rootdirectory: /var/lib/registry
auth:
	htpasswd:
		realm:basic-realm
		path: /etc/docker/registry/auth/nginx.htpasswd
http:
	addr: :443
	host: http://docker.domain.com
	headers:
		X-Content-Type-Options: [nosniff]
	http2:
		disabled: false
	tls:
		certificate: /etc/docker/registry/ssl/docker.domain.com.crt
		key: /etc/docker/registry/ssl/docker.domain.com.key
health:
	storagedrive: true
	interval: 10s
threshold: 3

生成http认证文件

mkdir auth
docker run --rm \
	--entrypoint htpasswd \
	registry \
	-Bbn username password > auth/nginx.htpasswd
# username password对应用户名和密码

编辑 docker-compose.yml

version: '3'

services:
	registry:
		image: registry
		ports:
			- "443:443"
		volumes:
			- ./:/etc/docker/registry
			- registry-data:/var/lib/registry
volumes:
	registry-data:

修改hosts

编辑 /etc/hosts

127.0.0.1 docker.domain.com

启动

docker-compose up -d

这样就搭建好了一个具有权限认证、TLS的私有仓库。

由于自行签发的CA根证书不被系统信任,需要将CA根证书 ssl/root-ca.crt移入 etc/docker/certs.d/docker.domain.com文件夹中

 sudo mkdir -p /etc/docker/certs.d/docker.domain.com
 sudo cp ssl/root-ca.crt /etc/docker/certs.d/docker.domain.com/ca.crt

关键在于退出登录,尝试推送镜像

docker logout docker.domain.com
docker push docker.domain.com/username/ubuntu:18.04
no basic auth credentials

数据管理

数据卷Volumes

数据卷是一个可供一个或者多个容器使用的特殊目录,它绕过UFS,可以提供很多有用的特征:

  • 数据卷可以在容器之间共享和重用
  • 对数据卷的修改会立马生效
  • 对数据卷的更新不会影响镜像
  • 数据卷默认一直存在,即使容器被删除

数据的使用,类似于Linux下对目录或者文件进行mount,镜像中的被指定为挂载点的目录中的文件会隐藏掉,能显示看的是挂载的数据卷

创建一个数据卷

docker volume create my-vol
# 查看所有数据卷
docker volume ls
local my-vol
# 在主机里面使用以下命令可以查看指定数据卷的信息
docker volume inspect my-vol
[
	{
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"		
	}
]

启动一个挂载数据卷的容器

# 用 docker run命令的时候,使用 --mount标记将数据卷挂载容器里。在一次docker run 中可以挂载多个数据卷
# 创建一个名为web的容器,并加载一个数据卷到容器的/webapp没有来
docker run -d - p \
	--name web \
	# -v my-vol:/wepapp \
	--mount source=my-vol,target=/webapp \
	training/webapp \
	python app.py

查看数据卷的具体信息

主机里面使用以下命令可以查看web容器的信息

docker inspect web
# 数据卷信息在Mounts Keys下面
"Mounts": [
    {
        "Type": "volume",
        "Name": "my-vol",
        "Source": "/var/lib/docker/volumes/my-vol/_data",
        "Destination": "/app",
        "Driver": "local",
        "Mode": "",
        "RW": true,
        "Propagation": ""
    }
],

删除数据卷

docker volume rm my-vol

数据卷是被设计用来持久化数据的,生命周期独立于容器,Docker不会在容器被删除后自动删除容器,并且也不会存在垃圾回收这样的机制来处理没有任何容器引用的数据卷。如果需要在删除容器的同时移除数据卷,可以再删除容器的时候使用 docker rm -v的命令

无主的数据卷可能会占据很多空间,清理可以使用:

docker volume prune

挂载主机目录Bind mounts

挂载一个主机目录作为数据卷

# 使用 --mount标记可以指定挂载一个本地主机的目录到容器中去
docker run -d -P \
	--name web \
	# -v /src/webapp:/opt/webapp \
	--mout type=bind,source=/src/webapp,target=/opt/webapp \
	training/webapp \
	python app.py

上面的命令加载主机的 /src/webapp目录到容器的 /opt/webapp目录,这个功能在进行测试比较方便,比如用户可以放置一些程序到本地目录中,来查看容器是否正常工作。本地目录的路径必须是绝对路径,使用 -v参数时如果本地目录不存在Docker会自动创建一个文件夹,使用 --mount采纳数如果本地目录不存在,Docker会报错

Docker挂载主机目录默认权限是读写,用户可以通过增加 readonly指定为只读

docker run -d -p \
	--name web \
	# -v /src/webapp:/opt/webapp:ro \
	--mount type=bind,source=/src/webapp,target=/opt/webapp,readonly \
	training/webapp \
	python app.py
# 加了 readonly之后就挂载为只读,在容器内 /opt/webapp目录创建文件,会显示错误
/opt/webapp # touch new.txt
touch: new.txt: Read-only file system

查看数据卷的具体信息

在主机里查看web容器的信息

docker inspect web 
# 挂载主机目录的配置信息在Mounts Key下面
"Mounts": [
    {
        "Type": "bind",
        "Source": "/src/webapp",
        "Destination": "/opt/webapp",
        "Mode": "",
        "RW": true,
        "Propagation": "rprivate"
    }
],

挂载一个本地主机文件作为数据卷

--mount标记也可以从主机挂载单个文件到容器中

docker run --rm -it \
	# -v $HOME/.bash_history:/root/.bash_history \
	--mount type=bind,source=$HOME/.bash_history,target=/root/.bash_history \
	ubuntu:17.10 \
	bash
	
root@2affd44b4667:/# history
1  ls
2  diskutil list 

#这样可以记录在容器输入过的命令

使用网络

外部访问容器

容器中可以运行一些网络应用,让外部也可以访问,可以通过 -P/p参数来指定端口映射

当使用-P,Docker会随机映射一个 49000~49900的端口到内部容器开放的端口

docker run -d -p training/webapp python app.py
docker container ls -l
CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES
bc533791f3f5  training/webapp:latest  python app.py 5 seconds ago  Up 2 seconds  0.0.0.0:49155->5000/tcp  nostalgic_morse
# 本地主机的49155被映射到容器的5000端口,此时访问本机的49155端口既可以访问容器内web应用提供的界面
# 可以通过docker logs命令查看应用的信息
docker logs -f nostalgic_morss
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p则可以指定要映射的端口,并且在一个指定端口上只可以绑定一个容器。支持的格式为:

ip:hostPort:containerPort|ip::containerPort|hostPort:containerPort

# 映射所有接口地址:使用 hostPort:containerPort格式本地的5000端口映射到容器的5000端口,此时会默认绑定本地所有接口上的所有地址
docker run -d -p 5000:5000 training/webapp python app.py

#映射到指定地址的指定端口:ip:hostPort:containerPort映射使用一个特定地址,比如localhost地址127.0.0.1
docker run -d -p 127.0.0.1::5000 training/webapp python app.py
# 使用udp来指定udp端口
docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

# 查看映射端口配置:docker port,还可以查到绑定的地址
docker port nostalgic_morse 5000
127.0.0.1:49155 .

# 容器有自己的内部网络和ip地址,-p标记可以多次使用来绑定多个端口
docker run -d \
	-p 5000:5000 \
	-p 3000:80
	training/webapp \
	python app.py

容器互联

随着Docker网络的完善,将容器加入自定义Docker网络来连接多个容器的做法优于使用 --link参数

新建网络

# 创建一个新的Docker网络,-d参数指定Docker网络类型,有bridge overlay,其中overlay网络类型适用于Swarm mode
docker network create -d bridge my-net

链接容器

# 运行一个容器并连接到新建的my net网络
docker run -it --rm --name busybox1 --network my-net busybox sh
# 打开新的终端,再运行一个容器加入到my-net网络
docker run -it --name busybox2 --newwork my-net busybox sh

# 再打开一个新的终端查看容器信息
docker container ls

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
b47060aca56b        busybox             "sh"                11 minutes ago      Up 11 minutes                           busybox2
8720575823ec        busybox             "sh"                16 minutes ago      Up 16 minutes                           busybox1

#通过 ping来证明busybox1容器和busybox2容器建立了互联关系,在busybox1容器输入以下命令
/ # ping busybox2
PING busybox2 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.072 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.118 ms

# 用ping来测试连接busybox2容器,会被解析成172.19.0.3,同理busybox2容器执行 ping busybox1,也会成功连接到
/ # ping busybox1
PING busybox1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.064 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.143 ms

# 这样busybox1和busybox2容器就建立互联关系

如果有多个容器之间需要互相连接,推荐使用Docker compose

配置DNS

Docker利用虚拟文件来挂载容器的3个相关配置文件

在容器中使用 mount命令可以看到挂载信息。

这种机制可以让宿主主机DNS信息发生更新后,所有Docker容器的DNS配置通过 /etc/resolv.conf文件立刻得到更新。

配置全部容器的DNS,可以在/etc/docker/daemon.json文件中增加以下内容来设置

{
	"dns":[
		"114.114.114.114",
		"8.8.8.8"
	]
}

这样每次启动容器DNS自动配置为114.114.114.1148.8.8.8。证明已经生效:

docker run -it --rm ubuntu:18.04 cat etc/resolv.conf

nameserver 114.114.114.114
nameserver 8.8.8.8

如果用户想要手动指定容器的配置,可以在使用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

# dockerTest/app.py
from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 该页面已被访问 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)


Dockerfile

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

docker-compose.yml

version: '3'
services:

  web:
    build: .
    ports:
     - "5000:5000"
     
  redis:
    image: "redis:alpine"


运行

docker-compose up

此时访问本地5000端口,每次刷新页面,计数就会增加1.

如果想要后台运行加 -d

命令说明

命令对象与格式

对于Compose来说,大部分命令的对象既可以是项目本身,也可以指定为项目中的服务或者容器。如果没有特别的说明,命令对象将是项目,这意味着项目中所有服务都会受到命令影响。

docker-compose [COMMAND] --help或者 docker-compose help [COMMAND]可以查看具体某个命令的使用格式,基本的使用格式是:

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参数来指定发送信号:

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...]

在指定服务上执行一个命令

docker-compose run ubuntu ping docker.com

将会启动一个ubuntu服务容器,并执行ping docker.com命令

默认情况下,如果存在关联,则所有关联的服务将会自动被启动,除非这些服务已经在运行中

该命令类似启动容器后执行指定的命令,相关卷、链接等等都会按照配置自动创建

两个不同点:给定命令将会覆盖原有的自动运行命令,不会自动创建端口,以避免冲突

如果不希望自动启动关联的容器,可以使用 --no-deps选项,例如

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的参数来设置数量。例如

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格式

version: "3"

services:
	webapp:
		image: example/web
		ports:
			- "80:80"
		volumes:
			- "/data"

每个服务都必须通过image指令指定镜像或build(需要Dockerfile)指令等来自动构建生成镜像

如果使用build命令,在Dockerfile中设置的选项,例如CMD/EXPOSE/VOLUME/ENV等将会被自动获取,无需在docker-compose.yml中再次设置。

build

指定Dockerfile所在文件夹的路径(可以是绝对路径,或者相对docker-compose.yml文件路径),Compose将会利用它自动构建这个镜像,然后使用这个镜像

version: '3'
services:
	webapps:
		build: ./dir

也可以使用context指令指定Dockerfile所在文件夹的路径,dockerfile指令指定Dockerfile文件名,arg指令指定构建镜像时的变量

version: '3'
services:
	webapp:
		build:
			context: ./dir
			dockerfile: Dockerfile-alternate
			args:
				buildno: 1

使用cache_from指定构建镜像的缓存

build:
	context: .
	cache_from:
		- alpine:latest
		- corp/web_app:3.14

cap_add,cap_drop

指定容器的内核能力(capacity)分配

# 让容器拥有所有能力
cap_add:
	- ALL
# 去掉 NET_ADMIN能力
cap_drop:
	- NET_ADMIN

configs、deploy

仅用于Swarm mode

cgroup_parent

指定父cgroup组,意味着将继承改组的资源限制

# 创建一个cgroup组名称为cgroups_1
cgroup_parent: cgroups_1

container_name

指定容器名称。默认使用项目名称_服务名称_序号

container_name:docker-web-container
# 指定容器名称后,该服务将无法进行扩展(scale),因此Docker不会允许多个容器具有相同的名称

devices

指定设备映射关系

devices:
	- "/dev/ttyUSB1:/dev/ttyUSB0"

depends_on

解决容器的依赖、启动先后问题。

# 先启动redis db 再启动web
version: '3'

services:
	web:
		build: .
		depends_on:
			- db
			- redis
	redis:
		image: redis
	db:
		image:postgres
# web服务不会等待redis db完全启动之后才启动

dns

自定义DNS服务器,可以是一个值,也可以是一个列表

dns: 8.8.8.8
dns:
	- 8.8.8.8
	- 114.114.114.114

配置DNS搜索域,可以是一个值,也可以是一个列表

dns_search:example.com

dns_search:
	- domain1.example.com
	- domain2.example.com

tmpfs

挂载一个tmpfs文件系统到容器

tmpfs: /run
tmpfs:
	- /run
	- /tmp

env_file

从文件中获取环境变量,可以为单独的文件路径或列表

通过 docker-compose -f FILE方式来指定Compose模板文件,则 env_file中变量的路径会基于模板文件路径。如果有变量名称与environment指令冲突,则按照惯例,以后者为准

env_file: .env

env_file:
	- ./common.env
	- ./apps/web.env
	- /opt/secrets.env

环境变量文件中每一行必须符合格式,支持#开头的注释行

# common.env: Set development environment
FROG_ENV=development

environment

设置环境变量,可以使用数组和字典两种格式

只给定名称的变量会自动获取运行Compose主机上对应变量的值,可以用阿里防止泄露不必要的数据

environment:
	RACK_ENV: development
	SESSION_SECRET:


environment:
	- RACK_ENV=development
	- SESSION_SECRET

如果变量名称或者值中用到true|false,yse|no等表示布尔含义的词汇,最好放到引号中,避免YAML自动解析某些内容为对应的布尔语义,这些特定词汇,包括

y|Y|Yes|YES|n|N|no|NO|true|True|TRUE|false|False|FALSE|on|ON|off|Off|OFF

expose

暴露端口,但不映射到宿主机,只被连接的服务访问。仅可以指定内部端口为参数

expose:
	- "3000"
	- "8000"

链接到docker-compose.yml外部的容器,甚至并非Compose管理的外部容器,不建议使用

external_links:
	- redis_1
	- project_db_1:mysql
	- project_db_1:postgresql

extra_hosts

类似Docker中的--add-host参数,指定额外的host名称映射信息

extra_hosts:
	- "googledns:8.8.8.8"
	- "dockerhub:52.1.157.61"

会在启动后的服务容器中 /etc/hosts文件中添加如下两条条目

8.8.8.8 googledns
52.1.157.61 dockerhub

healthcheck

通过命令检查容器是否健康运行

healthcheck:
	test: ["CMD","curl","-f","http://localhost"]
	interval: 1m30s
	timeout: 10s
	retries: 3

image

指定为镜像名称或者镜像ID,如果镜像在本地不存在,Compose将会尝试拉取这个镜像

image: ubuntu
image: orchardup/postgresql
image: a4bc65fd

labels

为容器添加Docker元数据(metadata)信息,可以为容器添加辅助说明信息

labels:
	com.startupteam.description: "webapp for a startup team"
	com.startupteam.department: "depvops department"
	com.startupteam.release: "rc3 for v1.0"

不推荐使用该指令

logging

配置日志选项

loggin:
	driver: syslog
	options:
		syslog-address: "tcp://192.168.0.42:123"

# 目前支持三种日志驱动类型
driver: "json-file"
driver: "syslog"
driver: "none"

# options配置日志驱动的相关参数
options:
	max-size: "200k"
	max-file: "10"

network_mode

设置网络模式,使用和 docker run 的 network参数一样的值

network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"

networks

配置容器连接的网络

version: "3"
services:
	some-service:
		networks:
			- some-network
			- other-network

networks:
	some-network:
	other-network:

pid

跟主机系统共享进程命名空间,打开该选项的容器之间,以及容器和宿主机系统之间可以通过进程ID来互相访问和操作

pid: "host"

ports

暴露端口信息

使用宿主端口:容器端口(HOST:CONTAINER)格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以

ports:
 - "3000"
 - "8000:8000"
 - "49100:22"
 - "127.0.0.1:8001:8001"

# 当使用 HOST:CONTAINER格式映射端口时,使用的容器端口小于60并且没有放到引号里,可能会得到错误结果,因为YAML会自动解析xx:yy,这种数字格式为60进制,为避免出现这个问题,建议数字串都采用引号包括起来的字符串格式

secrets

存储敏感数据,例如mysql服务密码

version: "3.1"
services:

mysql:
	image: mysql
	environment:
		MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
	secrets:
	- db_root_password
	- my_other_secret
	
secrets:
	my_secret:
		file: ./my_secret.txt
	my_other_secret:
		external: true		

security_opt

指定容器模板标签label机制的默认属性(用户、角色、类型、级别等)。例如配置标签的用户名和角色名

security_opt:
	- label:user:USER
	- label:role:ROLE

stop_signal

设置另一个信号来停止容器。在默认情况下使用的是SIGTERM停止容器

stop_signal: SIGUSR1

sysctls

配置容器内核参数

sysctls:
	net.core.somaxconn: 1024
	net.ipv4.tcp_syncookies: 0
	
sysctls:
	- net.core.somaxconn=1024
	- net.ipv4.tcp_syncookies=0

ulimits

指定容器的ulimits限制值

例如,指定最大容器的进程数为65535.指定文件句柄数为20000(软限制,应用可以随时修改,不能超过硬限制)和40000(系统硬限制,只能root用户提高)

ulimits:
 nproc: 65535
 nofile:
 	soft: 20000
 	hard: 40000

volumes

数据卷所挂载路径设置,可以设置宿主主机路径(HOST:CONTAINER)或加上访问模式(HOST:CONTAINER:ro)

该指令中路径支持相对路径

volumes:
 - /var/lib/mysql
 - cache/:tmp/cache
 - ~/configs:/etc/configs/:ro

其他指令

domainname,entrypoint,hostname,ipc,mac_address,privileged,read_only,shm_size,restart,stdin_open,tty,user,wor等指令,基本跟 docker run中对应参数的功能一致

# 指定服务容器启动后执行的入口文件
entrypoint: /code/entrypoint.sh

# 指定容器中运行应用的用户名
user: nginx

# 指定容器中工作目录
working_dir: /code

# 指定容器中搜索域名、主机名、mac地址等
domainname: your_website.com
hostname: test
mac_address: 08-00-27-00-0C-0A

# 允许容器中运行一些特权命令
privileged: true

# 指定容器退出后的重启策略为始终重启。该命令对保持服务始终运行十分有效,在生产环境中推荐配置为always或者unless-stopped
restart: always

# 只读模式挂载容器的 root 文件系统,意味着不能对容器内容进行修改
read_only: true

# 打开标准输入,可以接受外部输入
stdin_open: true

# 模拟一个伪终端
tty: true

读取变量

Compose模板文件支持动态读取主机的系统环境变量和当前目录下的 .env文件中的变量

例如,下面的Compose文件将从运行它的环境中读取变量 ${MONGO_VERSION}的值,并写入执行的指令中

verison: '3'
services:

db: 
	image: "mongo:${MONGO_VERSION}"

如果执行 MONGO_VERSION=3.2 docker-compose up 则会启动一个 mongo:3.2 镜像的容器;如果执行 MONGO_VERSION=2.8 docker-compose up 则会启动一个 mongo:2.8 镜像的容器。

若当前目录存在 .env 文件,执行 docker-compose 命令时将从该文件中读取变量。

在当前目录新建 .env 文件并写入以下内容。

# 支持 # 号注释
MONGO_VERSION=3.6

执行 docker-compose up 则会启动一个 mongo:3.6 镜像的容器。

posted @ 2022-09-19 00:55  Scok  阅读(307086)  评论(0编辑  收藏  举报