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 与 volumesnetworks创建自定义网络可实现服务间的名称解析如webapp可直接用服务名访问database并隔离不同网络。volumes命名卷由 Docker 管理用于持久化数据库等数据。也可直接绑定主机路径。2.4 常用指令速览services: api: build: ./api # 从 api 目录构建 ports: - 5000:5000 environment: - DB_HOSTdatabase - REDIS_URLredis://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.txtflask2.3.2 mysql-connector-python8.1.0 redis4.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_responsesTrue) def get_db_connection(): return mysql.connector.connect( hostDB_HOST, userDB_USER, passwordDB_PASSWORD, databaseDB_NAME ) app.route(/health) def health(): return jsonify(statusok) 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(dictionaryTrue) 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), ex30) # 缓存30秒 return jsonify(posts) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)api/DockerfileFROM 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 idposts/div script fetch(/api/posts) .then(res res.json()) .then(data { const container document.getElementById(posts); data.forEach(post { container.innerHTML h2${post.title}/h2p${post.content}/p; }); }); /script /body /htmlfrontend/nginx.confevents { 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/DockerfileFROM 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文件建议添加至.gitignoreDB_USERblog DB_PASSWORDblogpass DB_NAMEblogdb MYSQL_ROOT_PASSWORDrootsecret3.5 启动与验证启动所有服务bash docker-compose up -d首次构建需加上--buildbash 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_healthyCompose 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须注意容器内用户如mysqluid 999与宿主机权限的匹配否则可能写入失败。生产环境推荐使用命名卷。4.4 网络通信与端口暴露同一个自定义网络内的容器可直接用服务名通信如api访问database:3306。仅对外暴露必要的端口内部服务如数据库不必暴露到宿主机可注释掉ports。若多个 Compose 应用需通信可使用外部网络external: true。4.5 调试技巧进入容器docker-compose exec api bash查看环境变量docker-compose

相关新闻