尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Docker Compose实战:从零编排Spring Boot+Nginx+MySQL微服务应用

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 Desktopdocker 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?useSSLfalseserverTimezoneUTC 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.namemyapp 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.confevents { 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_dataDocker 会自动管理其存储位置至此一个完整的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?useSSLfalseserverTimezoneUTC 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, testexample.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中插入的测试数据。查看日志查看所有服务的聚合日志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 GatewayNginx 无法连接到后端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_PASSWORDyour_strong_root_password MYSQL_PASSWORDyour_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 --frombuilder /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)、开启调试端口等。生产配置可以包含移除端口映射、调整资源限制、配置日志驱动等。资源限制与日志管理为服务设置 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备份与监控定期备份命名卷docker run --rm -v db_data:/source -v /host/backup:/backup alpine tar czf /backup/db_data_backup.tar.gz -C /source .考虑集成监控工具如cAdvisorPrometheusGrafana也可以将其作为 Compose 服务加入编排。Docker Compose 就像一位尽职尽责的“技术宿管阿姨”它通过一份清晰的声明式配置接管了多服务应用部署中的繁琐工作。从本地开发到单机部署它极大地简化了环境管理和应用编排的复杂度。掌握它不仅能提升个人效率更是迈向更复杂的容器编排平台如 Kubernetes的重要基石。建议你将本文的示例项目作为模板根据自己项目的实际情况进行调整和扩展。当你熟悉了基本操作后可以进一步探索 Compose 的扩展配置、多文件组合以及 Docker Swarm 集群部署从而构建更健壮、可扩展的应用部署体系。
返回列表