第 92 课:Docker Compose 编排
学习目标
完成本课学习后,你将能够:
- 理解 Docker Compose 在多服务应用中的核心作用和价值。
- 掌握
docker-compose.yml文件的基本结构和核心指令。 - 编写一个能够定义和管理多个相互关联的 Go 服务(如 Web 服务和数据库)的 Compose 文件。
- 使用 Docker Compose 命令一键启动、查看、调试和停止整个应用栈。
- 了解如何为服务配置网络、卷和环境变量,实现服务间的通信和数据持久化。
核心概念
1. 什么是 Docker Compose?
想象一下,你开发了一个现代 Web 应用,它可能由多个部分组成:
- 前端应用:一个用 React/Vue 编写的静态页面服务。
- 后端 API:一个用 Go 编写的 RESTful API 服务器。
- 数据库:一个 PostgreSQL 或 MySQL 数据库。
- 缓存:一个 Redis 缓存服务。
- 消息队列:一个 RabbitMQ 或 Kafka 服务。
如果你手动用 docker run 命令一个一个地启动和管理它们,过程会非常繁琐且容易出错。你需要记住复杂的启动命令、参数,还要管理它们之间的网络连接和启动顺序。
Docker Compose 就是解决这个问题的工具。 它是一个用于定义和运行多容器 Docker 应用的工具。你只需在一个 YAML 文件(通常是 docker-compose.yml)中配置好应用的所有服务(容器),然后使用一个简单的命令,就能同时创建并启动所有服务。
通俗比喻: 如果把每个 Docker 容器比作一个演员,那么 docker-compose.yml 文件就是剧本,而 docker-compose up 命令就是导演喊“Action!”,所有演员按照剧本的设定同时登台演出。
2. 核心价值:多服务编排
Docker Compose 的核心价值在于编排。它不仅能启动容器,还能:
- 定义依赖关系:确保数据库在 API 服务之前启动。
- 配置共享网络:让所有服务在同一个虚拟网络中,可以通过服务名(而不是 IP)互相访问。
- 管理数据卷:方便地将容器内的数据(如数据库数据)持久化到宿主机,防止容器重启后数据丢失。
- 统一环境变量:集中管理所有服务的配置(如数据库密码、API 密钥)。
3. docker-compose.yml 文件结构
这个文件定义了你的应用栈。它包含几个顶级部分:
version:指定 Compose 文件格式的版本(现在常用"3.8")。services:核心部分。在这里定义每一个独立的服务(容器)。volumes:声明可用于服务间共享和持久化的命名卷。networks:声明可用于服务间通信的自定义网络(可选,Compose 会提供默认网络)。
在 services 部分,每个服务都有自己的配置,如:
image:要使用的 Docker 镜像。build:如果需要从本地Dockerfile构建镜像。ports:容器端口与宿主机端口的映射。volumes:卷挂载。environment:环境变量。depends_on:声明服务间的启动依赖关系。command:覆盖容器默认的启动命令。
代码示例
让我们通过一个具体的例子来实践。假设我们有一个简单的 Go Web 应用,它连接一个 PostgreSQL 数据库来存储“欢迎信息”。
项目结构:
my-app/
├── api/
│ ├── main.go
│ └── Dockerfile
├── docker-compose.yml
└── .env
1. Go API 服务 (api/main.go)
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"os"
_ "github.com/lib/pq"
)
func main() {
// 从环境变量获取数据库连接信息
dbHost := os.Getenv("DB_HOST")
dbUser := os.Getenv("DB_USER")
dbPass := os.Getenv("DB_PASSWORD")
dbName := os.Getenv("DB_NAME")
// 构建连接字符串
connStr := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", dbHost, dbUser, dbPass, dbName)
// 连接数据库
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal("数据库连接失败:", err)
}
defer db.Close()
// 创建表(如果不存在)
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, content TEXT NOT NULL)`)
if err != nil {
log.Fatal("创建表失败:", err)
}
// 插入一条初始消息
_, err = db.Exec(`INSERT INTO messages (content) VALUES ('Hello from Docker Compose!') ON CONFLICT DO NOTHING`)
if err != nil {
log.Println("插入初始消息可能已存在,忽略:", err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var message string
err := db.QueryRow("SELECT content FROM messages LIMIT 1").Scan(&message)
if err != nil {
http.Error(w, "数据库查询错误", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "API 响应: %s", message)
})
log.Println("API 服务器启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
2. API 服务的 Dockerfile (api/Dockerfile)
# 使用 Go 官方镜像作为构建环境
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod .
RUN go mod download
COPY . .
# 构建 Go 应用,生成静态二进制文件
RUN CGO_ENABLED=0 GOOS=linux go build -o api-server .
# 使用更小的镜像作为运行环境
FROM alpine:latest
WORKDIR /app
# 从构建阶段复制二进制文件
COPY --from=builder /app/api-server .
# 复制需要的文件(如静态资源,此处暂无)
# COPY --from=builder /app/templates ./templates
EXPOSE 8080
CMD ["./api-server"]
3. 环境变量文件 (.env)
# 数据库配置
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=mydb
# API 服务将使用这些变量来连接数据库
DB_HOST=db
DB_USER=${POSTGRES_USER}
DB_PASSWORD=${POSTGRES_PASSWORD}
DB_NAME=${POSTGRES_DB}
4. Docker Compose 编排文件 (docker-compose.yml)
version: '3.8'
services:
# 数据库服务
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
# 将数据库数据持久化到宿主机的一个命名卷
- postgres-data:/var/lib/postgresql/data
ports:
# 将容器的5432端口映射到宿主机的5433端口(避免与本地PostgreSQL冲突)
- "5433:5432"
healthcheck:
# 健康检查:检查PostgreSQL是否准备好接受连接
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
# Go API 服务
api:
# 使用当前目录下的 `api` 文件夹中的 Dockerfile 来构建镜像
build:
context: ./api
dockerfile: Dockerfile
# 依赖 db 服务,并且要求 db 服务“健康”后才启动
depends_on:
db:
condition: service_healthy
environment:
# 从 `.env` 文件和本节定义中读取环境变量
DB_HOST: db # 使用服务名 `db` 作为主机名,Compose 网络会自动解析
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: ${POSTGRES_DB}
ports:
- "8080:8080"
# 如果构建失败,会自动重启
restart: on-failure
# 声明一个命名卷 `postgres-data`
volumes:
postgres-data:
运行和测试:
- 在
my-app目录下,运行docker-compose up -d。-d表示在后台(detached mode)运行。
- 观察日志:
docker-compose logs -f api。 - 访问
http://localhost:8080,你应该能看到:“API 响应: Hello from Docker Compose!”。 - 停止并移除所有容器:
docker-compose down。
实践练习
练习 1:基础修改
在上面的 docker-compose.yml 中,将数据库服务的端口映射从 "5433:5432" 改为 "5432:5432"。然后尝试运行 docker-compose up -d。会发生什么?如何排查和解决这个端口冲突问题?
练习 2:添加新服务
假设我们还需要一个简单的 Nginx 反向代理服务来将 http://localhost 的请求转发到我们的 Go API 服务。
- 在
my-app目录下创建一个新的文件夹nginx。 - 在
nginx文件夹中创建一个nginx.conf配置文件,内容如下:server { listen 80; location / { proxy_pass http://api:8080; } } - 在
docker-compose.yml文件中,添加一个名为nginx的新服务。- 使用
nginx:alpine镜像。 - 将其端口
80映射到宿主机的80端口。 - 使用
volumes将本地的nginx.conf挂载到容器的/etc/nginx/conf.d/default.conf路径。 - 使用
depends_on确保它在api服务之后启动。
- 使用
- 运行
docker-compose up -d --build(加--build因为可能需要重新构建 API 镜像,虽然本例不需要),然后通过http://localhost访问,验证是否成功。
练习 3:健康检查与依赖
移除 docker-compose.yml 中 api 服务对 db 服务的 condition: service_healthy 依赖,只保留简单的 depends_on: [db]。多次运行 docker-compose up,观察 api 服务的日志。你有没有遇到过数据库未准备就绪导致 API 启动失败的情况?这说明了 condition: service_healthy 的重要性。
常见错误
- 服务启动顺序错误:仅使用
depends_on而不指定condition: service_healthy,无法保证被依赖的服务(如数据库)已完全初始化就绪,导致连接失败。 - 环境变量未传递:在
docker-compose.yml中忘记为容器定义必要的environment变量,或者变量名拼写错误,导致应用配置错误。可以使用docker-compose exec api env命令检查容器内的环境变量。 - 网络连接问题:容器间通信错误地使用了
localhost或宿主机 IP,而不是 Compose 提供的服务名。在同一个 Compose 文件定义的服务网络中,服务名就是可用的主机名。 - 卷和文件权限问题:在 Linux 上运行时,挂载的目录或文件可能因权限问题导致容器内应用无法读取。需要注意容器内进程的用户 ID 与宿主机文件权限的匹配。
- 忘记使用
.env文件:将敏感信息(如密码)直接写在docker-compose.yml中,然后将该文件提交到版本控制,造成安全风险。应使用.env文件(并将其加入.gitignore)或docker-compose.override.yml来管理环境特定配置。
小结
本节课我们学习了 Docker Compose 的核心概念和实践:
- Docker Compose 是用于定义和运行多容器 Docker 应用的强大工具,通过一个 YAML 文件描述整个应用栈。
- 核心文件
docker-compose.yml定义了服务、卷和网络。服务定义中包含了镜像/构建、端口、环境变量、依赖等关键配置。 - 服务间通信 依赖于 Docker Compose 自动创建的网络,使用服务名作为主机名即可相互访问。
- 数据持久化 通过定义命名卷(Named Volumes)实现,将容器内目录挂载到卷中,数据就不会随容器删除而丢失。
- 常用命令 包括
docker-compose up(创建并启动)、docker-compose down(停止并移除)、docker-compose logs(查看日志)、docker-compose ps(列出容器)等。
掌握 Docker Compose 是迈向云原生和微服务架构开发的重要一步。它让你能够以声明式、可重复的方式管理复杂的开发和测试环境,极大地提升了开发效率和应用的可移植性。在下一课中,我们将学习如何将这样的容器化应用接入 CI/CD 流水线,实现自动化构建、测试和部署。