Table of Contents

1. 前言

云原生概念风生云起,最近算是非常火热,而其最基本的依仗就在于能将任何程序运行在轻量级Cell上的容器Docker。Docker如果仅只是使用的话,是一点都不难的,但作为一个所有应用程序运行其上的基石,对其的细节了解是非常有必要的。

本文会记录从最基本的Docker基础,到日常的工作操作,到比较深入的原理细节,都会有所涉及。

版本信息如下:

$ docker version
Client: Docker Engine - Community
 Version:           18.09.2
 API version:       1.39
 Go version:        go1.10.8
 Git commit:        6247962
 Built:             Sun Feb 10 04:12:39 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.2
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.6
  Git commit:       6247962
  Built:            Sun Feb 10 04:13:06 2019
  OS/Arch:          linux/amd64
  Experimental:     false

此外,docker info比较长,就不贴这里了,放在下面的资料部分。简单看了下应该是不带敏感信息的,这部分应该是做了剔除的。

2. 安装

MAC下的安装比较简单,直接下载Docker官方的桌面版本即可:Docker Desktop for Mac。文档在:Get started with Docker Desktop for Mac

此外,为了能使用(下载)一些官方(软件公司)发布镜像文件,你需要注册一个docker hub账号(其实也就是docker官方账号):docker hub

3. 镜像仓库

Docker的核心功能点之一就是能将部署这个事情代码化。本来运维的工作中充斥着各种不确定性,版本不兼容、软件包不同、Linux内核不同导致的安装问题,等等。而Docker可以使用Dockerfile将一个镜像的制作(以代码形式)固定下来,保证只要是一份Dockerfile,制作出来的镜像是完全一致的。

此外,只要保证Docker的版本一致,docker镜像的运行状态及结果是可预期的。这就保证了一个软件的研发流程中,只要镜像被制作完成了,后续就可以使用这个镜像文件进行分发了,不再需要每个部署环境都从Dockerfile从头开始制作一份本地的镜像。

而分发这个过程就需要镜像仓库的介入:本地制作镜像 => 上传镜像到仓库 => 部署服务器拉取镜像 => 部署服务器本地运行镜像。

Docker官方有一个开放的镜像仓库,一般知名的第三方软件提供者都会将自己软件的镜像文件发布到这个开放的仓库中,方便第三方使用者下载。当然,私有仓库对于大部分软件公司来说都是必须的。

3.1 创建镜像仓库

制作一个私有的镜像仓库非常简单,直接使用docker命令即可,官方文档在:Docker Registry

运行:

docker run -d -p 5000:5000 --name registry registry:2

这样就在localhost:5000运行了一个镜像仓库。

3.2 login

从镜像仓库获得镜像以及上传镜像都需要有对应的身份认证。使用:

$ docker login --help

Usage:	docker login [OPTIONS] [SERVER]

Log in to a Docker registry

Options:
  -p, --password string   Password
      --password-stdin    Take the password from stdin
  -u, --username string   Username

即便是从官方镜像仓库获取镜像也是需要认证的,所以一开始需要注册一个账号。如果使用的是Docker Desktop的话,可以从UI界面上进行登录。

如果登录的是私有仓库,[SERVER]这块需要输入私有仓库地址。

3.3 push & pull

如果是从私有仓库上进行对应的拉取和推送镜像,需要在镜像名字之前补完仓库地址:

docker push localhost:5000/imagename:major.minor.patch
docker pull localhost:5000/imagename:major.minor.patch

3.4 list tags

docker的命令行工具没有提供查询一个镜像所有tags的命令(ridiculous),可以使用以下方法来查询:

# require tool jq
brew update --verbose
brew install jq --verbose

curl -s https://registry.hub.docker.com/v2/repositories/${repo_name}/tags/?page_size=1024 | jq .
# sample repo_name: elastic/filebeat

4. 日常使用

4.1 docker images

显示所有镜像(本地)

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.10               b977ae81df17        2 weeks ago         73.9MB

4.2 docker rmi

删除镜像,可以同时删除多个,给的名字可以是镜像名也可以是镜像id

$ docker rmi imagename imageid ...

4.3 docker ps -a

可以显示所有的容器(本地),包括不在运行状态的。-a一般来说是必须的,否则无法将停止状态的镜像也列出来。

$ docker ps -a

4.4 docker rm

删除容器,可以同时删除多个,给的名字可以是容器名也可以是容器id

$ docker rm containername containerid ...

4.5 docker build

从一个Dockerfile构建一个镜像,一般都会进入到Dockerfile同一层目录进行构建:docker build [OPTIONS] .,这里的.就是Dockerfile的路径。全选项参数可以通过:docker build --help来获得,这里就不贴了。

细节相当多,有需要的可以看下官方文档:docker build

此外,docker build命令与Dockerfile息息相关,部分优化相关内容描述在Dockerfile内,请去此处查看:5. Dockerfile

