Docker Compose多服务编排指南:微服务实战部署全解析
一、引言:从单体到微服务,编排为何重要?
随着微服务架构的普及,一个应用通常由多个独立的服务组成,例如 API 网关、业务逻辑、数据库、消息队列等。在容器化的浪潮中,每个服务被打包成独立的容器,这带来了极大的灵活性,但也引入了新的问题:
- 如何统一管理多个容器的启动顺序、依赖关系?
- 如何让容器之间彼此发现和通信?
- 如何持久化数据、共享配置?
- 如何避免每次都用冗长的
docker run命令?
Docker Compose正是为解决这些问题而生。它允许你通过一个docker-compose.yml文件定义所有服务、网络和卷,然后用一条docker-compose up命令启动整个应用。本文将带你从核心概念入手,通过一个完整可运行的全栈微服务示例,掌握 Docker Compose 的多服务编排能力。
二、核心概念解析
2.1 docker-compose.yml 文件结构
一个典型的 Compose 文件包含三个顶级配置块:
version: '3.8' # Compose 文件版本(建议 3.8+) services: # 定义所有服务(容器) webapp: ... database: ... networks: # 定义自定义网络(可选) app-network: volumes: # 定义命名卷(可选) db-data:2.2 services 配置要点
每个service代表一个容器,常用配置如下:
- image / build:使用已有镜像,或通过
Dockerfile构建。 - ports:映射端口,格式
"宿主机:容器"。 - environment / env_file:注入环境变量。
- volumes:挂载卷或绑定宿主目录,用于持久化或热加载。
- depends_on:声明服务间的启动顺序,但不保证服务已就绪(需配合
healthcheck)。 - restart:重启策略,如
always、on-failure。 - healthcheck:定义健康检查指令,Docker 据此判断容器状态。
2.3 networks 与 volumes
- networks:创建自定义网络可实现服务间的名称解析(如
webapp可直接用服务名访问database),并隔离不同网络。 - volumes:命名卷由 Docker 管理,用于持久化数据库等数据。也可直接绑定主机路径。
2.4 常用指令速览
services: api: build: ./api # 从 api 目录构建 ports: - "5000:5000" environment: - DB_HOST=database - REDIS_URL=redis://cache:6379 depends_on: - database - cache healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped volumes: pgdata: # 声明命名卷(需在顶级 volumes 中定义)三、实战:用 Compose 部署一个博客微服务
我们来实现一个简单的在线博客系统,包含四个服务:
- 前端:Nginx 提供静态页面
- 后端:Python Flask 编写的 REST API
- 数据库:MySQL 8.0
- 缓存:Redis 6
所有代码均可在本地运行,演示完整的编排流程。
3.1 项目目录结构
blog-app/ ├── docker-compose.yml ├── .env # 公共环境变量 ├── api/ │ ├── Dockerfile │ ├── requirements.txt │ └── app.py └── frontend/ ├── Dockerfile ├── index.html └── nginx.conf3.2 后端 Flask 服务
api/requirements.txt:
flask==2.3.2 mysql-connector-python==8.1.0 redis==4.6.0api/app.py(带健康检查端点):
from flask import Flask, jsonify import mysql.connector import redis import os app = Flask(__name__) # 从环境变量读取连接信息 DB_HOST = os.getenv("DB_HOST", "database") DB_USER = os.getenv("DB_USER", "blog") DB_PASSWORD = os.getenv("DB_PASSWORD", "blogpass") DB_NAME = os.getenv("DB_NAME", "blogdb") REDIS_URL = os.getenv("REDIS_URL", "redis://cache:6379/0") # 初始化 Redis 客户端 r = redis.Redis.from_url(REDIS_URL, decode_responses=True) def get_db_connection(): return mysql.connector.connect( host=DB_HOST, user=DB_USER, password=DB_PASSWORD, database=DB_NAME ) @app.route("/health") def health(): return jsonify(status="ok") @app.route("/posts") def get_posts(): # 尝试从 Redis 缓存读取 cached = r.get("posts") if cached: return jsonify(eval(cached)) # 仅作示例,生产环境用 JSON conn = get_db_connection() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT id, title, content FROM posts ORDER BY id DESC LIMIT 10") posts = cursor.fetchall() cursor.close() conn.close() r.set("posts", str(posts), ex=30) # 缓存30秒 return jsonify(posts) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)api/Dockerfile:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["python", "app.py"]3.3 前端 Nginx 服务
frontend/index.html(简单展示页面):
<!DOCTYPE html> <html> <head> <title>微服务博客</title> </head> <body> <h1>博客文章列表</h1> <div id="posts"></div> <script> fetch('/api/posts') .then(res => res.json()) .then(data => { const container = document.getElementById('posts'); data.forEach(post => { container.innerHTML += `<h2>${post.title}</h2><p>${post.content}</p>`; }); }); </script> </body> </html>frontend/nginx.conf:
events { worker_connections 1024; } http { server { listen 80; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://api:5000/; # 服务名 api 会被解析 proxy_set_header Host $host; } } }frontend/Dockerfile:
FROM nginx:alpine COPY nginx.conf /etc/nginx/nginx.conf COPY index.html /usr/share/nginx/html/index.html3.4 编写 docker-compose.yml(核心)
version: '3.8' services: # 前端 Nginx frontend: build: ./frontend ports: - "8080:80" # 宿主机 8080 映射容器 80 depends_on: - api networks: - blog-network # 后端 API api: build: ./api ports: - "5000:5000" depends_on: database: condition: service_healthy # 等待数据库健康才启动 cache: condition: service_started environment: DB_HOST: database DB_USER: ${DB_USER} DB_PASSWORD: ${DB_PASSWORD} DB_NAME: ${DB_NAME} REDIS_URL: redis://cache:6379/0 FLASK_ENV: development healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 10s timeout: 5s retries: 5 networks: - blog-network # MySQL 数据库 database: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_NAME} MYSQL_USER: ${DB_USER} MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - db-data:/var/lib/mysql # 持久化数据 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本(可选) healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 networks: - blog-network restart: unless-stopped # Redis 缓存 cache: image: redis:6-alpine volumes: - cache-data:/data networks: - blog-network restart: unless-stopped # 自定义网络 networks: blog-network: driver: bridge # 持久化卷 volumes: db-data: cache-data:配套.env文件(建议添加至.gitignore):
DB_USER=blog DB_PASSWORD=blogpass DB_NAME=blogdb MYSQL_ROOT_PASSWORD=rootsecret3.5 启动与验证
启动所有服务:
bash docker-compose up -d
首次构建需加上--build:bash docker-compose up -d --build查看运行状态:
bash docker-compose ps查看日志:
bash docker-compose logs -f api访问应用:浏览器打开
http://localhost:8080,前端页面会通过/api/posts调用后端。若数据库已存在posts表并有数据,即可显示。初始化数据库表(若无 init.sql,可手动进入容器执行):
bash docker-compose exec database mysql -u blog -pblogpass blogdb -e " CREATE TABLE IF NOT EXISTS posts ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO posts (title, content) VALUES ('Hello', 'Welcome to my blog!'); "停止并清理:
bash docker-compose down # 停止并删除容器 docker-compose down -v # 同时删除卷(会丢失数据)
四、常见问题与注意事项
4.1 服务启动顺序 ≠ 服务就绪
depends_on仅控制容器启动的顺序,并不检查服务是否已准备好接受请求。数据库可能还在初始化,API 就已经启动并尝试连接,导致报错。解决方案是:
- 使用
healthcheck并配合condition: service_healthy(Compose v3.x 需使用depends_on的长语法)。 - 在应用代码中加入重试逻辑。
注意:Docker Compose v3 不再支持
condition形式的depends_on(尽管某些工具如docker-composev1.29+ 部分支持),官方推荐使用healthcheck结合外部工具(如wait-for-it.sh)。在 Compose v2 和 v3 中,depends_on本身不等待健康状态,但在本文示例中我们使用了condition写法(需 docker-compose v1.29+ 或 Docker Compose V2)。若你的版本不支持,可改用depends_on简单依赖,并在 API 中实现数据库重连。
4.2 环境变量与 .env 文件
- Compose 会自动读取
.env文件中的变量,在docker-compose.yml中通过${VAR}引用。 - 敏感信息(密码)应避免直接写在 YAML 中,使用
.env并添加到.gitignore。 - 若多个服务共享变量,
.env是最佳实践;也可使用env_file为每个服务指定文件。
4.3 数据卷的持久化与权限
- MySQL 数据目录挂载到命名卷可防止容器删除后数据丢失。
- 若使用主机目录绑定(如
./data:/var/lib/mysql),须注意容器内用户(如mysql,uid 999)与宿主机权限的匹配,否则可能写入失败。生产环境推荐使用命名卷。
4.4 网络通信与端口暴露
- 同一个自定义网络内的容器可直接用服务名通信(如
api访问database:3306)。 - 仅对外暴露必要的端口,内部服务(如数据库)不必暴露到宿主机,可注释掉
ports。 - 若多个 Compose 应用需通信,可使用外部网络
external: true。
4.5 调试技巧
- 进入容器:
docker-compose exec api bash - 查看环境变量:`docker-compose
