Docker Compose实战:从零编排Spring Boot+Nginx+MySQL微服务应用
最近在技术社区里,我注意到一个有趣的现象:很多开发者,尤其是学生和初创团队,在搭建自己的第一个项目时,常常被“环境配置”和“服务管理”这两座大山拦住。想象一下,你刚写好一个微服务,兴致勃勃地想部署到服务器上,结果发现端口冲突、依赖缺失、日志找不到、服务挂了也不知道。这时候,你是不是特别希望有个“技术宿管阿姨”——一个能帮你自动处理这些琐碎杂事,让服务像住在管理良好的宿舍里一样稳定、有序的工具?
这个“宿管阿姨”就是Docker。但今天,我们不只谈 Docker 本身,而是要深入它的核心搭档之一:Docker Compose。很多人以为 Compose 只是个“多容器启动器”,这大大低估了它的价值。它真正的能力,是将复杂的多服务应用部署,从一份需要多人协作、容易出错的“手写说明书”,变成一个可版本化、一键启动、环境一致的“标准化公寓管理方案”。
如果你正在学习微服务、全栈开发,或者需要管理一个包含数据库、缓存、消息队列和后端 API 的本地开发环境,那么搞懂 Docker Compose 将极大提升你的效率。本文将带你从“为什么需要它”开始,通过一个完整的实战项目(一个包含 Nginx、Spring Boot 应用和 MySQL 的 Web 服务),手把手教你编写docker-compose.yml,理解网络、卷、依赖等核心概念,并分享生产环境的最佳实践和避坑指南。读完本文,你将能独立编排自己的多容器应用。
1. 这篇文章真正要解决的问题:从“混乱部署”到“声明式编排”
在 Docker 出现之前,部署一个多组件的应用是怎样的体验?你需要:
- 在服务器上手动安装 Java、Node.js、Python 等运行时。
- 配置 MySQL、Redis,修改各种
my.cnf、redis.conf。 - 将打包好的应用 JAR 包或代码上传到服务器。
- 写一堆启动脚本,管理进程,处理日志。
- 最头疼的是:另一台服务器的环境稍有不同,上述步骤可能全部失效。
Docker 通过容器化解决了“环境一致性”的问题。但当你拥有多个容器(比如一个前端、一个后端、一个数据库)时,新的问题出现了:你需要手动执行多条docker run命令,指定复杂的网络连接、卷挂载和环境变量。命令又长又容易出错,且无法记录和复现。
Docker Compose 的核心价值,就是解决“多容器应用的定义、编排和生命周期管理”问题。它允许你使用一个 YAML 文件(docker-compose.yml)来定义整个应用栈(Stack)所需的所有服务(Service)、网络(Network)和数据卷(Volume)。之后,只需一个命令(docker-compose up),所有服务就会按照定义的顺序和配置启动。
它特别适合以下场景:
- 本地开发环境搭建:新同事加入项目,无需再花半天配置环境,
git clone后一句docker-compose up就能获得一个完整的、隔离的、与生产环境类似的应用栈。 - 单机部署与测试:在单台服务器上快速部署包含多个组件的应用进行功能测试或演示。
- CI/CD 流水线:在自动化构建和测试环节,快速拉起一个干净的、包含所有依赖的服务环境。
简单说,Docker Compose 让你从“手工组装家具”的运维模式,升级到“提供一张设计图,机器自动组装”的声明式编排模式。
2. 基础概念与核心原理
在动手之前,我们需要厘清几个关键概念,这能帮助你理解 Compose 文件里每个配置项的意义。
服务 (Service)一个服务对应一个容器化的应用。在docker-compose.yml中,每个services:下的键(如web、db)都定义了一个服务。一个服务可以基于一个镜像运行一个或多个容器实例(通过scale参数,但在单机 Compose 中通常是一个)。
项目 (Project)Compose 管理的所有容器、网络、卷的集合,默认以当前目录名作为项目名。这实现了环境隔离。你在project_a目录下启动的 MySQL 容器,和project_b目录下的同名服务是完全隔离的。
网络 (Network)Compose 会为你的项目默认创建一个独立的桥接网络。在这个网络内的所有容器,可以使用服务名作为主机名互相访问。这是 Compose 最方便的特性之一,你不再需要查找容器的 IP 地址。
数据卷 (Volume)用于持久化容器内产生的数据(如数据库文件),或在容器和宿主机之间共享数据(如配置文件、日志目录)。Compose 可以定义和管理命名的数据卷。
工作原理简述: 当你运行docker-compose up时,Compose 会:
- 解析
docker-compose.yml文件。 - 根据定义,创建项目专属的网络和卷。
- 为每个服务拉取镜像(如果本地没有),并根据配置(端口、环境变量、卷、命令等)创建并启动容器。
- 管理容器的生命周期,并将所有容器的日志聚合输出到当前终端。
3. 环境准备与前置条件
为了完成后续的实战,你需要准备好以下环境。本文演示基于 Linux/macOS 系统,Windows 用户使用 Docker Desktop 也可获得类似体验。
安装 Docker
- 访问 Docker 官方文档,根据你的操作系统(Windows, macOS, Linux distributions)安装 Docker Engine 或 Docker Desktop。
- 安装完成后,在终端运行以下命令验证安装是否成功:
docker --version docker-compose --version # 对于较新版本的 Docker Desktop,`docker compose` 是内置命令 - 注意:新版本的 Docker 已经将
docker-compose作为docker的一个子命令(docker compose)集成。本文为了兼容性,仍使用docker-compose这个独立命令的语法,两者在核心功能上基本一致。如果你的系统只有docker compose,将命令中的docker-compose替换为docker compose即可。
基础命令行操作
- 你需要熟悉基本的终端命令,如
cd(切换目录)、ls(列出文件)、echo(输出)等。
- 你需要熟悉基本的终端命令,如
一个简单的 Spring Boot 应用(可选,本文会提供)
- 为了演示,我们将使用一个极简的 Spring Boot Web 应用,它提供一个 HTTP API 并连接 MySQL 数据库。如果你没有现成的应用,可以直接使用本文提供的代码。
4. 核心流程拆解:编写你的第一个 docker-compose.yml
我们将创建一个名为my-web-app的项目,它包含三个服务:
app: 一个 Spring Boot 后端应用。db: 一个 MySQL 数据库。proxy: 一个 Nginx 反向代理。
项目目录结构如下:
my-web-app/ ├── docker-compose.yml ├── nginx/ │ └── nginx.conf ├── app/ │ ├── Dockerfile │ └── (Spring Boot 项目文件) └── mysql/ └── init.sql (可选,数据库初始化脚本)4.1 第一步:创建项目根目录和 docker-compose.yml
在你的工作区创建一个新目录,并进入该目录。
mkdir my-web-app && cd my-web-app然后创建docker-compose.yml文件。
4.2 第二步:编写 docker-compose.yml 骨架
一个基本的docker-compose.yml以版本声明开头。我们使用广泛兼容的3.8版本。
version: '3.8' services: # 我们将在这里定义 app, db, proxy 三个服务 networks: # 自定义网络(可选,Compose会默认创建) volumes: # 数据卷定义4.3 第三步:定义 MySQL 数据库服务 (db)
我们先从最简单的、直接使用官方镜像的服务开始。
services: db: image: mysql:8.0 # 指定镜像版本,避免使用 latest 导致不可预期变化 container_name: mysql-db # 为容器指定一个易读的名字 restart: unless-stopped # 容器退出时总是重启,除非手动停止 environment: MYSQL_ROOT_PASSWORD: rootpassword123 # 设置 root 用户密码 MYSQL_DATABASE: myappdb # 容器启动时创建的数据库名 MYSQL_USER: appuser # 创建的用户名 MYSQL_PASSWORD: userpassword123 # 对应用户的密码 volumes: - db_data:/var/lib/mysql # 将数据持久化到名为 db_data 的卷中,防止容器删除后数据丢失 - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 挂载初始化SQL脚本(如果存在) networks: - backend # 将服务连接到名为 backend 的网络 ports: - "3306:3306" # 将宿主机的3306端口映射到容器的3306端口,方便宿主机工具连接关键点解释:
environment: 通过环境变量配置 MySQL,这是官方镜像推荐的方式。volumes: - db_data:/var/lib/mysql:db_data是一个在文件顶部volumes:块中定义的命名卷。这实现了数据持久化。networks: - backend: 我们创建了一个自定义网络backend,用于后端服务间的内部通信。ports: 将容器端口暴露给宿主机。在生产环境中,通常只暴露必要的服务(如反向代理),数据库端口不应直接暴露给外网。
4.4 第四步:定义 Spring Boot 应用服务 (app)
我们的应用需要从源代码构建镜像,所以需要指定build上下文和Dockerfile。
app: build: ./app # 构建上下文目录,Dockerfile 位于此目录下 container_name: springboot-app restart: unless-stopped depends_on: - db # 声明依赖,确保 db 服务先启动。但注意:这仅控制启动顺序,不保证 db 已“准备就绪”。 environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/myappdb?useSSL=false&serverTimezone=UTC SPRING_DATASOURCE_USERNAME: appuser SPRING_DATASOURCE_PASSWORD: userpassword123 networks: - backend # 注意:这里没有映射 ports,因为该服务只通过内部网络被 Nginx 访问。关键点解释:
build: ./app: 告诉 Compose 去./app目录下寻找Dockerfile来构建镜像。depends_on: 定义服务启动依赖关系。Compose 会先启动db,再启动app。重要提示:这只能保证db容器进程启动,不能保证 MySQL 服务已完成初始化并可以接受连接。对于生产环境,应用需要有连接重试机制。SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/myappdb: 这里使用了db作为主机名。因为在同一个 Compose 网络 (backend) 中,容器可以通过服务名直接通信。- 没有
ports:该服务不需要被宿主机或外部网络直接访问,所有流量通过 Nginx 代理。
现在,我们需要在./app目录下准备Dockerfile和 Spring Boot 应用。这里提供一个极简示例:
./app/Dockerfile
# 使用官方 OpenJDK 运行时作为父镜像 FROM openjdk:11-jre-slim # 设置工作目录 WORKDIR /app # 将构建好的 jar 包复制到容器中 # 假设你的 Spring Boot 打包后名为 app.jar COPY target/app.jar app.jar # 暴露端口(Spring Boot 默认 8080) EXPOSE 8080 # 指定容器启动时执行的命令 ENTRYPOINT ["java", "-jar", "app.jar"]一个简单的 Spring Boot 应用 (./app/src/main/java/com/example/demo/DemoApplication.java): 你需要一个基本的 Spring Boot 项目。这里仅展示主类和控制器。
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Value; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; @SpringBootApplication @RestController public class DemoApplication { @Value("${spring.application.name:unknown}") private String appName; @Autowired private DataSource dataSource; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping("/") public String home() { return "Hello from " + appName + ". Database connection is: " + (dataSource != null ? "OK" : "NULL"); } @GetMapping("/health") public String health() { return "UP"; } }确保你的application.properties或application.yml中数据库连接配置使用环境变量:
# ./app/src/main/resources/application.properties spring.application.name=myapp spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}使用 Maven 或 Gradle 将项目打包为app.jar并放入./app/target/目录。
4.5 第五步:定义 Nginx 反向代理服务 (proxy)
Nginx 将作为流量入口,将请求转发给后端的app服务。
proxy: image: nginx:alpine # 使用更小的 alpine 版本 container_name: nginx-proxy restart: unless-stopped depends_on: - app ports: - "80:80" # 将宿主机的80端口映射到Nginx容器的80端口 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # 挂载自定义的Nginx配置,ro表示只读 networks: - backend关键点解释:
ports: - "80:80": 这是唯一暴露给宿主机的端口,外部通过访问宿主机的 80 端口来访问整个应用。volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro: 用我们自定义的配置覆盖容器内的默认配置。:ro防止容器内进程意外修改配置文件。
现在创建 Nginx 配置目录和文件:
mkdir nginx./nginx/nginx.conf
events { worker_connections 1024; } http { upstream backend { server app:8080; # 使用服务名 app 和内部端口 8080 } server { listen 80; location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } }4.6 第六步:定义网络和数据卷
回到docker-compose.yml文件顶部,补充networks和volumes定义。
version: '3.8' services: db: # ... 如上所述 app: # ... 如上所述 proxy: # ... 如上所述 networks: backend: driver: bridge # 使用桥接驱动,这是默认值,可省略 volumes: db_data: # 定义命名卷 db_data,Docker 会自动管理其存储位置至此,一个完整的docker-compose.yml文件就编写完成了。
5. 完整示例与代码实现
让我们把上面所有的代码片段整合起来,形成一个完整的、可运行的示例。
完整的docker-compose.yml(位于项目根目录my-web-app/)
version: '3.8' services: db: image: mysql:8.0 container_name: mysql-db restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: rootpassword123 MYSQL_DATABASE: myappdb MYSQL_USER: appuser MYSQL_PASSWORD: userpassword123 volumes: - db_data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql networks: - backend ports: - "3306:3306" app: build: ./app container_name: springboot-app restart: unless-stopped depends_on: - db environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/myappdb?useSSL=false&serverTimezone=UTC SPRING_DATASOURCE_USERNAME: appuser SPRING_DATASOURCE_PASSWORD: userpassword123 networks: - backend proxy: image: nginx:alpine container_name: nginx-proxy restart: unless-stopped depends_on: - app ports: - "80:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro networks: - backend networks: backend: driver: bridge volumes: db_data:完整的项目结构树:
my-web-app/ ├── docker-compose.yml ├── nginx/ │ └── nginx.conf ├── app/ │ ├── Dockerfile │ ├── pom.xml (或 build.gradle) │ ├── src/ │ │ └── main/ │ │ ├── java/com/example/demo/ │ │ │ └── DemoApplication.java │ │ └── resources/ │ │ └── application.properties │ └── target/ │ └── app.jar (通过 mvn clean package 生成) └── mysql/ └── init.sql (可选)初始化 SQL 脚本示例 (./mysql/init.sql):
-- 创建额外的表或插入初始数据 USE myappdb; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL ); INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com');6. 运行结果与效果验证
一切就绪,现在让我们启动这个“公寓楼”。
启动所有服务: 在项目根目录 (
my-web-app/) 下,运行:docker-compose up -d-d参数表示在后台运行(守护进程模式)。你会看到 Compose 依次拉取镜像、构建app服务的镜像、创建网络和卷,并启动所有容器。查看运行状态:
docker-compose ps输出应类似如下,所有服务的状态应为
Up。Name Command State Ports -------------------------------------------------------------------------------- mysql-db docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp nginx-proxy /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp springboot-app java -jar app.jar Up 8080/tcp验证应用访问:
- 打开浏览器,访问
http://localhost(或你的服务器 IP)。 - 你应该能看到页面显示:
Hello from myapp. Database connection is: OK。 - 访问
http://localhost/health,应返回UP。 - 这证明 Nginx (
proxy) 成功接收请求,并转发给了 Spring Boot 应用 (app),而应用也成功连接到了 MySQL 数据库 (db)。
- 打开浏览器,访问
验证数据库:
- 你可以使用宿主机上的 MySQL 客户端(如
mysql命令行或 MySQL Workbench)连接数据库。 - 主机:
localhost,端口:3306,用户:appuser,密码:userpassword123。 - 连接后,执行
SELECT * FROM myappdb.users;,应该能看到我们在init.sql中插入的测试数据。
- 你可以使用宿主机上的 MySQL 客户端(如
查看日志:
- 查看所有服务的聚合日志:
docker-compose logs - 查看特定服务的日志:
docker-compose logs app - 实时跟踪日志:
docker-compose logs -f app
- 查看所有服务的聚合日志:
停止并清理环境:
- 停止所有服务但保留容器和数据:
docker-compose stop - 停止并移除所有容器、网络(但保留数据卷和镜像):
docker-compose down - 停止并移除所有容器、网络、数据卷和构建的镜像:
docker-compose down -v --rmi local - 谨慎使用
-v和--rmi,这会删除数据库数据!
- 停止所有服务但保留容器和数据:
7. 常见问题与排查思路
在实际使用中,你可能会遇到以下问题。这里提供一个排查清单。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
docker-compose up构建失败 | Dockerfile语法错误,或构建上下文缺少文件。 | 1. 运行docker-compose build --no-cache查看详细错误。2. 检查 ./app/Dockerfile路径和内容。3. 确认 ./app/target/app.jar文件是否存在。 | 修正Dockerfile,确保COPY的文件路径正确。先使用mvn clean package生成 jar 包。 |
应用 (app) 启动失败,报数据库连接错误 | 1.depends_on只保证db容器启动,不保证 MySQL 服务就绪。2. 环境变量配置错误。 3. 网络不通。 | 1.docker-compose logs app查看应用日志。2. docker-compose exec db mysql -uappuser -p尝试手动连接数据库。3. docker-compose exec app ping db检查网络连通性。 | 1. 为应用添加连接重试逻辑(如 Spring Boot 的spring.datasource.hikari.connection-timeout)。2. 检查 docker-compose.yml中的环境变量拼写和值。3. 确认所有服务都在同一个网络 ( backend) 中。 |
访问http://localhost返回 502 Bad Gateway | Nginx 无法连接到后端app服务。 | 1.docker-compose logs proxy查看 Nginx 错误日志。2. docker-compose ps确认app服务状态为Up。3. docker-compose exec proxy ping app检查网络。 | 1. 检查./nginx/nginx.conf中upstream的服务器地址 (app:8080) 是否正确。2. 确认 Spring Boot 应用确实在 8080 端口监听。 3. 重启 app服务:docker-compose restart app。 |
| 数据库数据丢失 | 数据卷 (db_data) 未正确挂载或配置。 | 1.docker volume ls查看卷列表。2. docker-compose down后是否使用了-v参数。 | 1. 确保docker-compose.yml中volumes配置正确。2.生产环境务必做好数据卷的备份。 3. 避免使用 docker-compose down -v除非确定要销毁数据。 |
| 端口冲突 | 宿主机 80、3306 端口已被其他程序占用。 | 在宿主机运行netstat -tuln | grep :80或lsof -i:80。 | 1. 修改docker-compose.yml中的ports映射,例如将"80:80"改为"8080:80",然后通过http://localhost:8080访问。2. 停止占用端口的进程。 |
8. 最佳实践与工程建议
将 Docker Compose 用于实际项目时,遵循以下建议可以避免很多坑。
永远指定镜像版本
- 避免使用
latest标签。它会导致不同时间、不同环境部署的版本不一致,是生产环境的不稳定因素。明确指定如mysql:8.0,nginx:1.23-alpine。
- 避免使用
使用
.env文件管理敏感信息- 不要在
docker-compose.yml中硬编码密码。创建一个.env文件(确保在.gitignore中):# .env MYSQL_ROOT_PASSWORD=your_strong_root_password MYSQL_PASSWORD=your_strong_user_password - 在
docker-compose.yml中引用:environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
- 不要在
编写健壮的 Dockerfile
- 使用多阶段构建减小镜像体积。
- 使用非 root 用户运行进程以增强安全。
- 示例(优化后的
./app/Dockerfile):# 构建阶段 FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:11-jre-slim RUN addgroup --system --gid 1000 appgroup && \ adduser --system --uid 1000 --ingroup appgroup appuser WORKDIR /app COPY --from=builder /build/target/app.jar app.jar RUN chown -R appuser:appgroup /app USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
处理服务依赖与健康检查
depends_on不检查应用健康。使用healthcheck指令让 Compose 等待服务就绪。- 在
docker-compose.yml中为db服务添加健康检查:db: # ... 其他配置 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uappuser", "-puserpassword123"] interval: 10s timeout: 5s retries: 5 start_period: 30s - 然后让
app服务依赖db的健康状态:app: # ... 其他配置 depends_on: db: condition: service_healthy
区分开发与生产配置
- 创建多个 Compose 文件:
docker-compose.yml(基础配置),docker-compose.override.yml(开发覆盖配置),docker-compose.prod.yml(生产配置)。 - 开发时,默认会合并
docker-compose.yml和docker-compose.override.yml。 - 生产部署时,指定生产配置:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d。 - 开发配置可以包含:挂载源代码目录用于热重载 (
volumes: - ./app/src:/app/src)、开启调试端口等。 - 生产配置可以包含:移除端口映射、调整资源限制、配置日志驱动等。
- 创建多个 Compose 文件:
资源限制与日志管理
- 为服务设置 CPU 和内存限制,防止单个容器耗尽主机资源。
app: deploy: # 注意:单机 Compose 中,`deploy` 部分仅在 `docker stack deploy` 时有效,单机限制使用 `resources` resources: limits: cpus: '1.0' memory: 512M - 配置日志驱动和大小,避免日志占满磁盘。
app: logging: driver: "json-file" options: max-size: "10m" max-file: "3"
- 为服务设置 CPU 和内存限制,防止单个容器耗尽主机资源。
备份与监控
- 定期备份命名卷:
docker run --rm -v db_data:/source -v /host/backup:/backup alpine tar czf /backup/db_data_backup.tar.gz -C /source . - 考虑集成监控工具,如
cAdvisor+Prometheus+Grafana,也可以将其作为 Compose 服务加入编排。
- 定期备份命名卷:
Docker Compose 就像一位尽职尽责的“技术宿管阿姨”,它通过一份清晰的声明式配置,接管了多服务应用部署中的繁琐工作。从本地开发到单机部署,它极大地简化了环境管理和应用编排的复杂度。掌握它,不仅能提升个人效率,更是迈向更复杂的容器编排平台(如 Kubernetes)的重要基石。建议你将本文的示例项目作为模板,根据自己项目的实际情况进行调整和扩展。当你熟悉了基本操作后,可以进一步探索 Compose 的扩展配置、多文件组合以及 Docker Swarm 集群部署,从而构建更健壮、可扩展的应用部署体系。