4.5.1 镜像命名及tag

通过-t在构建的时候对镜像进行命名,并打上标签。一般来说这是必须的,方便后续进行镜像查找和仓库内巨量镜像的维护。

$ docker build -t imagename:major.minor.patch .

4.5.2 指定Dockerfile

通过-f指定镜像文件路径。

$ docker build -f yourrepo/Dockerfile .

4.5.3 指定全局变量

在构建镜像的时候为了对内部的应用程序提供一些配置,常用环境变量的方式进行注入,而在镜像构建的时候,可以使用--build-args XXX=...这样的方式进行操作。

$ docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 --build-arg FTP_PROXY=http://40.50.60.5:4567 .

如果是裸用Docker的话,这样的操作还算是比较容易理解的,但一般来说大型分布系统肯定还使用了K8S这样的容器管理系统或者还甚至用了类似于Istio这样的服务编排系统,就不需要在制作镜像的时候这么弄了。

4.5.4 指定ulimit

通过--ulimit来指定该镜像文件在运行时候的ulimit。

4.5.5 指定父级cgroup

通过--cgroup-parent来指定该镜像文件在运行时候的资源限制群组。关于cgroup这个话题,后面会专门起一个章节来深入。详见:8.3 资源限制

4.5.6 添加/etc/hosts

通过--add-host=domain.com:10.180.0.1来向/etc/hosts里添加匹配。

4.5.7 构建目标环境

通过--target来指定构建的目标环境:

FROM debian AS build-env
...

FROM alpine AS production-env
...
$ docker build -t mybuildimage --target build-env .

4.5.8 其他

  • --no-cache:在构建镜像的时候不使用cache,在频繁更新某个镜像时很有用,防止cache污染
  • --ssh:SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|[=|[,]])

资源限制相关(会在后面的章节深入)

  • --cpu-period:Limit the CPU CFS (Completely Fair Scheduler) period
  • --cpu-quota:Limit the CPU CFS (Completely Fair Scheduler) quota
  • --cpu-shares:CPU shares (relative weight)
  • --cpuset-cpus:CPUs in which to allow execution (0-3, 0,1)
  • --cpuset-mems:MEMs in which to allow execution (0-3, 0,1)
  • --memory:Memory limit
  • --memory-swap:Swap limit equal to memory plus swap: ‘-1’ to enable unlimited swap

详见:8.3 资源限制

4.6 docker run

将镜像运行成容器的命令,虽然是一个很简单的命令,但细节相当多,用起来其实还蛮麻烦的。全选项参数可以通过:docker run --help来获得,这里就不贴了。中文的帖子也有,可以看了参考:Docker run 命令参数及使用

细节相当多,有需要的可以看下官方文档:docker run

命令的基本使用格式:

$ docker run [OPTIONS] IMAGE[:TAG\|@DIGEST] [COMMAND] [ARG...]

4.6.1 后台进程

通过-d来让docker run命令运行起来的容器转为后台常驻进程。

$ docker run -d -p 80:80 my_image service nginx start

4.6.2 指定名称

通过--name来让docker run命名运行起来的容器,后续可以使用这个名字来访问这个启动的容器,不再需要docker ps -a查找这个容器的UUID。在使用同一个镜像启动多个容器的时候特别有用。

$ docker run --name myconname -d ubuntu:18.10

4.6.3 网络设置

网络这块相关的配置内容也非常多,细节的内容建议直接阅读官方的文档:docker run > Network settings

--network=...用来控制网络模式:

  • none:No networking in the container.
  • bridge (default):Connect the container to the bridge via veth interfaces.
  • host:Use the host’s network stack inside the container.
  • container:<name\|id>:Use the network stack of another container, specified via its name or id.
  • NETWORK:Connects the container to a user created network (using docker network create command)

4.6.4 添加/etc/hosts

参数同docker build命令,但很奇怪的是docker官方给出的例子这里倒是没有=

$ docker run -it --add-host domain.com:10.180.0.1 ubuntu cat /etc/hosts

4.6.5 重启策略

通过--restart可以让docker run启动的容器在退出之后按策略重启。官方文档在:docker run > Restart policies (–restart)

  • no:Do not automatically restart the container when it exits. This is the default.
  • on-failure[:max-retries]:Restart only if the container exits with a non-zero exit status. Optionally, limit the number of restart retries the Docker daemon attempts.
  • always:Always restart the container regardless of the exit status. When you specify always, the Docker daemon will try to restart the container indefinitely. The container will also always start on daemon startup, regardless of the current state of the container.
  • unless-stopped:Always restart the container regardless of the exit status, including on daemon startup, except if the container was put into a stopped state before the Docker daemon was stopped.

该选项与--rm是冲突的。

4.6.6 退出代码 Exit Status

接上面的重试策略,on-failure会检查退出代码。官方文档在:docker run > Exit Status

4.6.7 退出清理

