一、概述
当广泛使用 Docker 时,管理几个不同的容器很快就会变得很麻烦。
Docker Compose 是一个帮助我们克服这个问题并轻松同时处理多个容器的工具。
在本教程中,我们将了解它的主要特性和强大的机制。
Compose 简介
Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose,您可以使用 YML 文件来配置应用程序需要的所有服务。然后,使用一个命令,就可以从 YML 文件配置中创建并启动所有服务。
Compose 使用的三个步骤:
- 使用 Dockerfile 定义应用程序的环境。
- 使用 docker-compose.yml 定义构成应用程序的服务,这样它们可以在隔离环境中一起运行。
- 最后,执行 docker-compose up 命令来启动并运行整个应用程序。
2. YAML 配置说明
简而言之,Docker Compose 通过应用在单个docker-compose.yml配置文件中声明的许多规则来工作。
这些YAML规则,无论是人类可读的还是机器优化的,都为我们提供了一种有效的方法,可以在几行代码中从一万英尺处对整个项目进行快照。
几乎每条规则都替换了一个特定的 Docker 命令,所以最后我们只需要运行:
docker-compose up
我们可以在后台获得 Compose 应用的数十种配置。这将为我们省去使用 Bash 或其他东西编写脚本的麻烦。
在这个文件中,我们需要指定 Compose 文件格式的版本、至少一个service以及可选的volumes和networks:
version: "3.7" services: ... volumes: ... networks: ...
让我们看看这些元素实际上是什么。
2.1. 服务
首先,服务是指容器的配置。
例如,让我们以一个由前端、后端和数据库组成的 dockerized Web 应用程序为例:我们可能会将这些组件拆分为三个图像,并在配置中将它们定义为三个不同的服务:
services: frontend: image: my-vue-app ... backend: image: my-springboot-app ... db: image: postgres ...
我们可以将多种设置应用于服务,稍后我们将深入探讨它们。
2.2. 卷和网络
另一方面,卷是主机和容器甚至容器之间共享的磁盘空间的物理区域。换句话说,卷是主机中的共享目录,可从部分或所有容器中看到。
类似地,网络定义了容器之间以及容器和主机之间的通信规则。公共网络区域将使容器的服务可被彼此发现,而私有区域将它们隔离在虚拟沙箱中。
同样,我们将在下一节中进一步了解它们。
3. 剖析服务
现在让我们开始检查服务的主要设置。
3.1 拉取图像
有时,我们服务所需的镜像已经(由我们或其他人)发布在Docker Hub或另一个 Docker Registry 中。
如果是这种情况,那么我们通过指定图像名称和标签来使用图像属性来引用它:
services: my-service: image: ubuntu:latest ...
3.2. 构建图像
相反,我们可能需要通过读取源代码的Dockerfile来构建镜像。
这一次,我们将使用build关键字,将 Dockerfile 的路径作为值传递:
services: my-custom-app: build: /path/to/dockerfile/ ...
我们也可以使用 URL代替路径:
services: my-custom-app: build: https://github.com/my-company/my-project.git ...
此外,我们可以结合build属性指定图像名称,该属性将在创建后命名图像,使其可供其他服务使用:
services: my-custom-app: build: https://github.com/my-company/my-project.git image: my-project-image ...
3.3. 配置网络
Docker 容器在由 Docker Compose 隐式或通过配置创建的网络中相互通信。一个服务可以通过简单地通过容器名称和端口(例如network-example-service:80)引用它来与同一网络上的另一个服务进行通信,前提是我们已经通过暴露关键字使端口可访问:
services: network-example-service: image: karthequian/helloworld:latest expose: - "80"
在这种情况下,顺便说一句,它也可以在不暴露它的情况下工作,因为暴露指令已经在图像 Dockerfile中。
要从主机访问容器,端口必须通过ports关键字以声明方式公开,这也允许我们选择是否在主机中以不同方式公开端口:
services: network-example-service: image: karthequian/helloworld:latest ports: - "80:80" ... my-custom-app: image: myapp:latest ports: - "8080:3000" ... my-custom-app-replica: image: myapp:latest ports: - "8081:3000" ...
现在可以从主机看到端口 80,而其他两个容器的端口 3000 将在主机的端口 8080 和 8081 上可用。这种强大的机制允许我们运行不同的容器暴露相同的端口而不会发生冲突。
最后,我们可以定义额外的虚拟网络来隔离我们的容器:
services: network-example-service: image: karthequian/helloworld:latest networks: - my-shared-network ... another-service-in-the-same-network: image: alpine:latest networks: - my-shared-network ... another-service-in-its-own-network: image: alpine:latest networks: - my-private-network ... networks: my-shared-network: {} my-private-network: {}
在最后一个示例中,我们可以看到another-service-in-the-same-network将能够 ping 并到达network-example-service 的端口 80 ,而another-service-in-its-own-network获胜不。
3.4. 设置卷
共有三种类型的卷:匿名、命名和主机卷。
Docker 管理匿名卷和命名卷,自动将它们挂载到主机中自己生成的目录中。虽然匿名卷对旧版本的 Docker(1.9 之前)很有用,但现在建议使用命名卷。主机卷还允许我们指定主机中的现有文件夹。
我们可以在服务级别配置主机卷,在配置的外部级别配置命名卷,以使后者对其他容器可见,而不仅仅是它们所属的容器:
services: volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/my-volumes/named-global-volume - /tmp:/my-volumes/host-volume - /home:/my-volumes/readonly-host-volume:ro ... another-volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/another-path/the-same-named-global-volume ... volumes: my-named-global-volume:
在这里,两个容器都将拥有对my-named-global-volume共享文件夹的读/写访问权限,无论它们映射到不同的路径。相反,这两个主机卷将仅对volumes-example-service可用。
主机文件系统的/tmp文件夹映射到容器的/my-volumes/host-volume文件夹。
文件系统的这一部分是可写的,这意味着容器不仅可以读取而且可以写入(和删除)主机中的文件。
我们可以通过将:ro附加到规则来以只读模式安装卷,例如/home文件夹(我们不希望 Docker 容器错误地擦除我们的用户)。
3.5. 声明依赖
通常,我们需要在我们的服务之间创建一个依赖链,以便一些服务在其他服务之前加载(并在之后卸载)。我们可以通过depends_on关键字来实现这个结果:
services: kafka: image: wurstmeister/kafka:2.11-0.11.0.3 depends_on: - zookeeper ... zookeeper: image: wurstmeister/zookeeper ...
然而,我们应该知道,Compose在启动kafka服务之前不会等待zookeeper服务完成加载:它只会等待它启动。如果我们需要在启动另一个服务之前完全加载一个服务,我们需要在 Compose 中更深入地控制启动和关闭顺序。
4. 管理环境变量
在 Compose 中使用环境变量很容易。我们可以定义静态环境变量,也可以使用${}符号定义动态变量:
services: database: image: "postgres:${POSTGRES_VERSION}" environment: DB: mydb USER: "${USER}"
有不同的方法可以将这些值提供给 Compose。
例如,将它们设置在同一目录中的.env文件中,其结构类似于.properties文件,key=value:
POSTGRES_VERSION=alpi USER=foo
否则,我们可以在调用命令之前在操作系统中设置它们:
export POSTGRES_VERSION=alpine export USER=foo docker-compose up
最后,我们可能会发现在 shell 中使用简单的单行代码会很方便:
POSTGRES_VERSION=alpine USER=foo docker-compose up
我们可以混合使用这些方法,但请记住,Compose 使用以下优先级顺序,用较高的优先级覆盖不太重要的:
- 编写文件
- 外壳环境变量
- 环境文件
- Dockerfile
- 变量未定义
5. 扩展和副本
在旧的 Compose 版本中,我们可以通过docker-compose scale命令来扩展容器的实例。较新的版本弃用了它并用– – scale选项取而代之。
另一方面,我们可以利用Docker Swarm——一个 Docker 引擎集群——并通过部署部分的副本属性以声明方式自动扩展我们的容器:
services: worker: image: dockersamples/examplevotingapp_worker networks: - frontend - backend deploy: mode: replicated replicas: 6 resources: limits: cpus: '0.50' memory: 50M reservations: cpus: '0.25' memory: 20M ...
在部署下,我们还可以指定许多其他选项,例如资源阈值。然而,Compose仅在部署到 Swarm 时才考虑整个部署部分,否则将忽略它。
6. 一个真实的例子:Spring Cloud 数据流
虽然小型实验有助于我们理解单齿轮,但看到实际代码在运行中肯定会揭示大局。
Spring Cloud Data Flow 是一个复杂的项目,但足够简单易懂。让我们下载它的 YAML 文件并运行:
DATAFLOW_VERSION=2.1.0.RELEASE SKIPPER_VERSION=2.0.2.RELEASE docker-compose up
Compose 将下载、配置和启动每个组件,然后将容器的日志交叉到当前终端中的单个流中。
它还将为每种颜色应用独特的颜色,以获得出色的用户体验:
在运行全新的 Docker Compose 安装时,我们可能会遇到以下错误:
lookup registry-1.docker.io: no such host
虽然对于这个常见的陷阱有不同的解决方案,但使用8.8.8.8作为 DNS 可能是最简单的。
7. 生命周期管理
最后让我们仔细看看 Docker Compose 的语法:
docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
虽然有许多可用的选项和命令,但我们至少需要知道正确激活和停用整个系统的选项和命令。
7.1。启动
我们已经看到,我们可以使用up创建和启动配置中定义的容器、网络和卷:
docker-compose up
然而,在第一次之后,我们可以简单地使用start来启动服务:
docker-compose start
如果我们的文件名称与默认名称不同(docker-compose.yml),我们可以利用-f和– –文件标志来指定备用文件名:
docker-compose -f custom-compose-file.yml start
当使用-d选项启动时,Compose 也可以作为守护进程在后台运行:
docker-compose up -d
7.2. 关闭
为了安全地停止活动服务,我们可以使用stop,它将保留容器、卷和网络,以及对它们所做的每一次修改:
docker-compose stop
相反,要重置项目的状态,我们只需运行down,这将破坏除外部卷之外的所有内容:
docker-compose down
8. 结论
在本教程中,我们了解了 Docker Compose 及其工作原理。
像往常一样,我们可以在 GitHub 上找到源docker-compose.yml文件,以及下图中立即可用的一组有用的测试: