这是我学习《Docker —— 从入门到实践》一书的笔记。书的内容可以在 gitbook 上找到。

什么是Docker

Docker是一个开源项目,诞生于2013年初,最初是dotCloud公司内部的一个业余项目,用Go语言实现。

Docker项目的目标是事先轻量级的操作系统虚拟化解决方案。Docker的基础是Linux容器等技术。容器是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是硬件层面的实现。

Docker的优点:

  • 启动时间为秒级。
  • 资源占用少,一台主机可以运行数千个Docker容器。
  • Overhead低。容器除了运行其中应用外,基本不消耗额外的系统资源。

Docker的优势:

  • 更快的交付和部署。一次开发可以在任何地方运行。
  • 更高的运行效率。
  • 更轻松的迁移和扩展。Docker容器可以在几乎所有的平台上运行。
  • 更简单的管理。所有的修改都以增量的方式分发和更新。

Docker分成两部分:Docker engine (daemon) 和可以从命令行访问的客户端工具。Docker engine提供了Docker Remote API (REST),而命令行上的docker命令则是通过访问API来实现操作的。

Docker Image

镜像是一个只读的模板。镜像可以用来创建Docker容器。镜像的ID唯一地标识了镜像。如果两个镜像Tag指向同一个ID则说明它们是一样的。Docker运行容器前需要本地存在对应的镜像。如果不存在则会从镜像仓库下载(默认Docker Hub)。

  • 从仓库获取镜像:docker pull (<registry_url>)/<repo_name>:<tag>。如果Registry被省略,默认的Docker Registry会被使用。在获取一个镜像的同时,所有的依赖层都会被获取。
  • 显示本地镜像:docker images。默认(未指定)的标记为latest。该命令有强大的查找功能,使用--filter或者-f。输出结果可以使用Go的模板语法,例如:docker images --format "{{.ID}}:{{.Repository}}
  • 在仓库中共享镜像:docker push.
  • 显示镜像的历史纪录:docker history <标签>

创建镜像

用户可以在已有镜像基础上进行修改,也可以用本地文件系统创建一个。

修改已有镜像

  • 使用已有镜像启动一个容器,并记住容器的ID。
  • 在容器内修改。结束后使用exit退出。
  • 使用docker commit <容器ID或名字> <仓库名>:<标签>将容器的存储层保存下来成为镜像。

然而,使用docker commit创建的镜像会有很多不必要的文件改动产生,而且改动操作都是黑箱操作。另外,每次更改镜像都会新增一层,这使得镜像更加臃肿。简而言之,该方法不应该被用来创建镜像,而只是用来学习和紧急情况下的现场保存。

利用Dockerfile创建镜像

docker build可以创建一个新的镜像。为此,首先需要创建一个Dockerfile,包含一些如何创建镜像的指令。Dockerfile的每一条指令都创建镜像的一层。创建过程中每一步都创建一个新的容器,在容器中执行指令并commit修改。注意一个镜像不能超过127层。

Dockerfile的基本语法:

  • 使用#来注释。
  • 最开始的FROM指令告诉Docker使用哪个镜像作为基础。它是Dockerfile中必备而且必须是第一条指令。如果不需要基础镜像,则使用scratch来代表空白镜像。
  • 维护者信息:MAINTAINER Baihu Qian <baihuqian@gmail.com>
  • RUN用来执行命令行程序。像shell一样可以使用&&来连接多个命令,用\来换行。
  • shell模式:RUN <命令>。在执行时会被包装成sh -c的参数形式,所以环境变量可以使用。但是要注意该命令必须是前台执行。如果该命令执行结束则容器退出。注意命令不用双引号!
  • exec模式:CMD ["executable", "arg1", "arg2"]。列表务必使用双引号,因为在解析时会被解析为JSON数组。
  • COPY拷贝文件至镜像。源文件必须是基于上下文路径的相对路径,并可以使用符合Go规范的通配符;目标路径可以是容器的绝对路径也可以是相对于工作目录(用WORKDIR指定)的相对路径。使用COPY时文件的所有元数据(权限等)都会保留。
  • ADD是更高级的COPY,可以进行文件下载(源文件是URL时),tar解压缩到目标路径(源文件是tar包时)。ADD应当仅在解压缩时使用,其他场合使用COPY
  • EXPOSE声明外部端口。注意这只是声明,而不是进行具体的端口映射。
  • CMD描述容器启动时运行的程序。该命令可以在启动容器时被重新指定。和RUN一样,CMD有shell和exec两种模式。
  • ENTRYPOINT入口点。同样有shell和exec模式。在指定ENTRYPOINT以后,CMD将变成参数传给ENTRYPOINT的指令,这样无论CMD是什么(运行时指定),必要的初始化都能完成。
  • ENV设置环境变量。ENV <key1> <value1>或者ENV <key1>=<value1> <key2>=<value2>,用\换行。定义后后续的指令中就可以使用该变量。
  • ARG设置构建环境的环境变量。这些变量在由镜像创建的容器中是不存在的。
  • VOLUME ["path1", "path2"]定义匿名卷。这样向容器内这些目录写入数据时,数据将存储在卷中。运行时可以覆盖这些设置。
  • WORKDIR <path>指定工作目录。指定后该层及以上层的工作目录都改为该目录。该目录需要事先存在。
  • USER <user>指定当前用户。和WORKDIR一样,它对当前层和以上层都起作用,而且用户必须事先存在。
  • HEALTHCHECK [option] CMD <cmd>指定如何进行健康检查。如果没有额外的健康检查,容器只能通过判断主进程是否退出来判断异常。HEALTHCHECK NONE屏蔽已有的健康检查。健康检查指令返回0为健康,1为失败。健康检查的输出可以用docker inspect查看。一个Dockerfile只能有一个HEALTHCHECK,若有多个则最后一个生效。选项:
  • --interval:默认为30秒。
  • --timeout:默认为30秒。
  • --retries:默认为3次。
  • ONBUILD后跟其他指令:这些指令将在该镜像被作为基础镜像创建其它镜像时执行。