通过--rm可以让docker run启动的容器在退出之后删除所有的留存信息。如果没有加这个参数的话,在容器退出、停止之后使用docker ps -a可以找到刚才启动的容器。这对于测试来说是很麻烦的,每次停止之后还要使用docker rm命令删除。这时候就可以通过--rm来命令容器退出后自动清理:

# not cleanup
$ docker run -i -t ubuntu:18.10 ps afx
$ docker ps -a

# cleanup
$ docker run -i -t --rm ubuntu:18.10 ps afx
$ docker ps -a

4.6.8 安全选项

参见官方文档:docker run > Security configuration

4.6.9 权限与Linux Capabilities

参见官方文档:docker run > Runtime privilege and Linux capabilities。后续详见:8.3 资源限制

4.6.10 日志输出

通过--log-driver容器可以指定和docker daemon不同的日志输出设备。参见官方文档:Logging drivers (–log-driver)

4.6.11 覆盖Dockerfile设置

主要是以下几项:

  • CMD (Default Command or Options)
  • ENTRYPOINT (Default Command to Execute at Runtime)
  • EXPOSE (Incoming Ports)
  • ENV (Environment Variables)
  • HEALTHCHECK
  • VOLUME (Shared Filesystems)
  • USER
  • WORKDIR

细节参见官方文档:Overriding Dockerfile image defaults

4.6.12 其他

命令测试

使用如下命令可以启动容器之后执行命令进行测试,并在退出之后自动清理刚才生成的容器。

$ docker run --rm -i -t $image_name:$version $command

隔离与共享

  • –pid
  • –uts
  • –ipc

容器之间拥有自己的PID命名空间,并相互隔离。通过--pid=...可以让容器之间共享进程。具体的例子可以直接阅读官方的文档:PID settings (–pid)

简单的例子可以尝试(看下输出的内容有什么不同):

# without pid
$ docker run -i -t --rm ubuntu:18.10 ps afx
...

# with pid
$ docker run -i -t --rm --pid=host ubuntu:18.10 ps afx
...

--uts--ipc也是类似的,可以参考官方文档里的:UTS settings (–uts)IPC settings (–ipc)。中文资料可以看:Docker run参考(5) – UTS(–uts)和IPC (–ipc)设置

资源限制相关(会在后面的章节深入)

参见官方文档:docker run > Runtime constraints on resources。后续详见:8.3 资源限制

4.7 docker attach

4.7.1 attach

将本地的输入输出及错误流附到一个运行中的容器上。简单来说可以理解为:进入到容器的命令行中。

如果需要在一个容器启动之后连上去的话,需要容器在启动(docker run)的时候指定:-i -t参数:

$ docker run -i -t -d --rm ubuntu:18.10

这样就可以在之后使用docker attach命令附到该容器上并打开命令行了:

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS
80172e3d5574        ubuntu:18.10        "/bin/bash"         38 seconds ago      Up 37 seconds

$ docker attach 80172e3d5574
[email protected]:/#

4.7.2 detach

新手常犯的一个错误就是使用exit命令退出附上的shell,结果就是非但退出了shell还把容器本身给stop了。正确的做法是在shell里ctrl+p然后ctrl+q,这里切记是先按p的组合键,然后按q的组合键,而不是同时ctrl+p+q

$ docker attach f269e680ae14
[email protected]:/# 
# ctrl+p & ctrl+q
[email protected]:/# read escape sequence
$ 

资料:

4.8 docker exec

在一个已经处于运行中状态的容器里执行一条命令。

一般来说,如果是有连续工作的话,还是会使用docker attach附上去之后再操作。但如果仅只是一两句命令,或自动化的脚本,就需要使用到docker exec了。这里需要注意,exec只能用在running的容器上,如果没有容器且需要执行命令测试的话,也可以使用docker run命令,直接附带上需要执行的命令即可。

也有有趣的用法:

docker exec -it e8c541f9fb33 /bin/bash

这条语句的效果和docker attach是一致的。

4.9 tips

  • 做开发的时候可以把镜像构建多包几层,一层层做成镜像存在本地。后面的build可以直接from前面的,节约docker build失败之后的重新测试时间。否则中间一个步骤出错,很多耗时很长的源码编译工作就要从头来一次,太伤
  • node.js docker里的npm安装记得要加上--unsafe-perm,具体可以看:grpc/grpc-node#604

5. Dockerfile

Dockerfile是用来进行镜像构建的文本文件,也是docker能将构建整个过程转化为文本固定下来的关键,可以说是docker能如此风靡的核心功能点也不为过。

官方的文档:

下面行文不会过于注重Dockerfile如何编写的语法,因为这东西你看看手册和几个例子也就会了。主要还是关注在几个比较麻烦的概念上。

几点零碎的:

  • Dockerfile里类似COPYEXPOSE之类的,都被称为指令(instruction),这个单词可以了解下
  • .dockerignore file
  • 因为在build命令执行之前,当前指定PATH的context会被发送给docker daemon,因此文件夹越小执行越效率

