docker基本概念
1. Image Definition
镜像 Image
就是一堆只读层 read-only layer
的统一视角。
对于某个镜像Image
实例,可能由多个只读层构成,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层都能够在主机的文件系统上访问到。Docker使用的文件系统为统一文件系统 union file system
,该技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件即 Image
实例,如右图视角的形式。
你可以在你的主机文件系统上找到有关这些层的文件。需要注意的是,在一个运行中的容器内部,这些层是不可见的。在linux系统中,镜像文件存在于/var/lib/docker/aufs
目录下。
$ sudo tree -L 1 /var/lib/docker/
/var/lib/docker/
├── aufs
├── containers
├── graph
├── init
├── linkgraph.db
├── repositories-aufs
├── tmp
├── trust
└── volumes
7 directories, 2 files
2. Container Definition
容器container
的定义和镜像image
几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。
容器的定义并没有提及容器是否在运行,没错,这是故意的。
要点:
容器 = 镜像 + 可读层
。并且容器的定义并没有提及是否要运行容器。
接下来,我们将会讨论运行态容器。
3. Running Container Definition
一个运行态容器running container
被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。
文件系统隔离技术使得Docker成为了一个前途无量的技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层(read-write layer)。
我们可以通过运行以下命令来验证我们上面所说的:
docker run ubuntu touch happiness.txt
即便是这个ubuntu容器不再运行,我们依旧能够在主机的文件系统上找到这个新文件。
find / -name happiness.txt
/var/lib/docker/aufs/diff/860a7b...889/happiness.txt
4. Image Layer Definition
为了将零星的数据整合起来,我们提出了镜像层image layer
这个概念。下面的这张图描述了一个镜像层,通过图片我们能够发现一个层并不仅仅包含文件系统的改变,它还能包含了其他重要信息。
元数据metadata
就是关于这个层的额外信息,它不仅能够让Docker获取运行和构建时的信息,还包括父层的层次信息。需要注意,只读层和读写层都包含元数据。
除此之外,每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于最底层。
Metadata Location:
我发现在我自己的主机上,镜像层(image layer)的元数据被保存在名为”json”的文件中,比如说:
/var/lib/docker/graph/e809f156dc985.../json
e809f156dc985...就是这层的id
一个容器的元数据好像是被分成了很多文件,但或多或少能够在/var/lib/docker/containers/<id>
目录下找到,<id>
就是一个可读层的id。这个目录下的文件大多是运行时的数据,比如说网络,日志等等。
5. docker create <image-id>
docker create
命令为指定的镜像image
添加了一个可读层,构成了一个新的容器。注意,这个容器并没有运行。
6. docker start <container-id>
Docker start
命令为容器文件系统创建了一个进程隔离空间。注意,每一个容器只能够有一个进程隔离空间。
7. docker run <image-id>
看到这个命令,读者通常会有一个疑问:docker start
和 docker run
命令有什么区别。
从图片可以看出,docker run
命令先是利用镜像创建了一个容器,然后运行这个容器。这个命令非常的方便,并且隐藏了两个命令的细节,但从另一方面来看,这容易让用户产生误解。
题外话:继续我们之前有关于Git的话题,
docker run
命令类似于git pull
命令。git pull
命令就是git fetch
和git merge
两个命令的组合,同样的,docker run
就是docker create
和docker start
两个命令的组合。
8. docker ps
docker ps
命令会列出所有运行中的容器。这隐藏了非运行态容器的存在,如果想要找出这些容器,我们需要使用下面这个命令。
docker ps –a
docker ps –a
命令会列出所有的容器,不管是运行的,还是停止的。
9. docker images
docker images
命令会列出了所有顶层top-level
镜像。实际上,在这里我们没有办法区分一个镜像和一个只读层,所以我们提出了top-level
镜像。只有创建容器时使用的镜像或者是直接pull下来的镜像能被称为顶层top-level
镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。
docker images –a
docker images –a
命令列出了所有的镜像,也可以说是列出了所有的可读层。如果你想要查看某一个image-id
下的所有层,可以使用docker history
来查看。
10. docker stop <container-id>
docker stop
命令会向运行中的容器发送一个SIGTERM
的信号,然后停止所有的进程。
11. docker kill <container-id>
docker kill
命令向所有运行在容器中的进程发送了一个不友好的SIGKILL信号。
12. docker pause <container-id>
docker stop
和docker kill
命令会发送UNIX的信号给运行中的进程,docker pause
命令则不一样,它利用了cgroups的特性将运行中的进程空间暂停。具体的内部原理你可以在这里找到:https://www.kernel.org/doc/Doc ... m.txt,但是这种方式的不足之处在于发送一个SIGTSTP
信号对于进程来说不够简单易懂,以至于不能够让所有进程暂停。
13. docker rm <container-id>
docker rm
命令会移除构成容器的可读写层。注意,这个命令只能对非运行态容器执行。
docker rmi <image-id>
docker rmi
命令会移除构成镜像的一个只读层。你只能够使用docker rmi
来移除最顶层top level layer
(也可以说是镜像),你也可以使用-f参数来强制删除中间的只读层。
14. docker commit <container-id>
docker commit
命令将容器的可读写层转换为一个只读层,这样就把一个容器转换成了不可变的镜像。
15. docker build
docker build
命令非常有趣,它会反复的执行多个命令。
我们从上图可以看到,build
命令根据Dockerfile
文件中的FROM
指令获取到镜像,然后重复执行:
- 1)run(create和start)
- 2)修改
- 3)commit
在循环中的每一步都会生成一个新的层,因此许多新的层会被创建。
16. docker exec <running-container-id>
docker exec
命令会在运行中的容器执行一个新进程。
17. docker inspect <container-id>
or <image-id>
docker inspect
命令会提取出容器或者镜像最顶层的元数据。
18. docker save <image-id>
docker save
命令会创建一个镜像的压缩文件,这个文件能够在另外一个主机的Docker
上使用。和export
命令不同,这个命令为每一个层都保存了它们的元数据。这个命令只能对镜像生效。
19. docker export <container-id>
docker export
命令创建一个tar
文件,并且移除了元数据和不必要的层,将多个层整合成了一个层,只保存了当前统一视角看到的内容(译者注:expoxt后的容器再import到Docker中,通过docker images –tree命令只能看到一个镜像;而save后的镜像则不同,它能够看到这个镜像的历史镜像)。
20. docker history <image-id>
docker history
命令递归地输出指定镜像的历史镜像。
21. 删除所有终止的容器
docker rm $(docker ps -a -q)
docker基本命令
1. 查看docker信息(version、info)
# 查看docker版本
$docker version
# 显示docker系统的信息
$docker info
2. 对image的操作(search、pull、images、rmi、history)
# 检索image
$docker search image_name
# 下载image
$docker pull image_name
# 列出镜像列表;
# -a, --all=false Show all images; --no-trunc=false Don't truncate output; -q, --quiet=false Only show numeric IDs
$docker images
# 删除一个或者多个镜像;
# -f, --force=false Force; --no-prune=false Do not delete untagged parents
$docker rmi image_name
# 显示一个镜像的历史;
# --no-trunc=false Don't truncate output; -q, --quiet=false Only show numeric IDs
$docker history image_name
3. 启动容器(run)
docker容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。你需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。
# 在容器中运行"echo"命令,输出"hello word"
$docker run image_name echo "hello word"
# 交互式进入容器中
$docker run -i -t image_name /bin/bash
# 在容器中安装新的程序
$docker run image_name apt-get install -y app_name
Note: 在执行apt-get 命令的时候,要带上-y参数。如果不指定-y参数的话,apt-get命令会进入交互模式,需要用户输入命令来进行确认,但在docker环境中是无法响应这种交互的。apt-get 命令执行完毕之后,容器就会停止,但对容器的改动不会丢失。
4. 查看容器(ps)
# 列出当前所有正在运行的container
$docker ps
# 列出所有的container
$docker ps -a
# 列出最近一次启动的container
$docker ps -l
5. 保存对容器的修改(commit)
当你对某一个容器做了修改之后(通过在容器中运行某一个命令),可以把对容器的修改保存下来,这样下次可以从保存后的最新状态运行该容器。
# 保存对容器的修改; -a, --author="" Author; -m, --message="" Commit message
$docker commit ID new_image_name
Note:image相当于类,container相当于实例,不过可以动态给实例安装新软件,然后把这个container用commit命令固化成一个image。
6. 对容器的操作(rm、stop、start、kill、logs、diff、top、cp、restart、attach)
# 删除所有容器
$docker rm `docker ps -a -q`
# 删除单个容器; -f, --force=false; -l, --link=false Remove the specified link and not the underlying container; -v, --volumes=false Remove the volumes associated to the container
$docker rm Name/ID
# 停止、启动、杀死一个容器
$docker stop Name/ID
$docker start Name/ID
$docker kill Name/ID
# 从一个容器中取日志;
# -f, --follow=false Follow log output; -t, --timestamps=false Show timestamps
$docker logs Name/ID
# 列出一个容器里面被改变的文件或者目录,list列表会显示出三种事件,A 增加的,D 删除的,C 被改变的
$docker diff Name/ID
# 显示一个运行的容器里面的进程信息
$docker top Name/ID
# 从容器里面拷贝文件/目录到本地一个路径
$docker cp Name:/container_path to_path
$docker cp ID:/container_path to_path
# 重启一个正在运行的容器;
# -t, --time=10 Number of seconds to try to stop for before killing the container, Default=10
$docker restart Name/ID
# 附加到一个运行的容器上面;
# --no-stdin=false Do not attach stdin; --sig-proxy=true Proxify all received signal to the process
$docker attach ID
Note: attach命令允许你查看或者影响一个运行的容器。你可以在同一时间attach同一个容器。你也可以从一个容器中脱离出来,是从CTRL-C。
7. 保存和加载镜像(save、load)
当需要把一台机器上的镜像迁移到另一台机器的时候,需要保存镜像与加载镜像。
# 保存镜像到一个tar包; -o, --output="" Write to an file
$docker save image_name -o file_path
# 加载一个tar包格式的镜像; -i, --input="" Read from a tar archive file
$docker load -i file_path
# 机器a
$docker save image_name > /home/save.tar
# 使用scp将save.tar拷到机器b上,然后:
$docker load < /home/save.tar
8、 登录registry server(login)
# 登陆registry server; -e, --email="" Email; -p, --password="" Password; -u, --username="" Username
$docker login
9. 发布image(push)
# 发布docker镜像
$docker push new_image_name
10. 根据Dockerfile 构建出一个容器
# build
# --no-cache=false Do not use cache when building the image
# -q, --quiet=false Suppress the verbose output generated by the containers
# --rm=true Remove intermediate containers after a successful build
# -t, --tag="" Repository name (and optionally a tag) to be applied to the resulting image in case of success
$docker build -t image_name Dockerfile_path
11. 修改镜像名称
docker tag server:latest myname/server:latest
or
docker tag d583c3ac45fd myname/server:latest
Docker Problem
Dockerfile 的问题
虽然 Dockerfile
简化了镜像构建的过程,并且把这个过程可以进行版本控制,但是不正当的 Dockerfile
使用也会导致很多问题:
-
docker
镜像太大:如果你经常使用镜像或者构建镜像,一定会遇到那种很大的镜像,甚至有些能达到 2G 以上 -
docker
镜像的构建时间过长:每个build
都会耗费很长时间,对于需要经常构建镜像(比如单元测试)的地方这可能是个大问题 - 重复劳动:多次镜像构建之间大部分内容都是完全一样而且重复的,但是每次都要做一遍,浪费时间和资源
Dockerfile 和镜像构建
Dockerfile
是由一个个指令组成的,每个指令都对应着最终镜像的一层。每行的第一个单词就是命令,后面所有的字符串是这个命令的参数,关于 Dockerfile
支持的命令以及它们的用法,可以参考 官方文档 ,这里不再赘述。
当运行 docker build 命令的时候,整个的构建过程是这样的:
- 读取
Dockerfile
文件发送到docker daemon
- 读取当前目录的所有文件(context),发送到
docker daemon
- 对
Dockerfile
进行解析,处理成命令加上对应参数的结构 - 按照顺序循环遍历所有的命令,对每个命令调用对应的处理函数进行处理
- 每个命令(除了 FROM)都会在一个容器执行,执行的结果会生成一个新的镜像
- 为最后生成的镜像打上标签
1. 使用统一的 base 镜像
有些文章讲优化镜像会提倡使用尽量小的基础镜像,比如 busybox 或者 alpine 等。我更推荐使用统一的大家比较熟悉的基础镜像,比如 ubuntu,centos 等,因为基础镜像只需要下载一次可以共享,并不会造成太多的存储空间浪费。它的好处是这些镜像的生态比较完整,方便我们安装软件,除了问题进行调试。
2. 动静分离
经常变化的内容和基本不会变化的内容要分开,把不怎么变化的内容放在下层,创建出来不同基础镜像供上层使用。比如可以创建各种语言的基础镜像,python2.7、python3.4、go1.7、java7等等,这些镜像包含了最基本的语言库,每个组可以在上面继续构建应用级别的镜像。
3. 最小原则:只安装必需的东西
为了降低复杂性、减少依赖、减小文件大小、节约构建时间,你应该避免安装任何不必要的包,不要仅仅为了“锦上添花”而安装某个包。因为镜像的扩展很容易,而且运行容器的时候也很方便地对其进行修改。这样可以保证镜像尽可能小,构建的时候尽可能快,也保证未来的更快传输、更省网络资源。例如,不要在数据库镜像中包含一个文本编辑器。
4. 一个原则:每个镜像只有一个功能
不要在容器里运行多个不同功能的进程,每个镜像中只安装一个应用的软件包和文件,需要交互的程序通过 pod(kubernetes 提供的特性) 或者容器之间的网络进行交流。这样可以保证模块化,不同的应用可以分开维护和升级,也能减小单个镜像的大小。
5. 使用更少的层
虽然看起来把不同的命令尽量分开来,写在多个命令中容易阅读和理解。但是这样会导致出现太多的镜像层,而不好管理和分析镜像,而且镜像的层是有限的。尽量把相关的内容放到同一个层,使用换行符进行分割,这样可以进一步减小镜像大小,并且方便查看镜像历史。
6. 减少每层的内容
尽管只安装必须的内容,在这个过程中也可能会产生额外的内容或者临时文件,我们要尽量让每层安装的东西保持最小。
- 比如使用 --no-install-recommends 参数告诉 apt-get 不要安装推荐的软件包
- 安装完软件包,清楚 /var/lib/apt/list/ 缓存
- 删除中间文件:比如下载的压缩包
- 删除临时文件:如果命令产生了临时文件,也要及时删除
- 使用
.dockerignore
文件:创建一个.dockerignore
文件来指定要忽略的文件和目录。.dockerignore
文件的排除模式语法和Git
的.gitignore
文件类似。
7. 不要在 Dockerfile 中修改文件的权限
因为 docker 镜像是分层的,任何修改都会新增一个层,修改文件或者目录权限也是如此。如果修改大文件或者目录的权限,会把这些文件复制一份,这样很容易导致镜像很大。
解决方案也很简单,要么在添加到 Dockerfile 之前就把文件的权限和用户设置好,要么在容器启动脚本(entrypoint)做这些修改。
8. 利用 cache 来加快构建速度
在镜像的构建过程中,Docker
会遍历 Dockerfile
文件中的指令,然后按顺序执行。在执行每条指令之前,Docker
都会在缓存中查找是否已经存在可重用的镜像,如果有就使用现存的镜像,不再重复创建。如果你不想在构建过程中使用缓存,你可以在 docker build
命令中使用--no-cache=true
选项。
不过从 1.10 版本开始,Content Addressable Storage 的引入导致缓存功能的失效,目前引入了 --cache-from 参数可以手动指定一个镜像来使用它的缓存。
但是,如果你想在构建的过程中使用缓存,你得明白什么时候会,什么时候不会找到匹配的镜像。Docker 遵循的基本规则如下:
- 从一个基础镜像开始(FROM 指令指定),下一条指令将和该基础镜像的所有子镜像进行匹配,检查这些子镜像被创建时使用的指令是否和被检查的指令完全一样。如果不是,则缓存失效。
- 在大多数情况下,只需要简单地对比
Dockerfile
中的指令和子镜像。然而,有些指令需要更多的检查和解释。 - 对于
ADD
和COPY
指令,镜像中对应文件的内容也会被检查,每个文件都会计算出一个校验和。文件的最后修改时间和最后访问时间不会纳入校验。在缓存的查找过程中,会将这些校验和和已存在镜像中的文件校验和进行对比。如果文件有任何改变,比如内容和元数据,缓存失效。 - 除了
ADD
和COPY
指令,缓存匹配过程不会查看临时容器中的文件来决定缓存是否匹配。例如,当执行完RUN apt-get -y update
指令后,容器中一些文件被更新,但Docker
不会检查这些文件。这种情况下,只有指令字符串本身被用来匹配缓存。
一旦缓存失效,所有后续的Dockerfile
指令都将产生新的镜像,缓存不会被使用。
9. 版本控制和自动构建
最好把 Dockerfile
和对应的应用代码一起放到版本控制中,然后能够自动构建镜像。这样的好处是可以追踪各个版本镜像的内容,方便了解不同镜像有什么区别,对于调试和回滚都有好处。
另外,如果运行镜像的参数或者环境变量很多,也要有对应的文档给予说明,并且文档要随着 Dockerfile
变化而更新,这样任何人都能参考着文档很容易地使用镜像,而不是下载了镜像不知道怎么用。
10. 一个容器只运行一个进程
在大多数情况下,你应该保证在一个容器中只运行一个进程。将多个应用解耦到不同容器中,可以保证应用的横向扩展性和重用容器。如果你一个服务依赖于另一个服务,可以利用容器链接(link)。
11. 将多行参数排序
将多行参数按字母顺序排序(比如要安装多个包时)。这可以帮助你避免重复包含同一个包,更新包列表时也更容易。也便于 PRs 阅读和省察。建议在反斜杠符号\之前添加一个空格,以增加可读性。
下面来自buildpack-deps镜像的例子:
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
12. 不要在 Dockerfile 定义公共端口
如果在 Dockerfile 中定义了公共端口,你就只能运行一个实例。一般来说都是自定义私有端口,然后在运行时使用 -p
参数指定公共端口。
# private and public mapping
EXPOSE 80:8080
# private only
EXPOSE 80
13. 在定义 CMD
和 ENTRYPOINT
时使用数组
定义定义 CMD
和 ENTRYPOINT
时可以使用下面2种方法。但是如果使用方法1的时候,docker 会在前面自动加上 /bin/sh -c
,这样就会导致某个非预期的结果。因此尽量使用方法2也就是数组方式。
CMD /bin/echo
# or
CMD ["/bin/echo"]
Dockerfile 指令
下面针对 Dockerfile 中各种指令的最佳编写方式给出建议。
FROM
只要有可能,请使用当前官方仓库作为构建你镜像的基础。我们推荐使用 Debian image
,因为它被严格控制并保持最小尺寸(当前小于 150 mb),但仍然是一个完整的发行版。
LABEL
你可以给镜像添加标签来帮助组织镜像、记录许可信息、辅助自动化构建,或者因为其他的原因。每个标签一行,由 LABEL
开头加上一个或多个标签对。下面的示例展示了各种不同的可能格式。注释内容是解释。
注意:如果你的字符串中包含空格,将字符串放入引号中或者对空格使用转义。如果字符串内容本身就包含引号,必须对引号使用转义。
# Set one or more individual labels
LABEL com.example.version="0.0.1-beta"
LABEL vendor="ACME Incorporated"
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""
# Set multiple labels on one line
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
# Set multiple labels at once, using line-continuation characters to break long lines
LABEL vendor=ACME\ Incorporated \
com.example.is-beta= \
com.example.is-production="" \
com.example.version="0.0.1-beta" \
com.example.release-date="2015-02-12"
RUN
一如往常,保持你的 Dockerfile
文件更具可读性,可理解性,以及可维护性,将长的或复杂的RUN声明用反斜杠分割成多行。
apt-get
也许 RUN
指令最常见的用例是安装包用的 apt-get
。因为 RUN apt-get
指令会安装包,所以有几个问题需要注意。
不要使用RUN apt-get upgrade
或dist-upgrade
,因为许多基础镜像中的“必须”包不会在一个非特权容器中升级。如果基础镜像中的某个包过时了,你应该联系它的维护者。如果你确定某个特定的包,比如 foo
,需要升级,使用apt-get install -y foo
就行,该指令会自动升级 foo
包。
永远将RUN apt-get update
和apt-get install
组合成一条RUN
声明,例如:
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo
将apt-get update
放在一条单独的RUN
声明中会导致缓存问题以及后续的apt-get install
失败。比如,假设你有一个 Dockerfile
文件:
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl
构建镜像后,所有的层都在 Docker
的缓存中。假设你后来又修改了其中的apt-get install
,添加了一个包:
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx
Docker
发现修改后的RUN apt-get update
指令和之前的完全一样。所以,apt-get update
不会执行,而是使用之前的缓存镜像。因为apt-get update
没有运行,后面的apt-get install
可能安装的是过时的curl
和nginx
版本。
使用RUN apt-get update && apt-get install -y
可以确保你的 Dockerfiles
每次安装的都是包的最新的版本,而且这个过程不需要进一步的编码或额外干预。这项技术叫作 cache busting
。你也可以显示指定一个包的版本号来达到 cache-busting
。这就是所谓的固定版本,例如:
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.*
固定版本会迫使构建过程检索特定的版本,而不管缓存中有什么。这项技术也可以减少因所需包中未预料到的变化而导致的失败。
下面是一个RUN
指令的示例模板,展示了所有关于apt-get
的建议。
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*
其中s3cmd指令指定了一个版本号1.1.0*。如果之前的镜像使用的是更旧的版本,指定新的版本会导致apt-get udpate
缓存失效并确保安装的是新版本。
另外,清理掉 apt
缓存,删除var/lib/apt/lists
可以减小镜像大小。因为RUN
指令的开头为apt-get udpate
,包缓存总是会在apt-get install
之前刷新。
注意:官方的
Debian
和Ubuntu
镜像会自动运行apt-get clean
,所以不需要显示的调用apt-get clean
。
CMD
CMD
指令用于执行目标镜像中包含的软件,可以包含参数。 CMD
大多数情况下都应该以 CMD ["executable", "param1", "param2"…]
的形式使用。因此,如果创建镜像的目的是为了部署某个服务(比如 Apache、Rails…),你可能会执行类似于CMD ["apache2","-DFOREGROUND"]
形式的命令。实际上,我们建议任何服务镜像都使用这种形式的命令。
多数情况下, CMD
都需要一个交互式的 shell
(bash,Python,perl,etc),例如,CMD ["perl","-de0"]
,CMD ["php","-a"]
。使用这种形式意味着,当你执行类似docker run -it python
时,你会进入一个准备好的 shell
中。 CMD
应该在极少的情况下才能以CMD ["param","param"]
的形式与 ENTRYPOINT
协同使用,除非你和你的预期用户都对 ENTRYPOINT
的工作方式十分熟悉。
EXPOSE
EXPOSE
指令用于指定容器将要监听连接的端口。因此,你应该为你的应用程序使用常见熟知的端口。例如,提供 Apache web
服务的镜像将使用 EXPOSE 80
,而提供 MongoDB
服务的镜像使用 EXPOSE 27017
,等等。
对于外部访问,镜像用户可以在执行docker run
时使用一个标志来指示如何将指定的端口映射到所选择的端口。对于容器 链接,Docker
提供环境变量从接收容器回溯到源容器(例如,MYSQL_PORT_3306_TCP)。
ENV
为了便于新程序运行,你可以使用ENV来为容器中安装的程序更新PATH环境变量。例如,ENV PATH /usr/local/nginx/bin:$PATH
将确保CMD ["nginx"]
能正确运行。
ENV
指令也可用于为你想要容器化的服务提供必要的环境变量,比如 Postgres
需要的 PGDATA
。
最后, ENV
也能用于设置常见的版本号,以便维护 version bumps
,参考下面的示例:
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
类似于程序中的常量(与硬编码的值相对),这种方法可以让你只需改变单条 ENV
指令来自动改变容器中的软件版本。
ADD 和 COPY
虽然ADD
和COPY
功能类似,但一般优先使用COPY
。因为它比ADD
更透明。COPY
只支持简单将本地文件拷贝到容器中,而ADD
有一些并不明显的功能(比如本地 tar 提取和远程 URL 支持)。因此,ADD
的最佳用例是将本地 tar
文件自动提取到镜像中,例如ADD rootfs.tar.xz
。
如果你的 Dockerfiles
有多个步骤需要使用上下文中不同的文件。单独COPY
每个文件,而不是一次性COPY
完。这将保证每个步骤的构建缓存只在特定的文件变化时失效。
例如:
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
如果将COPY . /tmp/
放置在RUN
指令之前,只要.目录
中任何一个文件变化,都会导致后续指令的缓存失效。
为了让镜像尽量小,最好不要使用ADD
指令从远程 URL
获取包,而是使用curl
和wget
。这样你可以在文件提取完之后删掉不再需要的文件,可以避免在镜像中额外添加一层。(译者注:ADD
指令不能和其他指令合并,所以前者ADD
指令会单独产生一层镜像。而后者可以将获取、提取、安装、删除合并到同一条RUN指令中,只有一层镜像。)比如,你应该尽量避免下面这种用法:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
而是使用下面这种:
RUN mkdir -p /usr/src/things \
&& curl -SL http://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
上面使用的管道操作,所以没有中间文件需要删除。
对于其他不需要ADD
的自动提取(tar
)功能的文件或目录,你应该坚持使用COPY
。
ENTRYPOINT
ENTRYPOINT
的最佳用处是设置镜像的主命令,允许将镜像当成命令本身来运行(用CMD提供默认选项)。
例如,下面的示例镜像提供了命令行工具s3cmd:
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
现在该镜像直接这么运行,显示命令帮助:
$ docker run s3cmd
或者提供正确的参数来执行某个命令:
$ docker run s3cmd ls s3://mybucket
这很有用,因为镜像名还可以当成命令行的参考。
ENTRYPOINT
指令也可以结合一个辅助脚本使用,和前面命令行风格类似,即使启动工具需要不止一个步骤。
例如,Postgres 官方镜像使用下面的脚本作为 ENTRYPOINT
:
#!/bin/bash
set -e
if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"
if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi
exec gosu postgres "$@"
fi
exec "$@"
注意:该脚本使用了 Bash 的内置命令 exec,所以最后运行的进程就是容器的 PID 为1的进程。这样,进程就可以接收到任何发送给容器的 Unix 信号了。
该辅助脚本被拷贝到容器,并在容器启动时通过ENTRYPOINT执行:
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
该脚本可以让用户用几种不同的方式和 Postgres
交互。
你可以很简单地启动 Postgres
:
$ docker run postgres
也可以执行 Postgres
并传递参数:
$ docker run postgres postgres --help
最后,你还可以启动另外一个完全不同的工具,比如 Bash
:
$ docker run --rm -it postgres bash
VOLUME
VOLUME
指令用于暴露任何数据库存储区域,配置文件,或容器创建的文件和目录。强烈建议使用 VOLUME
来管理镜像中的可变部分和镜像用户可以改变部分。
USER
如果某个服务不需要特权执行,建议使用 USER
指令切换到非 root
用户。先在 Dockerfile
中使用类似RUN groupadd -r postgres && useradd -r -g postgres
。 postgres
的指令创建用户和用户组。
注意:在镜像中,用户和用户组每次被分配的
UID/GID
都是不确定的,下次重新构建镜像时被分配到的UID/GID
可能会不一样。如果要依赖确定的UID/GID
,你应该显示的指定一个UID/GID
。
你应该避免使用sudo
,因为它不可预期的 TTY
和信号转发行为可能造成的问题比解决的还多。如果你真的需要和sudo类似的功能(例如,以 root
权限初始化某个守护进程,以非 root
权限执行它),你可以使用gosu
。
最后,为了减少层数和复杂度,避免频繁地使用USER
来回切换用户。
WORKDIR
为了清晰性和可靠性,你应该总是在WORKDIR中使用绝对路径。另外,你应该使用WORKDIR
来替代类似于RUN cd ... && do-something
的指令,后者难以阅读、排错和维护。
ONBUILD
ONBUILD
中的命令会在当前镜像的子镜像构建时执行。可以把ONBUILD
命令当成父镜像的 Dockerfile
传递给子镜像的 Dockerfile
的指令。
在子镜像的构建过程中,
Docker
会在执行Dockerfile
中的任何指令之前,先执行父镜像通过ONBUILD
传递的指令。当从给定镜像构建新镜像时,
ONBUILD
指令很有用。例如,你可能会在一个语言栈镜像中使用ONBUILD
,语言栈镜像用于在Dockerfile
中构建用户使用相应语言编写的任意软件,正如Ruby
的ONBUILD
变体使用
ONBUILD
构建的镜像应用一个单独的标签,例如:ruby:1.9-onbuild
或ruby:2.0-onbuild
。在
ONBUILD
中使用ADD
或COPY
时要格外小心。如果新的构建上下文中缺少对应的资源,onbuild
镜像会灾难性地失败。添加一个单独的标签,允许Dockerfile
的作者做出选择,将有助于缓解这种情况。