92·工程实践进阶

Docker Compose 编排

docker-composemulti-servicenetwork

第 92 课:Docker Compose 编排

学习目标

完成本课学习后,你将能够:

  1. 理解 Docker Compose 在多服务应用中的核心作用和价值。
  2. 掌握 docker-compose.yml 文件的基本结构和核心指令。
  3. 编写一个能够定义和管理多个相互关联的 Go 服务(如 Web 服务和数据库)的 Compose 文件。
  4. 使用 Docker Compose 命令一键启动、查看、调试和停止整个应用栈。
  5. 了解如何为服务配置网络、卷和环境变量,实现服务间的通信和数据持久化。

核心概念

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:

运行和测试:

  1. my-app 目录下,运行 docker-compose up -d
    • -d 表示在后台(detached mode)运行。
  2. 观察日志:docker-compose logs -f api
  3. 访问 http://localhost:8080,你应该能看到:“API 响应: Hello from Docker Compose!”。
  4. 停止并移除所有容器:docker-compose down

实践练习

练习 1:基础修改 在上面的 docker-compose.yml 中,将数据库服务的端口映射从 "5433:5432" 改为 "5432:5432"。然后尝试运行 docker-compose up -d。会发生什么?如何排查和解决这个端口冲突问题?

练习 2:添加新服务 假设我们还需要一个简单的 Nginx 反向代理服务来将 http://localhost 的请求转发到我们的 Go API 服务。

  1. my-app 目录下创建一个新的文件夹 nginx
  2. nginx 文件夹中创建一个 nginx.conf 配置文件,内容如下:
    server {
        listen 80;
        location / {
            proxy_pass http://api:8080;
        }
    }
    
  3. docker-compose.yml 文件中,添加一个名为 nginx 的新服务。
    • 使用 nginx:alpine 镜像。
    • 将其端口 80 映射到宿主机的 80 端口。
    • 使用 volumes 将本地的 nginx.conf 挂载到容器的 /etc/nginx/conf.d/default.conf 路径。
    • 使用 depends_on 确保它在 api 服务之后启动。
  4. 运行 docker-compose up -d --build(加 --build 因为可能需要重新构建 API 镜像,虽然本例不需要),然后通过 http://localhost 访问,验证是否成功。

练习 3:健康检查与依赖 移除 docker-compose.ymlapi 服务对 db 服务的 condition: service_healthy 依赖,只保留简单的 depends_on: [db]。多次运行 docker-compose up,观察 api 服务的日志。你有没有遇到过数据库未准备就绪导致 API 启动失败的情况?这说明了 condition: service_healthy 的重要性。

常见错误

  1. 服务启动顺序错误:仅使用 depends_on 而不指定 condition: service_healthy,无法保证被依赖的服务(如数据库)已完全初始化就绪,导致连接失败。
  2. 环境变量未传递:在 docker-compose.yml 中忘记为容器定义必要的 environment 变量,或者变量名拼写错误,导致应用配置错误。可以使用 docker-compose exec api env 命令检查容器内的环境变量。
  3. 网络连接问题:容器间通信错误地使用了 localhost 或宿主机 IP,而不是 Compose 提供的服务名。在同一个 Compose 文件定义的服务网络中,服务名就是可用的主机名。
  4. 卷和文件权限问题:在 Linux 上运行时,挂载的目录或文件可能因权限问题导致容器内应用无法读取。需要注意容器内进程的用户 ID 与宿主机文件权限的匹配。
  5. 忘记使用 .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 流水线,实现自动化构建、测试和部署。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「CI/CD 流水线」 以巩固所学知识。