5.1 Dockerfile指令

5.1.1 RUN

官方文档:RUN

运行指令,有两种语法模式:

  • RUN (shell form, the command is run in a shell, which by default is /bin/sh -c on Linux or cmd /S /C on Windows)
  • RUN [“executable”, “param1”, “param2”] (exec form)

这里需要注意exec模式并不是在shell里运行的:

Note: Unlike the shell form, the exec form does not invoke a command shell. This means that normal shell processing does not happen. For example, RUN [ “echo”, “$HOME” ] will not do variable substitution on $HOME. If you want shell processing then either use the shell form or execute a shell directly, for example: RUN [ “sh”, “-c”, “echo $HOME” ]. When using the exec form and executing a shell directly, as in the case for the shell form, it is the shell that is doing the environment variable expansion, not docker.

建议是使用exec模式,细节可以看下面一个小节。

5.1.2 CMD & ENTRYPOINT

官方文档:

这两个东西可以放在一起说,因为他们做的事情基本上是类似的。两者都是在容器启动之后执行一个命令,CMD提供的是在镜像启动后会默认执行的一个命令,而ENTRYPOINT则是在镜像启动后提供一个程序入口。

区别在于:

  • CMD:提供的默认执行命令可以在docker run $imagename $command后面接命令进行覆盖,这样操作的话写在Dockerfile CMD里的命令就不会执行了,被替代了;CMD在一个Dockerfile里只能有一个生效,如果编写了多个则只有最后的那个会被执行
  • ENTRYPOINT:提供的是镜像的功能入口,不会被docker run $imagename $command中的命令替换掉,除非指定--entrypoint=...进行替换;当然ENTRYPOINT也只允许有一个

一般来说通常的做法是:ENTRYPOINT提供容器运行之后程序入口,而CMD则提供供给给程序入口的参数,方便后续在docker run的时候进行替换。这两者以这样的方式进行协同工作。

官方文档里也有这部分内容:Understand how CMD and ENTRYPOINT interact

  • Dockerfile should specify at least one of CMD or ENTRYPOINT commands.
  • ENTRYPOINT should be defined when using the container as an executable.
  • CMD should be used as a way of defining default arguments for an ENTRYPOINT command or for executing an ad-hoc command in a container.
  • CMD will be overridden when running the container with alternative arguments.

这两个指令和5.2 RUN一样,都有多种模式可以选择,一般来说最常使用的是直接写命令的shell模式以及使用JSON格式编写的exec模式。最佳实践是:在所有的情况下都使用exec模式。原因如下:

  • shell模式的PID 1进程是/bin/sh,后续的信号不能很好传递到真正执行的命令上
  • shell模式依赖/bin/sh,但某些微型镜像不一定有

Best practices for writing Dockerfiles > ENTRYPOINT

Configure app as PID 1

This script uses the exec Bash command so that the final running application becomes the container’s PID 1. This allows the application to receive any Unix signals sent to the container. For more, see the ENTRYPOINT reference.

CMD指令还多一种模式:CMD ["param1","param2"] (as default parameters to ENTRYPOINT),这也是刚才提到的参数提供者角色的做法。

e.g

FROM ubuntu:trusty
ENTRYPOINT ["/bin/ping","-c","3"]
CMD ["localhost"]

$ docker run ping
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.025 ms
...

$ docker run ping docker.io
PING docker.io (162.242.195.84) 56(84) bytes of data.
64 bytes from 162.242.195.84: icmp_seq=1 ttl=61 time=76.7 ms
...

资料:

有时间的话,官方的ENTRYPOINT文档可以通读一下:Dockerfile > ENTRYPOINT,里面信息量不小。

5.1.3 VOLUME

官方文档:VOLUME

卷加载相关在docker里是一个比较麻烦的概念,这里不作展开,只列出文档里提到的注意点:

  • Changing the volume from within the Dockerfile: If any build steps change the data within the volume after it has been declared, those changes will be discarded.
  • The host directory is declared at container run-time: The host directory (the mountpoint) is, by its nature, host-dependent. This is to preserve image portability, since a given host directory can’t be guaranteed to be available on all hosts. For this reason, you can’t mount a host directory from within the Dockerfile. The VOLUME instruction does not support specifying a host-dir parameter. You must specify the mountpoint when you create or run the container.

更多的深入理解可以查看:9.2 存储

5.1.4 ARG

官方文档:ARG

除了用户定义的变量之外,docker还有一部分预定义的变量,可以通过docker build --build-arg name=value来加入:Predefined ARGs

  • HTTP_PROXY
  • http_proxy
  • HTTPS_PROXY
  • https_proxy
  • FTP_PROXY
  • ftp_proxy
  • NO_PROXY
  • no_proxy

5.1.5 HEALTHCHECK