完成后使用docker build -t="tag" <context_directory>创建镜像。由于Docker的client-server架构,该路径将被打包上传至docker engine,并且可以被docker engine所访问。所以COPY命令的源文件相对路径是相对这个上下文路径来说的,任何在这个路径之外的文件都无法被访问。如果有些文件不想传给docker engine,可以像.gitignore一样写一个.dockerignore

docker build的其它用法:

  • 从git repo创建:docker build <git-url>#:<folder>
  • 从tar包创建:docker build <url-to-tar>。docker会下载,解压缩,并以其作为上下文创建镜像。
  • 从标准输入读取Dockerfile
  • 从标准输入读取压缩文件

docker tag <id> <new_tag> 可以用来修改镜像的标签。

导入和导出镜像

docker save -o <file_name> <tag>将镜像保存到本地文件。

docker load --input <filename>将导出到文件的镜像导入到本地镜像库。

删除本地镜像

docker rmi <tag>删除存在于本地的docker镜像。注意docker rm是删除容器,在删除镜像前要用它删除所有依赖于该镜像的容器。

有的时候本地会有很多没有打过标签的镜像,用下列命令清除它们:

docker rmi $(docker images -q -f "dangling=true")

其中-q是quiet,-f是filter。

镜像的实现原理

Docker镜像由很多层次构成,Docker使用 UnionFS 将不同层结合到一个镜像中。

Unionfs is a filesystem service for Linux, FreeBSD and NetBSD which implements a union mount for other file systems. It allows files and directories of separate file systems, known as branches, to be transparently overlaid, forming a single coherent file system. Contents of directories which have the same path within the merged branches will be seen together in a single merged directory, within the new, virtual filesystem.

When mounting branches, the priority of one branch over the other is specified. So when both branches contain a file with the same name, one gets priority over the other.

The different branches may be either read-only and read-write file systems, so that writes to the virtual, merged copy are directed to a specific real file system. This allows a file system to appear as writable, but without actually allowing writes to change the file system, also known as copy-on-write. This may be desirable when the media is physically read-only, such as in the case of Live CDs.

因此,镜像并非是像ISO一样的打包文件,而是一个虚拟的概念。其具体实现并非由一个文件组成,而是由多层文件系统联合组成。

镜像的每一层都是后一层的基础。每一层构建完毕后就不发生任何改变,后一层上的任何改变都只发生在该层。因此,删除前一层的文件时仅在当前层标记该文件已经删除,而被删除的文件将一直都跟随着这个镜像。所以每一层需要尽量仅包含该层需要的东西。但是,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。

Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。

Docker 目前支持的联合文件系统包括 OverlayFS, AUFS, Btrfs, VFS, ZFSDevice Mapper

各 Linux 发行版 Docker 推荐使用的存储驱动如下表。

Linux 发行版 Docker 推荐使用的存储驱动
Docker CE on Ubuntu overlay2 (Ubuntu 14.04.4 +, 16.04 +)
Docker CE on Debian overlay2 (Debian Stretch), aufs, devicemapper
Docker CE on CentOS overlay2
Docker CE on Fedora overlay2

在可能的情况下,推荐使用 overlay2 存储驱动,overlay2 是目前 Docker 默认的存储驱动,以前则是 aufs。你可以通过配置来使用以上提到的其他类型的存储驱动。

Docker Container

容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。容器之间是相互隔离的,相当于一个简易版的Linux环境和运行在其中的应用程序。

容器的实质是进程。但与直接在宿主机上执行的进程不同,容器进程运行于属于自己的独立命名空间(namespace):

Namespaces are a feature of the Linux kernel that isolates and virtualizes system resources of a collection of processes. Examples of resources that can be virtualized include process IDs, hostnames, user IDs, network access, interprocess communication, and filesystems. Namespaces are a fundamental aspect of containers on Linux.

Linux developers use the term namespace to refer to both the namespace kinds, as well as to specific instances of these kinds.

A Linux system is initialized with a single instance of each namespace type. After initialization, additional namespaces can be created or joined.

所以容器可以拥有自己的root文件系统、网络配置、进程空间、用户ID空间。所以容器内的进程是运行在一个隔离的环境里,使得容器封装的应用比直接在宿主上运行更加安全。

和镜像一样,容器也是分层存储的。容器在启动时会创建一层可写层作为最上层,称为容器存储层。该层的生命周期同容器一样,随着容器的删除而丢失。按照Docker最佳实践的要求,容器不应该向其存储层内写入任何数据,而应保持其无状态化。所有文件的写入操作都应使用数据卷或者绑定宿主目录。这样写入的性能和稳定性更高。

和虚拟机不同的是,容器中的应用都以前台执行,而并没有后台服务的概念。容器就是为了主进程存在的。主进程退出,容器就是去了存在的意义,从而退出。容器的核心是所执行的程序,所需要的资源都是应用程序所必需的。如果在伪终端中用ps或者top查看的话会发现并没有其他的进程信息。

docker ps可以查看所有运行的容器。如果要查看终止的容器可以使用-a

启动容器

容器可以从镜像新建并启动,也可以从Stopped状态重新启动(使用docker start)。由于容器轻量的特性,很多时候容器都是随时删除而后重新创建的。

docker run -t -i <tag> <command>从镜像新建并启动一个容器。-t让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上,-i是交互式操作,使容器的标准输入保持打开。如果要在容器退出时删除容器则可以添加--rm

当利用docker run来创建容器时,Docker将执行以下标准操作:

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

以后台运行

-d会将容器以后台运行,不将运行结果输出到当前宿主机下。容器的输出内容可以使用docker logs查看。

