Docker Compose 简介
1. 概述
当广泛使用 Docker 时,管理多个不同的容器很快就会变得很麻烦。
Docker Compose 是一个工具,可以帮助我们克服这个问题,轻松地同时处理多个容器。
在本教程中,我们将了解它的主要功能和强大的机制。
2. YAML 配置说明
简而言之,Docker Compose 通过应用在单个docker-compose.yml配置文件中声明的许多规则来工作。
这些YAML 规则,既是人类可读的,也是机器优化的,为我们提供了一种有效的方法,可以用几行从一万英尺的高度拍摄整个项目的快照。
几乎每条规则都会替换特定的 Docker 命令,因此最后我们只需要运行:
docker-compose up
我们可以获得 Compose 在后台应用的数十种配置。这将使我们免于使用 Bash 或其他东西编写脚本的麻烦。
在此文件中,我们需要指定Compose 文件格式的version、至少一项service以及可选的volumes和networks:
version: "3.7"
services:
...
volumes:
...
networks:
...
让我们看看这些元素实际上是什么。
2.1. Services
首先,service是指容器的配置。
例如,让我们来看一个由前端、后端和数据库组成的 dockerized web 应用程序:我们可能会将这些组件拆分为三个映像,并在配置中将它们定义为三个不同的服务:
services:
frontend:
image: my-vue-app
...
backend:
image: my-springboot-app
...
db:
image: postgres
...
我们可以将多种设置应用于服务,稍后我们将深入探讨它们。
2.2. Volumes & Networks
另一方面,volumes是主机和容器之间甚至容器之间共享的磁盘空间的物理区域。换句话说,volumes是主机中的一个共享目录,对部分或所有容器可见。
类似地,networks定义了容器之间以及容器与主机之间的通信规则。公共网络区域将使容器的服务可以相互发现,而私有区域将它们隔离在虚拟沙箱中。
同样,我们将在下一节中详细了解它们。
3. 服务设置
现在让我们开始检查服务的主要设置。
3.1. 拉取镜像
有时,我们服务所需的镜像已经(由我们或其他人)在Docker Hub 或其他 Docker Registry 中发布。
如果是这种情况,那么我们通过指定镜像名称和标签,使用image属性引用它:
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属性指定一个image名称,这将在镜像创建后命名,使其可供其他服务 使用:
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 )引用它来与同一网络上的另一个服务通信,前提是我们已经通过expose关键字使端口可访问:
services:
network-example-service:
image: karthequian/helloworld:latest
expose:
- "80"
顺便说一句,在这种情况下,它也可以在不公开它的情况下工作,因为expose指令已经在镜像 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. 设置音量
共有三种类型的卷:anonymous、named和host 卷。
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
...
但是,我们应该知道,在启动kafka服务之前,Compose 不会等待zookeeper服务完成加载:它只会等待它启动。如果我们需要在启动另一个服务之前完全加载一个服务,我们需要在 Compose 中更深入地控制启动和关闭顺序 。
4. 管理环境变量
在 Compose 中使用环境变量很容易。我们可以定义静态环境变量,也可以用*${}*表示法定义动态变量:
services:
database:
image: "postgres:${POSTGRES_VERSION}"
environment:
DB: mydb
USER: "${USER}"
有多种方法可以将这些值提供给 Compose。
例如,将它们设置在同一目录中的*.env文件中,结构类似于.properties*文件,key=value:
POSTGRES_VERSION=alpine
USER=foo
否则,我们可以在调用命令之前在操作系统中设置它们:
export POSTGRES_VERSION=alpine
export USER=foo
docker-compose up
最后,我们可能会发现在 shell 中使用一个简单的单行代码很方便:
POSTGRES_VERSION=alpine USER=foo docker-compose up
我们可以混合使用这些方法,但请记住,Compose 使用以下优先级顺序,用较高的优先级覆盖较不重要的优先级:
- 撰写文件
- shell环境变量
- 环境文件
- 文件
- 变量未定义
5. 扩展和复制
在旧的 Compose 版本中,我们可以通过docker-compose scale 命令扩展容器的实例。较新的版本弃用了它,并将其替换为 –scale 选项。
另一方面,我们可以利用Docker Swarm ——一个 Docker 引擎集群——并通过部署部分的replicas属性以声明方式自动缩放我们的容器:
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
...
在deploy下,我们还可以指定许多其他选项,例如资源阈值。然而,Compose**仅在部署到 Swarm 时才考虑整个deploy **部分,否则忽略它。
6. 一个真实的例子:Spring Cloud Data Flow
虽然小实验可以帮助我们理解单个齿轮,但看到实际运行的代码肯定会揭开大局。
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 和 ––file 标志来指定自定义文件名:
docker-compose -f custom-compose-file.yml start
当使用*-d*选项启动时,Compose 还可以作为守护进程在后台运行:
docker-compose up -d
7.2. 关闭
为了安全地停止活动服务,我们可以使用stop,它将保留容器、卷和网络,以及对它们所做的每一个修改:
docker-compose stop
相反,要重置我们项目的状态,我们只需运行down,这将销毁除外部卷之外的所有内容:
docker-compose down