官方文档:HEALTHCHECK

类似于客户端服务器保持连接的心跳检查的概念,这个指令是用来检查当前的容器其提供的服务是否正常的。

用法:HEALTHCHECK [OPTIONS] CMD command。CMD之前的选项有:

  • –interval=DURATION (default: 30s)
  • –timeout=DURATION (default: 30s)
  • –start-period=DURATION (default: 0s)
  • –retries=N (default: 3)

范例,每5分钟检查一次WEB服务器是否正常工作(3秒内能响应请求):

HEALTHCHECK --interval=5m --timeout=3s \
  CMD curl -f http://localhost/ || exit 1

退出代码:

  • 0: success - the container is healthy and ready for use
  • 1: unhealthy - the container is not working correctly
  • 2: reserved - do not use this exit code

同样细节比较多,可以仔细阅读下官方文档:Dockerfile > HEALTHCHECK

资料:

5.1.6 其他指令

有相当多的指令在本文中并没有展开,可以查看其官方文档:

5.2 最佳实践

5.2.1 缩小镜像构建的Context

官方文档:Understand build context

简单理解就是保证运行docker build命令的根工作文件夹里的内容尽量少,最好只保有构建镜像必须的文件,能显著加速镜像构建的速度(减少Context传送需要花费的时间)。

举个例子:

FROM alpine:3.9.2
ENTRYPOINT ["/bin/sh"]
$ mkdir ~/Downloads/docker && cd ~/Downloads/docker
$ docker build -f Dockerfile -t empty_img:0.0.1 .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine:3.9.2
 ---> 5cb3aa00f899
Step 2/2 : ENTRYPOINT ["/bin/sh"]
 ---> Running in 6190582d6894
Removing intermediate container 6190582d6894
 ---> bd0e931e4706
Successfully built bd0e931e4706
Successfully tagged empty_img:0.0.1

然后拷贝点垃圾文件到~/Downloads/docker,再执行一次:

$ docker build -f Dockerfile --no-cache -t dummy_img:0.0.1 .

Sending build context to Docker daemon  630.1MB
Step 1/2 : FROM alpine:3.9.2
 ---> 5cb3aa00f899
Step 2/2 : ENTRYPOINT ["/bin/sh"]
 ---> Running in 0b6ebe8953ca
Removing intermediate container 0b6ebe8953ca
 ---> 681e135d49b6
Successfully built 681e135d49b6
Successfully tagged dummy_img:0.0.1

Sending build context to Docker daemon 630.1MB这一步花了很长时间(因为我拷贝过去的是630MB很零碎的小文件)。如果Context再大点,到百千GB级别的话,那影响更大。

对于构建出来的镜像大小是没有任何影响的:

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
dummy_img           0.0.1               681e135d49b6        40 seconds ago      5.53MB
empty_img           0.0.1               bd0e931e4706        8 minutes ago       5.53MB
alpine              3.9.2               5cb3aa00f899        3 weeks ago         5.53MB

5.2.2 使用多阶段构建

官方文档:Use multi-stage builds

一般来说总是希望镜像的体积越小越好,有利于传输也有利于减小容器的资源占用。而在镜像构建的时候,总会有很多中间产物。以go语言来举例,go语言最后编译产生的可执行文件是自包含的,对于外部的类库等都是不作要求的,而在go源码构建成可执行文件的过程中,则会有很多SDK等的环境需求。这就对构建生产两个环境做了不同的定义及隔离要求。而多阶段构建这个功能,就是为了这种需求而服务的。

简单来说就是在同一个Dockerfile里有多个FROM指令,每个FROM指令可以使用as ...这样的语法进行命名,这样的一行语句就定义了一个阶段。后面的阶段可以随意利用之前阶段里产生任何资源。通常的用法就是定义两个FROM即两个阶段,构建阶段和生产阶段。将所有的环境设置等都定义在构建阶段,然后将构建阶段编译产生的二进制文件拷贝到生产环境,这样生产环境就能够做到最小化了。

详细的例子可以直接查看官方文档,里面有一份非常详细的范例,正好就是按go制作的范例。

5.2.3 构建的缓存

官方文档:Leverage build cache

这部分建议通读后理解,因为在docker build的时候,只要不添加--no-cache选项的话,构建就会检查并使用缓存。除非在工作中每次都放弃使用cache(在某些情况下会大大增加构建的时间消耗),否则对于缓存的命中还是有理解的必要。

5.2.4 镜像层的创建

Only the instructions RUN, COPY, ADD create layers. Other instructions create temporary intermediate images, and do not increase the size of the build.

因此对于RUNCOPY以及ADD指令的使用需要非常小心,特别是RUN指令,尽量使用&&将其串起来。

6. Image

镜像文件的很多细节在之前的4.5 docker build以及5. Dockerfile都有提到了,所以讲到这里其实已经没什么很有价值的内容可以讲了。关于Image,有一篇非常不错的文章,只要过了一遍基本上Image本身就没什么神秘的了,可以看下:What’s in a Docker image?

一些额外的资料:

7. Docker Machine

官方文档:Docker Machine Overview

Docker machine是用来辅助Ops对大批量机器进行docker部署时使用的工具。现如今规模化使用容器一般都会使用类似K8S这样的工具,所以这东西了解下就好。

资料:

8. 深入

上面的文章基本上把docker的命令以及一些日常使用的细节都过了一遍,也包含了类似镜像体积等优化内容。后面就会进入一些docker比较底层的东西了,类似于docker的网络模式、docker的磁盘模式等。

8.1 网络

官方文档:docker network overview

docker的网络子系统是可插拔式设计,其中可选的有:

  • bridge:默认的网络驱动。如果不指定一个驱动的话,默认就是bridge模式。bridge模式通常应用在独立运行的容器相互之间需要通讯的应用场景
  • host:移除独立容器和Docker主机之间的网络隔离,并直接使用主机的网络。host模式仅针对高于Docker 17.06版本的swarm services可用
  • overlay:Overlay模式将多个Docker daemon连接起来,并启用swarm service来互通。你也可以使用overlay模式促进swarm service和独立容器之间的沟通,或是两个分别归属于不同Docker daemon的容器。这个策略简化了容器之间的互通,不再依赖于OS级别的路由
  • macvlan:Macvlan模式允许将一个MAC地址交付给一个容器,使得这个容器在你的网络中以一个硬件设备的身份出现。Docker daemon通过MAC地址将通讯路由到容器。macvlan模式通常用来处理需要直接连接到物理网络的遗留应用,而不需要路经Docker主机的网络栈
  • none:将某个容器禁用网络。通常与自定义的网络驱动结合使用。none驱动对swarm service不可用
  • Third-party network plugins:第三方网络插件,可自由安装使用。可以通过Docker Hub来安装,或通过第三方vendor安装

选择建议:

  • User-defined bridge networks:当相同Docker主机内的多个容器需要相互通讯的时候,这是最优解
  • Host networks:当容器的网络不应该与Docker主机隔离,且容器的其他方面应该与Docker主机隔离的情况下,这是最优解
  • Overlay networks:当需要运行在不同的Docker主机上的容器相互之间进行通讯,或当多个容器需要使用swarm service进行协同工作的时候,这是最优解
  • Macvlan networks:当从VM环境迁移到Docker环境或需要让容器看起来像物理主机的时候(每个容器都拥有一个唯一的MAC地址),这是最优解
  • Third-party network plugins:允许你将Docker整合到特殊的网络栈中

通篇过一下之后会发现,基本上常用的场景,只要有bridge驱动就够了。后面会主要看下bridge驱动,其他的可以查看官方文档。

此外,有一篇比较老的文章,用中文举了点例子进行网络模式的说明,可以一读:Docker网络模式

8.1.1 bridge

如果在创建容器的时候不指定网络设置的话,容器会使用默认的bridge驱动。而如果用户进行自定义bridge设置的话,使用的网络驱动就是稍微有点不同:User-defined bridge networks。这里还是有不少区别的:Differences between user-defined bridges and the default bridge

  • User-defined bridges provide better isolation and interoperability between containerized applications.
  • User-defined bridges provide automatic DNS resolution between containers.
  • Containers can be attached and detached from user-defined networks on the fly.
  • Each user-defined network creates a configurable bridge.
  • Linked containers on the default bridge network share environment variables.

自定义bridge驱动使用:

$ docker network create my-net
$ docker network rm my-net

$ docker create --name my-nginx \
  --network my-net \
  --publish 8080:80 \
  nginx:latest

$ docker network connect my-net my-nginx
$ docker network disconnect my-net my-nginx

官方文档里写清楚了,默认的bridge驱动已经属于遗留功能,不建议在生产环境上使用。

The default bridge network is considered a legacy detail of Docker and is not recommended for production use.

见文档:Use the default bridge network

8.1.2 其他

后面几种总的来说应用面都不大,在结合K8S使用的情况下(应该是大部分应用场景),只需要K8S即可。这里都可以略过。

8.2 存储

官方文档:Manage data in Docker

任何在容器内创建的文件都会存储在容器的可写层内。这意味着:

  • 当容器消失的时候这些数据会丢失,且当其他进程需要共享这些数据的时候很难获取到这些数据
  • 容器的可写层是和物理主机紧密关联的,很难轻易移动到其他机器上
  • 向容器的可写层写入数据要求一个存储驱动来管理文件系统。存储驱动是一个洋葱式的文件系统,使用Linux内核。这额外的抽象会降低性能,和直接使用data volumn相比,后者直接向主机的文件系统写入数据

Docker提供了选项来让容器在物理主机上存储文件,这样才能保证容器在停止之后文件仍旧存在:

  • volumes
  • bind mounts
  • tmpfs mount(仅限Linux)