终止容器

可以使用docker stop <identifier>来终止一个运行中的容器。此外,当容器中指定的应用退出时,容器也自动终止。处于终止状态的容器可以使用docker start <identifier>再次启动。docker restart <identifier>会将一个运行中的容器终止并重新启动。

进入容器

很多时候我们需要进入后台运行(例如使用-d启动)的容器进行操作。

docker attach <identifier>是Docker自带的命令。当多个窗口同时attach导同一个容器时所有窗口会同步显示,并且一个窗口因故阻塞时其它窗口也不能进行操作了。

docker exec <identifier> <command>将在docker容器中运行指定程序。如果使用-t -i运行/bin/bash的话可以获得一个伪终端。

导入和导出容器

如果要导出本地的一个容器,可以使用docker export命令。相反,可以使用docker import从容器快照文件中导入镜像。注意镜像文件和容器快照文件的区别:容器快照将丢弃所有的历史纪录和元数据,仅保存容器当时的状态,体积更小。

删除容器

可以使用docker rm删除一个在终止状态的容器。如果要删除一个运行状态的容器,使用-f参数,Docker会发送SIGKILL到容器中。

Docker Repository

仓库是集中存放镜像的场所。有的时候会把仓库和仓库注册服务器Registry混为一谈。一个Registry中可以包含多个仓库,每个仓库可以包含多个标签(tag),每个标签对应一个镜像。一般而言,一个仓库包含的是同一个软件不同版本的镜像,而标签则用于区分不同的版本。我们可以用<仓库名>:<标签>来指定镜像。

仓库分为公有和私有。最大的公有仓库是Docker Hub,国内的有时速云、网易云。用户可以在本地创建一个私有仓库。Docker仓库的概念和Git有些类似。

docker search可以搜索官方仓库中的镜像。

DockerHub支持自动创建镜像,即跟踪GitHub或BitBucket上项目。一旦项目发生新的提交则自动执行创建。

Docker官方提供docker-registry来构建私有的镜像仓库。

Docker数据管理

Data Volume数据卷

数据卷是一个可供一个或者多个容器使用的特殊目录,它绕过UFS并独立于容器和镜像之外。

创建数据卷

在用docker run的时候,使用-v来创建并挂载一个数据卷。在一次run中多次使用可以挂载多个数据卷。

docker run -d -P --name web -v /webapp training/webapp python app.py

这将创建一个数据卷并挂载到/webapp目录下。

docker volume create <volume_name>可以用来创建一个独立的卷,并随后挂载到容器中:

docker volume create my-named-volume
docker run -d -P -v my-named-volume:/webapp --name web training/webapp python app.py

也可以使用VOLUME来添加一个或者多个新的卷到Dockerfile创建的容器中。

挂载主机目录或文件为数据卷

docker run -d -P --name web -v /src/webapp:/webapp training/webapp python app.py

上述命令将主机的/src/webapp挂载到容器的/webapp。本地目录的路径必须为绝对路径,如果不存在则Docker会自动创建它。

上述指令也可以挂载单个文件。但是编辑文件时很多时候会造成文件的inode(matadata)改变而报错。简单的解决办法是挂载该文件所在目录。

Docker挂载数据卷的默认权限是读写,添加:ro可以指定为只读。

Dockerfile不支持这种用法。

挂载共享的数据卷

通过插件可以挂载各种分布式数据卷。

删除数据卷

数据卷的生命周期是独立于容器的,所以如果需要在删除容器时删除相应的数据卷,使用docker rm -v。可以使用docker volume ls -f dangling=true来寻找未被使用的数据卷。

查看数据卷信息

使用docker inspect可以看到容器的详细信息,其中Volumes部分是数据卷。所有的数据卷都是创建在主机的/var/lib/docker/volumes/下面的。

数据卷容器

数据卷容器是一个用来提供数据卷给其它容器挂载的容器。

创建数据卷容器:

docker create -v /dbdata --name dbstore training/postgres /bin/true

挂载数据卷到其它容器:

docker run -d --volumes-from dbstore --name db1 training/postgres

在一个容器中可以使用多个--volumes-from参数来指定多个容器挂载数据卷,也可以从其他已经挂载数据卷的容器来级联挂载:

docker run -d --name db3 --volumes-from db1 training/postgres

注意,作为--volumes-from参数的容器并不需要在运行状态。

利用数据卷容器来备份、恢复、迁移数据卷

  1. 创建一个数据卷容器;
  2. 挂载数据卷到容器1,并将数据存入数据卷;
  3. 创建一个新的容器2,并将数据卷挂载到容器2;
  4. 将数据卷中的数据恢复导容器2中。

网络

容器中可以运行一些网络应用,并可以通过端口映射使外部访问这些应用。

