Docker Compose 运维之道:配置、部署与维护

前言

在传统的服务搭建过程中,我常常面临一系列挑战:

  • 繁杂的配置过程,以及分散的配置文件,难以管理和自动化。
  • 特定版本的编译常常因为依赖处理而变复杂。
  • 特定版本的依赖库很容易对系统环境造成“污染”。

为了解决这些问题,我曾将每个项目的编译后二进制文件、依赖库与配置文件捆绑部署的策略,以便于扩展和迁移,保障了项目的一致性与可复现性。这一方法有点类似于 Linux 桌面环境下的 AppImage 和 Snap 的打包方式。在某些情况下,我会创建一个虚拟机镜像来作为项目的独立单元。

然而,Docker 以其优雅的依赖打包和命名空间隔离技术,提供了一种更高效的解决方案。它不仅能够轻松调整配置文件路径,还能保持操作的透明性和简洁性。Docker Compose 通过服务编排功能,进一步提升了容器内文件和命令更改的透明性。

这些工具不仅替代了我以前的打包和虚拟机镜像策略,还带来了脚本化和高度可复现性的便利。我已将几乎所有项目由 Docker Compose 编排。下面,我将分享在实践过程中积累的一些经验:这些理念旨在尽量简化依赖关系并阐明原理,但在具体实施时,可以根据需求选择恰当的系统来优化或替代某些步骤。

构建容器化环境

为了确保环境的一致性和可复现性,环境配置过程应该保存,形成配置文件或脚本。在 DevOps 环境中这些可能保存在服务或数据库中;对于单机运维,因为部署环境的服务供应商差别,建议给每个不同的环境创建独立的 Git 仓库;并用 Git 的标签(tag)实现来管理版本,从而实现快速回滚。
以下是我推荐的目录结构示例,旨在提供清晰和可管理的文件组织方式:

1
2
3
4
5
6
7
8
9
10
11
12
├── .gitignore
├── docker-compose.yaml # 描述当前环境的 Docker Compose 文件
├── images # 存放定制容器镜像,用于在无外网访问的环境中做临时中转
├── release # 项目的发布目录,存放可执行文件,静态文件等。
├── secret # 用于存储敏感信息和密钥。要小心管理,并考虑把从项目中剥离。考虑使用 .env 文件。
├── services # 第三方服务的 Dockerfile,依赖项和配置文件
├── src # 项目源码路径,每个项目一个文件夹。如果项目有 Dockerfile,应置于根目录。
├── var # 容器临时数据,如缓存、日志等,不需备份的数据。按‘文件类型/容器名’组织。
│   ├── log
│   └── cache
└── volumes # 需要永久保存、备份或迁移的持久化数据,根据容器进行分类。
   └── backups

推荐的.gitignore内容:

1
2
3
4
/images
/var
/src
/volumes

接下来,我将详述每个部分的设计意图和需要注意的点。

源码目录(src)

src下每个目录代表一个项目,方便进行迭代拉取后进行编译打包。除非项目只会用在这一个仓库,否则该目录应被添加到 .gitignore 文件中以避免被追踪。

发布目录(release)

src 编译打包的文件按项目名存放到release。在每次正式部署时,将发布文件添加到版本控制并打上 Git 标签。通过这种方式,我们可以轻松地实现对环境状态的回滚。

release 目录中的文件使用 bind mounts 挂载到容器中,而不是构建新的 Docker 镜像。这种方法大大减少了需要维护的镜像数量,进而避免了自建 Docker 镜像源的需求。接下来,我会进一步讨论从文件系统到容器的挂载方式的优势和潜在问题。暂时假设所有文件的用户为 root,并且挂载权限为只读。

服务目录(services)

services 目录与 docker-compose.yaml 中的 services 部分相对应,也使用 bind mounts 挂载到容器。在这里,你可能会遇到权限问题,即挂载目标目录必须可写的限制。最简单的解决方案是设置 release 目录的权限为可读写。然而,这样做带来了安全隐患,特别是当部署像 WordPress 这样的项目时,在公网上可能随时会有攻击尝试进行渗透。

services 目录存放容器的配置文件。你可能知道 12 因素应用原则,意识到 Docker 镜像的最佳配置方式是通过环境变量。但是,环境变量并不适合传递结构化数据,而且经常需要通过 shell 交互,这增加了容器复杂性和调试难度。因此我保留了配置文件挂载,替代环境变量不适合的情况。