8.2.1 几种类型的mount

  • Volumes:会将数据写入到主机磁盘上的Docker指定地点(/var/lib/docker/volumes/ on Linux),且这些文件是由Docker进程进行管理的,其他任何进程都不应该直接修改这些文件。Volumes是在Docker内存储数据的最佳选择
  • Bind mounts:可将数据写入到主机磁盘上的任何地点,甚至可以是重要的系统文件,或文件夹。非Docker进程及Docker容器都可以在任何时候修改这些文件
  • tmpfs mounts:仅将数据保存在内存中,并永远不会存储到磁盘上

更进一步的各种mount类型细节可以查阅官方文档:More details about mount types。这里做一下整理:

Volumns

  • 一个volume可以被多个容器共享
  • volume在容器都停止之后不会被删除,除非手动操作:docker volume prune
  • volume是可命名的,如果没有被命名,则会被自动附加一个唯一串作为名字
  • volume还可以加载volume drivers,为其提供更丰富的存储功能

Bind mounts

  • 使用bind mounts的时候,一个主机上的文件或文件夹被加载到容器中
  • 文件或文件夹需要完整路径
  • 文件或文件夹不需要已经存在
  • docker cli命令无法直接操作bind mounts资料
  • 官方更推荐命名化的volumes,而不是使用bind mounts

官方还给了最佳使用范例:

几点tips:

  • 如果把一个空的volume(使用docker create创建出来的)绑定到运行中容器的某个已经存在的文件夹上,会导致已经存在文件夹内的资料被拷贝到volume内
  • 如果把一个bind mount或已经有内容的volume绑定到某个运行中容器的某个已经存在的文件夹上,则容器内重合文件夹内的资料会被屏蔽(不会被删除),volume内的资料则可访问

在我看来:

  • volumes应该是最先选择,如果没有特殊需求的话,类似于web服务器这种无状态可以很快横向扩展的就很适合使用volumes
  • bind mounts可以用在其他进程有直接访问文件需求的场景。volumes是docker进程自组织的,对外就是一个block文件,无法看到细节,其实不太友好,在docker之外的进程也需要对文件进行访问的时候就很不方便了,这种场景就需要bind mounts

8.2.2 日常使用命令

$ docker volume create my-vol
$ docker volume ls
$ docker volume inspect my-vol
$ docker volume rm my-vol # delete volume
$ docker volume prune # delete all

8.2.3 Volumes

官方文档:Use volumes

优势:

  • Volumes are easier to back up or migrate than bind mounts.
  • You can manage volumes using Docker CLI commands or the Docker API.
  • Volumes work on both Linux and Windows containers.
  • Volumes can be more safely shared among multiple containers.
  • Volume drivers let you store volumes on remote hosts or cloud providers, to encrypt the contents of volumes, or to add other functionality.
  • New volumes can have their content pre-populated by a container.

可以使用--volume--mount来指定容器运行时需要mount的卷,使用上有点些微的不同:

$ docker run --volume \
    $volume_name:$path_mounted_in_container:$option1,$option2,...

$ docker run --mount \
    type=volume,source|src=$volume_name,destination|dst|target=$path_mounted_in_container,readonly,volume-opt=$key1=$val1,volume-opt=$key2=$val2,...

几点不常用但可能用得到的点:

8.2.4 Bind mounts

官方文档:Use bind mounts

可以使用--volume--mount来指定容器运行时需要mount的卷,使用上有点些微的不同:

$ docker run --volume \
    $path_on_host_tobe_bound:$path_mounted_in_container:$option1,$option2,...

$ docker run --mount \
    type=bind,source|src=$path_on_host_tobe_bound,destination|dst|target=$path_mounted_in_container,readonly,bind-propagation=rprivate|private|rshared|shared|rslave|slave

对bind mounts来说--volume--mount在使用上是有区别的:

  • --volume:指定的主机位置不存在的话,会主动创建出来(总是创建成文件夹)
  • --mount:指定的主机位置不存在的话,不会主动创建,而是报错

举个简单的例子:

$ cd /Users/XXX && mkdir ./docker && cd ./docker && touch ./dummy.txt && echo dummy > ./dummy.txt
$ docker run -it -d --rm -v /Users/XXX/Downloads/docker/:/dirinc --name bind_mount_test ubuntu:18.10
$ docker attach 8d11c4aad023

[email protected]:/# ll /dirinc
...
-rw-r--r-- 1 root root    8 Apr  3 02:23 dummy.txt
[email protected]:/# echo changed > /dirinc/dummy.txt

$ cat /Users/XXX/docker/dummy.txt
changed

几点不常用但可能用得到的点:

8.2.5 Storage drivers

官方文档:Docker storage drivers

这部分里会讲到的storage drivers和之前的volumes是不同的东西,所谓的storage drivers是指docker从镜像生成容器之后,在容器的最上面创建出来的那一层可写入层里,使用的文件处理策略(驱动)。