使用-P时,Docker会随机映射一个49000-49900的端口到容器开放的端口。-p`则可以指定要映射的端口,并且在一个指定端口上只可以绑定一个容器。支持的格式有:

  • ip:hostPort:containerPort。可以指定映射一个特定地址,比如127.0.0.1:5000:5000
  • ip::containerPort。此时会绑定任意一个端口(主机分配),比如127.0.0.1::5000会将容器的5000端口绑定到本地的任意端口。
  • hostPort:containerPort。此时会默认绑定本地接口上的所有地址,比如5000:5000

containerPort 可以加入 /udp 标记来指定UDP端口。注意 -p 可以用来映射多个端口。

使用docker port <container_name> <port>来查看端口映射配置。

容器互联

可以使用--link参数来使容器互联。但是Docker网络的完善提供了更好的方法。

新建一个Docker网络:docker network create -d bridge <network_name>-d指定Docker网络的类型,用bridgeoverlay,其中overlay用于Swarm mode。

在运行容器时加入一个Docker网络:使用--network <network_name>参数。

配置DNS

Docker利用虚拟文件来挂载容器的3个DNS相关配置文件:/etc/hostname, /etc/hosts,etc/resolv.conf。这种机制可以让宿主机的DNS信息发生更新后,所有的Docker容器的DNS配置通过etc/resolv.conf文件立即得到更新。

如果需要配置所有容器的DNS,也可以在/etc/docker/daemon.json文件中增加DNS server的地址。

在启动容器时可以加入以下参数来自定义容器的配置:

  • -h HOSTNAME 或者 --hostname=HOSTNAME 可以设置容器的主机名。它会被写入到 /etc/hostname/etc/hosts 中,但在容器外部看不到。
  • --dns=IP_ADDRESS 添加DNS服务器到 etc/resolv.conf 中。
  • --dns-search=DOMAIN 设定容器的搜索域。比如,当搜索域设为 .example.com 时,在搜索名为 host 的主机时,hosthost.example.com 都会被搜索到。

Docker 网络实现

Docker 的网络实现其实就是利用了 Linux 上的网络命名空间和虚拟网络设备。首先,要实现网络通信,机器需要至少一个网络接口(物理接口或虚拟接口)来收发数据包;此外,如果不同子网之间要进行通信,需要路由机制。

Docker 中的网络接口默认都是虚拟的接口。虚拟接口的优势之一是转发效率较高。Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中。对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,速度要快很多。Docker 容器网络就利用了这项技术。它在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做 veth pair)。

当 Docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 Linux 的一个 bridge,可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。

同时,Docker 随机分配一个本地未占用的私有网段(在 RFC1918 中定义)中的一个地址给 docker0 接口。比如典型的 172.17.42.1,掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段(172.17.0.0/16)的地址。

当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包)。这对接口一端在容器内,即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。

Docker 网络

Docker 本身提供了网络配置的功能,也可以直接使用 Linux 的网络配置。

Docker Compose

Docker Compose 用于快速部署分布式应用。Dockerfile 可以用来定义单独的容器,但是很多时候一个应用需要多个容器配合使用。Compose 允许用户通过一个单独的 docker-compose.yml 模板文件来定义一组相关的应用容器为一个项目 Project。

Compose中有两个重要概念:

  • service: 一个应用的容器,实际上是若干运行相同镜像的容器。
  • project: 由一组关联的容器组成的业务单元。

Compose默认的管理对象是项目。

Compose 由 Python 编写,实际上就是调用了 Docker 的 API。你可以通过 pip 安装,也可以将其安装为一个容器并通过 shell script 来运行。

在运行 Docker Compose 时,默认的项目名是当前的目录名(通过 -p 修改),默认的模板文件名是 docker-compose.yml(通过 -f 修改)。

Compose模板文件

version: "3"

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

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

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

举例:Django

我们现在将使用 Docker Compose 配置并运行一个 Django/PostgreSQL 应用。

在一切工作开始前,需要先编辑好三个必要的文件。

第一步,在项目的 requirements.txt 文件里面写明需要安装的具体依赖包名。

Django>=1.8,<2.0
psycopg2

第二步,因为应用将要运行在一个满足所有环境依赖的 Docker 容器里面,那么我们可以通过编辑 Dockerfile 文件来指定 Docker 容器要安装内容。内容如下:

FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN pip install -r requirements.txt
ADD . /code/

第三步,docker-compose.yml 文件将把所有的东西关联起来。它描述了应用的构成(一个 web 服务和一个数据库)、使用的 Docker 镜像、镜像之间的连接、挂载到容器的卷,以及服务开放的端口。

version: "3"
services:

  db:
    image: postgres

  web:
    build: .
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    links:
      - db

现在我们就可以使用 docker-compose run 命令启动一个 Django 应用了。

$ docker-compose run web django-admin.py startproject django_example .

Compose 会先使用 Dockerfile 为 web 服务创建一个镜像,接着使用这个镜像在容器里运行 django-admin.py startproject django_example 指令。这将在当前目录生成一个 Django 应用。

然后,我们要为应用设置好数据库的连接信息。用以下内容替换 django_example/settings.py 文件中 DATABASES = ... 定义的节点内容。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
    }
}

最后,运行 docker-compose up

$ docker-compose up

django_db_1 is up-to-date
Creating django_web_1 ...
Creating django_web_1 ... done
Attaching to django_db_1, django_web_1
db_1   | The files belonging to this database system will be owned by user "postgres".
db_1   | This user must also own the server process.
db_1   |
db_1   | The database cluster will be initialized with locale "en_US.utf8".
db_1   | The default database encoding has accordingly been set to "UTF8".
db_1   | The default text search configuration will be set to "english".

web_1  | Performing system checks...
web_1  |
web_1  | System check identified no issues (0 silenced).
web_1  |
web_1  | November 23, 2017 - 06:21:19
web_1  | Django version 1.11.7, using settings 'django_example.settings'
web_1  | Starting development server at http://0.0.0.0:8000/
web_1  | Quit the server with CONTROL-C.

这个 Django 应用已经开始在你的 Docker 守护进程里监听着 8000 端口了。打开 127.0.0.1:8000 即可看到 Django 欢迎页面。

你还可以在 Docker 上运行其它的管理命令,例如对于同步数据库结构这种事,在运行完 docker-compose up 后,在另外一个终端进入文件夹运行以下命令即可:

$ docker-compose run web python manage.py syncdb

常用指令

指令 用途
build Build or rebuild services
config Validate and view the Compose file
down Stop and remove containers, networks, images, and volumes
up Create and start containers
stop Stop services
start Start services
restart Restart services
run Run a one-off command
pull Pull service images
push Push service images
exec Execute a command in a running container
ps List containers
top Display the running processes

Docker Machine

Docker Machine 负责在多种平台上快速安装 Docker 环境。Docker Machine 项目基于 Go 语言实现。

Docker Swarm 和 Swarm Mode

Docker Swarm 提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。使用它,用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。Docker 1.12 Swarm mode 已经内嵌入 Docker 引擎,成为了 docker 子命令 docker swarm。请注意与旧的 Docker Swarm 区分开来。本节将介绍 Swarm Mode。

Swarm 是使用 SwarmKit 构建的 Docker 引擎内置(原生)的集群管理和编排工具。

节点

运行 Docker 的主机可以主动初始化一个 Swarm 集群或者加入一个已存在的 Swarm 集群,这样这个运行 Docker 的主机就成为一个 Swarm 集群的节点 (node) 。

节点分为管理 (manager) 节点和工作 (worker) 节点。

  • 管理节点用于 Swarm 集群的管理,docker swarm 命令基本只能在管理节点执行(节点退出集群命令 docker swarm leave 可以在工作节点执行)。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leaderleader 通过 raft 协议实现。
  • 工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。你也可以通过配置让服务只运行在管理节点。

来自 Docker 官网的这张图片形象的展示了集群中管理节点与工作节点的关系。

Docker Swarm

Docker Machine 可以用来在多个主机上快速建立 Docker 环境,Swarm Mode 可以通过建立集群来简化多台主机上的容器操作。

我们使用 docker swarm init 在管理节点初始化一个 Swarm 集群。如果你的 Docker 主机有多个网卡,拥有多个 IP,必须使用 --advertise-addr 指定 IP。注意,执行 docker swarm init 命令的节点自动成为管理节点。该命令的输出将提供加入集群的命令:

$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join \
    --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssmk743ojnwacrr2e7c \
    192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

在管理节点使用 docker node ls 查看集群。

$ docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
03g1y59jwfg7cf99w4lt0f662    worker2   Ready   Active
9j68exjopxe7wfl6yuxml7a7j    worker1   Ready   Active
dxn1zf6l61qsb1josjja83ngz *  manager   Ready   Active        Leader

服务和任务

任务 (Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。

服务 (Services) 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:

  • replicated services 按照一定规则在各个工作节点上运行指定个数的任务。
  • global services 每个工作节点上运行一个任务。

两种模式通过 docker service create--mode 参数指定。

来自 Docker 官网的这张图片形象的展示了容器、任务、服务的关系。

Docker Services Diagram

我们使用 docker service 命令来管理 Swarm 集群中的服务,该命令只能在管理节点运行。

新建服务: docker service create

$ docker service create --replicas 3 -p 80:80 --name nginx nginx:1.13.7-alpine

查看服务: 使用 docker service ls 来查看当前 Swarm 集群运行的服务。

$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                 PORTS
kc57xffvhul5        nginx               replicated          3/3                 nginx:1.13.7-alpine   *:80->80/tcp

使用 docker service ps 来查看某个服务的详情。

$ docker service ps nginx
ID                  NAME                IMAGE                 NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
pjfzd39buzlt        nginx.1             nginx:1.13.7-alpine   swarm2              Running             Running about a minute ago
hy9eeivdxlaa        nginx.2             nginx:1.13.7-alpine   swarm1              Running             Running about a minute ago
36wmpiv7gmfo        nginx.3             nginx:1.13.7-alpine   swarm3              Running             Running about a minute ago

使用 docker service logs 来查看某个服务的日志。

$ docker service logs nginx
nginx.3.36wmpiv7gmfo@swarm3    | 10.255.0.4 - - [25/Nov/2017:02:10:30 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.3.36wmpiv7gmfo@swarm3    | 10.255.0.4 - - [25/Nov/2017:02:10:30 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.3.36wmpiv7gmfo@swarm3    | 2017/11/25 02:10:30 [error] 5#5: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 10.255.0.4, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "192.168.99.102"
nginx.1.pjfzd39buzlt@swarm2    | 10.255.0.2 - - [25/Nov/2017:02:10:26 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.1.pjfzd39buzlt@swarm2    | 10.255.0.2 - - [25/Nov/2017:02:10:27 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:58.0) Gecko/20100101 Firefox/58.0" "-"
nginx.1.pjfzd39buzlt@swarm2    | 2017/11/25 02:10:27 [error] 5#5: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 10.255.0.2, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "192.168.99.101"

我们可以使用 docker service scale 对一个服务运行的容器数量进行伸缩。

当业务处于高峰期时,我们需要扩展服务运行的容器数量。

$ docker service scale nginx=5

当业务平稳时,我们需要减少服务运行的容器数量。

$ docker service scale nginx=2

使用 docker service rm 来从 Swarm 集群移除某个服务。

$ docker service rm nginx

在 Swarm 集群中使用 compose 文件

正如之前使用 docker-compose.yml 来一次配置、启动多个容器,在 Swarm 集群中也可以使用 compose 文件 (docker-compose.yml) 来配置、启动多个服务。

上一节中,我们使用 docker service create 一次只能部署一个服务,使用 docker-compose.yml 我们可以一次启动多个关联的服务。

部署服务使用 docker stack deploy,其中 -c 参数指定 compose 文件名。

$ docker stack deploy -c docker-compose.yml <stack_name>

使用docker stack ls来查看运行的服务。

$ docker stack ls
NAME                SERVICES
wordpress           3

要移除服务,使用 docker stack down <stack_name>。注意,如果该服务创建了数据卷,它们不会被这个命令移除。

在 Swarm 集群中管理敏感数据

在动态的、大规模的分布式集群上,管理和分发 密码证书 等敏感信息是极其重要的工作。传统的密钥分发方式(如密钥放入镜像中,设置环境变量,volume 动态挂载等)都存在着潜在的巨大的安全风险。

Docker 目前已经提供了 secrets 管理功能,用户可以在 Swarm 集群中安全地管理密码、密钥证书等敏感数据,并允许在多个 Docker 容器实例之间共享访问指定的敏感数据。

注意: secret 也可以在 Docker Compose 中使用。

我们可以用 docker secret 命令来管理敏感信息。

我们使用 docker secret create 命令以管道符的形式创建 secret:

$ openssl rand -base64 20 | docker secret create mysql_password -

$ openssl rand -base64 20 | docker secret create mysql_root_password -

使用 docker secret ls 命令来查看 secret

$ docker secret ls

ID                          NAME                  CREATED             UPDATED
l1vinzevzhj4goakjap5ya409   mysql_password        41 seconds ago      41 seconds ago
yvsczlx9votfw3l0nz5rlidig   mysql_root_password   12 seconds ago      12 seconds ago

在创建服务时,使用--secret source=<secret_name>,target=<secret_directory>secret 文件放置到指定位置。如果你没有在 target 中显式的指定路径时,secret 默认通过 tmpfs 文件系统挂载到容器的 /run/secrets 目录中。

在 Swarm 集群中管理配置数据

在动态的、大规模的分布式集群上,管理和分发配置文件也是很重要的工作。传统的配置文件分发方式(如配置文件放入镜像中,设置环境变量,volume 动态挂载等)都降低了镜像的通用性。

在 Docker 17.06 以上版本中,Docker 新增了 docker config 子命令来管理集群中的配置信息,以后你无需将配置文件放入镜像或挂载到容器中就可实现对服务的配置。

注意:config 仅能在 Swarm 集群中使用。

在管理节点上新建 redis.conf 文件(此项配置 Redis 监听 6380 端口):

port 6380

我们使用 docker config create 命令创建 config

$ docker config create redis.conf redis.conf

使用 docker config ls 命令来查看 config

$ docker config ls

ID                          NAME                CREATED             UPDATED
yod8fx8iiqtoo84jgwadp86yk   redis.conf          4 seconds ago       4 seconds ago

在创建服务时,使用 --config source=redis.conf,target=/etc/redis.conf 将配置文件挂载到指定位置。如果你没有在 target 中显式的指定路径时(如使用--config redis.conf),默认的 redis.conftmpfs 文件系统挂载到容器的 /config.conf

以前容器通过监听主机目录来获取配置文件,就需要在集群的每个节点放置该文件,如果采用 docker config 来管理服务的配置信息,我们只需在集群中的管理节点创建 config,当部署服务时,集群会自动的将配置文件分发到运行服务的各个节点中,大大降低了配置信息的管理和分发难度。

Swarm mode 与滚动升级

使用 docker service update 命令可以对集群进行滚动升级。

$ docker service update \
    --image nginx:1.13.12-alpine \
    nginx

以上命令使用 --image 选项更新了服务的镜像。当然我们也可以使用 docker service update 更新任意的配置。

  • --secret-add 选项可以增加一个密钥
  • --secret-rm 选项可以删除一个密钥
  • 更多选项可以通过 docker service update -h 命令查看。

假设我们发现 nginx 服务的镜像升级到 nginx:1.13.12-alpine 出现了一些问题,我们可以使用 docker service rollback nginx 命令一键回退。

附录:Docker 的底层实现

Docker 底层的核心技术包括 Linux 上的命名空间(Namespaces)、控制组(Control groups)、Union 文件系统(Union file systems)和容器格式(Container format)。

传统的虚拟机通过在宿主主机中运行 hypervisor 来模拟一整套完整的硬件环境提供给虚拟机的操作系统。虚拟机系统看到的环境是可限制的,也是彼此隔离的。 这种直接的做法实现了对资源最完整的封装,但很多时候往往意味着系统资源的浪费。 例如,以宿主机和虚拟机系统都为 Linux 系统为例,虚拟机中运行的应用其实可以利用宿主机系统中的运行环境。

在操作系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU 等等,所有的资源都是应用进程直接共享的。 要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等等的相互隔离。 前者相对容易实现一些,后者则需要宿主机系统的深入支持。随着 Linux 系统对于命名空间功能的完善实现,程序员已经可以实现上面的所有需求,让某些进程在彼此隔离的命名空间中运行。大家虽然都共用一个内核和某些运行时环境(例如一些系统命令和系统库),但是彼此却看不到,都以为系统中只有自己的存在。这种机制就是容器(Container),利用命名空间来做权限的隔离控制,利用 cgroups 来做资源分配。

命名空间

命名空间是 Linux 内核一个强大的特性。每个容器都有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统中运行一样。命名空间保证了容器之间彼此互不影响。

pid 命名空间

不同用户的进程就是通过 pid 命名空间隔离开的,且不同命名空间中可以有相同 pid。所有的 LXC 进程在 Docker 中的父进程为Docker进程,每个 LXC 进程具有不同的命名空间。同时由于允许嵌套,因此可以很方便的实现嵌套的 Docker 容器。

net 命名空间

有了 pid 命名空间, 每个命名空间中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net 命名空间实现的, 每个 net 命名空间有独立的 网络设备, IP 地址, 路由表, /proc/net 目录。这样每个容器的网络就能隔离开来。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。

ipc 命名空间

容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC), 包括信号量、消息队列和共享内存等。然而同 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 pid 命名空间中的进程间交互,因此需要在 IPC 资源申请时加入命名空间信息,每个 IPC 资源有一个唯一的 32 位 id。

mnt 命名空间

类似 chroot,将一个进程放到一个特定的目录执行。mnt 命名空间允许不同命名空间的进程看到的文件结构不同,这样每个命名空间 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point。

uts 命名空间

UTS(“UNIX Time-sharing System”) 命名空间允许每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非 主机上的一个进程。

user 命名空间

每个容器可以有不同的用户和组 id, 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户。

更多关于 Linux 上命名空间的信息,请阅读 这篇文章

控制组

控制组(cgroups)是 Linux 容器机制的另外一个关键组件,负责实现资源的审计和限制。

它提供了很多有用的特性;以及确保各个容器可以公平地分享主机的内存、CPU、磁盘 IO 等资源;当然,更重要的是,控制组确保了当容器内的资源使用产生压力时不会连累主机系统。

尽管控制组不负责隔离容器之间相互访问、处理数据和进程,它在防止拒绝服务(DDOS)攻击方面是必不可少的。尤其是在多用户的平台(比如公有或私有的 PaaS)上,控制组十分重要。例如,当某些应用程序表现异常的时候,可以保证一致地正常运行和性能。

控制组机制始于 2006 年,内核从 2.6.24 版本开始被引入。

附录:Docker 安全

评估 Docker 的安全性时,主要考虑三个方面:

  • 由内核的命名空间和控制组机制提供的容器内在安全
  • Docker 程序(特别是服务端)本身的抗攻击性
  • 内核安全性的加强机制对容器安全性的影响

服务端防护

Docker 使用C/S架构,包括客户端和服务端。Docker 守护进程 (Daemon)作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)。客户端和服务端既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。运行一个容器或应用程序的核心是通过 Docker 服务端。Docker 服务的运行目前需要 root 权限,因此其安全性十分关键。

首先,确保只有可信的用户才可以访问 Docker 服务。Docker 允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制。例如,恶意用户启动容器的时候将主机的根目录/映射到容器的 /host 目录中,那么容器理论上就可以对主机的文件系统进行任意修改了。这听起来很疯狂?但是事实上几乎所有虚拟化系统都允许类似的资源共享,而没法禁止用户共享主机根文件系统到虚拟机系统。

这将会造成很严重的安全后果。因此,当提供容器创建服务时(例如通过一个 web 服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器。

为了加强对服务端的保护,Docker 的 REST API(客户端用来跟服务端通信)在 0.5.2 之后使用本地的 Unix 套接字机制替代了原先绑定在 127.0.0.1 上的 TCP 套接字,因为后者容易遭受跨站脚本攻击。现在用户使用 Unix 权限检查来加强套接字的访问安全。

用户仍可以利用 HTTP 提供 REST API 访问。建议使用安全机制,确保只有可信的网络或 VPN,或证书保护机制(例如受保护的 stunnel 和 ssl 认证)下的访问可以进行。此外,还可以使用 HTTPS 和证书来加强保护。

最近改进的 Linux 命名空间机制将可以实现使用非 root 用户来运行全功能的容器。这将从根本上解决了容器和主机之间共享文件系统而引起的安全问题。

终极目标是改进 2 个重要的安全特性:

  • 将容器的 root 用户映射到本地主机上的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题;
  • 允许 Docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作,例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。

最后,建议采用专用的服务器来运行 Docker 和相关的管理服务(例如管理服务比如 ssh 监控和进程监控、管理工具 nrpe、collectd 等)。其它的业务服务都放到容器中去运行。

内核能力机制

能力机制(Capability)是 Linux 内核一个强大的特性,可以提供细粒度的权限访问控制。 Linux 内核自 2.2 版本起就支持能力机制,它将权限划分为更加细粒度的操作能力,既可以作用在进程上,也可以作用在文件上。

例如,一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限,并不需要 root 权限。那么它只需要被授权 net_bind_service 能力即可。此外,还有很多其他的类似能力来避免进程获取 root 权限。

默认情况下,Docker 启动的容器被严格限制只允许使用内核的一部分能力。

使用能力机制对加强 Docker 容器的安全有很多好处。通常,在服务器上会运行一堆需要特权权限的进程,包括有 ssh、cron、syslogd、硬件管理工具模块(例如负载模块)、网络配置工具等等。容器跟这些进程是不同的,因为几乎所有的特权进程都由容器以外的支持系统来进行管理。

  • ssh 访问被主机上ssh服务来管理;
  • cron 通常应该作为用户进程执行,权限交给使用它服务的应用来处理;
  • 日志系统可由 Docker 或第三方服务管理;
  • 硬件管理无关紧要,容器中也就无需执行 udevd 以及类似服务;
  • 网络管理也都在主机上设置,除非特殊需求,容器不需要对网络进行配置。

从上面的例子可以看出,大部分情况下,容器并不需要“真正的” root 权限,容器只需要少数的能力即可。为了加强安全,容器可以禁用一些没必要的权限。

  • 完全禁止任何 mount 操作;
  • 禁止直接访问本地主机的套接字;
  • 禁止访问一些文件系统的操作,比如创建新的设备、修改文件属性等;
  • 禁止模块加载。

这样,就算攻击者在容器中取得了 root 权限,也不能获得本地主机的较高权限,能进行的破坏也有限。

默认情况下,Docker采用白名单机制,禁用必需功能之外的其它权限。 当然,用户也可以根据自身需求来为 Docker 容器启用额外的权限。

其它安全特性

除了能力机制之外,还可以利用一些现有的安全机制来增强使用 Docker 的安全性,例如 TOMOYO, AppArmor, SELinux, GRSEC 等。

Docker 当前默认只启用了能力机制。用户可以采用多种方案来加强 Docker 主机的安全,例如:

  • 在内核中启用 GRSEC 和 PAX,这将增加很多编译和运行时的安全检查;通过地址随机化避免恶意探测等。并且,启用该特性不需要 Docker 进行任何配置。
  • 使用一些有增强安全特性的容器模板,比如带 AppArmor 的模板和 Redhat 带 SELinux 策略的模板。这些模板提供了额外的安全特性。
  • 用户可以自定义访问控制机制来定制安全策略。

跟其它添加到 Docker 容器的第三方工具一样(比如网络拓扑和文件系统共享),有很多类似的机制,在不改变 Docker 内核情况下就可以加固现有的容器。

附录:高级网络配置

快速配置指南

下面是一个跟 Docker 网络相关的命令列表。

其中有些命令选项只有在 Docker 服务启动的时候才能配置,而且不能马上生效。

  • -b BRIDGE--bridge=BRIDGE 指定容器挂载的网桥
  • --bip=CIDR 定制 docker0 的掩码
  • -H SOCKET...--host=SOCKET... Docker 服务端接收命令的通道
  • --icc=true|false 是否支持容器之间进行通信
  • --ip-forward=true|false 请看下文容器之间的通信
  • --iptables=true|false 是否允许 Docker 添加 iptables 规则
  • --mtu=BYTES 容器网络中的 MTU

下面2个命令选项既可以在启动服务时指定,也可以在启动容器时指定。在 Docker 服务启动的时候指定则会成为默认值,后面执行 docker run 时可以覆盖设置的默认值。

  • --dns=IP_ADDRESS... 使用指定的DNS服务器
  • --dns-search=DOMAIN... 指定DNS搜索域

最后这些选项只有在 docker run 执行时使用,因为它是针对容器的特性内容。

  • -h HOSTNAME--hostname=HOSTNAME 配置容器主机名
  • --link=CONTAINER_NAME:ALIAS 添加到另一个容器的连接
  • --net=bridge|none|container:NAME_or_ID|host 配置容器的桥接模式
  • -p SPEC--publish=SPEC 映射容器端口到宿主主机
  • -P or --publish-all=true|false 映射容器所有端口到宿主主机

容器访问控制

容器的访问控制,主要通过 Linux 上的 iptables 防火墙来进行管理和实现。iptables 是 Linux 上默认的防火墙软件,在大部分发行版中都自带。

容器访问外部网络

容器要想访问外部网络,需要本地系统的转发支持。在Linux 系统中,检查转发是否打开。

$sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

如果为 0,说明没有开启转发,则需要手动打开。

$sysctl -w net.ipv4.ip_forward=1

如果在启动 Docker 服务的时候设定 --ip-forward=true, Docker 就会自动设定系统的 ip_forward 参数为 1。

容器之间访问

容器之间相互访问,需要两方面的支持。

  • 容器的网络拓扑是否已经互联。默认情况下,所有容器都会被连接到 docker0 网桥上。
  • 本地系统的防火墙软件 – iptables 是否允许通过。

访问所有端口

当启动 Docker 服务(即 dockerd)的时候,默认会添加一条转发策略到本地主机 iptables 的 FORWARD 链上。策略为通过(ACCEPT)还是禁止(DROP)取决于配置--icc=true(缺省值)还是 --icc=false。当然,如果手动指定 --iptables=false 则不会添加 iptables 规则。

可见,默认情况下,不同容器之间是允许网络互通的。如果为了安全考虑,可以在 /etc/docker/daemon.json 文件中配置 {"icc": false} 来禁止它(Ubuntu 14.04 等使用 upstart 的系统在文件 /etc/default/docker 中配置 DOCKER_OPTS=--icc=false)。

访问指定端口

在通过 -icc=false 关闭网络访问后,还可以通过 --link=CONTAINER_NAME:ALIAS 选项来访问容器的开放端口。

例如,在启动 Docker 服务时,可以同时使用 icc=false --iptables=true 参数来关闭允许相互的网络访问,并让 Docker 可以修改系统中的 iptables 规则。

此时,系统中的 iptables 规则可能是类似

$ sudo iptables -nL
...
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DROP       all  --  0.0.0.0/0            0.0.0.0/0
...

之后,启动容器(docker run)时使用 --link=CONTAINER_NAME:ALIAS 选项。Docker 会在 iptable 中为 两个容器分别添加一条 ACCEPT 规则,允许相互访问开放的端口(取决于 Dockerfile 中的 EXPOSE 指令)。

当添加了 --link=CONTAINER_NAME:ALIAS 选项后,添加了 iptables 规则。

$ sudo iptables -nL
...
Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
ACCEPT     tcp  --  172.17.0.2           172.17.0.3           tcp spt:80
ACCEPT     tcp  --  172.17.0.3           172.17.0.2           tcp dpt:80
DROP       all  --  0.0.0.0/0            0.0.0.0/0

注意:--link=CONTAINER_NAME:ALIAS 中的 CONTAINER_NAME 目前必须是 Docker 分配的名字,或使用 --name 参数指定的名字。主机名则不会被识别。

映射容器端口到宿主主机的实现

默认情况下,容器可以主动访问到外部网络的连接,但是外部网络无法访问到容器。

容器访问外部实现

容器所有到外部网络的连接,源地址都会被 NAT 成本地系统的 IP 地址。这是使用 iptables 的源地址伪装操作实现的。

查看主机的 NAT 规则。

$ sudo iptables -t nat -nL
...
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16       !172.17.0.0/16
...

其中,上述规则将所有源地址在 172.17.0.0/16 网段,目标地址为其他网段(外部网络)的流量动态伪装为从系统网卡发出。MASQUERADE 跟传统 SNAT 的好处是它能动态从网卡获取地址。

外部访问容器实现

容器允许外部访问,可以在 docker run 时候通过 -p-P 参数来启用。

不管用那种办法,其实也是在本地的 iptable 的 nat 表中添加相应的规则。

使用 -P 时:

$ iptables -t nat -nL
...
Chain DOCKER (2 references)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:49153 to:172.17.0.2:80

使用 -p 80:80 时:

$ iptables -t nat -nL
Chain DOCKER (2 references)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:80

注意:

  • 这里的规则映射了 0.0.0.0,意味着将接受主机来自所有接口的流量。用户可以通过 -p IP:host_port:container_port-p IP::port 来指定允许访问容器的主机上的 IP、接口等,以制定更严格的规则。

  • 如果希望永久绑定到某个固定的 IP 地址,可以在 Docker 配置文件 /etc/docker/daemon.json 中添加如下内容。

{
  "ip": "0.0.0.0"
}