此外,services 目录也存放容器的依赖文件,用于满足现有 Docker 镜像无法提供的功能。比如版本控制的模块和插件。也可以直接将编译好的服务的二进制文件放到这里,并挂载到容器中执行,以此来避免构建镜像的步骤。

刚刚没有讲srcDockerfilesrcDockerfile可能包含项目的编译和执行。而在 services 目录中的 Dockerfile 应仅包含环境搭建和执行。在 docker-compose.yaml 中,可以使用 build 标签来指定 Dockerfile,并在运行时构建。由于我们采取了 Git 仓库的版本控制,不需要 docker 的版本标签。docker compose up -d --build会自动判断是否需要重新构建镜像。

构建服务端的 Dockerfile 需要精心设计,确保它能够适应服务器的网络环境、承受的负载,以及满足对部署速度的要求。为了提高效率和确保一致性,通常我们避免在 Dockerfile 中直接编译源码。如果存在网络限制或其他问题,考虑使用本地的 images 目录来迁移预先构建的镜像,可以查阅下一章的内容实现远程部署。

动态数据目录(var)

var 目录借鉴了 Linux 中的 /var 设计理念,用于存储变动频繁的数据,如日志文件。我建议按照各个容器的名称进行分类,以便快速定位和管理。
提起日志管理,我的偏好不管理日志,将日志直接写到标准输出。不过,并不是所有的服务都支持这种机制,比如 nginx。对于这类服务,log 目录就是为他们准备的。

数据卷(volumes)

volumes 目录对应 docker-compose.yaml 文件中映射的卷(volumes),同样以 bind mounts 挂载到容器中。按‘容器名/文件类型’进行组织。在 volumes 目录下存放容器持久化的数据,是日常运维中应该异地备份的数据。

Docker Compose 文件(docker-compose.yaml)

对于挂载(mounts)的处理,我们遵循一个简单的规则:项目内部的挂载路径使用相对路径,并以 ./ 开头;而对于项目外部的资源,则使用绝对路径。以下用node-exporter作为部署示例,这个例子不好,请尽量使用quay.io/prometheus/node-exporter镜像

1
2
3
4
5
6
7
8
node-exporter:
image: debian:bullseye
pid: host
restart: always
volumes:
- ./servers/node-exporter:/usr/local/bin # 项目内的挂载路径用相对路径
- /:/host:ro,rslave # 项目外用绝对路径
command: node_exporter --path.rootfs=/host

在处理 Docker Compose 的路径时,避免使用环境变量 $PWD,因为 Docker Compose 本身支持在子目录中运行,并且能解析相对路径。这不仅减少了配置的复杂性,也使得我们的文件更加的“可移植”。

我们选择不使用数据卷(data volumes)的原因是为了简化 Docker Compose 配置,并使得管理更加集中。

通过这样高效而结构化的组织方式,我们不仅能够保障运维环境的稳定性和可维护性,而且还能增强系统的安全性。这对于管理和部署容器化应用是非常重要的,可以极大地简化日常的运维工作,并提升应用的可靠性。接下来,我将继续阐述如何执行部署和备份,以确保服务的可迁移和可恢复。

高效发布与部署策略

在本文中,虽然不深入探讨在各种环境中的项目发布方法,但仍需重申几个核心原则以确保部署流程的高效和可靠性:

  • 明确构建与运行的界限:确保构建过程与运行环境区分开来,避免产生不必要的依赖和潜在问题。
  • 确保构建的一致性:每次构建应可复现,确保所有环境中应用的一致性和可靠性。

为了实现这些原则,推荐每个项目配置一个编译脚本。项目发布时逐个 clone 每个项目,执行其中的编译脚本,将编译输出到 release 目录中。

文件同步推荐使用 rsync 工具基于 SSH 同步,其命令 rsync -avuzP --del 不仅高效同步文件变更,还会删除目标位置中已经不存在于目标位置的文件。基于 SSH 的加密传输让敏感数据传输过程更加安全。文件结构的严格管理让同步过程更加清晰,通常只需同步包含releaseservices目录和docker-compose.yaml文件。

