Docker-编排指南(全)

Docker 编排指南(全)

原文:zh.annas-archive.org/md5/1B8FD79C063269548A48D0E2E43C2BF6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

开始使用 Docker,这种彻底改变了应用程序沙盒化的 Linux 容器技术。通过本书,您将学会如何使用 Docker 使开发更快,应用程序部署更简单。

本指南将向您展示如何在沙盒式 Docker 容器中构建应用程序,并使其在任何地方运行 - 您的开发机器,私人服务器,甚至云上,成本仅为虚拟机的一小部分。在您的开发设置中构建 PaaS,部署集群等等。

本书涵盖的内容

第一章 解开 Docker,教您如何在您的环境中运行 Docker。

第二章 Docker CLI 和 Dockerfile,帮助您适应 Docker 命令行工具,并通过编写 Dockerfile 开始构建自己的容器。

第三章 配置 Docker 容器,向您展示如何控制您的容器并配置它们以实现细粒度的资源管理。

第四章 自动化和最佳实践,涵盖了帮助管理容器的各种技术 - 使用监控程序协调多个服务,服务发现,以及有关 Docker 安全性的知识。

第五章 Docker 的朋友,向您展示了围绕 Docker 的世界。您将了解使用 Docker 的开源项目。然后,您可以使用 CoreOS 构建自己的 PaaS 并部署集群。

本书所需内容

本书希望您之前使用过 Linux 和 Git,但新手用户在运行示例中提供的命令时不会遇到困难。您需要在操作系统的用户帐户中具有管理员权限才能安装 Docker。Windows 和 OSX 用户需要安装 VirtualBox。

本书适合谁

无论您是开发人员、系统管理员还是介于两者之间,本书都将为您提供使用 Docker 构建、测试和部署应用程序的指导,使其变得更加简单,甚至令人愉快。

从安装开始,本书将带您了解启动 Docker 容器所需的不同命令。然后它将向您展示如何构建自己的应用程序,并带您完成如何微调这些容器的资源分配的说明,最后以管理一组 Docker 容器的注意事项结束。

通过按照每一章中的步骤顺序进行工作,您将很快掌握 Docker,并准备好在不需要为部署而彻夜不眠的情况下发布您的应用程序。

约定

在这本书中,您将找到许多不同类型信息的文本样式。以下是一些样式的示例,以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“我们可以使用ENV指令设置环境变量。”

一块代码设置如下:

WORKDIR code.it
RUN     git submodule update --init --recursive
RUN     npm install

任何命令行输入或输出都以以下方式编写:

$ docker run --d -p '8000:8000' -e 'NODE_PORT=8000' -v '/var/log/code.it:/var/log/code.it' shrikrishna/code.it .

新术语重要单词以粗体显示。例如,屏幕上看到的单词,菜单或对话框中出现的单词会在文本中显示为:“转到您的存储库中的设置”。

注意

警告或重要说明会以以下方式出现在方框中。

提示

提示和技巧会以这种方式出现。

第一章:打开 Docker

Docker是一种轻量级的容器化技术,在近年来广受欢迎。它利用了 Linux 内核的一系列特性,如命名空间、cgroups、AppArmor 配置文件等,将进程隔离到可配置的虚拟环境中。

在本章中,您将学习如何在各种系统上安装 Docker,无论是在开发还是生产环境。对于基于 Linux 的系统,由于内核已经可用,安装就像apt-get installyum install命令一样简单。然而,要在 OSX 和 Windows 等非 Linux 操作系统上运行 Docker,您需要安装 Docker Inc.开发的一个辅助应用程序,称为Boot2Docker。这将在VirtualBox上安装一个轻量级的 Linux 虚拟机,通过Internet Assigned Numbers Authority (IANA)分配的端口 2375,使 Docker 可用。

在本章结束时,您将在您的系统上安装了 Docker,无论是在开发还是生产环境,并进行了验证。

本章将涵盖以下内容:

  • 介绍 Docker

  • 安装 Docker

  • Ubuntu(14.04 和 12.04)

  • Mac OSX 和 Windows

  • OpenStack

  • Inception:在 Docker 中构建 Docker

  • 验证安装:Hello World 输出

  • 介绍 Docker

Docker 是由 DotCloud Inc.(目前是 Docker Inc.)开发的,作为他们构建的Platform as a Service (PaaS)的框架。当他们发现开发人员对这项技术越来越感兴趣时,他们将其作为开源发布,并自那时起宣布他们将完全专注于 Docker 技术的发展,这是一个好消息,因为这意味着平台将得到持续的支持和改进。

已经有许多旨在使分布式应用程序成为可能,甚至易于设置的工具和技术,但没有一个像 Docker 一样具有如此广泛的吸引力,这主要是因为它的跨平台性和对系统管理员和开发人员的友好性。在任何操作系统上都可以设置 Docker,无论是 Windows、OSX 还是 Linux,Docker 容器在任何地方都可以以相同的方式工作。这是非常强大的,因为它实现了一次编写,到处运行的工作流程。Docker 容器保证在开发桌面、裸机服务器、虚拟机、数据中心或云上以相同的方式运行。不再出现程序在开发人员的笔记本电脑上运行但在服务器上不运行的情况。

Docker 的工作流程的性质使得开发人员可以完全专注于构建应用程序并在容器内运行它们,而系统管理员可以专注于在部署中运行容器。这种角色的分离和一个单一的基础工具的存在使得代码管理和部署过程变得简单。

但是虚拟机不是已经提供了所有这些功能吗?

虚拟机VMs)是完全虚拟化的。这意味着它们在彼此之间共享最少的资源,每个虚拟机都有其自己分配的资源集。虽然这允许对各个虚拟机进行细粒度配置,但最小的共享也意味着更大的资源使用、冗余的运行进程(需要运行整个操作系统!)和因此性能开销。

另一方面,Docker 建立在容器技术之上,它隔离一个进程并使其相信自己在独立的操作系统上运行。该进程仍然在与其主机相同的操作系统中运行,共享其内核。它使用了一个名为Another UnionfsAUFS)的分层写时复制文件系统,它在容器之间共享操作系统的公共部分。更大的共享当然只能意味着更少的隔离,但 Linux 进程资源管理解决方案的巨大改进,如命名空间和 cgroups,已经使 Docker 实现了类似虚拟机的进程隔离,同时保持了非常小的资源占用。

让我们来看一下以下图片:

拆箱 Docker

这是一个 Docker 与虚拟机的比较。容器与其他容器和进程共享主机的资源,而虚拟机必须为每个实例运行整个操作系统。

安装 Docker

Docker 在大多数主要 Linux 发行版的标准存储库中都有。我们将看一下 Ubuntu 14.04 和 12.04(Trusty 和 Precise)、Mac OSX 和 Windows 中 Docker 的安装程序。如果您目前使用的操作系统不在上述列表中,您可以在docs.docker.com/installation/#installation上查找您操作系统的说明。

在 Ubuntu 中安装 Docker

Ubuntu 从 Ubuntu 12.04 开始支持 Docker。请记住,您仍然需要 64 位操作系统才能运行 Docker。让我们来看一下 Ubuntu 14.04 的安装说明。

在 Ubuntu Trusty 14.04 LTS 中安装 Docker

Docker 作为一个软件包在 Ubuntu Trusty 版本的软件存储库中以docker.io的名称可用:

$ sudo apt-get update
$ sudo apt-get -y install docker.io

就是这样!您现在已经在系统上安装了 Docker。但是,由于命令已更名为docker.io,您将不得不使用docker.io而不是docker来运行所有 Docker 命令。

注意

该软件包的名称为docker.io,因为它与另一个名为docker的 KDE3/GNOME2 软件包冲突。如果您更愿意以docker运行命令,可以创建一个符号链接到/usr/local/bin目录。第二个命令将自动完成规则添加到 bash:

$ sudo ln -s /usr/bin/docker.io /usr/local/bin/docker
$ sudo sed -i '$acomplete -F _docker docker' \> /etc/bash_completion.d/docker.io

在 Ubuntu Precise 12.04 LTS 中安装 Docker

Ubuntu 12.04 带有较旧的内核(3.2),与 Docker 的一些依赖项不兼容。因此,我们需要升级它:

$ sudo apt-get update
$ sudo apt-get -y install linux-image-generic-lts-raring linux-headers-generic-lts-raring
$ sudo reboot

我们刚刚安装的内核内置了 AUFS,这也是 Docker 的要求。

现在让我们结束安装:

$ curl -s https://get.docker.io/ubuntu/ | sudo sh

这是一个用于简单安装的curl脚本。查看此脚本的各个部分将帮助我们更好地理解该过程:

  1. 首先,脚本检查我们的高级 软件包 工具APT)系统是否能处理https URL,并在无法处理时安装apt-transport-https
# Check that HTTPS transport is available to APT
if [ ! -e /usr/lib/apt/methods/https ]; then  apt-get update  apt-get install -y apt-transport-https
fi

  1. 然后它将 Docker 存储库添加到我们的本地密钥链中:
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9

提示

您可能会收到一个警告,表示软件包不受信任。回答yes以继续安装。

  1. 最后,它将 Docker 存储库添加到 APT 源列表中,并更新并安装lxc-docker软件包:
$ sudo sh -c "echo deb https://get.docker.io/ubuntu docker main\
> /etc/apt/sources.list.d/docker.list"
$ sudo apt-get update
$ sudo apt-get install lxc-docker

注意

0.9 版本之前的 Docker 对 LXC(Linux 容器)有严格依赖,因此无法安装在 OpenVZ 托管的 VM 上。但自 0.9 版本以来,执行驱动程序已从 Docker 核心中解耦,这使我们可以使用众多隔离工具之一,如 LXC、OpenVZ、systemd-nspawn、libvirt-lxc、libvirt-sandbox、qemu/kvm、BSD Jails、Solaris Zones,甚至 chroot!但是,它默认使用 Docker 自己的容器化引擎的执行驱动程序,称为 libcontainer,这是一个纯 Go 库,可以直接访问内核的容器 API,而无需任何其他依赖关系。

要使用任何其他容器化引擎,比如 LXC,您可以使用-e 标志,如下所示:$ docker -d -e lxc

现在我们已经安装了 Docker,我们可以全速前进了!不过有一个问题:像 APT 这样的软件仓库通常滞后于时代,经常有较旧的版本。Docker 是一个快速发展的项目,在最近的几个版本中发生了很多变化。因此,建议始终安装最新版本。

升级 Docker

您可以根据 APT 仓库中的更新来升级 Docker。另一种(更好的)方法是从源代码构建。此方法的教程在标题为Inception: Docker in Docker的部分中。建议升级到最新的稳定版本,因为更新的版本可能包含关键的安全更新和错误修复。此外,本书中的示例假定 Docker 版本大于 1.0,而 Ubuntu 的标准仓库中打包了一个更旧的版本。

Mac OSX 和 Windows

Docker 依赖于 Linux 内核,因此我们需要在虚拟机中运行 Linux,并通过它安装和使用 Docker。Boot2Docker 是由 Docker Inc.构建的辅助应用程序,它安装了一个包含轻量级 Linux 发行版的虚拟机,专门用于运行 Docker 容器。它还带有一个客户端,提供与 Docker 相同的应用程序 接口 (API),但与在虚拟机中运行的docker守护程序进行交互,允许我们从 OSX/Windows 终端运行命令。要安装 Boot2Docker,请执行以下步骤:

  1. boot2docker.io/下载适用于您操作系统的最新版本的 Boot2Docker。

  2. 安装镜像如下所示:Mac OSX 和 Windows

  3. 运行安装程序,它将安装 VirtualBox 和 Boot2Docker 管理工具。

运行 Boot2docker。第一次运行时会要求您输入安全 Shell (SSH)密钥密码。脚本的后续运行将连接您到虚拟机中的 shell 会话。如果需要,后续运行将初始化一个新的虚拟机并启动它。

或者,要运行 Boot2Docker,您也可以使用终端命令boot2docker

$ boot2docker init # First run
$ boot2docker start
$ export DOCKER_HOST=tcp://$(boot2docker ip 2>/dev/null):2375

您只需要运行boot2docker init一次。它会要求您输入 SSH 密钥密码。随后,boot2docker ssh将使用此密码来验证 SSH 访问。

初始化 Boot2Docker 后,您随后可以使用boot2docker startboot2docker stop命令。

DOCKER_HOST 是一个环境变量,设置后,指示 Docker 客户端 docker 守护程序的位置。端口转发规则设置为 boot2Docker VM 的端口 2375(docker 守护程序运行的位置)。您将需要在每个要在其中使用 Docker 的终端 shell 中设置此变量。

注意