这部分不会太过展开,主要是因为:

  • 会在容器的可写层里大量产生数据的应用场景一般不会存在,如果存在那一般也是有问题的
  • 在真的需要容器对存储设备进行大量写入操作的情况,需要考虑是否使用volumes,而不是直接在容器的可写入层里进行写操作(两者在性能上也有差距)

因此实际上这块的实用性并不高。后面主要以认识storage drivers为主。

Docker支持的storage drivers:

  • overlay2:首选的驱动,对现行的所有Linux发行版本可用,且无需额外的配置
  • aufs:对 Docker 18.06 及更老旧的版本来说是首选驱动,或 Ubuntu 14.04 kernel 3.13 不支持 overlay2 的环境
  • devicemapper:受到支持,但在生产环境需要direct-lvm,是因为loopback-lvm虽然无需配置,但性能不是很好
  • The btrfs and zfs storage drivers are used if they are the backing filesystem (the filesystem of the host on which Docker is installed). These filesystems allow for advanced options, such as creating “snapshots”, but require more maintenance and setup. Each of these relies on the backing filesystem being configured correctly.
  • The vfs storage driver is intended for testing purposes, and for situations where no copy-on-write filesystem can be used. Performance of this storage driver is poor, and is not generally recommended for production use.

要查看各Linux发行版本支持的驱动,可以查看:Supported storage drivers per Linux distribution

要查看各文件系统支持的驱动,可以查看:Supported backing filesystems

后面还有很多各种驱动的细节,这里就不展开了,给出一些资料:

8.3 资源限制

这块的话题有点大,本质上来说,Docker的资源限制利用的还是Linux本身的机制。即便撇开Docker,这些知识点也是值得一看的,但鉴于主题,这里就不多展开了。后续这个章节的主要目标是将Docker的一些限制手段以及后面的Linux原理相关的思路整理出来,并附上资料,不会过于深入。

实际上在真实场景使用的时候一般也会使用类似K8S这样的系统来进行集群管理,不太会直接在Docker这一层工具上对资源限制这块过多设置。

8.3.1 Linux

Linux中的namespace、cgroup、capabilities等核心概念,可以看下面几篇:

8.3.2 Docker

Docker官方对于资源限制也有不小的篇幅进行解说,在使用上有需要的时候可以查看:

8.3.3 其他

其他资料:

8.4 监控 & Metrics

这块和上面的内容类似也比较鸡肋,一般来说不会直接裸用Docker,如果有K8S之类的,那监控也不会从Docker来了。所以这里就给点资料即可,有需要的可以深入下官方文档:

8.5 安全

官方文档:Docker security

老话重提,里面的知识点仍旧是上面提到过的Linux cgroup、capabilities等几个知识点。

8.7 其他

9. OS镜像选择

9.1 Alpine

技术过硬的话,Alpine应该说是制作生产环境产品镜像的首选了,毕竟体积摆在那边,只有几兆的基本镜像可真的没几个好选的。

一些资料:

DNS issue

DNS问题算是Alpine被诟病得比较多的一点,可以看一个issue:DNS Issue #255。官方在github的文档上也给了说明,可以看下:caveats.md » DNS,当然具体是否有这个情况以及如何解决,就要看实践了。

10. 其他

10.1 什么是docker host

在很多docker的文档里都提到了docker host这个概念。至于什么是docker host,可以参见链接:Clarify what is the host

I think you pretty much found out the answer by yourself. In that diagram you mention a ‘docker host’ is any machine that is running the Docker daemon. This means that you’re either running Docker natively in your operating system, or a virtual machine that has Docker installed.

Docker machine is basically the second option. When you do a docker-machine create it creates a new virtual machine with Docker already installed. This means that to access any containers running on this machine you’ll have to use the IP address of the virtual machine. Also, to give Docker access to a file you’ll either have to copy it to the VM that’s running Docker, or share a directory between your OS and the VM that’s running Docker.

资料

Docker官方

Docker命令

镜像相关

其他

docker info

docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 18.09.2
Storage Driver: overlay2
 Backing Filesystem: extfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 9754871865f7fe2f4e74d43e2fc7ccd237edcbce
runc version: 09c8266bf2fcf9519a651b04ae54c967b9ab86ec
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 4.9.125-linuxkit
Operating System: Docker for Mac
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 1.952GiB
Name: linuxkit-025000000001
ID: CNVU:5KZS:A2M7:WY5W:NUEW:KPW3:WXOA:IH2Q:EBAN:LP7C:3EQR:36U4
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
 File Descriptors: 24
 Goroutines: 50
 System Time: 2019-03-27T06:19:11.1044801Z
 EventsListeners: 2
HTTP Proxy: gateway.docker.internal:3128
HTTPS Proxy: gateway.docker.internal:3129
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine

EOF