在部署更新时,根据变更的具体内容,可以选择通过 docker compose up -d --build 命令重建并启动服务,或者简单重启相关服务。此外,建议考虑实现无中断服务的策略,如热更新或滚动更新,以进一步提升用户体验和服务可用性。

在处理需要远程初始化的应用时,例如 WordPress、GitLab、Jenkins 等,采取本地初始化的方法能够提供多重益处。行本地初始化允许您在安全的环境中进行配置和数据设置,避免了在公网环境下的潜在安全风险。设置完成后,将 volumes 目录同步至远程服务器,确保服务的一致性和安全性。

通过这种精心设计的发布和部署流程,你可以确保每次部署都是可控、可追溯和最小化风险的,无论是手动执行还是自动化操作。

持续集成/持续交付(CI/CD)

若项目集成了持续集成(CI),忽略高效发布与部署策略这一章,让 CI 系统自动管理构建和发布。

如果环境中有镜像源,用镜像源代替线上构建和配置文件映射,从而加快部署速度。

之后将构建同步到远程环境或镜像源,进而更新服务实现持续部署(CD)。

数据备份策略

备份是确保数据持久性和业务连续性的关键步骤。在我们的目录结构设计中,有预设的 backups 目录专门存储备份。针对数据库等频繁更改的数据服务,为确保备份的一致性,可能需要对应业务实施专门的备份策略。本着每个容器只承载一个服务的原则,我们应运行一个独立的 backup 容器负责导出和保存数据。将这些备份文件存放在 backups 目录中,以便于管理和恢复。

对于文件备份,再次推荐使用 rsync 工具。通过 rsync--link-dest 参数可以实现差异化备份。参考下边的脚本实现 30天保留的备份策略:

1
2
3
4
5
6
7
8
9
10
11
bakPath=<备份路径>

rsync -avuzP --del \
--exclude 'sync.sh' \
--exclude 'recovery.sh' \
-e "ssh -p 22" \
--link-dest=$bakPath/`date "+%Y-%m-%d" -d "-1day"` \
<用户>@<主机>:<部署路径>/volumes $bakPath/`date "+%Y-%m-%d"`

# 清理 30 天前的备份数据
rm -rf $bakPath/`date "+%Y-%m-%d" -d "-30day"`

通过按功能划分的目录结构,备份的 volumes 目录可以复制到项目中快速启动一个模拟线上环境。

进阶备份策略考虑:

  • 存储服务器配置:考虑配置 RAID,ZFS 或分布式存储保证存储的可靠性。
  • 数据安全:确保数据传输和存储时的安全性。是否有必要实行加密措施和访问控制,以及在日常运维中对必要的信息脱敏。
  • 备份介质可靠性:不同的储存介质的数据保留时间不同,应该定期维护备份防止数据丢失。
  • 「3-2-1 原则」:至少拥有三份数据副本,两种储存介质,一份位于远程位置。

以上仅是对备份策略的基础介绍。其中每个都可以展开讲讲,但这超过了本篇范畴。

恢复

以上我们完成了备份,恢复时同样将备份的 volumes 目录可以复制到项目中,快速测试后同步到线上,实现恢复。在实施任何恢复操作之前,请务必考虑对现有系统的影响,以及在恢复期间如何确保业务的连续性。应该定期进行恢复测试,以确保恢复流程的有效性,同时也是对整体备份策略的考验。

结语

本文详细阐述了利用 Docker Compose 从零开始搭建和部署应用的全过程,并深入探讨了如何实现项目的精简化构建和有效的运维管理。我们还探讨了如何巧妙地融合 CI/CD 流程,优化了日常运维的策略和方法。

为了将这些理论转化为实践,我近期开发了 drops 项目。该项目不仅具体实现了前述的理念,而且提供了一个实际的应用参考。我邀请您访问 https://github.com/szerr/drops 一探究竟。

进一步探索

  • 持续集成/持续部署(CI/CD):探索如何将现有流程自动化实现,以便更高效的交付价值。
  • 云原生和微服务架构:考虑过度到云原生和微服务架构,以实现更强的伸缩性和可靠性。
  • DevOps 文化的融合:探讨如何建立跨职能团队,以实现更快速和响应性更强的软件交付。
  • 持续学习和改进:鼓励团队持续学习新工具和技术,以及定期回顾和优化现有流程。

本作品采用 知识共享许可协议 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。