Bash 允许您通过在 `或者$()中包含子命令来插入命令。这些将首先被评估,结果将被替换到外部命令中。

如果您是那种喜欢四处探索的人,Boot2Docker 的默认用户是 docker,密码是 tcuser

boot2Docker 管理工具提供了几个命令:


$ boot2docker

Usage: boot2docker [<options>] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|delete|download|version} [<args>]

使用 boot2Docker 时,必须在终端会话中使 DOCKER_HOST 环境变量可用,以使 Docker 命令起作用。因此,如果您遇到 Post http:///var/run/docker.sock/v1.12/containers/create: dial unix /var/run/docker.sock: no such file or directory 错误,意味着环境变量未分配。当您打开新终端时很容易忘记设置此环境变量。对于 OSX 用户,为了简化操作,请将以下行添加到您的 .bashrc.bash_profile shell 中:


alias setdockerhost='export DOCKER_HOST=tcp://$(boot2docker ip 2>/dev/null):2375'

现在,每当您打开一个新的终端或出现上述错误时,只需运行以下命令:


$ setdockerhost

Mac OSX 和 Windows

此图像显示了当您登录到 Boot2Docker VM 后终端屏幕的外观。

升级 Boot2Docker

  1. boot2docker.io/ 下载 OSX 的最新版本 Boot2Docker Installer。

  2. 运行安装程序,将更新 VirtualBox 和 Boot2Docker 管理工具。

要升级现有的虚拟机,请打开终端并运行以下命令:


$ boot2docker stop

$ boot2docker download

OpenStack

OpenStack** 是一款免费开源软件,可让您建立一个云。它主要用于部署公共和私有 Infrastructure as a Service (IaaS) 解决方案。它由一组相互关联的项目组成,用于云设置的不同组件,如计算调度程序、密钥链管理器、网络管理器、存储管理器、仪表板等等。

Docker 可以作为 OpenStack Nova Compute 的虚拟化驱动程序。Docker 对 OpenStack 的支持是从 Havana 版本开始引入的。

但是... 如何做到呢?

Nova 的 Docker 驱动程序嵌入了一个微型 HTTP 服务器,通过 UNIX TCP socket 与 Docker 引擎的内部 Representational State Transfer (REST) API 通信(稍后您将了解更多)。

Docker 有其自己的镜像仓库系统,称为 Docker-Registry,可以嵌入到 Glance(OpenStack 的镜像仓库)中以推送和拉取 Docker 镜像。Docker-Registry 可以作为 docker 容器或独立模式运行。

使用 DevStack 安装

如果您只是设置 OpenStack 并采用 DevStack 路线,那么配置设置以使用 Docker 很容易。

在运行 DevStack 路线的 stack.sh 脚本之前,请在 localrc 文件中配置 virtual driver 选项以使用 Docker:


VIRT_DRIVER=docker

然后从 devstack 目录运行 Docker 安装脚本。此脚本需要 socat 实用程序(通常由 stack.sh 脚本安装)。如果您尚未安装 socat 实用程序,请运行以下命令:


$ apt-get install socat

$ ./tools/docker/install_docker.sh

最后,从 devstack 目录运行 stack.sh 脚本:


$ ./stack.sh

手动为 OpenStack 安装 Docker

如果您已经设置了 OpenStack,或者 DevStack 方法不起作用,则可以手动安装 Docker:

  1. 首先,根据 Docker 的一个安装程序安装 Docker。

如果您正在将 docker 注册表与 Glance 服务放置在一起,请运行以下命令:


$ sudo yum -y install docker-registry

/etc/sysconfig/docker-registry 文件夹中,设置 REGISTRY_PORTSETTINGS_FLAVOR 注册表如下:


$ export SETTINGS_FLAVOR=openstack

$ export REGISTRY_PORT=5042

docker 注册文件中,您还需要指定 OpenStack 认证变量。以下命令完成此操作:


$ source /root/keystonerc_admin

$ export OS_GLANCE_URL=http://localhost:9292

默认情况下,/etc/docker-registry.yml 为 openstack 配置设置了本地或替代 storage_path 路径为 /tmp。您可能希望将路径更改为更永久的位置:


openstack:

storage: glance

storage_alternate: local

storage_path: /var/lib/docker-registry

  1. 为了使 Nova 能够通过其本地套接字与 Docker 通信,请将 nova 添加到 docker 组,并重新启动 compute 服务以接收更改:

$ usermod -G docker nova

$ service openstack-nova-compute restart

  1. 启动 Redis(Docker Registry 使用),如果尚未启动:

$ sudo service redis start

$ sudo chkconfig redis on

  1. 最后,启动注册表:

$ sudo service docker-registry start

$ sudo chkconfig docker-registry on

Nova 配置

Nova 需要配置为使用 virt Docker 驱动程序。

根据以下选项编辑 /etc/nova/nova.conf 配置文件:


[DEFAULT]

compute_driver = docker.DockerDriver

或者,如果您想使用您自己的 Docker-Registry,并且监听的端口不同于 5042,则可以覆盖以下选项:


docker_registry_default_port = 5042

Glance 配置

Glance 需要配置以支持 Docker 容器格式。只需在 Glance 配置文件中将 Docker 添加到容器格式列表中即可:


[DEFAULT]

container_formats = ami,ari,aki,bare,ovf,docker

提示

为了不破坏现有的 glance 安装,请保留默认格式。

Docker-OpenStack 流程

一旦您配置了 Nova 使用 docker 驱动程序,流程与任何其他驱动程序中的流程相同:


$ docker search hipache

Found 3 results matching your query ("hipache")

NAME                             DESCRIPTION

samalba/hipache                  https://github.com/dotcloud/hipache

然后,使用 Docker-Registry 位置标记图像并推送它:


$ docker pull samalba/hipache

$ docker tag samalba/hipache localhost:5042/hipache

$ docker push localhost:5042/hipache

推送引用了一个仓库:


[localhost:5042/hipache] (len: 1)

Sending image list

Pushing repository localhost:5042/hipache (1 tags)

Push 100% complete

在这种情况下,Docker-Registry(在一个端口映射为 5042 的 Docker 容器中运行)将图像推送到 Glance。从那里,Nova 可以访问它们,并且您可以使用 Glance Command-Line InterfaceCLI)验证图像:


$ glance image-list

注意

只有具有 Docker 容器格式的图像才能启动。图像基本上包含容器文件系统的 tarball。

您可以使用 nova boot 命令引导实例:


$ nova boot --image "docker-busybox:latest" --flavor m1.tiny test

提示

使用的命令将是在图像中配置的命令。每个容器图像都可以为运行配置一个命令。驱动程序不会覆盖此命令。

一旦实例引导完成,它将在 nova list 中列出:


$ nova list

您还可以在 Docker 中查看相应的容器:


$ docker ps

Inception:构建 Docker 中的 Docker

虽然从标准仓库安装更容易,但它们通常包含较旧的版本,这意味着您可能会错过关键的更新或功能。保持更新的最佳方法是定期从公共 GitHub 仓库获取最新版本。传统上,从源代码构建软件是很痛苦的,只有实际从事项目工作的人才会这样做。但 Docker 不是这样。从 Docker 0.6 开始,就可以在 Docker 中构建 Docker。这意味着升级 Docker 就像在 Docker 中构建新版本并替换二进制文件一样简单。让我们看看如何做到这一点。

依赖关系

您需要在 64 位 Linux 机器(虚拟机或裸机)上安装以下工具才能构建 Docker:

  • **Git

  • **Make

Git** 是一个免费且开放源代码的分布式版本控制系统,旨在以速度和效率处理从小型到非常大型的项目。它在此用于克隆 Docker 公共源代码仓库。有关更多详细信息,请查看 git-scm.org

make 实用程序是用于管理和维护计算机程序的软件工程工具。Make 在程序由许多组件文件组成时提供了最大的帮助。在这里,使用一个 Makefile 文件以一种可重复和一致的方式启动 Docker 容器。

从源代码构建 Docker

要在 Docker 中构建 Docker,我们首先会获取源代码,然后运行几个 make 命令,最终创建一个 docker 二进制文件,该文件将替换 Docker 安装路径中的当前二进制文件。

在终端中运行以下命令:


$ git clone https://git@github.com/dotcloud/docker

此命令将官方 Docker 源代码仓库从 Github 仓库克隆到名为 docker 的目录中:


$ cd docker

$ sudo make build

这将准备开发环境并安装创建二进制文件所需的所有依赖项。在第一次运行时可能需要一些时间,所以你可以去喝杯咖啡。

提示

如果遇到任何难以调试的错误,您可以随时转到 #docker 上的 freenode IRC。开发人员和 Docker 社区都非常乐意帮助。

现在我们已经准备好编译二进制文件了:


$ sudo make binary

这将编译一个二进制文件,并将其放置在 ./bundles/<version>-dev/binary/ 目录中。然后!您现在有一个准备就绪的 Docker 新版本。

但在替换现有二进制文件之前,请运行测试:


$ sudo make test

如果测试通过,则可以安全地用您刚刚编译的二进制文件替换当前的二进制文件。停止 docker 服务,创建现有二进制文件的备份,然后将新鲜出炉的二进制文件复制到其位置:


$ sudo service docker stop

$ alias wd='which docker'

$ sudo cp $(wd) $(wd)_

$ sudo cp $(pwd)/bundles/<version>-dev/binary/docker-<version>-dev $(wd)

$ sudo service docker start

恭喜!您现在拥有最新版本的 Docker 运行。

提示

OSX 和 Windows 用户可以按照 SSH 进入 boot2Docker VM 的相同步骤进行操作。

验证安装

要验证您的安装是否成功,请在终端控制台中运行以下命令:


$ docker run -i -t ubuntu echo Hello World!

docker run 命令使用ubuntu基础镜像启动容器。由于这是您首次启动ubuntu容器,容器的输出将类似于这样:


Unable to find image 'ubuntu' locally
Pulling repository ubuntu
e54ca5efa2e9: Download complete
511136ea3c5a: Download complete
d7ac5e4f1812: Download complete
2f4b4d6a4a06: Download complete
83ff768040a0: Download complete
6c37f792ddac: Download complete

Hello World!

当您发出docker run ubuntu命令时,Docker 将在本地查找ubuntu镜像,如果找不到,它将从公共docker注册表下载ubuntu镜像。您还将看到它显示正在拉取依赖层

这意味着它正在下载文件系统层。默认情况下,Docker 使用 AUFS,一种分层的写时复制文件系统,这意味着容器镜像的文件系统是多个只读文件系统层的结合体。而这些层是在运行的容器之间共享的。如果你启动了一个会写入此文件系统的操作,它将创建一个新的层,该层将是底层层和新数据的差异。共享常见层意味着只有第一个容器会占用大量内存,而后续容器将占用微不足道的内存,因为它们将共享只读层。这意味着即使在相对性能较低的笔记本电脑上,你也可以运行数百个容器。

验证安装

一旦镜像完全下载完成,它将启动容器并在您的控制台中回显Hello``World!。这是 Docker 容器的另一个显著特点。每个容器都与一个命令关联,并且应该运行该命令。请记住,Docker 容器不像虚拟机那样虚拟化整个操作系统。每个docker容器只接受一个单一命令,并在一个独立环境中运行它。

有用的提示

以下是两个有用的提示,以后可能会为您节省大量麻烦。第一个显示了如何为 Docker 客户端提供非根访问权限,第二个显示了如何配置 Ubuntu 防火墙规则以启用转发网络流量。

注意

如果您使用的是 Boot2Docker,则无需遵循这些步骤。

给予非根访问权限

创建一个名为docker的组,并将您的用户添加到该组,以避免每个docker命令都需要添加sudo前缀。默认情况下,您需要使用sudo前缀运行docker命令的原因是docker守护程序需要以root权限运行,但 docker 客户端(您运行的命令)不需要。因此,通过创建一个docker组,您可以在不使用sudo前缀的情况下运行所有客户端命令,而守护程序则以root权限运行:


$ sudo groupadd docker # Adds the docker group
$ sudo gpasswd -a $(whoami) docker # Adds the current user to the group
$ sudo service docker restart

你可能需要退出并重新登录以使更改生效。

UFW 设置

Docker 使用桥接来管理容器中的网络。简化防火墙UFW)是 Ubuntu 中的默认防火墙工具。它会拒绝所有转发流量。您需要像这样启用转发:


$ sudo vim /etc/default/ufw
# Change:
# DEFAULT_FORWARD_POLICY="DROP"
# to
DEFAULT_FORWARD_POLICY="ACCEPT"

运行以下命令重新加载防火墙:


$ sudo ufw reload

或者,如果你想要能够从其他主机访问你的容器,那么你应该在 Docker 端口(default 2375)上启用入站连接:


$ sudo ufw allow 2375/tcp

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载示例代码文件,用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您

总结

我希望这个介绍性的章节让你着迷于 Docker。接下来的章节将带你进入 Docker 的世界,并试图用它的神奇之处来迷住你。

在本章中,您学习了一些关于 Docker 的历史和基础知识,以及它的工作原理。我们看到了它与虚拟机的不同之处以及优势。

然后,我们继续在我们的开发环境中安装 Docker,无论是 Ubuntu、Mac 还是 Windows。然后我们看到如何用 Docker 替换 OpenStack 的 hypervisor。后来,我们在 Docker 中构建了 Docker 源代码!说到吃自己的狗粮!

最后,我们下载了我们的第一个镜像并运行了我们的第一个容器。现在你可以拍拍自己的背,继续下一章,在那里我们将深入介绍主要的 Docker 命令,并看看我们如何创建自己的镜像。

第二章:Docker CLI 和 Dockerfile

在上一章中,我们在开发环境中设置了 Docker 并运行了我们的第一个容器。在本章中,我们将探索 Docker 命令行界面。在本章后面,我们将看到如何使用 Dockerfiles 创建自己的 Docker 镜像以及如何自动化这个过程。

在本章中,我们将涵盖以下主题:

  • Docker 术语

  • Docker 命令

  • Dockerfiles

  • Docker 工作流程-拉取-使用-修改-提交-推送工作流程

  • 自动化构建

Docker 术语

在我们开始激动人心的 Docker 领域之前,让我们更好地了解本书中将使用的 Docker 术语。与 VM 镜像类似,Docker 镜像是系统的快照。VM 镜像和 Docker 镜像之间的区别在于 VM 镜像可以运行服务,而 Docker 镜像只是文件系统的快照,这意味着虽然你可以配置镜像来拥有你喜欢的软件包,但你只能在容器中运行一个命令。不过不要担心,由于限制是一个命令,而不是一个进程,所以有方法让 Docker 容器几乎可以执行任何 VM 实例可以执行的任务。

Docker 还实现了类似 Git 的分布式版本管理系统,用于 Docker 镜像。镜像可以存储在本地和远程的仓库中。其功能和术语大量借鉴自 Git-快照被称为提交,你拉取一个镜像仓库,你将本地镜像推送到仓库,等等。

Docker 容器

一个 Docker 容器可以与虚拟机的实例相关联。它运行沙盒化的进程,这些进程与主机共享相同的内核。术语容器来自于集装箱的概念。其想法是你可以从开发环境将容器运送到部署环境,容器中运行的应用程序无论在哪里运行,都会表现出相同的行为。

以下图片显示了 AUFS 的层次结构:

Docker 容器

这与集装箱的情境类似,集装箱在交付之前保持密封,但可以在装卸货物、堆叠和运输之间进行操作。

容器中进程的可见文件系统基于 AUFS(尽管您也可以配置容器以使用不同的文件系统)。AUFS 是一种分层文件系统。这些层都是只读的,这些层的合并是进程可见的。但是,如果进程在文件系统中进行更改,将创建一个新层,该层代表原始状态和新状态之间的差异。当您从此容器创建图像时,这些层将被保留。因此,可以基于现有图像构建新图像,创建一个非常方便的图像层次模型。

Docker 守护程序

docker守护程序是管理容器的进程。很容易将其与 Docker 客户端混淆,因为相同的二进制文件用于运行这两个进程。然而,docker守护程序需要root权限,而客户端不需要。

不幸的是,由于docker守护程序以 root 权限运行,它也引入了一个攻击向量。阅读docs.Docker.com/articles/security/获取更多详细信息。

Docker 客户端

Docker 客户端是与docker守护程序交互以启动或管理容器的工具。Docker 使用 RESTful API 在客户端和守护程序之间进行通信。

注意

REST 是一种架构风格,由一组协调的架构约束应用于分布式超媒体系统中的组件、连接器和数据元素。简而言之,RESTful 服务使用标准的 HTTP 方法,如GETPOSTPUTDELETE方法。

Dockerfile

Dockerfile 是一个用特定领域语言(DSL)编写的文件,其中包含设置 Docker 镜像的指令。可以将其视为 Docker 的 Makefile 等效文件。

Docker 注册表

这是 Docker 社区发布的所有 Docker 镜像的公共存储库。您可以自由地从该注册表中拉取镜像,但要推送镜像,您必须在hub.docker.com注册。Docker 注册表和 Docker Hub 是由 Docker Inc.运营和维护的服务,并提供无限免费的存储库。您也可以购买私人存储库。

Docker 命令

现在让我们在 Docker CLI 上动手。我们将看一下最常用的命令及其用法。Docker 命令是模仿 Linux 和 Git 的,所以如果您使用过其中任何一个,您将发现在 Docker 中也能得心应手。

这里只提到了最常用的选项。要获取完整的参考信息,您可以查看官方文档docs.docker.com/reference/commandline/cli/

守护程序命令

如果您通过标准存储库安装了docker守护程序,则启动docker守护程序的命令将被添加到init脚本中,以便在启动时自动启动服务。否则,您将需要自己运行docker守护程序,以使客户端命令正常工作。

现在,在启动守护程序时,您可以使用控制 系统DNS)配置、存储驱动程序和容器的执行驱动程序的参数来运行它:

$ export DOCKER_HOST="tcp://0.0.0.0:2375"
$ Docker -d -D -e lxc -s btrfs –-dns 8.8.8.8 –-dns-search example.com

注意

只有在您想要自己启动守护程序时才需要这些。否则,您可以使用$ sudo service Docker start启动docker守护程序。对于 OSX 和 Windows,您需要运行第一章中提到的命令,安装 Docker

以下表格描述了各种标志:

标志 说明

|

-d

这以守护程序运行 Docker。

|

-D

这以调试模式运行 Docker。

|

-e [option]

这是要使用的执行驱动程序。默认的执行驱动程序是本机,它使用libcontainer

|

-s [option]

这会强制 Docker 使用不同的存储驱动程序。默认值为"",Docker 使用 AUFS。

|

--dns [option(s)]

这为所有 Docker 容器设置 DNS 服务器(或服务器)。

|

--dns-search [option(s)]

这为所有 Docker 容器设置 DNS 搜索域(或域)。

|

-H [option(s)]

这是要绑定的套接字(或套接字)。可以是一个或多个tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd

如果同时运行多个docker守护程序,则客户端将遵循DOCKER_HOST参数设置的值。您还可以使用-H标志使其连接到特定的守护程序。

考虑这个命令:

$ docker -H tcp://0.0.0.0:2375 run -it ubuntu /bin/bash

前面的命令与以下命令相同:

$ DOCKER_HOST="tcp://0.0.0.0:2375" docker run -it ubuntu /bin/bash

版本命令

version命令打印版本信息:

$ docker -vDocker version 1.1.1, build bd609d2

信息命令

info命令打印docker守护程序配置的详细信息,例如执行驱动程序、正在使用的存储驱动程序等:

$ docker info # The author is running it in boot2docker on OSX
Containers: 0
Images: 0
Storage Driver: aufs
Root Dir: /mnt/sda1/var/lib/docker/aufs
Dirs: 0
Execution Driver: native-0.2
Kernel Version: 3.15.3-tinycore64
Debug mode (server): true
Debug mode (client): false
Fds: 10
Goroutines: 10
EventsListeners: 0
Init Path: /usr/local/bin/docker
Sockets: [unix:///var/run/docker.sock tcp://0.0.0.0:2375]

run 命令

run 命令是我们将经常使用的命令。它用于运行 Docker 容器:

$ docker run [options] IMAGE [command] [args]

标志 解释

|

-a, --attach=[]

附加到stdinstdoutstderr文件(标准输入,输出和错误文件)。

|

-d, --detach

在后台运行容器。

|

-i, --interactive

以交互模式运行容器(保持stdin文件打开)。

|

-t, --tty

分配伪tty标志(如果要附加到容器的终端,则需要)。

|

-p, --publish=[]

将容器的端口发布到主机(ip:hostport:containerport)。

|

--rm

退出时自动删除容器(不能与-d标志一起使用)。

|

--privileged

这为该容器提供了额外的特权。

|

-v, --volume=[]

绑定挂载卷(从主机=>/host:/container;从 docker=>/container)。

|

--volumes-from=[]

从指定的容器中挂载卷。

|

-w, --workdir=""

这是容器内的工作目录。

|

--name=""

为容器分配一个名称。

|

-h, --hostname=""

为容器分配一个主机名。

|

-u, --user=""

这是容器应该运行的用户名或 UID。

|

-e, --env=[]

设置环境变量。

|

--env-file=[]

从新的行分隔文件中读取环境变量。

|

--dns=[]

设置自定义 DNS 服务器。

|

--dns-search=[]

设置自定义 DNS 搜索域。

|

--link=[]

添加到另一个容器的链接(name:alias)。

|

-c, --cpu-shares=0

这是此容器的相对 CPU 份额。

|

--cpuset=""

这些是允许执行的 CPU;从 0 开始。(例如,0 到 3)。

|

-m, --memory=""

这是此容器的内存限制`(<b k m g>`)。

|

--restart=""

(v1.2+)指定容器崩溃时的重启策略。

|

--cap-add=""

(v1.2+)这向容器授予一个功能(参考第四章,“安全最佳实践”)。

|

--cap-drop=""

(v1.2+)这将把一个功能限制到一个容器中(参考第四章,“安全最佳实践”)。

|

--device=""

(v1.2+)这在容器上挂载设备。

在运行容器时,重要的是要记住,容器的生命周期与启动容器时运行的命令的生命周期相关联。现在尝试运行这个:

$ docker run -dt ubuntu ps
b1d037dfcff6b076bde360070d3af0d019269e44929df61c93dfcdfaf29492c9
$ docker attach b1d037
2014/07/16 16:01:29 You cannot attach to a stopped container, start it first

发生了什么?当我们运行简单命令ps时,容器运行了该命令并退出。因此,我们得到了一个错误。

注意

attach命令将标准输入和输出附加到正在运行的容器上。

这里还有一条重要的信息,您不需要为所有需要容器 ID 的命令使用完整的 64 字符 ID。前面的几个字符就足够了。使用与以下代码中显示的相同示例:

$ docker attach b1d03
2014/07/16 16:09:39 You cannot attach to a stopped container, start it first
$ docker attach b1d0
2014/07/16 16:09:40 You cannot attach to a stopped container, start it first
$ docker attach b1d
2014/07/16 16:09:42 You cannot attach to a stopped container, start it first
$ docker attach b1
2014/07/16 16:09:44 You cannot attach to a stopped container, start it first
$ docker attach b
2014/07/16 16:09:45 Error: No such container: b

一个更方便的方法是自己为容器命名:

$ docker run -dit --name OD-name-example ubuntu /bin/bash
1b21af96c38836df8a809049fb3a040db571cc0cef000a54ebce978c1b5567ea
$ docker attach OD-name-example
root@1b21af96c388:/#

-i标志是必要的,以便在容器中进行任何交互,-t标志是必要的,以创建一个伪终端。

前面的示例还让我们意识到,即使我们退出容器,它仍处于stopped状态。也就是说,我们可以重新启动容器,并保留其文件系统层。您可以通过运行以下命令来查看:

$ docker ps -a
CONTAINER ID IMAGE         COMMAND CREATED    STATUS    NAMES
eb424f5a9d3f ubuntu:latest ps      1 hour ago Exited OD-name-example

虽然这很方便,但很快您的主机磁盘空间可能会耗尽,因为保存了越来越多的容器。因此,如果您要运行一个一次性容器,可以使用--rm标志运行它,这将在进程退出时删除容器:

$ docker run --rm -it --name OD-rm-example ubuntu /bin/bash
root@0fc99b2e35fb:/# exit
exit
$ docker ps -a
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS   PORTS   NAMES

运行服务器

现在,对于我们的下一个示例,我们将尝试运行一个 Web 服务器。选择此示例是因为 Docker 容器最常见的实际用例是运行 Web 应用程序:

$ docker run -it –-name OD-pythonserver-1 --rm python:2.7 \
python -m SimpleHTTPServer 8000;
Serving HTTP on 0.0.0.0 port 8000

现在我们知道问题所在;我们在一个容器中运行了一个服务器,但由于 Docker 动态分配了容器的 IP,这使事情变得困难。但是,我们可以将容器的端口绑定到主机的端口,Docker 会负责转发网络流量。现在让我们再次尝试这个命令,加上-p标志:

$ docker run -p 0.0.0.0:8000:8000 -it --rm –-name OD-pythonserver-2 \ python:2.7 python -m SimpleHTTPServer 8000;
Serving HTTP on 0.0.0.0 port 8000 ...
172.17.42.1 - - [18/Jul/2014 14:25:46] "GET / HTTP/1.1" 200 -

现在打开浏览器,转到http://localhost:8000。大功告成!

如果您是 OS X 用户,并且意识到无法访问http://localhost:8000,那是因为 VirtualBox 尚未配置为响应对 boot2Docker VM 的网络地址转换NAT)请求。将以下函数添加到您的别名文件(bash_profile.bashrc)将节省很多麻烦:

natboot2docker () { VBoxManage controlvm boot2docker-vm natpf1 \
   "$1,tcp,127.0.0.1,$2,,$3"; }

removeDockerNat() {
    VBoxManage modifyvm boot2docker-vm \
    --natpf1 delete $1;
}

之后,您应该能够使用$ natboot2docker mypythonserver 8000 8000命令来访问 Python 服务器。但是请记住,在完成后运行$ removeDockerDockerNat mypythonserver命令。否则,当您下次运行 boot2Docker VM 时,您将面临一个错误,它将不允许您获取 IP 地址或ssh脚本:

$ boot2docker ssh
ssh_exchange_identification: Connection closed by remote host
2014/07/19 11:55:09 exit status 255

您的浏览器现在显示容器的/root路径。如果您想要提供主机的目录怎么办?让我们尝试挂载设备:

root@eb53f7ec79fd:/# mount -t tmpfs /dev/random /mnt
mount: permission denied

正如您所见,mount命令不起作用。实际上,除非包括--privileged标志,否则大多数潜在危险的内核功能都会被禁用。

但是,除非您知道自己在做什么,否则永远不要使用此标志。Docker 提供了一种更简单的方式来绑定挂载主机卷和使用-v–volumes选项绑定挂载主机卷。让我们在我们当前所在的目录中再次尝试这个例子:

$ docker run -v $(pwd):$(pwd) -p 0.0.0.0:8000:8000 -it –rm \
--name OD-pythonserver-3 python:2.7 python -m SimpleHTTPServer 8000;
Serving HTTP on 0.0.0.0 port 8000 ...
10.0.2.2 - - [18/Jul/2014 14:40:35] "GET / HTTP/1.1" 200 -

现在,您已经将您从中运行命令的目录绑定到了容器。但是,当您访问容器时,仍然会得到容器根目录的目录列表。为了提供已绑定到容器的目录,让我们使用-w标志将其设置为容器的工作目录(容器化进程运行的目录):

$ docker run -v $(pwd):$(pwd) -w $(pwd) -p 0.0.0.0:8000:8000 -it \ --name OD-pythonserver-4 python:2.7 python -m SimpleHTTPServer 8000;
Serving HTTP on 0.0.0.0 port 8000 ...
10.0.2.2 - - [18/Jul/2014 14:51:35] "GET / HTTP/1.1" 200 -

注意

Boot2Docker 用户目前还无法利用这一功能,除非您使用了增强功能并设置了共享文件夹,可以在medium.com/boot2docker-lightweight-linux-for-docker/boot2docker-together-with-virtualbox-guest-additions-da1e3ab2465c找到相关指南。尽管这种解决方案有效,但这是一种 hack 方法,不建议使用。与此同时,Docker 社区正在积极寻找解决方案(请查看 boot2Docker GitHub 存储库中的问题#64和 Docker 存储库中的问题#4023)。

现在,http://localhost:8000将提供您当前正在运行的目录,但是从 Docker 容器中提供。但要小心,因为您所做的任何更改也会写入主机的文件系统中。

提示

自 v1.1.1 版本开始,您可以使用$ docker run -v /:/my_host:ro ubuntu ls /my_host将主机的根目录绑定到容器,但是禁止在容器的/路径上进行挂载。

卷可以选择地以:ro:rw命令作为后缀,以只读或读写模式挂载卷。默认情况下,卷以与主机相同的模式(读写或只读)挂载。

此选项主要用于挂载静态资产和写入日志。

但是,如果我想挂载外部设备呢?

在 v1.2 之前,您必须在主机中挂载设备,并在特权容器中使用-v标志进行绑定挂载,但是 v1.2 添加了一个--device标志,您可以使用它来挂载设备,而无需使用--privileged标志。

例如,要在容器中使用网络摄像头,请运行此命令:

$ docker run --device=/dev/video0:/dev/video0

Docker v1.2 还添加了一个--restart标志,用于为容器指定重新启动策略。目前有三种重新启动策略:

  • no:如果容器死掉,则不重新启动(默认)。

  • on-failure:如果以非零退出代码退出,则重新启动容器。它还可以接受一个可选的最大重新启动计数(例如,on-failure:5)。

  • always:无论返回的退出代码是什么,都始终重新启动容器。

以下是一个无限重新启动的示例:

$ docker run --restart=always code.it

下一行用于在放弃之前尝试五次:

$ docker run --restart=on-failure:5 code.it

search 命令

search命令允许我们在公共注册表中搜索 Docker 镜像。让我们搜索与 Python 相关的所有镜像:

$ docker search python | less

pull 命令

pull命令用于从注册表中拉取镜像或仓库。默认情况下,它们从公共 Docker 注册表中拉取,但如果您运行自己的注册表,也可以从中拉取它们:

$ docker pull python # pulls repository from Docker Hub
$ docker pull python:2.7 # pulls the image tagged 2.7
$ docker pull <path_to_registry>/<image_or_repository>

start 命令

我们在讨论docker run时看到,容器状态在退出时会被保留,除非明确删除。docker start命令用于启动已停止的容器:

$ docker start [-i] [-a] <container(s)>

考虑以下start命令的示例:

$ docker ps -a
CONTAINER ID IMAGE         COMMAND   CREATED STATUS    NAMES
e3c4b6b39cff ubuntu:latest python -m 1h ago  Exited OD-pythonserver-4
81bb2a92ab0c ubuntu:latest /bin/bash 1h ago  Exited evil_rosalind
d52fef570d6e ubuntu:latest /bin/bash 1h ago  Exited prickly_morse
eb424f5a9d3f ubuntu:latest /bin/bash 20h ago Exited OD-name-example
$ docker start -ai OD-pythonserver-4
Serving HTTP on 0.0.0.0 port 8000

选项的含义与docker run命令相同。

stop 命令

stop 命令通过发送SIGTERM信号然后在宽限期之后发送SIGKILL信号来停止正在运行的容器:

注意

SIGTERMSIGKILL是 Unix 信号。信号是 Unix、类 Unix 和其他符合 POSIX 的操作系统中使用的一种进程间通信形式。SIGTERM信号指示进程终止。SIGKILL信号用于强制终止进程。

docker run -dit --name OD-stop-example ubuntu /bin/bash
$ docker ps
CONTAINER ID IMAGE         COMMAND   CREATED  STATUS    NAMES
679ece6f2a11 ubuntu:latest /bin/bash 5h ago   Up 3s   OD-stop-example
$ docker stop OD-stop-example
OD-stop-example
$ docker ps
CONTAINER ID IMAGE         COMMAND   CREATED  STATUS    NAMES

您还可以指定-t标志或--time标志,允许您设置等待时间。

restart 命令

restart命令重新启动正在运行的容器:

$ docker run -dit --name OD-restart-example ubuntu /bin/bash
$ sleep 15s # Suspends execution for 15 seconds
$ docker ps
CONTAINER ID IMAGE         COMMAND   STATUS    NAMES
cc5d0ae0b599 ubuntu:latest /bin/bash Up 20s    OD-restart-example

$ docker restart OD-restart-example
$ docker ps
CONTAINER ID IMAGE         COMMAND   STATUS    NAMES
cc5d0ae0b599 ubuntu:latest /bin/bash Up 2s    OD-restart-example

如果您观察状态,您会注意到容器已经重新启动。

rm 命令

rm命令用于删除 Docker 容器:

$ Docker ps -a # Lists containers including stopped ones
CONTAINER ID  IMAGE  COMMAND   CREATED  STATUS NAMES
cc5d0ae0b599  ubuntu /bin/bash 6h ago   Exited OD-restart-example
679ece6f2a11  ubuntu /bin/bash 7h ago   Exited OD-stop-example
e3c4b6b39cff  ubuntu /bin/bash 9h ago   Exited OD-name-example

在我们的冒险之后,似乎有很多容器剩下。让我们移除其中一个:

$ dockerDocker rm OD-restart-example
cc5d0ae0b599

我们还可以组合两个 Docker 命令。让我们将docker ps -a -q命令(打印docker ps -a中容器的 ID 参数)和docker rm命令结合起来,一次性删除所有容器:

$ docker rm $(docker ps -a -q)
679ece6f2a11
e3c4b6b39cff
$ docker ps -a
CONTAINER ID    IMAGE    COMMAND     CREATED    STATUS      NAMES

首先对docker ps -a -q命令进行评估,然后输出由docker rm命令使用。

ps 命令

ps命令用于列出容器。它的使用方式如下:

$ docker ps [option(s)]

标志 解释

|

-a, --all

这显示所有容器,包括已停止的容器。

|

-q, --quiet

这仅显示容器 ID 参数。

|

-s, --size

这打印出容器的大小。

|

-l, --latest

这只显示最新的容器(包括已停止的容器)。

|

-n=""

这显示最后n个容器(包括已停止的容器)。其默认值为-1。

|

--before=****""

这显示了在指定 ID 或名称之前创建的容器。它包括已停止的容器。

|

--after=""

这显示了在指定 ID 或名称之后创建的容器。它包括已停止的容器。

docker ps命令默认只显示正在运行的容器。要查看所有容器,请运行docker ps -a命令。要仅查看容器 ID 参数,请使用-q标志运行它。

logs 命令

logs命令显示容器的日志:

Let us look at the logs of the python server we have been running
$ docker logs OD-pythonserver-4
Serving HTTP on 0.0.0.0 port 8000 ...
10.0.2.2 - - [18/Jul/2014 15:06:39] "GET / HTTP/1.1" 200 -
^CTraceback (most recent call last):
File ...
...
KeyboardInterrupt

你还可以提供一个--tail参数来跟踪容器运行时的输出。

inspect 命令

inspect命令允许你获取容器或镜像的详细信息。它将这些详细信息作为 JSON 数组返回:

$ Docker inspect ubuntu # Running on an image
[{
"Architecture": "amd64",
"Author": "",
"Comment": "",
.......
.......
.......
"DockerVersion": "0.10.0",
"Id": "e54ca5efa2e962582a223ca9810f7f1b62ea9b5c3975d14a5da79d3bf6020f37",
"Os": "linux",
"Parent": "6c37f792ddacad573016e6aea7fc9fb377127b4767ce6104c9f869314a12041e",
"Size": 178365
}]

同样,对于一个容器,我们运行以下命令:

$ Docker inspect OD-pythonserver-4 # Running on a container
[{
"Args": [
"-m",
"SimpleHTTPServer",
"8000"
],
......
......
"Name": "/OD-pythonserver-4",
"NetworkSettings": {
"Bridge": "Docker0",
"Gateway": "172.17.42.1",
"IPAddress": "172.17.0.11",
"IPPrefixLen": 16,
"PortMapping": null,
"Ports": {
"8000/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "8000"
}
]
}
},
......
......
"Volumes": {
"/home/Docker": "/home/Docker"
},
"VolumesRW": {
"/home/Docker": true
}
}]

Docker inspect 提供了关于容器或镜像的所有低级信息。在上面的例子中,找出容器的 IP 地址和暴露的端口,并向IP:port发出请求。你会发现你直接访问了在容器中运行的服务器。

然而,手动查看整个 JSON 数组并不是最佳选择。因此,inspect命令提供了一个标志-f(或--follow标志),允许你使用Go模板精确地指定你想要的内容。例如,如果你只想获取容器的 IP 地址,运行以下命令:

$ docker inspect -f  '{{.NetworkSettings.IPAddress}}' \
OD-pythonserver-4;
172.17.0.11

{{.NetworkSettings.IPAddress}}是在 JSON 结果上执行的Go模板。Go模板非常强大,你可以在golang.org/pkg/text/template/上列出一些你可以用它们做的事情。

top 命令

top命令显示容器中正在运行的进程及其统计信息,模仿 Unix 的top命令。

让我们下载并运行ghost博客平台,并查看其中运行的进程:

$ docker run -d -p 4000:2368 --name OD-ghost dockerfile/ghost
ece88c79b0793b0a49e3d23e2b0b8e75d89c519e5987172951ea8d30d96a2936

$ docker top OD-ghost-1
PID                 USER                COMMAND
1162                root                bash /ghost-start
1180                root                npm
1186                root                sh -c node index
1187                root                node index

是的!我们只需一条命令就设置了我们自己的ghost博客。这带来了另一个微妙的优势,并展示了可能是未来趋势的东西。现在,通过 TCP 端口暴露其服务的每个工具都可以被容器化,并在其自己的沙盒世界中运行。你只需要暴露它的端口并将其绑定到你的主机端口。你不需要担心安装、依赖关系、不兼容性等,卸载将是干净的,因为你只需要停止所有的容器并删除镜像。

注意

Ghost 是一个开源的发布平台,设计精美,易于使用,对所有人免费。它是用 Node.js 编写的,是一个服务器端 JavaScript 执行引擎。

附加命令

attach命令用于附加到正在运行的容器。

让我们启动一个带有 Node.js 的容器,将 node 交互式 shell 作为守护进程运行,然后稍后附加到它。

注意

Node.js 是一个事件驱动的、异步 I/O 的 Web 框架,它在 Google 的 V8 运行环境上运行用 JavaScript 编写的应用程序。

带有 Node.js 的容器如下:

$ docker run -dit --name OD-nodejs shykes/nodejs node
8e0da647200efe33a9dd53d45ea38e3af3892b04aa8b7a6e167b3c093e522754

$ docker attach OD-nodejs
console.log('Docker rocks!');Docker rocks!

杀死命令

kill命令会杀死一个容器,并向容器中运行的进程发送SIGTERM信号:

Let us kill the container running the ghost blog.
$ docker kill OD-ghost-1
OD-ghost-1

$ docker attach OD-ghost-1 # Verification
2014/07/19 18:12:51 You cannot attach to a stopped container, start it first

cp 命令

cp命令将文件或文件夹从容器的文件系统复制到主机路径。路径是相对于文件系统的根目录的。

是时候玩一些游戏了。首先,让我们用/bin/bash命令运行一个 Ubuntu 容器:

$ docker run -it –name OD-cp-bell ubuntu /bin/bash

现在,在容器内部,让我们创建一个带有特殊名称的文件:

# touch $(echo -e '\007')

\ 007字符是 ASCIIBEL字符,当在终端上打印时会响铃系统。你可能已经猜到我们要做什么了。所以让我们打开一个新的终端,并执行以下命令将这个新创建的文件复制到主机:

$ docker cp OD-cp-bell:/$(echo -e '\007') $(pwd)

提示

要使docker cp命令工作,容器路径和主机路径都必须完整,所以不要使用.,*等快捷方式。

所以我们在容器中创建了一个文件名为BEL字符的空文件。然后我们将文件复制到主机容器中的当前目录。只剩最后一步了。在执行docker cp命令的主机标签中,运行以下命令:

$ echo *

你会听到系统铃声响起!我们本可以从容器中复制任何文件或目录到主机。但玩一些游戏也无妨!

注意

如果您觉得这很有趣,您可能会喜欢阅读www.dwheeler.com/essays/fixing-unix-linux-filenames.html。这是一篇很棒的文章,讨论了文件名中的边缘情况,这可能会在程序中引起简单到复杂的问题。

端口命令

port命令查找绑定到容器中公开端口的公共端口:

$ docker port CONTAINER PRIVATE_PORT
$ docker port OD-ghost 2368
4000

Ghost 在2368端口运行一个服务器,允许您编写和发布博客文章。在示例中,我们将主机端口绑定到OD-ghost容器的端口2368

运行您自己的项目

到目前为止,我们已经相当熟悉基本的 Docker 命令。让我们提高赌注。在接下来的几个命令中,我将使用我的一个副业项目。请随意使用您自己的项目。

让我们首先列出我们的要求,以确定我们必须传递给docker run命令的参数。

我们的应用程序将在 Node.js 上运行,因此我们将选择维护良好的dockerfile/nodejs镜像来启动我们的基础容器:

  • 我们知道我们的应用程序将绑定到端口8000,因此我们将将端口暴露给主机的8000端口。

  • 我们需要为容器指定一个描述性名称,以便我们可以在将来的命令中引用它。在这种情况下,让我们选择应用程序的名称:

$ docker run -it --name code.it dockerfile/nodejs /bin/bash
[ root@3b0d5a04cdcd:/data ]$ cd /home
[ root@3b0d5a04cdcd:/home ]$

一旦您启动了容器,您需要检查应用程序的依赖项是否已经可用。在我们的情况下,除了 Node.js 之外,我们只需要 Git,它已经安装在dockerfile/nodejs镜像中。

既然我们的容器已经准备好运行我们的应用程序,剩下的就是获取源代码并进行必要的设置来运行应用程序:

$ git clone https://github.com/shrikrishnaholla/code.it.git
$ cd code.it && git submodule update --init --recursive

这将下载应用程序中使用的插件的源代码。

然后运行以下命令:

$ npm install

现在所有运行应用程序所需的节点模块都已安装。

接下来,运行此命令:

$ node app.js

现在您可以转到localhost:8000来使用该应用程序。

差异命令

diff命令显示容器与其基于的镜像之间的差异。在这个例子中,我们正在运行一个带有code.it的容器。在一个单独的标签中,运行此命令:

$ docker diff code.it
C /home
A /home/code.it
...

提交命令

commit命令使用容器的文件系统创建一个新的镜像。就像 Git 的commit命令一样,您可以设置描述镜像的提交消息:

$ docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

标志 解释

|

-p, --pause

这在提交期间暂停容器(从 v1.1.1+开始可用)。

|

-m, --message=""

这是提交消息。它可以是对图像功能的描述。

|

-a, --author=""

这显示了作者的详细信息。

例如,让我们使用这个命令来提交我们设置的容器:

$ docker commit -m "Code.it – A browser based text editor and interpreter" -a "Shrikrishna Holla <s**a@gmail.com>" code.it shrikrishna/code.it:v1

提示

如果您正在复制这些示例,请替换作者详细信息和图像名称的用户名部分。

输出将是一个冗长的图像 ID。如果您仔细查看命令,我们已经命名了图像shrikrishna/code.it:v1。这是一个约定。图像/存储库名称的第一部分(斜杠之前)是作者的 Docker Hub 用户名。第二部分是预期的应用程序或图像名称。第三部分是一个标签(通常是版本描述),用冒号与第二部分分隔。

注意

Docker Hub是由 Docker,Inc 维护的公共注册表。它托管公共 Docker 图像,并提供帮助您构建和管理 Docker 环境的服务。有关更多详细信息,请访问hub.docker.com

带有不同版本标签的图像集合是一个存储库。通过运行docker commit命令创建的图像将是本地图像,这意味着您将能够从中运行容器,但它不会公开可用。要使其公开或推送到您的私有 Docker 注册表,请使用docker push命令。

images 命令

images命令列出系统中的所有图像:

$ docker images [OPTIONS] [NAME]

标志 说明

|

-a, --all

这显示所有图像,包括中间层。

|

-f, --filter=[]

这提供过滤值。

|

--no-trunc

这不会截断输出(显示完整的 ID)。

|

-q, --quiet

这只显示图像 ID。

现在让我们看一下image命令的几个用法示例:

$ docker images
REPOSITORY           TAG   IMAGE ID       CREATED    VIRTUAL SIZE
shrikrishna/code.it  v1    a7cb6737a2f6   6m ago     704.4 MB

这列出了所有顶层图像,它们的存储库和标签,以及它们的虚拟大小。

Docker 图像只是一堆只读文件系统层。然后,像 AUFS 这样的联合文件系统合并这些层,它们看起来像是一个文件系统。

在 Docker 术语中,只读层就是一个图像。它永远不会改变。当运行一个容器时,进程会认为整个文件系统是可读写的。但是更改只会发生在最顶层的可写层,这是在容器启动时创建的。图像的只读层保持不变。当你提交一个容器时,它会冻结顶层(底层已经冻结)并将其转换为图像。现在,当一个容器启动这个图像时,图像的所有层(包括之前的可写层)都是只读的。所有的更改现在都是在所有底层的顶部创建一个新的可写层。然而,由于联合文件系统(如 AUFS)的工作方式,进程认为文件系统是可读写的。

我们code.it示例中涉及的层次的大致示意图如下:

图像命令

注意

在这一点上,可能明智地考虑联合文件系统需要多大的努力来合并所有这些层,并提供一致的性能。在某个时候,事情不可避免地会出错。例如,AUFS 有一个 42 层的限制。当层数超过这个限制时,它就不允许创建更多的层,构建就会失败。阅读github.com/docker/docker/issues/1171获取更多关于这个问题的信息。

以下命令列出了最近创建的图像:

$ docker images | head

-f标志可以给出key=value类型的参数。它经常用于获取悬空图像的列表:

$ docker images -f "dangling=true"

这将显示未标记的图像,也就是说,已经提交或构建而没有标记的图像。

rmi 命令

rmi命令删除图像。删除一个图像也会删除它所依赖的所有底层图像,并在拉取时下载的图像:

$ docker rmi [OPTION] {IMAGE(s)]

标志 解释

|

-f, --force

这将强制删除图像(或图像)。

|

--no-prune

这个命令不会删除未标记的父级。

这个命令从你的机器中删除一个图像:

$ docker rmi test

保存命令

save命令将图像或存储库保存在一个 tarball 中,并将其流到stdout文件,保留有关图像的父层和元数据:

$ docker save -o codeit.tar code.it

-o标志允许我们指定一个文件而不是流到stdout文件。它用于创建一个备份,然后可以与docker load命令一起使用。

加载命令

load命令从 tarball 中加载图像,恢复文件系统层和与图像相关的元数据:

$ docker load -i codeit.tar

-i标志允许我们指定一个文件,而不是尝试从stdin文件获取流。

导出命令

export命令将容器的文件系统保存为 tarball 并流式传输到stdout文件。它会展平文件系统层。换句话说,它会合并所有文件系统层。在此过程中,图像历史的所有元数据都会丢失:

$ sudo Docker export red_panda > latest.tar

在这里,red_panda是我其中一个容器的名称。

导入命令

import命令创建一个空的文件系统映像,并将 tarball 的内容导入其中。您可以选择为该图像打标签:

$ docker import URL|- [REPOSITORY[:TAG]]

URL 必须以http开头。

$ docker import http://example.com/test.tar.gz # Sample url

如果您想要从本地目录或存档中导入,可以使用-参数从stdin文件中获取数据:

$ cat sample.tgz | docker import – testimage:imported

标签命令

您可以向图像添加tag命令。它有助于识别图像的特定版本。

例如,python图像名称表示python:latest,即可用的最新版本的 Python,这可能会随时更改。但每当它更新时,旧版本都会用相应的 Python 版本标记。因此,python:2.7命令将安装 Python 2.7。因此,tag命令可用于表示图像的版本,或用于需要识别不同图像版本的任何其他目的:

$ docker tag IMAGE [REGISTRYHOST/][USERNAME/]NAME[:TAG]

REGISTRYHOST命令仅在您使用自己的私有注册表时才需要。同一图像可以有多个标签:

$ docker tag shrikrishna/code.it:v1 shrikrishna/code.it:latest

提示

每当您给图像打标签时,请遵循username/repository:tag约定。

现在,再次运行docker images命令将显示相同的图像已被标记为v1latest命令:

$ docker images
REPOSITORY            TAG     IMAGE ID      CREATED     VIRTUAL SIZE
shrikrishna/code.it   v1      a7cb6737a2f6  8 days ago  704.4 MB
shrikrishna/code.it   latest  a7cb6737a2f6  8 days ago  704.4 MB

登录命令

login命令用于注册或登录到 Docker 注册服务器。如果未指定服务器,默认为index.docker.io/v1/

$ Docker login [OPTIONS] [SERVER]

标志 解释

|

-e, --email=""

电子邮件

|

-p, --password=""

密码

|

-u, --username=""

用户名

如果未提供标志,则服务器将提示您提供详细信息。第一次登录后,详细信息将存储在$HOME/.dockercfg路径中。

推送命令

push命令用于将图像推送到公共图像注册表或私有 Docker 注册表:

$ docker push NAME[:TAG]

历史命令

history命令显示图像的历史记录:

$ docker history shykes/nodejs
IMAGE         CREATED        CREATED BY                      SIZE
6592508b0790  15 months ago  /bin/sh -c wget http://nodejs.  15.07 MB
0a2ff988ae20  15 months ago  /bin/sh -c apt-get install ...  25.49 MB
43c5d81f45de  15 months ago  /bin/sh -c apt-get update       96.48 MB
b750fe79269d  16 months ago  /bin/bash                       77 B
27cf78414709  16 months ago                                  175.3 MB

事件命令

一旦启动,events命令会实时打印docker守护程序处理的所有事件:

$ docker events [OPTIONS]

标志 解释

|

--since=""

这显示自 Unix 时间戳以来创建的所有事件。

|

--until=""

这个流事件直到时间戳。

例如,events命令的使用如下:

$ docker events

现在,在另一个标签中,运行以下命令:

$ docker start code.it

然后运行以下命令:

$ docker stop code.it

现在回到运行 Docker 事件的标签并查看输出。它将沿着这些线路进行:

[2014-07-21 21:31:50 +0530 IST] c7f2485863b2c7d0071477e6cb8c8301021ef9036afd4620702a0de08a4b3f7b: (from dockerfile/nodejs:latest) start

[2014-07-21 21:31:57 +0530 IST] c7f2485863b2c7d0071477e6cb8c8301021ef9036afd4620702a0de08a4b3f7b: (from dockerfile/nodejs:latest) stop

[2014-07-21 21:31:57 +0530 IST] c7f2485863b2c7d0071477e6cb8c8301021ef9036afd4620702a0de08a4b3f7b: (from dockerfile/nodejs:latest) die

您可以使用--since--until等标志来获取特定时间范围内的事件日志。

等待命令

wait命令会阻塞,直到容器停止,然后打印其退出代码:

$ docker wait CONTAINER(s)

构建命令

构建命令从指定路径的源文件构建镜像:

$ Docker build [OPTIONS] PATH | URL | -

标志 解释

|

-t, --tag=""

这是要应用于成功时生成的图像的存储库名称(和可选标签)。

|

-q, --quiet

这会抑制默认情况下冗长的输出。

|

--rm=true

这会在成功构建后删除中间容器。

|

--force-rm

这总是在构建失败后删除中间容器。

|

--no-cache

此命令在构建镜像时不使用缓存。

此命令使用 Dockerfile 和上下文来构建 Docker 镜像。

Dockerfile 就像一个 Makefile。它包含了各种配置和命令的指令,需要运行以创建一个镜像。我们将在下一节中讨论编写 Dockerfiles。

提示

最好先阅读关于 Dockerfiles 的部分,然后再回到这里,以更好地理解这个命令以及它是如何工作的。

PATHURL路径下的文件被称为构建的上下文。上下文用于指代 Dockerfile 中的文件或文件夹,例如在ADD指令中(这就是为什么诸如ADD ../file.txt这样的指令不起作用。它不在上下文中!)。

当给出 GitHub URL 或带有git://协议的 URL 时,该存储库将被用作上下文。该存储库及其子模块将递归克隆到您的本地机器,然后作为上下文上传到docker守护程序。这允许您在私人 Git 存储库中拥有 Dockerfiles,您可以从本地用户凭据或虚拟私人网络VPN)访问。

上传到 Docker 守护程序

请记住,Docker 引擎既有docker守护程序又有 Docker 客户端。您作为用户给出的命令是通过 Docker 客户端,然后再与docker守护程序(通过 TCP 或 Unix 套接字)进行通信,它会执行必要的工作。docker守护程序和 Docker 主机可以在不同的主机上(这是 boot2Docker 的前提),并且DOCKER_HOST环境变量设置为远程docker守护程序的位置。

当您为docker build命令提供上下文时,本地目录中的所有文件都会被打包并发送到docker守护程序。PATH变量指定了在docker守护程序中构建上下文的文件的位置。因此,当您运行docker build .时,当前文件夹中的所有文件都会被上传,而不仅仅是 Dockerfile 中列出要添加的文件。

由于这可能会有些问题(因为一些系统如 Git 和一些 IDE 如 Eclipse 会创建隐藏文件夹来存储元数据),Docker 提供了一种机制来忽略某些文件或文件夹,方法是在PATH变量中创建一个名为.dockerignore的文件,并添加必要的排除模式。例如,查看github.com/docker/docker/blob/master/.dockerignore

如果提供了一个普通的 URL,或者 Dockerfile 通过stdin文件流传输,那么不会设置上下文。在这些情况下,只有当ADD指令引用远程 URL 时才起作用。

现在让我们通过 Dockerfile 构建code.it示例图像。如何创建这个 Dockerfile 的说明在Dockerfile部分提供。

到目前为止,您已经创建了一个目录,并在其中放置了 Dockerfile。现在,在您的终端上,转到该目录并执行docker build命令:

$ docker build -t shrikrishna/code.it:docker Dockerfile .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM Dockerfile/nodejs
---> 1535da87b710
Step 1 : MAINTAINER Shrikrishna Holla <s**a@gmail.com>
---> Running in e4be61c08592
---> 4c0eabc44a95
Removing intermediate container e4be61c08592
Step 2 : WORKDIR /home
---> Running in 067e8951cb22
---> 81ead6b62246
Removing intermediate container 067e8951cb22
. . . . .
. . . . .
Step 7 : EXPOSE  8000
---> Running in 201e07ec35d3
---> 1db6830431cd
Removing intermediate container 201e07ec35d3
Step 8 : WORKDIR /home
---> Running in cd128a6f090c
---> ba05b89b9cc1
Removing intermediate container cd128a6f090c
Step 9 : CMD     ["/usr/bin/node", "/home/code.it/app.js"]
---> Running in 6da5d364e3e1
---> 031e9ed9352c
Removing intermediate container 6da5d364e3e1
Successfully built 031e9ed9352c

现在,您将能够在 Docker 镜像的输出中查看您新构建的图像

REPOSITORY          TAG        IMAGE ID     CREATED      VIRTUAL SIZE
shrikrishna/code.it Dockerfile 031e9ed9352c 21 hours ago 1.02 GB

要查看缓存的实际效果,请再次运行相同的命令

$ docker build -t shrikrishna/code.it:dockerfile .
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM dockerfile/nodejs
---> 1535da87b710
Step 1 : MAINTAINER Shrikrishna Holla <s**a@gmail.com>
---> Using cache
---> 4c0eabc44a95
Step 2 : WORKDIR /home
---> Using cache
---> 81ead6b62246
Step 3 : RUN     git clone https://github.com/shrikrishnaholla/code.it.git
---> Using cache
---> adb4843236d4
Step 4 : WORKDIR code.it
---> Using cache
---> 755d248840bb
Step 5 : RUN     git submodule update --init --recursive
---> Using cache
---> 2204a519efd3
Step 6 : RUN     npm install
---> Using cache
---> 501e028d7945
Step 7 : EXPOSE  8000
---> Using cache
---> 1db6830431cd
Step 8 : WORKDIR /home
---> Using cache
---> ba05b89b9cc1
Step 9 : CMD     ["/usr/bin/node", "/home/code.it/app.js"]
---> Using cache
---> 031e9ed9352c
Successfully built 031e9ed9352c

提示

现在尝试使用缓存。更改中间的一行(例如端口号),或者在中间的某个地方添加一个RUN echo "testing cache"行,看看会发生什么。

使用存储库 URL 构建图像的示例如下:

$ docker build -t shrikrishna/optimus:git_url \ git://github.com/shrikrishnaholla/optimus
Sending build context to Docker daemon 1.305 MB
Sending build context to Docker daemon
Step 0 : FROM        dockerfile/nodejs
---> 1535da87b710
Step 1 : MAINTAINER  Shrikrishna Holla
---> Running in d2aae3dba68c
---> 0e8636eac25b
Removing intermediate container d2aae3dba68c
Step 2 : RUN         git clone https://github.com/pesos/optimus.git /home/optimus
---> Running in 0b46e254e90a
. . . . .
. . . . .
. . . . .
Step 5 : CMD         ["/usr/local/bin/npm", "start"]
---> Running in 0e01c71faa0b
---> 0f0dd3deae65
Removing intermediate container 0e01c71faa0b
Successfully built 0f0dd3deae65

Dockerfile

我们已经看到了通过提交容器来创建镜像。如果您想要使用依赖项的新版本或您自己应用程序的新版本来更新镜像怎么办?反复启动、设置和提交步骤很快就变得不切实际。我们需要一种可重复的方法来构建镜像。这就是 Dockerfile 的作用,它不过是一个包含指令的文本文件,用于自动化构建镜像的步骤。docker build将按顺序读取这些指令,途中提交它们,并构建一个镜像。

docker build命令使用此 Dockerfile 和上下文来执行指令,并构建 Docker 镜像。上下文是指提供给docker build命令的路径或源代码仓库 URL。

Dockerfile 以这种格式包含指令:

# Comment
INSTRUCTION arguments

任何以#开头的行将被视为注释。如果#符号出现在其他地方,它将被视为参数的一部分。指令不区分大小写,尽管按照惯例,指令应该大写以便与参数区分开。

让我们看看在 Dockerfile 中可以使用的指令。

FROM指令

FROM指令设置了后续指令的基础镜像。有效的 Dockerfile 的第一行非注释行将是一个FROM指令:

FROM <image>:<tag>

镜像可以是任何有效的本地或公共镜像。如果在本地找不到,Docker build命令将尝试从公共注册表中拉取。这里tag命令是可选的。如果没有给出,将假定为latest命令。如果给出了不正确的tag命令,将返回错误。

MAINTAINER指令

MAINTAINER指令允许您为生成的镜像设置作者:

MAINTAINER <name>

RUN指令

RUN指令将在当前镜像的新层上执行任何命令,并提交此镜像。因此提交的镜像将用于 Dockerfile 中的下一条指令。

RUN指令有两种形式:

  • RUN <command>形式

  • RUN ["executable", "arg1", "arg2"...]形式

在第一种形式中,命令在 shell 中运行,具体来说是/bin/sh -c <command> shell。第二种形式在基础镜像没有/bin/sh shell 的情况下很有用。Docker 对这些镜像构建使用缓存。因此,如果您的镜像构建在中间某个地方失败,下一次运行将重用先前成功的部分构建,并从失败的地方继续。

在以下情况下,缓存将被使无效:

  • 当使用--no-cache标志运行docker build命令时。

  • 如果给出了诸如apt-get update之类的不可缓存命令,则所有后续的RUN指令将再次运行。

  • 当首次遇到ADD指令时,如果上下文的内容发生了变化,将使 Dockerfile 中所有后续指令的缓存无效。这也将使RUN指令的缓存无效。

CMD 指令

CMD指令提供了容器执行的默认命令。它有以下形式:

  • CMD ["executable", "arg1", "arg2"...]形式

  • CMD ["arg1", "arg2"...]形式

  • CMD command arg1 arg2 …形式

第一种形式类似于一个 exec,这是首选形式,其中第一个值是可执行文件的路径,后面跟着它的参数。

第二种形式省略了可执行文件,但需要ENTRYPOINT指令来指定可执行文件。

如果您使用CMD指令的 shell 形式,那么<command>命令将在/bin/sh -c shell 中执行。

注意

如果用户在docker run中提供了一个命令,它将覆盖CMD命令。

RUNCMD指令之间的区别在于,RUN指令实际上运行命令并提交它,而CMD指令在构建时不会被执行。这是一个默认的命令,在用户启动容器时运行,除非用户提供了一个启动命令。

例如,让我们编写一个Dockerfile,将Star Wars的输出带到您的终端:

FROM ubuntu:14.04
MAINTAINER shrikrishna
RUN apt-get -y install telnet
CMD ["/usr/bin/telnet", "towel.blinkenlights.nl"]

将其保存在名为star_wars的文件夹中,并在此位置打开您的终端。然后运行此命令:

$ docker build -t starwars .

现在您可以使用以下命令运行它:

$ docker run -it starwars

以下截图显示了starwars的输出:

CMD 指令

因此,您可以在终端上观看星球大战

注意

这个星球大战致敬是由 Simon Jansen,Sten Spans 和 Mike Edwards 创建的。当您已经看够时,按住Ctrl + ]。您将收到一个提示,您可以在其中输入close以退出。

ENTRYPOINT 指令

ENTRYPOINT指令允许你将 Docker 镜像变成一个可执行文件。换句话说,当你在ENTRYPOINT中指定一个可执行文件时,容器将运行得就像是那个可执行文件一样。

ENTRYPOINT指令有两种形式:

  1. ENTRYPOINT ["executable", "arg1", "arg2"...]形式。

  2. ENTRYPOINT command arg1 arg2 …形式。

这个指令添加了一个入口命令,当参数传递给docker run命令时,不会被覆盖,不像CMD指令的行为。这允许参数传递给ENTRYPOINT指令。docker run <image> -arg命令将-arg参数传递给ENTRYPOINT指令中指定的命令。

如果在ENTRYPOINT指令中指定了参数,它们不会被docker run的参数覆盖,但是通过CMD指令指定的参数会被覆盖。

例如,让我们编写一个带有cowsayENTRYPOINT指令的 Dockerfile:

注意

cowsay是一个生成带有消息的牛的 ASCII 图片的程序。它还可以使用其他动物的预制图片生成图片,比如 Tux 企鹅,Linux 吉祥物。

FROM ubuntu:14.04
RUN apt-get -y install cowsay
ENTRYPOINT ["/usr/games/cowsay"]
CMD ["Docker is so awesomoooooooo!"]

将其保存为名为Dockerfile的文件,放在名为cowsay的文件夹中。然后通过终端,进入该目录,并运行以下命令:

$ docker build -t cowsay .

构建完镜像后,运行以下命令:

$ docker run cowsay

以下截图显示了前面命令的输出:

ENTRYPOINT 指令

如果你仔细看截图,第一次运行没有参数,并且使用了我们在 Dockerfile 中配置的参数。然而,当我们在第二次运行中给出自己的参数时,它覆盖了默认值,并将所有参数(-f标志和句子)传递给了cowsay文件夹。

注意

如果你是那种喜欢恶作剧的人,这里有一个提示:应用superuser.com/a/175802中给出的指令来设置一个预执行脚本(每次执行命令时调用的函数),将每个命令传递给这个 Docker 容器,并将其放在.bashrc文件中。现在,cowsay 将打印出它在文本气球中执行的每个命令,由一个 ASCII 牛说出来!

WORKDIR 指令

WORKDIR指令为接下来的RUNCMDENTRYPOINT Dockerfile 命令设置工作目录:

WORKDIR /path/to/working/directory

此指令可以在同一个 Dockerfile 中多次使用。如果提供了相对路径,则WORKDIR指令将相对于先前的WORKDIR指令的路径。

EXPOSE 指令

EXPOSE指令通知 Docker 在启动容器时要公开某个端口:

EXPOSE port1 port2 …

即使在暴露端口之后,在启动容器时,仍然需要使用-p标志来提供端口映射给Docker run。这个指令在链接容器时很有用,我们将在第三章中看到链接容器

ENV 指令

ENV 命令用于设置环境变量:

ENV <key> <value>

这将把<key>环境变量设置为<value>。这个值将传递给所有未来的RUN指令。这相当于在命令前加上<key>=<value>

使用ENV命令设置的环境变量将持久存在。这意味着当从生成的镜像运行容器时,环境变量也将对运行的进程可用。docker inspect命令显示了在创建镜像过程中分配的值。但是,可以使用$ docker run –env <key>=<value>命令覆盖这些值。

USER 指令

USER 指令设置在运行镜像和任何后续RUN指令时要使用的用户名或 UID:

USER xyz

VOLUME 指令

VOLUME指令将创建一个具有给定名称的挂载点,并将其标记为保存来自主机或其他容器的外部挂载卷:

VOLUME [path]

以下是VOLUME指令的示例:

VOLUME ["/data"]

以下是此指令的另一个示例:

VOLUME /var/log

两种格式都可以接受。

ADD 指令

ADD指令用于将文件复制到镜像中:

ADD <src> <dest>

ADD指令将文件从<src>复制到<dest>的路径中。

<src>路径必须是相对于正在构建的源目录(也称为构建上下文)的文件或目录的路径,或者是远程文件 URL。

<dest>路径是源将被复制到目标容器内部的绝对路径。

注意

如果通过stdin文件(docker build - < somefile)构建 Dockerfile,则没有构建上下文,因此 Dockerfile 只能包含基于 URL 的ADD语句。您还可以通过stdin文件(docker build - < archive.tar.gz)传递压缩存档。Docker 将在存档的根目录查找 Dockerfile,并且存档的其余部分将用作构建的上下文。

ADD指令遵循以下规则:

  • <src>路径必须在构建的上下文中。您不能使用ADD ../file as ..语法,因为它超出了上下文。

  • 如果<src>是一个 URL,并且<dest>路径不以斜杠结尾(它是一个文件),则将 URL 处的文件复制到<dest>路径。

  • 如果<src>是一个 URL,并且<dest>路径以斜杠结尾(它是一个目录),则会获取 URL 处的内容,并且会从 URL 中推断出一个文件名,并将其保存到<dest>/filename路径中。因此,在这种情况下,URL 不能具有简单的路径,例如example.com

  • 如果<src>是一个目录,则整个目录将被复制,连同文件系统元数据一起。

  • 如果<src>是本地 tar 存档,则它将被提取到<dest>路径中。<dest>处的结果是:

  • <dest>路径处存在的任何内容。

  • 提取的 tar 存档的内容,以文件为基础解决冲突,优先考虑<src>路径。

  • 如果<dest>路径不存在,则将创建该路径以及其路径中的所有缺失目录。

COPY 指令

COPY 指令将文件复制到镜像中:

COPY <src> <dest>

COPY指令类似于ADD指令。不同之处在于COPY指令不允许超出上下文的任何文件。因此,如果您通过stdin文件或 URL(指向源代码存储库的 URL)流式传输 Dockerfile,则无法使用COPY指令。

ONBUILD 指令

ONBUILD指令将触发器添加到镜像中,当镜像用作另一个构建的基础镜像时,将执行该触发器。

ONBUILD [INSTRUCTION]

当源应用程序涉及需要在使用之前编译的生成器时,这是有用的。除了FROMMAINTAINERONBUILD指令之外的任何构建指令都可以注册。

以下是此指令的工作方式:

  1. 在构建过程中,如果遇到ONBUILD指令,它会注册一个触发器并将其添加到镜像的元数据中。当前构建不会以任何方式受到影响。

  2. 所有这些触发器的列表都被添加到镜像清单中,作为一个名为OnBuild的键,在构建结束时可以通过Docker inspect命令看到。

  3. 当这个镜像后来被用作新构建的基础镜像时,在处理FROM指令的过程中,OnBuild key触发器按照注册的顺序被读取和执行。如果其中任何一个失败,FROM指令将中止,导致构建失败。否则,FROM指令完成,构建将继续进行。

  4. 触发器在执行后会从最终镜像中清除。换句话说,它们不会被grand-child builds继承。

让我们把cowsay带回来!这是一个带有ONBUILD指令的 Dockerfile:

FROM ubuntu:14.04
RUN apt-get -y install cowsay
RUN apt-get -y install fortune
ENTRYPOINT ["/usr/games/cowsay"]
CMD ["Docker is so awesomoooooooo!"]
ONBUILD RUN /usr/games/fortune | /usr/games/cowsay

现在将这个文件保存在一个名为OnBuild的文件夹中,打开该文件夹中的终端,并运行这个命令:

$ Docker build -t shrikrishna/onbuild .

我们需要编写另一个基于这个镜像的 Dockerfile。让我们写一个:

FROM shrikrishna/onbuild
RUN  apt-get moo
CMD ['/usr/bin/apt-get', 'moo']

注意

apt-get moo命令是许多开源工具中通常找到的彩蛋的一个例子,只是为了好玩!

构建这个镜像现在将执行我们之前给出的ONBUILD指令:

$ docker build -t shrikrishna/apt-moo apt-moo/
Sending build context to Docker daemon  2.56 kB
Sending build context to Docker daemon
Step 0 : FROM shrikrishna/onbuild
# Executing 1 build triggers
Step onbuild-0 : RUN /usr/games/fortune | /usr/games/cowsay
---> Running in 887592730f3d
________________________________
/ It was all so different before \
\ everything changed.            /
--------------------------------
\   ^__^
\  (oo)\_______
(__)\       )\/\
||----w |
||     ||
---> df01e4ca1dc7
---> df01e4ca1dc7
Removing intermediate container 887592730f3d
Step 1 : RUN  apt-get moo
---> Running in fc596cb91c2a
(__)
(oo)
/------\/
/ |    ||
*  /\---/\
~~   ~~
..."Have you mooed today?"...
---> 623cd16a51a7
Removing intermediate container fc596cb91c2a
Step 2 : CMD ['/usr/bin/apt-get', 'moo']
---> Running in 22aa0b415af4
---> 7e03264fbb76
Removing intermediate container 22aa0b415af4
Successfully built 7e03264fbb76

现在让我们利用我们新获得的知识来为我们之前通过手动满足容器中的依赖关系并提交构建的code.it应用程序编写一个 Dockerfile。Dockerfile 看起来会像这样:

# Version 1.0
FROM dockerfile/nodejs
MAINTAINER Shrikrishna Holla <s**a@gmail.com>

WORKDIR /home
RUN     git clone \ https://github.com/shrikrishnaholla/code.it.git

WORKDIR code.it
RUN     git submodule update --init --recursive
RUN     npm install

EXPOSE  8000

WORKDIR /home
CMD     ["/usr/bin/node", "/home/code.it/app.js"]

创建一个名为code.it的文件夹,并将这个内容保存为一个名为Dockerfile的文件。

注意

即使不需要上下文,为每个 Dockerfile 创建一个单独的文件夹是一个很好的做法。这样可以在不同的项目之间分离关注点。当你继续前进时,你可能会注意到许多 Dockerfile 作者会将RUN指令合并在一起(例如,查看dockerfile.github.io中的 Dockerfile)。原因是 AUFS 将可能的层的数量限制为 42。更多信息,请查看github.com/docker/docker/issues/1171

你可以回到Docker build部分,看看如何从这个 Dockerfile 构建一个镜像。

Docker 工作流程 - 拉取-使用-修改-提交-推送

现在,当我们接近本章的结束时,我们可以猜测一个典型的 Docker 工作流程是什么样的:

  1. 准备运行应用程序的要求清单。

  2. 确定哪个公共图像(或您自己的图像)可以满足大多数要求,同时也要维护良好(这很重要,因为您需要图像在可用时更新为新版本)。

  3. 接下来,通过运行容器并执行满足要求的命令(可以是安装依赖项、绑定挂载卷或获取源代码),或者编写 Dockerfile(这更可取,因为您将能够使构建可重复)来满足其余要求。

  4. 将新图像推送到公共 Docker 注册表,以便社区也可以使用它(或者根据需要推送到私有注册表或存储库)。

自动构建

自动构建自动化了从 GitHub 或 BitBucket 直接在 Docker Hub 上构建和更新图像。它们通过向您选择的 GitHub 或 BitBucket 存储库添加commit挂钩来工作,在您推送提交时触发构建和更新。因此,每次更新时都不需要手动构建和推送图像到 Docker Hub。以下步骤将向您展示如何执行此操作:

  1. 要设置自动构建,请登录到您的 Docker Hub 帐户。自动构建

  2. 通过链接 帐户菜单链接您的 GitHub 或 BitBucket 帐户。

  3. 添加 存储库菜单中选择自动 构建自动构建

  4. 选择包含您想要构建的 Dockerfile 的 GitHub 或 BitBucket 项目。(您需要授权 Docker Hub 访问您的存储库。)

  5. 选择包含源代码和 Dockerfile 的分支(默认为主分支)。

  6. 为自动构建命名。这也将是存储库的名称。

  7. 为构建分配一个可选的 Docker 标签。默认为lastest标签。

  8. 指定 Dockerfile 的位置。默认为/自动构建

一旦配置完成,自动构建将触发构建,并且您将能够在几分钟内在 Docker Hub 注册表中看到它。它将与您的 GitHub 和 BitBucket 存储库保持同步,直到您自己停用自动构建。

构建状态和历史记录可以在 Docker Hub 中您的个人资料的自动构建页面中查看。

自动构建

创建自动构建后,您可以停用或删除它。

注意

但是,您不能使用 Docker 的push命令推送到自动构建。您只能通过向 GitHub 或 BitBucket 存储库提交代码来管理它。

您可以为每个存储库创建多个自动构建,并将它们配置为指向特定的 Dockerfile 或 Git 分支。

构建触发器

也可以通过 Docker Hub 上的 URL 触发自动构建。这允许您根据需要重新构建自动构建的图像。

Webhooks

Webhooks 是在成功构建事件发生时调用的触发器。通过 webhook,您可以指定目标 URL(例如通知您的服务)和在推送图像时将传递的 JSON 有效负载。如果您有持续集成工作流程,webhooks 非常有用。

要将 webhook 添加到您的 Github 存储库,请按照以下步骤进行:

  1. 转到存储库中的SettingsWebhooks

  2. 从左侧菜单栏转到Webhooks and ServicesWebhooks

  3. 单击Add ServiceWebhooks

  4. 在打开的文本框中,输入Docker并选择该服务。Webhooks

  5. 您已经准备就绪!现在每当您提交到存储库时,Docker Hub 都会触发构建。

摘要

在本章中,我们查看了Docker命令行工具并尝试了可用的命令。然后,我们找出如何使用 Dockerfile 使构建可重复。此外,我们使用 Docker Hub 的自动构建服务自动化了此构建过程。

在下一章中,我们将尝试通过查看帮助我们配置它们的各种命令来更好地控制容器的运行方式。我们将研究限制容器可消耗的资源(CPU、RAM 和存储)的数量。

第三章:配置 Docker 容器

在上一章中,我们看到了 Docker 中所有可用的不同命令。我们看了一些示例,涵盖了如何拉取镜像、运行容器、将镜像附加到容器、提交并将镜像推送到存储库的过程。我们还学习了如何编写 Dockerfile,使构建镜像成为一个可重复的过程。

在本章中,我们将更仔细地了解如何控制容器的运行方式。尽管 Docker 容器被隔离,但这并不能阻止其中一个容器中的流浪进程占用其他容器(包括主机)可用的资源。例如,要小心这个命令(不要运行它):

$ docker run ubuntu /bin/bash -c ":(){ :|:& };:"

通过运行前面的命令,您将 fork bomb 容器以及运行它的主机。

fork bomb的维基百科定义如下:

"在计算机中,fork bomb 是一种拒绝服务攻击,其中一个进程不断复制自身以耗尽可用的系统资源,导致资源匮乏并减慢或崩溃系统。"

由于预计 Docker 将用于生产,一个容器使所有其他容器停滞的可能性将是致命的。因此,有机制来限制容器可以拥有的资源量,我们将在本章中进行讨论。

在上一章中,当我们谈论docker run 时,我们对卷进行了基本介绍。现在我们将更详细地探讨卷,并讨论它们为什么重要以及如何最好地使用它们。我们还将尝试更改docker守护程序使用的存储驱动程序。

另一个方面是网络。在检查运行中的容器时,您可能已经注意到 Docker 会随机选择一个子网并分配一个 IP 地址(默认通常是范围 172.17.42.0/16)。我们将尝试通过设置自己的子网来覆盖这一点,并探索其他可用的帮助管理网络方面的选项。在许多情况下,我们需要在容器之间进行通信(想象一个容器运行您的应用程序,另一个容器运行您的数据库)。由于 IP 地址在构建时不可用,我们需要一种机制来动态发现在其他容器中运行的服务。我们将探讨实现这一点的方法,无论容器是在同一主机上运行还是在不同主机上运行。

简而言之,在本章中,我们将涵盖以下主题:

  • 限制资源

  • CPU

  • RAM

  • 存储

  • 使用卷在容器中管理数据

  • 配置 Docker 使用不同的存储驱动程序

  • 配置网络

  • 端口转发

  • 自定义 IP 地址范围

  • 链接容器

  • 使用容器链接在同一主机内进行链接

  • 使用大使容器进行跨主机链接

约束资源

对于任何承诺提供沙箱功能的工具来说,提供一种约束资源分配的机制是至关重要的。Docker 在容器启动时提供了限制 CPU 内存和 RAM 使用量的机制。

设置 CPU 份额

可以使用docker run命令中的-c选项来控制容器所占用的 CPU 份额:

$ docker run -c 10 -it ubuntu /bin/bash

10是相对于其他容器给予该容器的优先级。默认情况下,所有容器都具有相同的优先级,因此具有相同的 CPU 处理周期比率,您可以通过运行$ cat /sys/fs/cgroup/cpu/docker/cpu.shares来检查(如果您使用的是 OS X 或 Windows,请在执行此操作之前将 SSH 添加到 boot2Docker VM)。但是,您可以在运行容器时提供自己的优先级值。

在容器已经运行时设置 CPU 份额是否可能?是的。编辑/sys/fs/cgroup/cpu/docker/<container-id>/cpu.shares文件,并输入您想要给它的优先级。

注意

如果提到的位置不存在,请通过运行命令$ grep -w cgroup /proc/mounts | grep -w cpu找出cpu cgroup挂载的位置。

然而,这是一个 hack,如果 Docker 决定改变 CPU 共享的实现方式,将来可能会发生变化。有关更多信息,请访问groups.google.com/forum/#!topic/docker-user/-pP8-KgJJGg

设置内存限制

类似地,容器被允许消耗的 RAM 量在启动容器时也可以受到限制:

$ docker run -m <value><optional unit>

在这里,unit可以是bkmg,分别表示字节、千字节、兆字节和千兆字节。

单位的示例可以表示如下:

$ docker run -m 1024m -dit ubuntu /bin/bash

这为容器设置了 1GB 的内存限制。

与限制 CPU 份额一样,您可以通过运行以下代码来检查默认的内存限制:

$ cat /sys/fs/cgroup/memory/docker/memory.limit_in_bytes
18446744073709551615

正如文件名所示,前面的代码以字节为单位打印限制。输出中显示的值对应于 1.8 x 1010 千字节,这实际上意味着没有限制。

在容器已经运行时设置内存限制是否可能?

与 CPU 份额一样,内存限制是通过cgroup文件强制执行的,这意味着我们可以通过更改容器的cgroup内存文件的值来动态更改限制:

$ echo 1073741824 > \ /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes

注意

如果cgroup文件的位置不存在,请通过运行$ grep -w cgroup /proc/mounts | grep -w memory找出文件的挂载位置。

这也是一个黑客,如果 Docker 决定改变内部实现内存限制的方式,可能会在将来发生变化。

有关此更多信息,请访问groups.google.com/forum/#!topic/docker-user/-pP8-KgJJGg

在虚拟文件系统(Devicemapper)上设置存储限制

限制磁盘使用可能有点棘手。没有直接的方法来限制容器可以使用的磁盘空间的数量。默认存储驱动程序 AUFS 不支持磁盘配额,至少没有没有黑客(困难是因为 AUFS 没有自己的块设备。访问aufs.sourceforge.net/aufs.html以获取有关 AUFS 工作原理的深入信息)。在撰写本书时,需要磁盘配额的 Docker 用户选择devicemapper驱动程序,该驱动程序允许每个容器使用一定量的磁盘空间。但是,正在进行跨存储驱动程序的更通用机制,并且可能会在将来的版本中引入。

注意

devicemapper驱动程序是用于将块设备映射到更高级虚拟块设备的 Linux 内核框架。

devicemapper驱动程序基于两个块设备(将它们视为虚拟磁盘),一个用于数据,另一个用于元数据,创建一个存储块的thin池。默认情况下,这些块设备是通过将稀疏文件挂载为回环设备来创建的。

注意

稀疏文件是一个文件,其中大部分是空白空间。因此,100 GB 的稀疏文件实际上可能只包含一点字节在开头和结尾(并且只占据磁盘上的这些字节),但对应用程序来说,它可能是一个 100 GB 的文件。在读取稀疏文件时,文件系统会在运行时将空块透明地转换为实际填充了零字节的实际块。它通过文件的元数据跟踪已写入和空块的位置。在类 UNIX 操作系统中,回环设备是一个伪设备,它使文件作为块设备可访问。

“薄”池之所以被称为“薄”,是因为只有在实际写入块时,它才将存储块标记为已使用(来自池)。每个容器都被配置了一个特定大小的基础薄设备,容器不允许累积超过该大小限制的数据。

默认限制是什么?“薄”池的默认限制为 100 GB。但由于用于此池的回环设备是稀疏文件,因此最初不会占用这么多空间。

为每个容器和镜像创建的基础设备的默认大小限制为 10 GB。同样,由于这是稀疏的,因此最初不会占用物理磁盘上的这么多空间。但是,随着大小限制的增加,它占用的空间也会增加,因为块设备的大小越大,稀疏文件的(虚拟)大小就越大,需要存储的元数据也越多。

如何更改这些默认值?您可以在运行docker守护程序时使用--storage-opts选项更改这些选项,该选项带有dm(用于devicemapper)前缀。

注意

在运行本节中的任何命令之前,请使用docker save备份所有镜像并停止docker守护程序。完全删除/var/lib/docker(Docker 存储图像数据的路径)可能也是明智的。

Devicemapper 配置

可用的各种配置如下:

  • dm.basesize:这指定了基础设备的大小,容器和镜像将使用它。默认情况下,这被设置为 10 GB。创建的设备是稀疏的,因此最初不会占用 10 GB。相反,它将随着数据写入而填满,直到达到 10 GB 的限制。
$ docker -d -s devicemapper --storage-opt dm.basesize=50G

  • dm.loopdatasize:这是“薄”池的大小。默认大小为 100 GB。需要注意的是,这个文件是稀疏的,因此最初不会占用这个空间;相反,随着越来越多的数据被写入,它将逐渐填满:
$ docker -d -s devicemapper --storage-opt dm.loopdatasize=1024G

  • dm.loopmetadatasize:如前所述,创建了两个块设备,一个用于数据,另一个用于元数据。此选项指定创建此块设备时要使用的大小限制。默认大小为 2 GB。这个文件也是稀疏的,因此最初不会占用整个大小。建议的最小大小是总池大小的 1%:
$ docker -d -s devicemapper --storage-opt dm.loopmetadatasize=10G

  • dm.fs:这是用于基础设备的文件系统类型。支持ext4xfs文件系统,尽管默认情况下采用ext4
$ docker -d -s devicemapper --storage-opt dm.fs=xfs

  • dm.datadev:这指定要使用的自定义块设备(而不是回环)用于thin池。如果您使用此选项,建议同时为数据和元数据指定块设备,以完全避免使用回环设备:
$ docker -d -s devicemapper --storage-opt dm.datadev=/dev/sdb1 \-storage-opt dm.metadatadev=/dev/sdc1

还有更多选项可用,以及关于所有这些工作原理的清晰解释,请参阅github.com/docker/docker/tree/master/daemon/graphdriver/devmapper/README.md

另一个很好的资源是 Docker 贡献者 Jérôme Petazzoni 在jpetazzo.github.io/2014/01/29/docker-device-mapper-resize/上发布的有关调整容器大小的博文。

注意

如果切换存储驱动程序,旧容器和图像将不再可见。

在本节的开头提到了有可能通过一个黑客手段实现配额并仍然使用 AUFS。这个黑客手段涉及根据需要创建基于ext4文件系统的回环文件系统,并将其作为容器的卷进行绑定挂载:

$ DIR=$(mktemp -d)
$ DB_DIR=(mktemp -d)
$ dd if=/dev/zero of=$DIR/data count=102400
$ yes | mkfs -t ext4 $DIR/data
$ mkdir $DB_DIR/db
$ sudo mount -o loop=/dev/loop0 $DIR/data $DB_DIR

您现在可以使用docker run命令的-v选项将$DB_DIR目录绑定到容器:

$ docker run -v $DB_DIR:/var/lib/mysql mysql mysqld_safe.

使用卷管理容器中的数据

Docker 卷的一些显着特点如下所述:

  • 卷是与容器的root文件系统分开的目录。

  • 它由docker守护程序直接管理,并可以在容器之间共享。

  • 卷还可以用于在容器内挂载主机系统的目录。

  • 对卷进行的更改不会在从运行中的容器更新图像时包含在内。

  • 由于卷位于容器文件系统之外,它没有数据层或快照的概念。因此,读取和写入直接在卷上进行。

  • 如果多个容器使用相同的卷,则卷将持久存在,直到至少有一个容器使用它。

创建卷很容易。只需使用-v选项启动容器:

$ docker run -d -p 80:80 --name apache-1 -v /var/www apache.

现在请注意,卷没有ID参数,因此您无法像命名容器或标记图像一样确切地命名卷。但是,可以利用一个容器使用它至少一次的条件来使卷持久存在,这引入了仅包含数据的容器的概念。

注意

自 Docker 版本 1.1 以来,如果您愿意,可以使用-v选项将主机的整个文件系统绑定到容器,就像这样:

$ docker run -v /:/my_host ubuntu:ro ls /my_host****.

但是,禁止挂载到容器的/,因此出于安全原因,您无法替换容器的root文件系统。

仅数据容器

数据专用容器是一个除了公开其他数据访问容器可以使用的卷之外什么也不做的容器。数据专用容器用于防止容器访问卷停止或由于意外崩溃而被销毁。

使用另一个容器的卷

一旦我们使用-v选项启动容器,就创建了一个卷。我们可以使用--volumes-from选项与其他容器共享由容器创建的卷。此选项的可能用例包括备份数据库、处理日志、对用户数据执行操作等。

用例 - 在 Docker 上生产中使用 MongoDB

作为一个用例,假设您想在生产环境中使用MongoDB,您将运行一个 MongoDB 服务器以及一个cron作业,定期备份数据库快照。

注意

MongoDB 是一个文档数据库,提供高性能、高可用性和易扩展性。您可以在www.mongodb.org获取有关 MongoDB 的更多信息。

让我们看看如何使用docker卷设置 MongoDB:

  1. 首先,我们需要一个数据专用容器。该容器的任务只是公开 MongoDB 存储数据的卷:
$ docker run -v /data/db --name data-only mongo \ echo "MongoDB stores all its data in /data/db"

  1. 然后,我们需要运行 MongoDB 服务器,该服务器使用数据专用容器创建的卷:
$ docker run -d --volumes-from data-only -p 27017:27017 \ --name mongodb-server mongo mongod

注意

mongod命令运行 MongoDB 服务器,通常作为守护程序/服务运行。它通过端口27017访问。

  1. 最后,我们需要运行backup实用程序。在这种情况下,我们只是将 MongoDB 数据存储转储到主机上的当前目录:
$ docker run -d --volumes-from data-only --name mongo-backup \ -v $(pwd):/backup mongo $(mkdir -p /backup && cd /backup && mongodump)

注意

这绝不是在生产中设置 MongoDB 的详尽示例。您可能需要一个监视 MongoDB 服务器健康状况的过程。您还需要使 MongoDB 服务器容器可以被您的应用程序容器发现(我们将在后面详细学习)。

配置 Docker 以使用不同的存储驱动程序

在使用不同的存储驱动程序之前,使用docker save备份所有图像,并停止docker守护程序。一旦备份了所有重要图像,删除/var/lib/docker。更改存储驱动程序后,可以恢复保存的图像。

我们现在将把默认存储驱动程序 AUFS 更改为两种备用存储驱动程序-devicemapper 和 btrfs。

使用 devicemapper 作为存储驱动程序

切换到 devicemapper 驱动程序很容易。只需使用-s 选项启动 docker 守护程序:

$ docker -d -s devicemapper

此外,您可以使用--storage-opts 标志提供各种 devicemapper 驱动程序选项。devicemapper 驱动程序的各种可用选项和示例已在本章的限制资源存储部分中介绍。

注意

如果您在没有 AUFS 的 RedHat/Fedora 上运行,Docker 将使用 devicemapper 驱动程序,该驱动程序可用。

切换存储驱动程序后,您可以通过运行 docker info 来验证更改。

使用 btrfs 作为存储驱动程序

要将 btrfs 用作存储驱动程序,您必须首先设置它。本节假定您正在运行 Ubuntu 14.04 操作系统。根据您运行的 Linux 发行版,命令可能会有所不同。以下步骤将设置一个带有 btrfs 文件系统的块设备:

  1. 首先,您需要安装 btrfs 及其依赖项:
# apt-get -y btrfs-tools

  1. 接下来,您需要创建一个 btrfs 文件系统类型的块设备:
# mkfs btrfs /dev/sdb

  1. 现在为 Docker 创建目录(到此时,您应该已经备份了所有重要的镜像并清理了/var/lib/docker)。
# mkdir /var/lib/docker

  1. 然后在/var/lib/docker 挂载 btrfs 块设备:
# mount /dev/sdb var/lib/docker

  1. 检查挂载是否成功:
$ mount | grep btrfs
/dev/sdb on /var/lib/docker type btrfs (rw)

注意

来源:serverascode.com/2014/06/09/docker-btrfs.html

现在,您可以使用-s 选项启动 docker 守护程序:

$ docker -d -s btrfs

切换存储驱动程序后,您可以通过运行 docker info 命令来验证其中的更改。

配置 Docker 的网络设置

Docker 为每个容器创建一个单独的网络堆栈和一个虚拟桥(docker0)来管理容器内部、容器与主机之间以及两个容器之间的网络通信。

有一些网络配置可以作为 docker run 命令的参数设置。它们如下:

  • --dns:DNS 服务器是将 URL(例如www.docker.io)解析为运行网站的服务器的 IP 地址。

  • --dns-search:这允许您设置 DNS 搜索服务器。

注意

如果将 DNS 搜索服务器解析abcabc.example.com,则example.com设置为 DNS 搜索域。如果您有许多子域在您的公司网站中需要经常访问,这将非常有用。反复输入整个 URL 太痛苦了。如果您尝试访问一个不是完全合格的域名的站点(例如xyz.abc.com),它会为查找添加搜索域。来源:superuser.com/a/184366

  • -h--hostname:这允许您设置主机名。这将被添加为对容器的面向主机的 IP 的/etc/hosts路径的条目。

  • --link:这是另一个可以在启动容器时指定的选项。它允许容器与其他容器通信,而无需知道它们的实际 IP 地址。

  • --net:此选项允许您为容器设置网络模式。它可以有四个值:

  • 桥接:这为 docker 容器创建了一个网络堆栈。

  • none:不会为此容器创建任何网络堆栈。它将完全隔离。

  • container:<name|id>:这使用另一个容器的网络堆栈。

  • host:这使用主机的网络堆栈。

提示

这些值具有副作用,例如本地系统服务可以从容器中访问。此选项被认为是不安全的。

  • --expose:这将暴露容器的端口,而不在主机上发布它。

  • --publish-all:这将所有暴露的端口发布到主机的接口。

  • --publish:这将以以下格式将容器的端口发布到主机:ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort

提示

如果未给出--dns--dns-search,则容器的/etc/resolv.conf文件将与守护程序正在运行的主机的/etc/resolv.conf文件相同。

然而,当你运行它时,docker守护进程也可以给出一些配置。它们如下所述:

注意

这些选项只能在启动docker守护程序时提供,一旦运行就无法调整。这意味着您必须在docker -d命令中提供这些参数。

  • --ip:此选项允许我们在面向容器的docker0接口上设置主机的 IP 地址。因此,这将是绑定容器端口时使用的默认 IP 地址。例如,此选项可以显示如下:
$ docker -d --ip 172.16.42.1

  • --ip-forward:这是一个布尔选项。如果设置为false,则运行守护程序的主机将不会在容器之间或从外部世界到容器之间转发数据包,从网络角度完全隔离它。

注意

可以使用sysctl命令来检查此设置:

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

  • --icc:这是另一个布尔选项,代表容器间通信。如果设置为false,容器将彼此隔离,但仍然可以向包管理器等发出一般的 HTTP 请求。

注意

如何只允许那两个容器之间的通信?通过链接。我们将在链接容器部分详细探讨链接。

  • -b 或--bridge:您可以让 Docker 使用自定义桥接而不是docker0。(创建桥接超出了本讨论的范围。但是,如果您感兴趣,可以在docs.docker.com/articles/networking/#building-your-own-bridge找到更多信息。)

  • -H 或--host:此选项可以接受多个参数。Docker 具有 RESTful API。守护程序充当服务器,当您运行客户端命令(如runps)时,它会向服务器发出GETPOST请求,服务器执行必要的操作并返回响应。-H标志用于告诉docker守护程序必须监听哪些通道以接收客户端命令。参数可以如下:

  • tcp://<host>:<port>形式表示的 TCP 套接字

  • unix:///path/to/socket形式的 UNIX 套接字

在容器和主机之间配置端口转发

容器可以在没有任何特殊配置的情况下连接到外部世界,但外部世界不允许窥视它们。这是一项安全措施,显而易见,因为容器都通过虚拟桥连接到主机,从而有效地将它们放置在虚拟网络中。但是,如果您在容器中运行一个希望暴露给外部世界的服务呢?

端口转发是暴露在容器中运行的服务的最简单的方法。在镜像的 Dockerfile 中提到需要暴露的端口是明智的。在早期版本的 Docker 中,可以在 Dockerfile 本身指定 Dockerfile 应绑定到的主机端口,但这样做是因为有时主机中已经运行的服务会干扰容器。现在,您仍然可以在 Dockerfile 中指定要暴露的端口(使用EXPOSE指令),但如果要将其绑定到您选择的端口,需要在启动容器时执行此操作。

有两种方法可以启动容器并将其端口绑定到主机端口。它们的解释如下:

  • -P 或--publish-all:使用docker run启动容器,并使用-P选项将发布在镜像的 Dockerfile 中使用EXPOSE指令暴露的所有端口。Docker 将浏览暴露的端口,并将它们绑定到4900049900之间的随机端口。

  • -p 或--publish:此选项允许您明确告诉 Docker 应将哪个 IP 上的哪个端口绑定到容器上的端口(当然,主机中的一个接口应该具有此 IP)。可以多次使用该选项进行多个绑定:

  1. docker run -p ip:host_port:container_port

  2. docker run -p ip::container_port

  3. docker run -p host_port:container_port

自定义 IP 地址范围

我们已经看到了如何将容器的端口绑定到主机的端口,如何配置容器的 DNS 设置,甚至如何设置主机的 IP 地址。但是,如果我们想要自己设置容器和主机之间网络的子网怎么办?Docker 在 RFC 1918 提供的可用私有 IP 地址范围中创建了一个虚拟子网。

设置自己的子网范围非常容易。docker守护程序的--bip选项可用于设置桥接的 IP 地址以及它将创建容器的子网:

$ docker -d --bip 192.168.0.1/24

在这种情况下,我们已将 IP 地址设置为192.168.0.1,并指定它必须将 IP 地址分配给子网范围192.168.0.0/24中的容器(即从192.168.0.2192.168.0.254,共 252 个可能的 IP 地址)。

就是这样!在docs.docker.com/articles/networking/上有更多高级网络配置和示例。一定要查看它们。

链接容器

如果您只有一个普通的 Web 服务器想要暴露给互联网,那么将容器端口绑定到主机端口就可以了。然而,大多数生产系统由许多不断相互通信的单独组件组成。诸如数据库服务器之类的组件不应绑定到公开可见的 IP,但运行前端应用程序的容器仍然需要发现数据库容器并连接到它们。在应用程序中硬编码容器的 IP 地址既不是一个干净的解决方案,也不会起作用,因为 IP 地址是随机分配给容器的。那么我们如何解决这个问题呢?答案如下。

在同一主机内链接容器

可以在启动容器时使用--link选项指定链接:

$ docker run --link CONTAINER_IDENTIFIER:ALIAS . . .

这是如何工作的?当给出链接选项时,Docker 会向容器的/etc/hosts文件添加一个条目,其中ALIAS命令作为主机名,容器命名为CONTAINER_IDENTIFIER的 IP 地址。

注意

/etc/hosts文件可用于覆盖 DNS 定义,即将主机名指向特定的 IP 地址。在主机名解析期间,在向 DNS 服务器发出请求之前,将检查/etc/hosts

例如,下面显示了命令行代码:

$ docker run --name pg -d postgres
$ docker run --link pg:postgres postgres-app

上面的命令运行了一个 PostgreSQL 服务器(其 Dockerfile 公开了端口 5432,PostgeSQL 的默认端口),第二个容器将使用postgres别名链接到它。

注意

PostgreSQL 是一个完全符合ACID的功能强大的开源对象关系数据库系统。

使用 ambassador 容器进行跨主机链接

当所有容器都在同一主机上时,链接容器可以正常工作,但是 Docker 的容器通常可能分布在不同的主机上,在这些情况下链接会失败,因为在当前主机上运行的docker守护程序不知道在不同主机上运行的容器的 IP 地址。此外,链接是静态的。这意味着如果容器重新启动,其 IP 地址将更改,并且所有链接到它的容器将失去连接。一个可移植的解决方案是使用 ambassador 容器。

以下图显示了 ambassador 容器:

使用 ambassador 容器进行跨主机链接

在这种架构中,一个主机中的数据库服务器暴露给另一个主机。同样,如果数据库容器发生更改,只需要重新启动host1阶段的 ambassador 容器。

用例 - 多主机 Redis 环境

让我们使用progrium/ambassadord命令设置一个多主机 Redis 环境。还有其他可以用作大使容器的镜像。它们可以使用docker search命令搜索,或者在registry.hub.docker.com上搜索。

注意

Redis 是一个开源的、网络化的、内存中的、可选的持久性键值数据存储。它以快速的速度而闻名,无论是读取还是写入。

在这个环境中,有两个主机,Host 1Host 2Host 1的 IP 地址是192.168.0.100,是私有的(不暴露给公共互联网)。Host 2在 192.168.0.1,并绑定到一个公共 IP。这是运行您的前端 Web 应用程序的主机。

注意

要尝试这个例子,启动两个虚拟机。如果您使用 Vagrant,我建议使用安装了 Docker 的 Ubuntu 镜像。如果您有 Vagrant v1.5,可以通过运行$ vagrant init phusion/ubuntu-14.04-amd64来使用 Phusion 的 Ubuntu 镜像。

主机 1

在第一个主机上,运行以下命令:

$ docker run -d --name redis --expose 6379 dockerfile/redis

这个命令启动了一个 Redis 服务器,并暴露了端口6379(这是 Redis 服务器运行的默认端口),但没有将其绑定到任何主机端口。

以下命令启动一个大使容器,链接到 Redis 服务器,并将端口 6379 绑定到其私有网络 IP 地址的 6379 端口(在这种情况下是 192.168.0.100)。这仍然不是公开的,因为主机是私有的(不暴露给公共互联网):

$ docker run -d --name redis-ambassador-h1 \
-p 192.168.0.100:6379:6379 --link redis:redis \
progrium/ambassadord --links

主机 2

在另一个主机(如果您在开发中使用 Vagrant,则是另一个虚拟机),运行以下命令:

$ docker run -d --name redis-ambassador-h2 --expose 6379 \
progrium/ambassadord 192.168.0.100:6379

这个大使容器监听目标 IP 的端口,这种情况下是主机 1 的 IP 地址。我们已经暴露了端口 6379,这样它现在可以被我们的应用容器连接:

$ docker run -d --name application-container \--link redis-ambassador-h2:redis myimage mycommand

这将是在互联网上公开的容器。由于 Redis 服务器在私有主机上运行,因此无法从私有网络外部受到攻击。

总结

在本章中,我们看到了如何在 Docker 容器中配置 CPU、RAM 和存储等资源。我们还讨论了如何使用卷和卷容器来管理容器中应用程序产生的持久数据。我们了解了切换 Docker 使用的存储驱动程序以及各种网络配置及其相关用例。最后,我们看到了如何在主机内部和跨主机之间链接容器。

在下一章中,我们将看看哪些工具和方法可以帮助我们考虑使用 Docker 部署我们的应用程序。我们将关注的一些内容包括多个服务的协调、服务发现以及 Docker 的远程 API。我们还将涵盖安全考虑。

第四章:自动化和最佳实践

此时,我们现在知道如何在开发环境中设置 Docker,熟悉 Docker 命令,并且对 Docker 适用的情况有一个很好的了解。我们还知道如何配置 Docker 及其容器以满足我们所有的需求。

在这一章中,我们将专注于各种使用模式,这些模式将帮助我们在生产环境中部署我们的 Web 应用程序。我们将从 Docker 的远程 API 开始,因为登录到生产服务器并运行命令总是被认为是危险的。因此,最好运行一个监视和编排主机中容器的应用程序。如今有许多用于 Docker 的编排工具,并且随着 v1.0 的宣布,Docker 还宣布了一个新项目libswarm,它提供了一个标准接口来管理和编排分布式系统,这将是我们将要深入探讨的另一个主题。

Docker 开发人员建议每个容器只运行一个进程。如果您想要检查已经运行的容器,这可能有些困难。我们将看一下一个允许我们将进程注入到已经运行的容器中的命令。

随着组织的发展,负载也会增加,您将需要开始考虑扩展。Docker 本身是用于在单个主机中使用的,但是通过使用一系列工具,如etcdcoreos,您可以轻松地在集群中运行一堆 Docker 主机并发现该集群中的每个其他容器。

每个在生产环境中运行 Web 应用程序的组织都知道安全性的重要性。在本章中,我们将讨论与docker守护程序相关的安全方面,以及 Docker 使用的各种 Linux 功能。总之,在本章中,我们将看到以下内容:

  • Docker 远程 API

  • 使用 Docker exec 命令将进程注入容器

  • 服务发现

  • 安全性

Docker 远程 API

Docker 二进制文件可以同时作为客户端和守护程序运行。当 Docker 作为守护程序运行时,默认情况下会将自己附加到 Unix 套接字unix:///var/run/docker.sock(当然,在启动 docker 时可以更改此设置),并接受 REST 命令。然后,相同的 Docker 二进制文件可以用于运行所有其他命令(这只是客户端向docker守护程序发出 REST 调用)。

下图显示了docker守护程序的图表:

Docker 远程 API

本节将主要通过示例来解释,因为我们在查看 Docker 命令时已经遇到了这些操作的工作原理。

要测试这些 API,运行docker守护程序,使用 TCP 端口,如下所示:

$ export DOCKER_HOST=tcp://0.0.0.0:2375
$ sudo service docker restart
$ export DOCKER_DAEMON=http://127.0.0.1:2375 # or IP of your host

注意

这不会是一个参考指南,因为我们在第二章中已经涵盖了 Docker 可用的功能,Docker CLI 和 Dockerfile。相反,我们将涵盖一些 API,并且您可以在docs.docker.com/reference/api/docker_remote_api上查找其余部分。

在我们开始之前,让我们确保docker守护程序正在响应我们的请求:

$ curl $DOCKER_DAEMON/_ping
OK

好的,一切都很好。让我们开始吧。

容器的远程 API

让我们首先看一下可用的一些端点,这些端点有助于创建和管理容器。

创建命令

create命令创建一个容器:

$ curl \
> -H "Content-Type: application/json" \
> -d '{"Image":"ubuntu:14.04",\
> "Cmd":["echo", "I was started with the API"]}' \
> -X POST $DOCKER_DAEMON/containers/create?\
> name=api_container;
{"Id":"4e145a6a54f9f6bed4840ac730cde6dc93233659e7eafae947efde5caf583f c3","Warnings":null}

注意

curl实用程序是一个简单的 Unix 实用程序,可用于构造 HTTP 请求和分析响应。

在这里,我们向/containers/create端点发出POST请求,并传递一个包含我们希望容器基于的镜像的详细信息以及我们期望容器运行的命令的JSON对象。

请求类型:POST

POST请求一起发送的JSON数据:

参数 类型 说明

|

config

JSON 描述要启动的容器的配置

POST 请求的查询参数:

参数 类型 说明

|

name

String 这为容器分配一个名称。它必须匹配/?[a-zA-Z0-9_-]+正则表达式。

以下表格显示了响应的状态码:

状态码 意义

|

201

无错误

|

404

没有这样的容器

|

406

无法附加(容器未运行)

|

500

内部服务器错误

列表命令

list命令获取容器列表:

$ curl $DOCKER_DAEMON/containers/json?all=1\&limit=1
[{"Command":"echo 'I was started with the API'","Created":1407995735,"Id":"96bdce1493715c2ca8940098db04b99e3629 4a333ddacab0e04f62b98f1ec3ae","Image":"ubuntu:14.04","Names":["/api_c ontainer"],"Ports":[],"Status":"Exited (0) 3 minutes ago"}

这是一个GET请求 API。对/containers/json的请求将返回一个JSON响应,其中包含满足条件的容器列表。在这里,传递all查询参数将列出未运行的容器。limit参数是响应中将列出的容器数量。

有一些查询参数可以与这些 API 调用一起提供,可以微调响应。

请求类型:GET

参数 类型 说明

|

all

1/True/true 或 0/False/false 这告诉是否显示所有容器。默认情况下只显示正在运行的容器。

|

limit

整数 这显示最后 [n] 个容器,包括非运行中的容器。

|

since

容器 ID 这只显示自 [x] 以来启动的容器,包括非运行中的容器。

|

before

容器 ID 这只显示在 [x] 之前启动的容器,包括非运行中的容器。

|

size

1/True/true 或 0/False/false 这告诉是否在响应中显示容器大小。

响应的状态码遵循相关的 请求 评论 (RFC) 2616:

状态码 意义

|

200

没有错误

|

400

错误的参数和客户端错误

|

500

服务器错误

有关容器的其他端点可以在docs.docker.com/reference/api/docker_remote_api_v1.13/#21-containers上阅读。

图像的远程 API

与容器类似,还有用于构建和管理图像的 API。

列出本地 Docker 图像

以下命令列出本地图像:

$ curl $DOCKER_DAEMON/images/json
[{"Created":1406791831,"Id":"7e03264fbb7608346959378f270b32bf31daca14d15e9979a5803ee32e9d2221","ParentId":"623cd16a51a7fb4ecd539eb1e5d9778 c90df5b96368522b8ff2aafcf9543bbf2","RepoTags":["shrikrishna/apt- moo:latest"],"Size":0,"VirtualSize":281018623} ,{"Created":1406791813,"Id":"c5f4f852c7f37edcb75a0b712a16820bb8c729a6 a5093292e5f269a19e9813f2","ParentId":"ebe887219248235baa0998323342f7f 5641cf5bff7c43e2b802384c1cb0dd498","RepoTags":["shrikrishna/onbuild:l atest"],"Size":0,"VirtualSize":281018623} ,{"Created":1406789491,"Id":"0f0dd3deae656e50a78840e58f63a5808ac53cb4 dc87d416fc56aaf3ab90c937","ParentId":"061732a839ad1ae11e9c7dcaa183105 138e2785954ea9e51f894f4a8e0dc146c","RepoTags":["shrikrishna/optimus:g it_url"],"Size":0,"VirtualSize":670857276}

这是一个 GET 请求 API。对 /images/json 的请求将返回一个包含满足条件的图像详细信息列表的 JSON 响应。

请求类型:GET

参数 类型 解释

|

all

1/True/true 或 0/False/false 这告诉是否显示中间容器。默认为假。

|

filters

JSON 这些用于提供图像的筛选列表。

有关图像的其他端点可以在docs.docker.com/reference/api/docker_remote_api_v1.13/#22-images上阅读。

其他操作

还有其他 API,比如我们在本节开头检查的 ping API。其中一些在下一节中探讨。

获取系统范围的信息

以下命令获取 Docker 的系统范围信息。这是处理 docker info 命令的端点:

$ curl $DOCKER_DAEMON/info
{"Containers":41,"Debug":1,"Driver":"aufs","DriverStatus":[["Root Dir","/mnt/sda1/var/lib/docker/aufs"],["Dirs","225"]],"ExecutionDrive r":"native- 0.2","IPv4Forwarding":1,"Images":142,"IndexServerAddress":"https://in dex.docker.io/v1/","InitPath":"/usr/local/bin/docker","InitSha1":""," KernelVersion":"3.15.3- tinycore64","MemoryLimit":1,"NEventsListener":0,"NFd":15,"NGoroutines ":15,"Sockets":["unix:///var/run/docker.sock","tcp://0.0.0.0:2375"]," SwapLimit":1}

从容器中提交图像

以下命令从容器中提交图像:

$ curl \
> -H "Content-Type: application/json" \
> -d '{"Image":"ubuntu:14.04",\
> "Cmd":["echo", "I was started with the API"]}' \
> -X POST $DOCKER_DAEMON/commit?\
> container=96bdce149371\
> \&m=Created%20with%20remote%20api\&repo=shrikrishna/api_image;

{"Id":"5b84985879a84d693f9f7aa9bbcf8ee8080430bb782463e340b241ea760a5a 6b"}

提交是对/commit参数的POST请求,其中包含有关其基础图像和与将在提交时创建的图像相关联的命令的数据。关键信息包括要提交的container ID参数,提交消息以及它所属的存储库,所有这些都作为查询参数传递。

请求类型:POST

POST请求一起发送的JSON数据:

参数 类型 解释

|

config

JSON 这描述了要提交的容器的配置

以下表格显示了POST请求的查询参数:

参数 类型 解释

|

container

Container ID 您打算提交的容器的ID

|

repo

String 要在其中创建图像的存储库

|

tag

String 新图像的标签

|

m

String 提交消息

|

author

String 作者信息

以下表格显示了响应的状态码:

状态码 意义

|

201

没有错误

|

404

没有这样的容器

|

500

内部服务器错误

保存图像

从以下命令获取存储库的所有图像和元数据的 tarball 备份:

$ curl $DOCKER_DAEMON/images/shrikrishna/code.it/get > \
> code.it.backup.tar.gz

这将需要一些时间,因为图像首先必须被压缩成一个 tarball,然后被流式传输,但然后它将被保存在 tar 存档中。

其他端点可以在docs.docker.com/reference/api/docker_remote_api_v1.13/#23-misc上了解。

docker run 的工作原理

既然我们意识到我们运行的每个 Docker 命令实际上都是客户端执行的一系列 RESTful 操作,让我们加深一下对运行docker run命令时发生的事情的理解:

  1. 要创建一个 API,需要调用/containers/``create参数。

  2. 如果响应的状态码是 404,则表示图像不存在。尝试使用/images/create参数拉取图像,然后返回到步骤 1。

  3. 获取创建的容器的ID并使用/containers/(id)/start参数启动它。

这些 API 调用的查询参数将取决于传递给docker run命令的标志和参数。

使用 Docker execute 命令将进程注入容器

在您探索 Docker 的过程中,您可能会想知道 Docker 强制执行的每个容器规则是否限制了其功能。事实上,您可能会原谅认为 Docker 容器只运行一个进程。但不是!一个容器可以运行任意数量的进程,但只能以一个命令启动,并且与命令相关联的进程存在的时间就是容器的生存时间。这种限制是因为 Docker 相信一个容器一个应用程序的理念。与其将所有内容加载到单个容器中,典型的依赖于 Docker 的应用程序架构将包括多个容器,每个容器运行一个专门的服务,所有这些服务都链接在一起。这有助于保持容器轻便,使调试更容易,减少攻击向量,并确保如果一个服务崩溃,其他服务不受影响。

然而,有时您可能需要在容器运行时查看容器。随着时间的推移,Docker 社区采取了许多方法来调试运行中的容器。一些成员将 SSH 加载到容器中,并运行了一个进程管理解决方案,如supervisor来运行 SSH +应用程序服务器。然后出现了诸如nsinitnsenter之类的工具,这些工具帮助在容器正在运行的命名空间中生成一个 shell。然而,所有这些解决方案都是黑客攻击。因此,随着 v1.3 的到来,Docker 决定提供docker exec命令,这是一个安全的替代方案,可以调试运行中的容器。

docker exec命令允许用户通过 Docker API 和 CLI 在其 Docker 容器中生成一个进程,例如:

$ docker run -dit --name exec_example -v $(pwd):/data -p 8000:8000 dockerfile/python python -m SimpleHTTPServer
$ docker exec -it exec_example bash

第一个命令启动一个简单的文件服务器容器。使用-d选项将容器发送到后台。在第二个命令中,使用docker exec,我们通过在其中创建一个 bash 进程来登录到容器。现在我们将能够检查容器,读取日志(如果我们已经登录到文件中),运行诊断(如果需要检查是因为错误而出现),等等。

注意

Docker 仍然没有从其一个应用程序每个容器的理念中移动。 docker exec命令存在只是为了提供一种检查容器的方法,否则将需要变通或黑客攻击。

服务发现

Docker 会动态地从可用地址池中为容器分配 IP。虽然在某些方面这很好,但当您运行需要相互通信的容器时,就会产生问题。在构建镜像时,您无法知道其 IP 地址将是什么。您可能的第一反应是启动容器,然后通过docker exec登录到它们,并手动设置其他容器的 IP 地址。但请记住,当容器重新启动时,此 IP 地址可能会更改,因此您将不得不手动登录到每个容器并输入新的 IP 地址。有没有更好的方法?是的,有。

服务发现是一系列需要完成的工作,让服务知道如何找到并与其他服务通信。在服务发现下,容器在刚启动时并不知道它们的对等体。相反,它们会动态地发现它们。这应该在容器位于同一主机以及位于集群中时都能正常工作。

有两种技术可以实现服务发现:

  • 使用默认的 Docker 功能,如名称和链接

  • 使用专用服务,如EtcdConsul

使用 Docker 名称、链接和大使容器

我们在第三章的链接容器部分学习了如何链接容器。为了提醒您,这就是它的工作原理。

使用链接使容器彼此可见

以下图表显示了链接的使用:

使用链接使容器彼此可见

链接允许容器连接到另一个容器,而无需硬编码其 IP 地址。在启动第二个容器时,将第一个容器的 IP 地址插入/etc/hosts中即可实现这一点。

可以在启动容器时使用--link选项指定链接:

$ docker run --link CONTAINER_IDENTIFIER:ALIAS . . .

您可以在第三章中了解更多关于链接的信息。

使用大使容器进行跨主机链接

以下图表代表了使用大使容器进行跨主机链接:

使用大使容器进行跨主机链接

大使容器用于链接跨主机的容器。在这种架构中,您可以重新启动/替换数据库容器,而无需重新启动应用程序容器。

您可以在第三章配置 Docker 容器中了解有关 ambassador 容器的更多信息。

使用 etcd 进行服务发现

为什么我们需要专门的服务发现解决方案?虽然 ambassador 容器和链接解决了在不需要知道其 IP 地址的情况下找到容器的问题,但它们确实有一个致命的缺陷。您仍然需要手动监视容器的健康状况。

想象一种情况,您有一组后端服务器和通过 ambassador 容器与它们连接的前端服务器的集群。如果其中一个服务器宕机,前端服务器仍然会继续尝试连接到后端服务器,因为在它们看来,那是唯一可用的后端服务器,这显然是错误的。

现代服务发现解决方案,如etcdConsuldoozerd,不仅提供正确的 IP 地址和端口,它们实际上是分布式键值存储,具有容错和一致性,并在故障发生时处理主节点选举。它们甚至可以充当锁服务器。

etcd服务是由CoreOS开发的开源分布式键值存储。在集群中,etcd客户端在集群中的每台机器上运行。etcd服务在网络分区和当前主节点丢失期间优雅地处理主节点选举。

您的应用程序可以读取和写入etcd服务中的数据。etcd服务的常见示例包括存储数据库连接详细信息、缓存设置等。

etcd服务的特点在这里列出:

  • 简单的可 curl API(HTTP + JSON)

  • 可选的安全套接字层(SSL)客户端证书认证

  • 键支持生存时间TTL

注意

Consul服务是etcd服务的一个很好的替代方案。没有理由选择其中一个而不是另一个。本节只是为了向您介绍服务发现的概念。

我们在两个阶段使用etcd服务如下:

  1. 我们使用etcd服务注册我们的服务。

  2. 我们进行查找以查找已注册的服务。

以下图显示了etcd服务:

使用 etcd 进行服务发现

这似乎是一个简单的任务,但构建一个容错和一致的解决方案并不简单。您还需要在服务失败时收到通知。如果以天真的集中方式运行服务发现解决方案本身,它可能会成为单点故障。因此,服务发现服务器集群中的所有实例都需要与正确的答案同步,这就需要有趣的方法。CoreOS 团队开发了一种称为Raft的共识算法来解决这个问题。您可以在raftconsensus.github.io上阅读更多信息。

让我们来看一个例子,了解一下情况。在这个例子中,我们将在一个容器中运行etcd服务器,并看看注册服务和发现服务有多么容易。

  1. 步骤 1:运行etcd服务器:
$ docker run -d -p 4001:4001 coreos/etcd:v0.4.6 -name myetcd

  1. 步骤 2:一旦镜像下载完成并且服务器启动,运行以下命令注册一条消息:
$ curl -L -X PUT http://127.0.0.1:4001/v2/keys/message -d value="Hello"
{"action":"set","node":{"key":"/message","value":"Hello","modifiedIndex":3,"createdIndex":3}}

这只是对/v2/keys/message路径上的服务器发出的PUT请求(这里的密钥是message)。

  1. 步骤 3:使用以下命令获取密钥:
$ curl -L http://127.0.0.1:4001/v2/keys/message
{"action":"get","node":{"key":"/message","value":"Hello","modifiedIndex":4,"createdIndex":4}}

您可以继续尝试更改值,尝试无效的密钥等。您会发现响应是JSON格式的,这意味着您可以轻松地将其与应用程序集成,而无需使用任何库。

但是我该如何在我的应用程序中使用它呢?如果您的应用程序需要运行多个服务,它们可以通过链接和大使容器连接在一起,但如果其中一个变得不可用或需要重新部署,就需要做很多工作来恢复链接。

现在想象一下,您的服务使用etcd服务。每个服务都会根据其名称注册其 IP 地址和端口号,并通过其名称(恒定的)发现其他服务。现在,如果容器因崩溃/重新部署而重新启动,新容器将根据修改后的 IP 地址进行注册。这将更新etcd服务返回的值,以供后续发现请求使用。但是,这意味着单个etcd服务器也可能是单点故障。解决此问题的方法是运行一组etcd服务器。这就是 CoreOS(创建etcd服务的团队)开发的 Raft 一致性算法发挥作用的地方。可以在jasonwilder.com/blog/2014/07/15/docker-service-discovery/找到使用etcd服务部署的应用服务的完整示例。

Docker 编排

一旦您超越简单的应用程序进入复杂的架构,您将开始使用诸如etcdconsulserf之类的工具和服务,并且您会注意到它们都具有自己的一套 API,即使它们具有重叠的功能。如果您设置基础设施为一组工具,并发现需要切换,这需要相当大的努力,有时甚至需要更改代码才能切换供应商。这种情况可能导致供应商锁定,这将破坏 Docker 设法创建的有前途的生态系统。为了为这些服务提供商提供标准接口,以便它们几乎可以用作即插即用的解决方案,Docker 发布了一套编排服务。在本节中,我们将对它们进行介绍。但是,请注意,在撰写本书时,这些项目(Machine、Swarm 和 Compose)仍处于 Alpha 阶段,并且正在积极开发中。

Docker Machine

Docker Machine 旨在提供一个命令,让您从零开始进行 Docker 项目。

在 Docker Machine 之前,如果您打算在新主机上开始使用 Docker,无论是虚拟机还是基础设施提供商(如亚马逊网络服务(AWS)或 Digital Ocean)上的远程主机,您都必须登录到实例,并运行特定于其中运行的操作系统的设置和配置命令。

使用 Docker Machine,无论是在新笔记本电脑上、数据中心的虚拟机上,还是在公共云实例上配置docker守护程序,都可以使用相同的单个命令准备目标主机以运行 Docker 容器:

$ machine create -d [infrastructure provider] [provider options] [machine name]

然后,您可以从相同的界面管理多个 Docker 主机,而不管它们的位置,并在它们上运行任何 Docker 命令。

除此之外,该机器还具有可插拔的后端,这使得很容易为基础设施提供商添加支持,同时保留了常见的用户界面 API。Machine 默认提供了用于在 Virtualbox 上本地配置 Docker 以及在 Digital Ocean 实例上远程配置的驱动程序。

请注意,Docker Machine 是 Docker Engine 的一个独立项目。您可以在其 Github 页面上找到有关该项目的更新详细信息:github.com/docker/machine

Swarm

Swarm是 Docker 提供的本地集群解决方案。它使用 Docker Engine 并对其进行扩展,以使您能够在容器集群上工作。使用 Swarm,您可以管理 Docker 主机的资源池,并安排容器在其上透明地运行,自动管理工作负载并提供故障转移服务。

要进行安排,它需要容器的资源需求,查看主机上的可用资源,并尝试优化工作负载的放置。

例如,如果您想要安排一个需要 1GB 内存的 Redis 容器,可以使用 Swarm 进行安排:

$ docker run -d -P -m 1g redis

除了资源调度,Swarm 还支持基于策略的调度,具有标准和自定义约束。例如,如果您想在支持 SSD 的主机上运行您的MySQL容器(以确保更好的写入和读取性能),可以按照以下方式指定:

$ docker run -d -P -e constraint:storage=ssd mysql

除了所有这些,Swarm 还提供了高可用性和故障转移。它不断监视容器的健康状况,如果一个容器发生故障,会自动重新平衡,将失败主机上的 Docker 容器移动并重新启动到新主机上。最好的部分是,无论您是刚开始使用一个实例还是扩展到 100 个实例,界面都保持不变。

与 Docker Machine 一样,Docker Swarm 处于 Alpha 阶段,并不断发展。请前往其 Github 存储库了解更多信息:github.com/docker/swarm/

Docker Compose

Compose是这个谜题的最后一块。通过 Docker Machine,我们已经配置了 Docker 守护程序。通过 Docker Swarm,我们可以放心,我们将能够从任何地方控制我们的容器,并且如果有任何故障,它们将保持可用。Compose 帮助我们在这个集群上组合我们的分布式应用程序。

将这与我们已经了解的东西进行比较,可能有助于我们理解所有这些是如何一起工作的。Docker Machine 的作用就像操作系统对程序的作用一样。它提供了容器运行的地方。Docker Swarm 就像程序的编程语言运行时一样。它管理资源,提供异常处理等等。

Docker Compose 更像是一个 IDE,或者是一种语言语法,它提供了一种表达程序需要做什么的方式。通过 Compose,我们指定了我们的分布式应用程序在集群中的运行方式。

我们使用 Docker Compose 通过编写YAML文件来声明我们的多容器应用程序的配置和状态。例如,假设我们有一个使用 Redis 数据库的 Python 应用程序。以下是我们为 Compose 编写YAML文件的方式:

containers:
  web:
     build: .
     command: python app.py
     ports:
     - "5000:5000"
     volumes:
     - .:/code
     links:
     - redis
     environment:
     - PYTHONUNBUFFERED=1
  redis:
     image: redis:latest
     command: redis-server --appendonly yes

在上面的例子中,我们定义了两个应用程序。一个是需要从当前目录的 Dockerfile 构建的 Python 应用程序。它暴露了一个端口(5000),并且要么有一个卷,要么有一段代码绑定到当前工作目录。它还定义了一个环境变量,并且与第二个应用程序容器redis链接。第二个容器使用了 Docker 注册表中的redis容器。

有了定义的配置,我们可以使用以下命令启动两个容器:

$ docker up

通过这个单一的命令,Python 容器使用 Dockerfile 构建,并且redis镜像从注册表中拉取。然而,redis容器首先启动,因为 Python 容器的规范中有 links 指令,并且 Python 容器依赖于它。

与 Docker Machine 和 Docker Swarm 一样,Docker Compose 是一个“正在进行中”的项目,其开发可以在github.com/docker/docker/issues/9459上跟踪。

有关 swarm 的更多信息可以在blog.docker.com/2014/12/announcing-docker-machine-swarm-and-compose-for-orchestrating-distributed-apps/找到。

安全

安全性在决定是否投资于技术时至关重要,特别是当该技术对基础设施和工作流程有影响时。Docker 容器大多是安全的,而且由于 Docker 不会干扰其他系统,您可以使用额外的安全措施来加固docker守护程序周围的安全性。最好在专用主机上运行docker守护程序,并将其他服务作为容器运行(除了诸如sshcron等服务)。

在本节中,我们将讨论 Docker 中用于安全性的内核特性。我们还将考虑docker守护程序本身作为可能的攻击向量。

图片来源xkcd.com/424/

安全性

内核命名空间

命名空间为容器提供了沙盒功能。当容器启动时,Docker 会为容器创建一组命名空间和控制组。因此,属于特定命名空间的容器无法看到或影响属于其他命名空间或主机的另一个容器的行为。

以下图表解释了 Docker 中的容器:

内核命名空间

内核命名空间还为容器创建了一个网络堆栈,可以进行最后的详细配置。默认的 Docker 网络设置类似于简单的网络,主机充当路由器,docker0桥充当以太网交换机。

命名空间功能是模仿 OpenVZ 而设计的,OpenVZ 是基于 Linux 内核和操作系统的操作系统级虚拟化技术。OpenVZ 是目前市场上大多数廉价 VPS 所使用的技术。它自 2005 年以来就存在,命名空间功能是在 2008 年添加到内核中的。从那时起就被用于生产环境,因此可以称之为“经过严峻考验的”。

控制组

控制组提供资源管理功能。虽然这与特权无关,但由于其可能作为拒绝服务攻击的第一道防线,因此与安全性相关。控制组也存在已经相当长时间,因此可以认为在生产环境中是安全的。

有关控制组的进一步阅读,请参阅www.kernel.org/doc/Documentation/cgroups/cgroups.txt

容器中的根

容器中的root命令被剥夺了许多特权。例如,默认情况下,您无法使用mount命令挂载设备。另一方面,使用--privileged标志运行容器将使容器中的root用户完全访问主机中root用户拥有的所有特权。Docker 是如何实现这一点的呢?

您可以将标准的root用户视为具有广泛能力的人。其中之一是net_bind_service服务,它绑定到任何端口(甚至低于 1024)。另一个是cap_sys_admin服务,这是挂载物理驱动器所需的。这些被称为功能,是进程用来证明其被允许执行操作的令牌。

Docker 容器是以减少的能力集启动的。因此,您会发现您可以执行一些 root 操作,但不能执行其他操作。具体来说,在非特权容器中,root用户无法执行以下操作:

  • 挂载/卸载设备

  • 管理原始套接字

  • 文件系统操作,如创建设备节点和更改文件所有权

在 v1.2 之前,如果您需要使用任何被列入黑名单的功能,唯一的解决方案就是使用--privileged标志运行容器。但是 v1.2 引入了三个新标志,--cap-add--cap-drop--device,帮助我们运行需要特定功能的容器,而不会影响主机的安全性。

--cap-add标志向容器添加功能。例如,让我们改变容器接口的状态(这需要NET_ADMIN服务功能):

$ docker run --cap-add=NET_ADMIN ubuntu sh -c "ip link eth0 down"

--cap-drop标志在容器中列入黑名单功能。例如,让我们在容器中列入黑名单除chown命令之外的所有功能,然后尝试添加用户。这将失败,因为它需要CAP_CHOWN服务。

$ docker run --cap-add=ALL --cap-drop=CHOWN -it ubuntu useradd test
useradd: failure while writing changes to /etc/shadow

--devices标志用于直接在容器上挂载外部/虚拟设备。在 v1.2 之前,我们必须在主机上挂载它,并在--privileged容器中使用-v标志进行绑定挂载。使用--device标志,您现在可以在容器中使用设备,而无需使用--privileged容器。

例如,要在容器上挂载笔记本电脑的 DVD-RW 设备,请运行以下命令:

$ docker run --device=/dev/dvd-rw:/dev/dvd-rw ...

有关这些标志的更多信息,请访问blog.docker.com/tag/docker-1-2/

Docker 1.3 版本还引入了其他改进。CLI 中添加了--security-opts标志,允许您设置自定义的SELinuxAppArmor标签和配置文件。例如,假设您有一个策略,允许容器进程仅监听 Apache 端口。假设您在svirt_apache中定义了此策略,您可以将其应用于容器,如下所示:

$ docker run --security-opt label:type:svirt_apache -i -t centos \ bash

这一功能的好处之一是,用户将能够在支持 SELinux 或 AppArmor 的内核上运行 Docker,而无需在容器上使用docker run --privileged。不像--privileged容器一样给予运行容器所有主机访问权限,这显著减少了潜在威胁的范围。

来源:blog.docker.com/2014/10/docker-1-3-signed-images-process-injection-security-options-mac-shared-directories/

您可以在github.com/docker/docker/blob/master/daemon/execdriver/native/template/default_template.go上查看已启用功能的完整列表。

注意

对于好奇的人,所有可用功能的完整列表可以在 Linux 功能手册页中找到。也可以在man7.org/linux/man-pages/man7/capabilities.7.html上找到。

Docker 守护程序攻击面

docker守护程序负责创建和管理容器,包括创建文件系统,分配 IP 地址,路由数据包,管理进程等需要 root 权限的许多任务。因此,将守护程序作为sudo用户启动是必不可少的。这就是为什么docker守护程序默认绑定到 Unix 套接字,而不是像 v5.2 之前那样绑定到 TCP 套接字的原因。

Docker 的最终目标之一是能够将守护程序作为非 root 用户运行,而不影响其功能,并将确实需要 root 的操作(如文件系统操作和网络)委托给具有提升权限的专用子进程。

如果您确实希望将 Docker 的端口暴露给外部世界(以利用远程 API),建议确保只允许受信任的客户端访问。一个简单的方法是使用 SSL 保护 Docker。您可以在docs.docker.com/articles/https找到设置此项的方法。

安全最佳实践

现在让我们总结一些在您的基础设施中运行 Docker 时的关键安全最佳实践:

  • 始终在专用服务器上运行docker守护程序。

  • 除非您有多个实例设置,否则在 Unix 套接字上运行docker守护程序。

  • 特别注意将主机目录作为卷进行绑定挂载,因为容器有可能获得完全的读写访问权限,并在这些目录中执行不可逆的操作。

  • 如果必须绑定到 TCP 端口,请使用基于 SSL 的身份验证进行安全保护。

  • 避免在容器中以 root 权限运行进程。

  • 在生产中绝对没有理智的理由需要运行特权容器。

  • 考虑在主机上启用 AppArmor/SELinux 配置文件。这使您可以为主机添加额外的安全层。

  • 与虚拟机不同,所有容器共享主机的内核。因此,保持内核更新以获取最新的安全补丁非常重要。

总结

在本章中,我们了解了各种工具、API 和实践,这些工具、API 和实践帮助我们在基于 Docker 的环境中部署应用程序。最初,我们研究了远程 API,并意识到所有 Docker 命令都不过是对docker守护程序的基于 REST 的调用的结果。

然后我们看到如何注入进程来帮助调试运行中的容器。

然后,我们看了各种方法来实现服务发现,既使用本机 Docker 功能,如链接,又借助专门的config存储,如etcd服务。

最后,我们讨论了在使用 Docker 时的各种安全方面,它所依赖的各种内核特性,它们的可靠性以及它们对容器所在主机安全性的影响。

在下一章中,我们将进一步采取本章的方法,并查看各种开源项目。我们将学习如何集成或使用它们,以充分实现 Docker 的潜力。

第五章:Docker 的朋友

到目前为止,我们一直忙于学习有关 Docker 的一切。影响开源项目寿命的一个主要因素是其周围的社区。Docker 的创建者 Docker Inc.(dotCloud的分支)负责开发和维护 Docker 及其姊妹项目,如 libcontainer、libchan、swarm 等(完整列表可在github.com/docker找到)。然而,像任何其他开源项目一样,开发是公开的(在 GitHub 上),他们接受拉取请求。

行业也接受了 Docker。像谷歌、亚马逊、微软、eBay 和 RedHat 这样的大公司积极使用和贡献 Docker。大多数流行的 IaaS 解决方案,如亚马逊网络服务、谷歌计算云等,都支持创建预加载和优化为 Docker 的镜像。许多初创公司也在 Docker 上押注他们的财富。CoreOS、Drone.io 和 Shippable 是一些初创公司,它们提供基于 Docker 的服务。因此,您可以放心,它不会很快消失。

在本章中,我们将讨论围绕 Docker 的一些项目以及如何使用它们。我们还将看看您可能已经熟悉的项目,这些项目可以促进您的 Docker 工作流程(并使您的生活变得更加轻松)。

首先,我们将讨论如何使用 Chef 和 Puppet 配方与 Docker。你们中的许多人可能已经在工作流程中使用这些工具。本节将帮助您将 Docker 与当前工作流程集成,并使您逐步进入 Docker 生态系统。

接下来,我们将尝试设置一个apt-cacher,这样我们的 Docker 构建就不会花费大量时间从 Canonical 服务器获取经常使用的软件包。这将大大减少使用 Dockerfile 构建镜像所需的时间。

在早期阶段,给 Docker 带来如此大的关注的一件事是,一些本来被认为很难的事情在 Docker 实现时似乎变得很容易。其中一个项目是Dokku,一个 100 行的 bash 脚本,可以设置一个类似于mini-Heroku的 PaaS。在本章中,我们将使用 Dokku 设置我们自己的 PaaS。本书中我们将讨论的最后一件事是使用 CoreOS 和 Fleet 部署高可用服务。

简而言之,在我们旅程的最后一段,我们将讨论以下主题:

  • 使用 Docker 与 Chef 和 Puppet

  • 设置一个 apt-cacher

  • 设置您自己的 mini-Heroku

  • 设置一个高可用的服务

使用 Docker 与 Chef 和 Puppet

当企业开始进入云时,扩展变得更加容易,因为可以从一台单机扩展到数百台而不费吹灰之力。但这也意味着需要配置和维护这些机器。配置管理工具,如 Chef 和 Puppet,是为了自动部署公共/私有云中的应用而产生的需求。如今,Chef 和 Puppet 每天都被全球各地的初创公司和企业用来管理他们的云环境。

使用 Docker 与 Chef

Chef 的网站上写着:

"Chef 将基础设施转化为代码。使用 Chef,您可以自动化构建、部署和管理基础设施。您的基础设施变得像应用代码一样可版本化、可测试和可重复。"

现在,假设您已经设置好了 Chef,并熟悉了 Chef 的工作流程,让我们看看如何使用 chef-docker 食谱在 Chef 中使用 Docker。

您可以使用任何食谱依赖管理器安装此食谱。有关 Berkshelf、Librarian 和 Knife 的安装说明可在该食谱的 Chef 社区网站上找到(supermarket.getchef.com/cookbooks/docker)。

安装和配置 Docker

安装 Docker 很简单。只需将recipe[docker]命令添加到运行列表(配置设置列表)即可。举个例子,让我们看看如何编写一个 Chef 配方来在 Docker 上运行code.it文件(我们的示例项目)。

编写一个 Chef 配方,在 Docker 上运行 Code.it

以下 Chef 配方基于code.it启动一个容器:

# Include Docker recipe
include_recipe 'docker'

# Pull latest image
docker_image 'shrikrishna/code.it'

# Run container exposing ports
docker_container 'shrikrishna/code.it' do
  detach true
  port '80:8000'
  env 'NODE_PORT=8000'
  volume '/var/log/code.it:/var/log/code.it'
end

第一个非注释语句包括 Chef-Docker 配方。docker_image 'shrikrishna/code.it'语句相当于在控制台中运行$ docker pull shrikrishna/code.it命令。配方末尾的语句块相当于运行$ docker run --d -p '8000:8000' -e 'NODE_PORT=8000' -v '/var/log/code.it:/var/log/code.it' shrikrishna/code.it命令。

使用 Docker 与 Puppet

PuppetLabs 的网站上写着:

“Puppet 是一个配置管理系统,允许您定义 IT 基础架构的状态,然后自动强制执行正确的状态。无论您是管理几台服务器还是成千上万台物理和虚拟机,Puppet 都会自动化系统管理员经常手动执行的任务,从而节省时间和精力,使系统管理员可以专注于提供更大商业价值的项目。”

Puppet 的等效于 Chef cookbooks 的模块。有一个为 Docker 提供支持的模块可用。通过运行以下命令来安装它:

$ puppet module install garethr-docker

编写一个 Puppet 清单来在 Docker 上运行 Code.it

以下 Puppet 清单启动了一个code.it容器:

# Installation
include 'docker'

# Download image
docker::image {'shrikrishna/code.it':}

# Run a container
docker::run { 'code.it-puppet':
  image   => 'shrikrishna/code.it',
  command => 'node /srv/app.js',
  ports   => '8000',
  volumes => '/var/log/code.it'
}

第一个非注释语句包括docker模块。docker::image {'shrikrishna/code.it':}语句相当于在控制台中运行$ docker pull shrikrishna/code.it命令。在配方末尾的语句块相当于运行$ docker run --d -p '8000:8000' -e 'NODE_PORT=8000' -v '/var/log/code.it:/var/log/code.it' shrikrishna/code.it node /srv/app.js命令。

设置 apt-cacher

当您有多个 Docker 服务器,或者当您正在构建多个不相关的 Docker 镜像时,您可能会发现每次都必须下载软件包。这可以通过在服务器和客户端之间设置缓存代理来防止。它在您安装软件包时缓存软件包。如果您尝试安装已经缓存的软件包,它将从代理服务器本身提供,从而减少获取软件包的延迟,大大加快构建过程。

让我们编写一个 Dockerfile 来设置一个 apt 缓存服务器作为缓存代理服务器:

FROM        ubuntu

VOLUME      ["/var/cache/apt-cacher-ng"]
RUN       apt-get update ; apt-get install -yq apt-cacher-ng

EXPOSE      3142
RUN     echo "chmod 777 /var/cache/apt-cacher-ng ;" + "/etc/init.d/apt-cacher-ng start ;" + "tail -f /var/log/apt-cacher-ng/*" >> /init.sh
CMD     ["/bin/bash", "/init.sh"]

这个 Dockerfile 在镜像中安装了apt-cacher-ng软件包,并暴露端口3142(供目标容器使用)。

使用此命令构建镜像:

$ sudo docker build -t shrikrishna/apt_cacher_ng

然后运行它,绑定暴露的端口:

$ sudo docker run -d -p 3142:3142 --name apt_cacher shrikrishna/apt_cacher_ng

要查看日志,请运行以下命令:

$ sudo docker logs -f apt_cacher

在构建 Dockerfiles 时使用 apt-cacher

所以我们已经设置了一个 apt-cacher。现在我们必须在我们的 Dockerfiles 中使用它:

FROM ubuntu
RUN  echo 'Acquire::http { Proxy "http://<host's-docker0-ip- here>:3142"; };' >> /etc/apt/apt.conf.d/01proxy

在第二条指令中,用您的 Docker 主机的 IP 地址(在docker0接口处)替换<host's-docker0-ip-here>命令。在构建这个 Dockerfile 时,如果遇到任何已经安装过的软件包的apt-get install安装命令(无论是为了这个镜像还是其他镜像),它将从本地代理服务器获取软件包,从而加快构建过程中的软件包安装速度。如果要安装的软件包不在缓存中,则从 Canonical 仓库获取并保存在缓存中。

提示

apt-cacher 只对使用 Apt 软件包管理工具的基于 Debian 的容器(如 Ubuntu)有效。

设置您自己的迷你 Heroku

现在让我们做一些酷炫的事情。对于初学者来说,Heroku 是一个云 PaaS,这意味着您在构建应用程序时只需要将其推送到 Heroku,它就会部署在www.herokuapp.com上。您不需要担心应用程序运行的方式或位置。只要 PaaS 支持您的技术栈,您就可以在本地开发并将应用程序推送到服务上,让其在公共互联网上实时运行。

除了 Heroku 之外,还有许多 PaaS 提供商。一些流行的提供商包括 Google App Engine、Red Hat Cloud 和 Cloud Foundry。Docker 是由一个这样的 PaaS 提供商 dotCloud 开发的。几乎每个 PaaS 都通过在预定义的沙盒环境中运行应用程序来工作,而这正是 Docker 擅长的。如今,Docker 已经使得设置 PaaS 变得更加容易,如果不是简单的话。证明这一点的项目是 Dokku。Dokku 与 Heroku 共享使用模式和术语(如buildpacksslug builder脚本),这使得它更容易使用。在本节中,我们将使用 Dokku 设置一个迷你 PaaS,并推送我们的code.it应用程序。

注意

接下来的步骤应该在虚拟专用服务器(VPS)或虚拟机上完成。您正在使用的主机应该已经设置好了 git 和 SSH。

使用 bootstrapper 脚本安装 Dokku

有一个bootstrapper脚本可以设置 Dokku。在 VPS/虚拟机内运行此命令:

$ wget -qO- https://raw.github.com/progrium/dokku/v0.2.3/bootstrap.sh | sudo DOKKU_TAG=v0.2.3 bash

注意

12.04 版本的用户需要在运行上述bootstrapper脚本之前运行$ apt-get install -y python-software-properties命令。

bootstrapper脚本将下载所有依赖项并设置 Dokku。

使用 Vagrant 安装 Dokku

步骤 1:克隆 Dokku:

$ git clone https://github.com/progrium/dokku.git

步骤 2:在您的/etc/hosts文件中设置 SSH 主机:

10.0.0.2 dokku.app

步骤 3:在~/.ssh/config中设置 SSH 配置

Host dokku.app
Port 2222

步骤 4:创建虚拟机

以下是一些可选的 ENV 参数设置:

# - `BOX_NAME`
# - `BOX_URI`
# - `BOX_MEMORY`
# - `DOKKU_DOMAIN`
# - `DOKKU_IP`.
cd path/to/dokku
vagrant up

步骤 5:使用此命令复制您的 SSH 密钥:

$ cat ~/.ssh/id_rsa.pub | pbcopy

http://dokku.app的 dokku-installer 中粘贴您的 SSH 密钥(指向/etc/hosts文件中分配的10.0.0.2)。在Dokku 设置屏幕上更改主机名字段为您的域名,然后选中使用虚拟主机命名的复选框。然后,单击完成设置以安装您的密钥。您将从这里被引导到应用程序部署说明。

您现在已经准备好部署应用程序或安装插件。

配置主机名并添加公钥

我们的 PaaS 将子域路由到使用相同名称部署的应用程序。这意味着设置了 Dokku 的机器必须对您的本地设置以及运行 Dokku 的机器可见。

设置一个通配符域,指向 Dokku 主机。运行bootstrapper脚本后,检查 Dokku 主机中的/home/dokku/VHOST文件是否设置为此域。只有当 dig 工具可以解析主机名时,它才会被创建。

在此示例中,我已将我的 Dokku 主机名设置为dokku.app,方法是将以下配置添加到我的本地主机的/etc/hosts文件中:

10.0.0.2 dokku.app

我还在本地主机的~/.ssh/config文件中设置了 SSH 端口转发规则:

Host dokku.app
Port 2222

注意

根据维基百科,域名信息检索器dig)是一个用于查询 DNS 名称服务器的网络管理命令行工具。这意味着给定一个 URL,dig 将返回 URL 指向的服务器的 IP 地址。

如果/home/dokku/VHOST文件没有自动创建,您将需要手动创建它并将其设置为您喜欢的域名。如果在部署应用程序时缺少此文件,Dokku 将使用端口名称而不是子域名发布应用程序。

最后要做的事情是将您的公共ssh密钥上传到 Dokku 主机并将其与用户名关联起来。要这样做,请运行此命令:

$ cat ~/.ssh/id_rsa.pub | ssh dokku.app "sudo sshcommand acl-add dokku shrikrishna"

在上述命令中,将dokku.app名称替换为您的域名,将shrikrishna替换为您的名称。

太好了!现在我们已经准备好了,是时候部署我们的应用程序了。

部署应用程序

我们现在有了自己的 PaaS,可以在那里部署我们的应用程序。让我们在那里部署code.it文件。您也可以尝试在那里部署您自己的应用程序:

$ cd code.it
$ git remote add dokku dokku@dokku.app:codeit
$ git push dokku master
Counting objects: 456, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (254/254), done.
Writing objects: 100% (456/456), 205.64 KiB, done.
Total 456 (delta 34), reused 454 (delta 12)
-----> Building codeit ...
Node.js app detected
-----> Resolving engine versions

......
......
......

-----> Application deployed:
http://codeit.dokku.app

就是这样!我们现在在我们的 PaaS 中有一个可用的应用程序。有关 Dokku 的更多详细信息,您可以查看其 GitHub 存储库页面github.com/progrium/dokku

如果您想要一个生产就绪的 PaaS,您必须查找 Deis deis.io/,它提供多主机和多租户支持。

建立一个高可用的服务

虽然 Dokku 非常适合部署偶尔的副业,但对于较大的项目可能不太合适。大规模部署基本上具有以下要求:

  • 水平可扩展:单个服务器实例只能做这么多。随着负载的增加,处于快速增长曲线上的组织将发现自己必须在一组服务器之间平衡负载。在早期,这意味着必须设计数据中心。今天,这意味着向云中添加更多实例。

  • 容错:即使有广泛的交通规则来避免交通事故,事故也可能发生,但即使您采取了广泛的措施来防止事故,一个实例的崩溃也不应该导致服务停机。良好设计的架构将处理故障条件,并使另一个服务器可用以取代崩溃的服务器。

  • 模块化:虽然这可能看起来不是这样,但模块化是大规模部署的一个定义特征。模块化架构使其灵活且具有未来可塑性(因为模块化架构将随着组织的范围和影响力的增长而容纳新的组件)。

这绝不是一个详尽的清单,但它标志着构建和部署高可用服务所需的努力。然而,正如我们到目前为止所看到的,Docker 仅用于单个主机,并且(直到现在)没有可用于管理运行 Docker 的一组实例的工具。

这就是 CoreOS 的用武之地。它是一个精简的操作系统,旨在成为 Docker 大规模部署服务的构建模块。它配备了一个高可用的键值配置存储,称为etcd,用于配置管理和服务发现(发现集群中其他组件的位置)。etcd服务在第四章中进行了探讨,自动化和最佳实践。它还配备了 fleet,这是一个利用etcd提供的一种在整个集群上执行操作的工具,而不是在单个实例上执行操作。

注意

您可以将 fleet 视为在集群级别而不是机器级别运行的systemd套件的扩展。systemd套件是单机初始化系统,而 fleet 是集群初始化系统。您可以在coreos.com/using-coreos/clustering/了解更多关于 fleet 的信息。

在本节中,我们将尝试在本地主机上的三节点 CoreOS 集群上部署我们的标准示例code.it。这是一个代表性的示例,实际的多主机部署将需要更多的工作,但这是一个很好的起点。这也帮助我们欣赏多年来在硬件和软件方面所做的伟大工作,使得部署高可用服务成为可能,甚至变得容易,而这在几年前只有在大型数据中心才可能。

安装依赖项

运行上述示例需要以下依赖项:

  1. VirtualBox:VirtualBox 是一种流行的虚拟机管理软件。您可以从www.virtualbox.org/wiki/Downloads下载适用于您平台的安装可执行文件。

  2. Vagrant:Vagrant 是一个开源工具,可以被视为 Docker 的虚拟机等效物。它可以从www.vagrantup.com/downloads.html下载。

  3. Fleetctl:Fleet 简而言之是一个分布式初始化系统,这意味着它将允许我们在集群级别管理服务。Fleetctl 是一个 CLI 客户端,用于运行 fleet 命令。要安装 fleetctl,请运行以下命令:

$ wget \ https://github.com/coreos/fleet/releases/download/v0.3.2/fleet -v0.3.2-darwin-amd64.zip && unzip fleet-v0.3.2-darwin-amd64.zip
$ sudo cp fleet-v0.3.2-darwin-amd64/fleetctl /usr/local/bin/

获取并配置 Vagrantfile

Vagrantfiles 是 Dockerfiles 的 Vagrant 等价物。Vagrantfile 包含诸如获取基本虚拟机、运行设置命令、启动虚拟机镜像实例数量等详细信息。CoreOS 有一个包含 Vagrantfile 的存储库,可用于下载和在虚拟机中使用 CoreOS。这是在开发环境中尝试 CoreOS 功能的理想方式:

$ git clone https://github.com/coreos/coreos-vagrant/
$ cd coreos-vagrant

上述命令克隆了包含 Vagrantfile 的coreos-vagrant存储库,该文件下载并启动基于 CoreOS 的虚拟机。

注意

Vagrant 是一款免费开源软件,用于创建和配置虚拟开发环境。它可以被视为围绕虚拟化软件(如 VirtualBox、KVM 或 VMware)和配置管理软件(如 Chef、Salt 或 Puppet)的包装器。您可以从www.vagrantup.com/downloads.html下载 Vagrant。

不过,在启动虚拟机之前,我们需要进行一些配置。

获取发现令牌

每个 CoreOS 主机都运行etcd服务的一个实例,以协调在该机器上运行的服务,并与集群中其他机器上运行的服务进行通信。为了实现这一点,etcd实例本身需要相互发现。

CoreOS 团队构建了一个发现服务(discovery.etcd.io),它提供了一个免费服务,帮助etcd实例通过存储对等信息相互通信。它通过提供一个唯一标识集群的令牌来工作。集群中的每个etcd实例都使用此令牌通过发现服务识别其他etcd实例。生成令牌很容易,只需通过GET请求发送到discovery.etcd.io/new即可。

$ curl -s https://discovery.etcd.io/new
https://discovery.etcd.io/5cfcf52e78c320d26dcc7ca3643044ee

现在打开coreos-vagrant目录中名为user-data.sample的文件,并找到包含etcd服务下的discovery配置选项的注释行。取消注释并提供先前运行的curl命令返回的令牌。完成后,将文件重命名为user-data

注意

user-data文件用于为 CoreOS 实例中的cloud-config程序设置配置参数。Cloud-config 受cloud-init项目中的cloud-config文件的启发,后者定义自己为处理云实例的早期初始化的事实多发行包(cloud-init文档)。简而言之,它有助于配置各种参数,如要打开的端口,在 CoreOS 的情况下,etcd配置等。您可以在以下网址找到更多信息:

coreos.com/docs/cluster-management/setup/cloudinit-cloud-config/cloudinit.readthedocs.org/en/latest/index.html

以下是 CoreOS 代码的示例:

coreos:
  etcd:
    # generate a new token for each unique cluster from https://discovery.etcd.io/new
    # WARNING: replace each time you 'vagrant destroy'
    discovery: https://discovery.etcd.io/5cfcf52e78c320d26dcc7ca3643044ee
    addr: $public_ipv4:4001
    peer-addr: $public_ipv4:7001
  fleet:
    public-ip: $public_ipv4
  units:

提示

每次运行集群时,您都需要生成一个新的令牌。简单地重用令牌将不起作用。

设置实例数量

coreos-vagrant目录中,还有另一个名为config.rb.sample的文件。找到该文件中的注释行,其中写着$num_instances=1。取消注释并将值设置为3。这将使 Vagrant 生成三个 CoreOS 实例。现在将文件保存为config.rb

注意

cnfig.rb文件保存了 Vagrant 环境的配置以及集群中的机器数量。

以下是 Vagrant 实例的代码示例:

# Size of the CoreOS cluster created by Vagrant
$num_instances=3

生成实例并验证健康

现在我们已经准备好配置,是时候在本地机器上看到一个运行的集群了:

$ vagrant up
Bringing machine 'core-01' up with 'virtualbox' provider...
Bringing machine 'core-02' up with 'virtualbox' provider...
Bringing machine 'core-03' up with 'virtualbox' provider...
==> core-01: Box 'coreos-alpha' could not be found. Attempting to find and install...
core-01: Box Provider: virtualbox
core-01: Box Version: >= 0
==> core-01: Adding box 'coreos-alpha' (v0) for provider: virtualbox
. . . . .
. . . . .
. . . . .

创建完机器后,您可以 SSH 登录到它们,尝试以下命令,但您需要将ssh密钥添加到您的 SSH 代理中。这样做将允许您将 SSH 会话转发到集群中的其他节点。要添加密钥,请运行以下命令:

$ ssh-add ~/.vagrant.d/insecure_private_key
Identity added: /Users/CoreOS/.vagrant.d/insecure_private_key (/Users/CoreOS/.vagrant.d/insecure_private_key)
$ vagrant ssh core-01 -- -A

现在让我们验证一下机器是否正常运行,并要求 fleet 列出集群中正在运行的机器:

$ export FLEETCTL_TUNNEL=127.0.0.1:2222
$ fleetctl list-machines
MACHINE     IP           METADATA
daacff1d... 172.17.8.101 -
20dddafc... 172.17.8.102 -
eac3271e... 172.17.8.103 -

启动服务

要在新启动的集群中运行服务,您将需要编写unit-files文件。单元文件是列出必须在每台机器上运行的服务以及如何管理这些服务的一些规则的配置文件。

创建三个名为code.it.1.servicecode.it.2.servicecode.it.3.service的文件。用以下配置填充它们:

code.it.1.service

[Unit]
Description=Code.it 1
Requires=docker.service  
After=docker.service

[Service]
ExecStart=/usr/bin/docker run --rm --name=code.it-1 -p 80:8000 shrikrishna/code.it
ExecStartPost=/usr/bin/etcdctl set /domains/code.it-1/%H:%i running  
ExecStop=/usr/bin/docker stop code.it-1  
ExecStopPost=/usr/bin/etcdctl rm /domains/code.it-1/%H:%i

[X-Fleet]
X-Conflicts=code.it.*.service

code.it.2.service

[Unit]
Description=Code.it 2  
Requires=docker.service  
After=docker.service

[Service]
ExecStart=/usr/bin/docker run --rm --name=code.it-2 -p 80:8000 shrikrishna/code.it
ExecStartPost=/usr/bin/etcdctl set /domains/code.it-2/%H:%i running  
ExecStop=/usr/bin/docker stop code.it-2  
ExecStopPost=/usr/bin/etcdctl rm /domains/code.it-2/%H:%i

[X-Fleet]
X-Conflicts=code.it.2.service

code.it.3.service

[Unit]
Description=Code.it 3  
Requires=docker.service  
After=docker.service

[Service]
ExecStart=/usr/bin/docker run --rm --name=code.it-3 -p 80:8000 shrikrishna/code.it
ExecStartPost=/usr/bin/etcdctl set /domains/code.it-3/%H:%i running  
ExecStop=/usr/bin/docker stop code.it-3  
ExecStopPost=/usr/bin/etcdctl rm /domains/code.it-3/%H:%i

[X-Fleet]
X-Conflicts=code.it.*.service  

您可能已经注意到这些文件中的模式。ExecStart参数保存了必须执行的命令,以启动服务。在我们的情况下,这意味着运行code.it容器。ExecStartPost是在ExecStart参数成功后执行的命令。在我们的情况下,服务的可用性被注册在etcd服务中。相反,ExecStop命令将停止服务,而ExecStopPost命令在ExecStop命令成功后执行,这在这种情况下意味着从etcd服务中删除服务的可用性。

X-Fleet是 CoreOS 特有的语法,告诉 fleet 两个服务不能在同一台机器上运行(因为它们在尝试绑定到相同端口时会发生冲突)。现在所有的块都就位了,是时候将作业提交到集群了:

$ fleetctl submit code.it.1.service code.it.2.service code.it.3.service

让我们验证服务是否已提交到集群:

$ fleetctl list-units
UNIT              LOAD  ACTIVE  SUB  DESC                 MACHINE
code.it.1.service  -     -       -   Code.it 1  -
code.it.2.service  -     -       -   Code.it 2  -
code.it.3.service  -     -       -   Code.it 3  -

机器列为空,活动状态未设置。这意味着我们的服务尚未启动。让我们启动它们:

$ fleetctl start code.it.{1,2,3}.service
Job code.it.1.service scheduled to daacff1d.../172.17.8.101
Job code.it.1.service scheduled to 20dddafc.../172.17.8.102
Job code.it.1.service scheduled to eac3271e.../172.17.8.103

让我们通过再次执行$ fleetctl list-units文件来验证它们是否正在运行:

$ fleetctl list-units
UNIT               LOAD    ACTIVE   SUB     DESC                     MACHINE
code.it.1.service  loaded  active  running  Code.it 1 daacff1d.../172.17.8.101
code.it.1.service  loaded  active  running  Code.it 2 20dddafc.../172.17.8.102
code.it.1.service  loaded  active  running  Code.it 3 eac3271e.../172.17.8.103

恭喜!您刚刚建立了自己的集群!现在在 Web 浏览器中转到172.17.8.101172.17.8.102172.17.8.103,看看code.it应用程序正在运行!

在这个例子中,我们只是建立了一个运行高可用服务的机器集群。如果我们添加一个负载均衡器,它与etcd服务保持连接,将请求路由到可用的机器,我们将在我们的系统中运行一个完整的端到端生产级服务。但这样做会偏离主题,所以留给你作为练习。

通过这个,我们来到了尽头。Docker 仍在积极发展,像 CoreOS、Deis、Flynn 等项目也是如此。因此,尽管我们在过去几个月看到了很棒的东西,但即将到来的将会更好。我们生活在激动人心的时代。因此,让我们充分利用它,构建能让这个世界变得更美好的东西。祝愉快!

总结

在本章中,我们学习了如何使用 Docker 与 Chef 和 Puppet。然后我们设置了一个 apt-cacher 来加快软件包的下载速度。接下来,我们用 Dokku 搭建了自己的迷你 PaaS。最后,我们使用 CoreOS 和 Fleet 搭建了一个高可用性的服务。恭喜!我们一起获得了使用 Docker 构建容器、"dockerize"我们的应用甚至运行集群所需的知识。我们的旅程到此结束了。但是对于你,亲爱的读者,一个新的旅程刚刚开始。这本书旨在奠定基础,帮助你使用 Docker 构建下一个大事件。我祝你世界上一切成功。如果你喜欢这本书,在 Twitter 上给我发消息@srikrishnaholla。如果你不喜欢,也请告诉我如何改进。

posted @ 2024-05-06 18:32  绝不原创的飞龙  阅读(17)  评论(0编辑  收藏  举报