Docker Compose 深度实战:从编排模型、网络卷、环境变量到生产部署

欢迎你来读这篇博客。

这篇文章专门写 Docker Compose,不再把它当作 Docker 基础知识里的一小节,而是把它当成一个“单机多容器应用编排工具”来系统讲清楚。

如果说 Docker 解决的是:

一个容器怎么跑起来?

那么 Docker Compose 解决的是:

一组互相依赖的容器,如何用一份声明式配置稳定、可重复、可维护地跑起来?

对于 Java 后端开发来说,一个服务通常不会孤零零地运行。它旁边大概率还会有 MySQL、Redis、Nginx、MQ、ES、Prometheus、Grafana、Adminer 等组件。如果每个容器都靠 docker run 手写命令启动,短期能跑,长期就是灾难。

Docker Compose 的价值就在这里:

  • 把多个容器服务写成一份 compose.yaml
  • 自动创建网络、数据卷、容器
  • 支持服务间通过服务名通信
  • 支持环境变量、健康检查、依赖顺序
  • 支持多环境覆盖配置
  • 支持一键启动、停止、重建、查看日志
  • 适合本地开发、测试环境、CI 环境、单机生产部署

它不是 Kubernetes,也不想假装自己是 Kubernetes。Compose 更像是一个务实工具:不追求“航母级编排”,但非常适合开发、测试、中小型单机部署。说人话就是:别拿大炮打蚊子,也别拿牙签撬地球。

一、为什么需要 Docker Compose?

1. 只用 docker run 的问题

假设我们要部署一个 Spring Boot 应用,依赖 MySQL 和 Redis。如果不用 Compose,可能要写这些命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 创建网络
docker network create mall-net

# 启动 MySQL
docker run -d \
--name mall-mysql \
--network mall-net \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=mall \
-v mall-mysql-data:/var/lib/mysql \
mysql:8.0

# 启动 Redis
docker run -d \
--name mall-redis \
--network mall-net \
-p 6379:6379 \
-v mall-redis-data:/data \
redis:7 redis-server --appendonly yes

# 启动应用
docker run -d \
--name mall-app \
--network mall-net \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e SPRING_DATASOURCE_URL='jdbc:mysql://mall-mysql:3306/mall?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai' \
-e SPRING_DATASOURCE_USERNAME=root \
-e SPRING_DATASOURCE_PASSWORD=123456 \
-e SPRING_DATA_REDIS_HOST=mall-redis \
mall-app:1.0.0

这些命令的问题很明显:

  1. 太长,容易写错。
  2. 不好版本管理。
  3. 迁移到别的机器不方便。
  4. 环境变量散落在命令里。
  5. 容器启动顺序靠人肉记忆。
  6. 数据卷、网络、日志、健康检查都难统一管理。
  7. 团队协作时,新人不一定知道完整启动步骤。

而 Compose 可以把这些命令收敛成一份文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
services:
app:
image: mall-app:1.0.0
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mall?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: 123456
SPRING_DATA_REDIS_HOST: redis
depends_on:
- mysql
- redis

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mall
volumes:
- mysql-data:/var/lib/mysql

redis:
image: redis:7
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data

volumes:
mysql-data:
redis-data:

然后启动:

1
docker compose up -d

停止:

1
docker compose down

这就是 Compose 的第一个价值:把命令式部署变成声明式部署

2. Compose 的工作方式

Compose 本质上做了几件事:

  1. 读取 compose.yaml
  2. 解析出服务、网络、卷、配置、密钥等模型
  3. 根据项目名生成资源名称
  4. 按依赖关系创建网络和卷
  5. 构建或拉取镜像
  6. 创建并启动容器
  7. 将同一项目下的服务接入同一个默认网络
  8. 提供统一的日志、执行命令、停止、重建、清理能力

流程图如下:

flowchart TB
    A[compose.yaml] --> B[docker compose 解析配置]
    B --> C[生成 Compose Application Model]
    C --> D[创建 Network]
    C --> E[创建 Volumes]
    C --> F[构建或拉取 Images]
    F --> G[创建 Containers]
    D --> G
    E --> G
    G --> H[服务运行]
    H --> I[logs / exec / restart / down]

3. Compose 适合什么场景?

适合:

  • 本地开发环境
  • 中间件快速搭建
  • 测试环境
  • 自动化集成测试环境
  • 单机部署
  • 演示环境
  • 小型内部系统
  • 开发者个人服务器
  • CI 中启动依赖服务

不适合:

  • 大规模多节点调度
  • 复杂弹性伸缩
  • 跨机器服务发现
  • 自动故障迁移
  • 多租户资源调度
  • 大型生产集群治理

如果你只是想在一台机器上把应用、MySQL、Redis、Nginx 跑起来,Compose 很顺手。
如果你要治理几百个服务、几十台机器、多可用区、多副本滚动发布,那应该看 Kubernetes。


二、Docker Compose 的核心概念

1. Project 项目

Compose 会把一份 compose.yaml 视为一个项目。项目名会影响资源命名。

默认项目名通常来自当前目录名。例如目录叫:

1
mall-system

那么 Compose 创建的资源可能类似:

1
2
3
4
mall-system-app-1
mall-system-mysql-1
mall-system_default
mall-system_mysql-data

可以用 -p 指定项目名:

1
docker compose -p mall-prod up -d

也可以在文件里写:

1
name: mall-prod

项目名很重要,因为同一台机器上可以同时跑多套同名服务,只要项目名不同:

1
2
3
docker compose -p mall-dev up -d
docker compose -p mall-test up -d
docker compose -p mall-prod up -d

这三套环境的容器、网络、卷会被隔离开。

2. Service 服务

services 是 Compose 最核心的部分。一个 service 通常对应一类容器。

1
2
3
services:
app:
image: mall-app:1.0.0

这里的 app 是服务名。服务名非常关键,因为同一个 Compose 网络中,服务名就是 DNS 名称。

例如应用访问 MySQL,不应该写宿主机 IP,而应该写:

1
mysql:3306

因为服务名是 mysql

3. Container 容器

Service 是声明,Container 是运行实例。

一个服务默认启动一个容器:

1
docker compose up -d

也可以扩容某个无状态服务:

1
docker compose up -d --scale app=3

注意,如果服务配置了固定的 container_name,就不适合扩容,因为容器名必须唯一。

4. Network 网络

Compose 默认会为项目创建一个网络。项目内服务默认都加入这个网络。

这意味着:

1
2
3
4
5
services:
app:
image: mall-app
mysql:
image: mysql:8.0

app 可以直接访问:

1
mysql:3306

不需要手动写 --network

5. Volume 数据卷

Compose 可以声明命名数据卷:

1
2
volumes:
mysql-data:

然后挂载到服务:

1
2
3
4
5
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql

这样容器删除后,数据卷还在。

6. Config 和 Secret

Compose 也支持 configssecrets,用于挂载配置和敏感文件。

不过要注意:在普通单机 Docker Compose 场景下,secrets 的安全级别不能和 Kubernetes Secret 或云厂商密钥管理服务完全等同。它更像是一种“文件挂载式管理方式”,比把密码写死进镜像好,但不是万能保险箱。

7. Compose 模型总览

flowchart TB
    A[Compose Project] --> B[Services]
    A --> C[Networks]
    A --> D[Volumes]
    A --> E[Configs]
    A --> F[Secrets]

    B --> B1[app]
    B --> B2[mysql]
    B --> B3[redis]
    B --> B4[nginx]

    C --> C1[default network]
    C --> C2[backend network]
    C --> C3[frontend network]

    D --> D1[mysql-data]
    D --> D2[redis-data]

三、Compose V1、V2 和现代写法

1. docker-composedocker compose 的区别

老版本命令是:

1
docker-compose up -d

现代推荐命令是:

1
docker compose up -d

区别:

命令 说明
docker-compose 老的 Compose V1,独立 Python 工具
docker compose Compose V2,作为 Docker CLI 插件使用

现在更推荐使用 docker compose

查看版本:

1
docker compose version

2. 不再推荐写 version: "3.8"

很多老文章会这样写:

1
2
3
4
version: "3.8"
services:
app:
image: nginx

现代 Compose Specification 中,顶层 version 字段已经主要用于兼容历史格式。新写法可以直接:

1
2
3
services:
app:
image: nginx

如果你继续写 version,某些新版本 Compose 会提示它已经过时。
所以新文章、新项目、新模板,建议直接省略 version

3. 文件名建议

推荐文件名:

1
compose.yaml

兼容文件名:

1
2
docker-compose.yml
docker-compose.yaml

现在官方文档更常用 compose.yaml,但很多历史项目仍然用 docker-compose.yml。实际项目里两者都能见到。


四、Compose 文件结构详解

1. 最小可运行示例

1
2
3
4
5
services:
nginx:
image: nginx:latest
ports:
- "8080:80"

启动:

1
docker compose up -d

访问:

1
curl http://localhost:8080

停止并删除容器和默认网络:

1
docker compose down

2. 标准结构

一个较完整的 Compose 文件通常长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: mall-dev

services:
app:
image: mall-app:1.0.0
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
env_file:
- .env
volumes:
- ./logs:/app/logs
networks:
- backend
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mall
volumes:
- mysql-data:/var/lib/mysql
networks:
- backend

networks:
backend:

volumes:
mysql-data:

核心层级:

顶层字段 说明
name 项目名
services 服务定义,最核心
networks 网络定义
volumes 数据卷定义
configs 配置定义
secrets 密钥定义

五、services 详解

1. image

使用现成镜像:

1
2
3
services:
redis:
image: redis:7

建议固定版本,不建议生产长期使用 latest

1
image: redis:7.2

或者更严格地固定 digest:

1
image: redis@sha256:xxxx

2. build

从 Dockerfile 构建镜像:

1
2
3
4
5
6
services:
app:
build:
context: .
dockerfile: Dockerfile
image: mall-app:1.0.0

字段含义:

字段 说明
context 构建上下文目录
dockerfile Dockerfile 文件路径
args 构建参数
target 多阶段构建目标阶段

示例:

1
2
3
4
5
6
7
8
9
services:
app:
build:
context: .
dockerfile: Dockerfile
target: runner
args:
APP_VERSION: 1.0.0
image: mall-app:1.0.0

对应 Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre AS runner
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

3. container_name

可以指定容器名:

1
2
3
4
services:
app:
image: mall-app:1.0.0
container_name: mall-app

但是不建议滥用。原因:

  1. Compose 默认会根据项目名生成容器名,避免冲突。
  2. 写死 container_name 后,同一台机器不能轻松启动多套环境。
  3. 写死容器名后,不适合使用 --scale 扩容。

本地调试可以用,团队项目和生产部署建议谨慎。

4. ports

端口发布,把容器端口映射到宿主机。

1
2
3
4
5
services:
nginx:
image: nginx
ports:
- "8080:80"

含义:

1
宿主机 8080 -> 容器 80

只监听本机:

1
2
ports:
- "127.0.0.1:8080:80"

长语法:

1
2
3
4
5
ports:
- target: 80
published: 8080
protocol: tcp
mode: host

什么时候需要 ports

  • 需要宿主机访问容器
  • 需要外部机器访问容器
  • 需要浏览器访问服务
  • 需要暴露 Nginx、应用 HTTP 接口

什么时候不需要?

  • 只给 Compose 内部其他服务访问
  • 例如 app 访问 mysql,mysql 不一定要映射 3306:3306

5. expose

expose 只声明容器对内部网络暴露端口,不发布到宿主机。

1
2
3
4
5
services:
app:
image: mall-app:1.0.0
expose:
- "8080"

它更多是文档化表达,服务间通信主要靠同网络和服务名。

6. environment

设置容器环境变量:

1
2
3
4
5
6
services:
app:
image: mall-app:1.0.0
environment:
SPRING_PROFILES_ACTIVE: prod
JAVA_OPTS: "-Xms256m -Xmx512m"

也可以写列表形式:

1
2
3
environment:
- SPRING_PROFILES_ACTIVE=prod
- JAVA_OPTS=-Xms256m -Xmx512m

更推荐 map 形式,可读性更好。

7. env_file

从文件中读取环境变量并注入容器:

1
2
3
4
5
services:
app:
image: mall-app:1.0.0
env_file:
- ./app.env

app.env

1
2
SPRING_PROFILES_ACTIVE=prod
JAVA_OPTS=-Xms256m -Xmx512m

注意:env_file 是给容器注入环境变量,不完全等同于 Compose 文件变量插值用的 .env。这是很多人踩坑的地方。

8. command

覆盖镜像默认命令。

Redis 示例:

1
2
3
4
services:
redis:
image: redis:7
command: ["redis-server", "--appendonly", "yes"]

Nginx 示例:

1
2
3
4
services:
nginx:
image: nginx
command: ["nginx", "-g", "daemon off;"]

9. entrypoint

覆盖镜像入口。

1
2
3
4
5
services:
app:
image: mall-app:1.0.0
entrypoint: ["sh", "-c"]
command: ["java $JAVA_OPTS -jar app.jar"]

一般不建议在 Compose 里随便覆盖 entrypoint,除非你非常清楚镜像原本怎么启动。

10. volumes

挂载数据卷或宿主机目录。

命名卷:

1
2
3
4
5
6
7
8
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql

volumes:
mysql-data:

绑定宿主机目录:

1
2
3
4
5
6
services:
nginx:
image: nginx
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./html:/usr/share/nginx/html:ro

建议:

  • 数据库数据用命名卷。
  • 配置文件可以用 bind mount。
  • 本地开发代码热更新可以用 bind mount。
  • 生产环境不要把应用代码目录挂进去,生产应使用构建好的镜像。

11. networks

指定服务加入哪些网络。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
app:
image: mall-app
networks:
- frontend
- backend

mysql:
image: mysql:8.0
networks:
- backend

networks:
frontend:
backend:

这样 app 可以同时连接前端网络和后端网络,而 mysql 只在后端网络中。

12. depends_on

声明服务依赖关系。

短语法:

1
2
3
4
5
6
services:
app:
image: mall-app
depends_on:
- mysql
- redis

这表示 mysqlredis 会先创建,再创建 app

但是要注意:短语法通常只保证“启动顺序”,不保证 MySQL 已经可以接受连接。MySQL 容器进程启动了,不代表数据库初始化完成。这个坑很经典,堪称 Compose 入门税。

更推荐结合健康检查:

1
2
3
4
5
6
7
8
services:
app:
image: mall-app
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

13. healthcheck

健康检查用于判断服务是否真正可用。

MySQL:

1
2
3
4
5
6
7
8
9
10
11
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p123456"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s

Redis:

1
2
3
4
5
6
7
8
services:
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

Spring Boot:

1
2
3
4
5
6
7
8
9
services:
app:
image: mall-app:1.0.0
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep UP || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 30s

如果镜像里没有 wgetcurl,健康检查会失败。要么换一种检查方式,要么在镜像中加入工具。

14. restart

容器异常退出后的重启策略。

1
2
3
4
services:
app:
image: mall-app:1.0.0
restart: unless-stopped

常用值:

策略 说明
no 默认,不自动重启
always 总是重启
unless-stopped 除非手动停止,否则自动重启
on-failure 非 0 状态码退出时重启

服务类应用推荐:

1
restart: unless-stopped

15. logging

控制容器日志大小,避免日志撑爆磁盘。

1
2
3
4
5
6
7
8
services:
app:
image: mall-app:1.0.0
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

这非常重要。线上磁盘爆满,很多时候不是业务数据爆了,而是日志悄悄变成了“数字垃圾山”。

16. resource 限制

Compose 可以限制容器资源。

1
2
3
4
5
services:
app:
image: mall-app:1.0.0
mem_limit: 768m
cpus: 1.0

Java 应用要特别注意:

1
2
3
environment:
JAVA_OPTS: "-Xms256m -Xmx512m"
mem_limit: 768m

不要容器限制 512MB,JVM 也设置 -Xmx512m。因为 JVM 堆外内存、线程栈、元空间也要占内存。

17. working_dir 和 user

指定工作目录:

1
working_dir: /app

指定用户运行:

1
user: "1000:1000"

生产环境建议镜像本身使用非 root 用户,而不是完全依赖 Compose 层覆盖。


六、Docker Compose 常用命令

1. 启动服务

前台启动:

1
docker compose up

后台启动:

1
docker compose up -d

构建并启动:

1
docker compose up -d --build

强制重建容器:

1
docker compose up -d --force-recreate

不启动依赖,只重建某个服务:

1
docker compose up -d --no-deps app

2. 停止和删除

停止服务但不删除容器:

1
docker compose stop

启动已停止服务:

1
docker compose start

停止并删除容器、默认网络:

1
docker compose down

停止并删除容器、网络、数据卷:

1
docker compose down -v

down -v 会删除数据卷。数据库环境不要随手敲,除非你真的想体验“删库跑路模拟器”。

3. 查看服务状态

1
docker compose ps

查看运行进程:

1
docker compose top

查看项目列表:

1
docker compose ls

4. 查看日志

查看所有服务日志:

1
docker compose logs

实时日志:

1
docker compose logs -f

查看某个服务日志:

1
docker compose logs -f app

只看最近 200 行:

1
docker compose logs --tail=200 -f app

5. 进入容器

1
docker compose exec app sh

如果镜像里有 bash:

1
docker compose exec app bash

进入 MySQL:

1
docker compose exec mysql mysql -uroot -p123456

进入 Redis:

1
docker compose exec redis redis-cli

6. 临时运行命令

1
docker compose run --rm app java -version

execrun 的区别:

命令 作用
exec 在已经运行的容器中执行命令
run 基于服务配置新建一个临时容器执行命令

7. 构建、拉取、推送镜像

构建全部服务:

1
docker compose build

构建指定服务:

1
docker compose build app

拉取镜像:

1
docker compose pull

推送镜像:

1
docker compose push

8. 校验和展开配置

这是非常重要的命令:

1
docker compose config

它会输出最终生效的 Compose 配置,包括变量插值、多文件合并后的结果。

检查环境变量插值:

1
docker compose config --environment

多环境部署前建议先执行:

1
docker compose -f compose.yaml -f compose.prod.yaml config

别等容器炸了才发现 YAML 缩进写错了。YAML 这个东西,看着像配置,生气起来像玄学。


七、环境变量与配置管理

Compose 里有几类变量,很容易混:

  1. Compose 文件变量插值
  2. 容器环境变量
  3. .env 文件
  4. env_file
  5. Shell 环境变量
  6. 应用内部配置

1. Compose 变量插值

Compose 支持类似 Bash 的变量语法:

1
2
3
4
5
services:
app:
image: mall-app:${APP_VERSION}
ports:
- "${APP_PORT}:8080"

.env

1
2
APP_VERSION=1.0.0
APP_PORT=8080

启动:

1
docker compose up -d

最终等价于:

1
2
3
image: mall-app:1.0.0
ports:
- "8080:8080"

2. 默认值

1
image: mall-app:${APP_VERSION:-latest}

如果 APP_VERSION 不存在或为空,则使用 latest

3. 必填值

1
image: mall-app:${APP_VERSION:?APP_VERSION is required}

如果没有设置 APP_VERSION,Compose 会直接报错。

这在生产部署时非常好用,避免误用默认值。

4. .env 文件

.env 通常放在 compose.yaml 同级目录。

1
2
3
4
5
6
PROJECT_NAME=mall-prod
APP_VERSION=1.0.3
APP_PORT=8080
MYSQL_ROOT_PASSWORD=change-me
MYSQL_DATABASE=mall
REDIS_PORT=6379

Compose 文件:

1
2
3
4
5
6
7
name: ${PROJECT_NAME:-mall-dev}

services:
app:
image: mall-app:${APP_VERSION:?APP_VERSION required}
ports:
- "${APP_PORT:-8080}:8080"

5. Shell 环境变量优先级

如果 Shell 里设置了变量:

1
export APP_VERSION=2.0.0

即使 .env 中是:

1
APP_VERSION=1.0.0

Compose 插值时也可能优先使用 Shell 中的值。

所以部署脚本里建议显式打印当前配置:

1
docker compose config | sed -n '1,120p'

6. environmentenv_file

.env 主要用于 Compose 文件插值。
environment / env_file 主要用于给容器注入运行时环境变量。

示例:

1
2
3
4
5
6
7
services:
app:
image: mall-app:${APP_VERSION}
env_file:
- ./app.env
environment:
SPRING_PROFILES_ACTIVE: prod

app.env

1
2
JAVA_OPTS=-Xms256m -Xmx512m
LOG_LEVEL=INFO

容器内能看到:

1
docker compose exec app env

7. 环境变量最佳实践

建议:

  • .env 用于非敏感的项目级变量,例如端口、版本号、项目名。
  • environment 用于少量明确配置。
  • env_file 用于较多运行时配置。
  • 密码、Token、私钥不要提交到 Git。
  • 生产环境敏感配置建议使用外部密钥系统,至少也要 .gitignore

.gitignore

1
2
3
4
.env
*.env
.env.*
!.env.example

提供 .env.example

1
2
3
4
5
PROJECT_NAME=mall-dev
APP_VERSION=1.0.0
APP_PORT=8080
MYSQL_ROOT_PASSWORD=please-change-me
MYSQL_DATABASE=mall

八、服务依赖、启动顺序和健康检查

1. depends_on 的误区

很多人以为:

1
2
depends_on:
- mysql

等价于:

等 MySQL 完全启动好之后,再启动 app。

这不准确。

短语法主要表达创建和启动顺序,不能保证 MySQL 已经完成初始化、建库、加载权限、开始接受连接。

所以你可能看到这种日志:

1
2
Communications link failure
Connection refused

但等几秒再重启 app 又好了。这不是玄学,是数据库还没 ready。

2. 正确姿势:depends_on + healthcheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
services:
app:
image: mall-app:1.0.0
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p123456"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s

redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

启动流程:

flowchart TB
    A[docker compose up -d] --> B[创建 mysql]
    A --> C[创建 redis]
    B --> D{mysql healthcheck 通过?}
    C --> E{redis healthcheck 通过?}
    D -->|yes| F[创建 app]
    E -->|yes| F
    D -->|no| G[等待 / 重试]
    E -->|no| H[等待 / 重试]

3. 应用自身也要有重试机制

即使 Compose 做了健康检查,应用也不应该假设依赖永远稳定。

实际生产中,MySQL、Redis 可能重启、网络可能抖动、连接池可能断开。应用侧仍然需要:

  • 数据库连接池重试
  • Redis 客户端自动重连
  • MQ 消费重试
  • 启动失败快速暴露日志
  • 健康检查接口能反映真实依赖状态

Compose 是编排工具,不是保姆。它可以帮你开门,但不能替你进屋写代码。


九、Compose 网络深入

1. 默认网络

如果没有显式声明网络,Compose 会自动创建一个默认网络。

1
2
3
4
5
services:
app:
image: mall-app
mysql:
image: mysql:8.0

app 可以访问:

1
mysql:3306

因为服务名 mysql 会被注册到 Docker 内部 DNS。

2. 服务名就是 DNS 名

示例:

1
2
3
4
5
services:
app:
image: mall-app
redis:
image: redis:7

应用配置应写:

1
2
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379

不要写:

1
SPRING_DATA_REDIS_HOST: 127.0.0.1

因为在容器里,127.0.0.1 指的是 app 容器自己,不是 Redis 容器,也不是宿主机。

3. 自定义网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services:
nginx:
image: nginx
networks:
- frontend

app:
image: mall-app
networks:
- frontend
- backend

mysql:
image: mysql:8.0
networks:
- backend

networks:
frontend:
backend:

网络隔离图:

flowchart LR
    subgraph frontend[frontend 网络]
        N[Nginx]
        A[App]
    end

    subgraph backend[backend 网络]
        A2[App]
        M[MySQL]
        R[Redis]
    end

    N --> A
    A2 --> M
    A2 --> R

这样:

  • Nginx 能访问 App
  • App 能访问 MySQL 和 Redis
  • Nginx 不能直接访问 MySQL

4. aliases 网络别名

1
2
3
4
5
6
7
8
9
10
11
services:
mysql:
image: mysql:8.0
networks:
backend:
aliases:
- db
- mysql-master

networks:
backend:

同网络下可以通过这些名字访问:

1
2
3
mysql:3306
db:3306
mysql-master:3306

5. internal 内部网络

1
2
3
networks:
backend:
internal: true

internal: true 表示该网络是内部网络,通常用于加强隔离。

6. external 外部网络

如果网络已经提前创建:

1
docker network create shared-net

Compose 中可以引用:

1
2
3
4
5
6
7
8
9
services:
app:
image: mall-app
networks:
- shared-net

networks:
shared-net:
external: true

适合多个 Compose 项目共享同一个网络。

7. host 网络模式

1
2
3
4
services:
app:
image: mall-app
network_mode: host

使用 host 网络时,容器直接使用宿主机网络。此时 ports 映射通常不再有意义。

不建议默认使用 host 网络,除非你明确知道自己要绕开容器网络隔离。


十、端口映射深入

1. ports 基本语法

1
2
ports:
- "8080:8080"

含义:

1
宿主机端口:容器端口

也就是:

1
宿主机 8080 -> 容器 8080

2. 指定协议

1
2
3
ports:
- "8080:8080/tcp"
- "5353:5353/udp"

3. 只绑定本机

1
2
ports:
- "127.0.0.1:8080:8080"

这表示只有宿主机本机能访问,外部机器不能访问。

适合:

  • 本地开发数据库
  • 本机调试面板
  • 不希望公网暴露的管理服务

4. 不要把所有中间件都暴露出去

很多人本地开发习惯这样写:

1
2
3
4
5
6
7
services:
mysql:
ports:
- "3306:3306"
redis:
ports:
- "6379:6379"

本地可以,生产环境要谨慎。

如果只有 app 需要访问 MySQL 和 Redis,那它们根本不需要发布到宿主机:

1
2
3
4
5
6
services:
mysql:
image: mysql:8.0

redis:
image: redis:7

App 在内部网络中通过 mysqlredis 访问即可。

5. 端口冲突排查

如果启动报错:

1
Bind for 0.0.0.0:8080 failed: port is already allocated

说明宿主机端口已被占用。

排查:

1
sudo lsof -i :8080

或:

1
sudo netstat -tunlp | grep 8080

修改 .env

1
APP_PORT=8081

Compose:

1
2
ports:
- "${APP_PORT:-8080}:8080"

十一、数据卷与持久化深入

1. 命名卷

1
2
3
4
5
6
7
8
services:
mysql:
image: mysql:8.0
volumes:
- mysql-data:/var/lib/mysql

volumes:
mysql-data:

实际 volume 名通常会带项目名前缀:

1
mall-dev_mysql-data

查看:

1
docker volume ls

2. 绑定挂载

1
2
3
4
5
6
services:
nginx:
image: nginx
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/html:/usr/share/nginx/html:ro

适合:

  • Nginx 配置
  • 本地开发代码挂载
  • 本地临时日志目录
  • 测试配置文件

3. tmpfs

1
2
3
4
5
services:
app:
image: mall-app
tmpfs:
- /tmp

适合临时文件,不需要落盘。

4. external volume

提前创建:

1
docker volume create mysql-prod-data

Compose 引用:

1
2
3
4
5
6
7
8
9
services:
mysql:
image: mysql:8.0
volumes:
- mysql-prod-data:/var/lib/mysql

volumes:
mysql-prod-data:
external: true

这样即使 Compose 项目名变化,也不会改变实际 volume 名。

5. 数据库备份:mysqldump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env bash
set -e

BACKUP_DIR="./backup"
MYSQL_SERVICE="mysql"
MYSQL_USER="root"
MYSQL_PASSWORD="${MYSQL_ROOT_PASSWORD:-123456}"
MYSQL_DATABASE="${MYSQL_DATABASE:-mall}"
TIME=$(date +%Y%m%d_%H%M%S)

mkdir -p "${BACKUP_DIR}"

docker compose exec -T ${MYSQL_SERVICE} \
mysqldump -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
--databases ${MYSQL_DATABASE} \
> "${BACKUP_DIR}/${MYSQL_DATABASE}_${TIME}.sql"

echo "backup success: ${BACKUP_DIR}/${MYSQL_DATABASE}_${TIME}.sql"

6. 数据库恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
set -e

SQL_FILE="$1"
MYSQL_SERVICE="mysql"
MYSQL_USER="root"
MYSQL_PASSWORD="${MYSQL_ROOT_PASSWORD:-123456}"

if [ -z "${SQL_FILE}" ]; then
echo "Usage: ./restore-mysql.sh <sql-file>"
exit 1
fi

cat "${SQL_FILE}" | docker compose exec -T ${MYSQL_SERVICE} \
mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD}

echo "restore success: ${SQL_FILE}"

7. volume 文件级备份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
set -e

VOLUME_NAME="$1"
BACKUP_DIR="./volume-backup"
TIME=$(date +%Y%m%d_%H%M%S)

if [ -z "${VOLUME_NAME}" ]; then
echo "Usage: ./backup-volume.sh <volume-name>"
exit 1
fi

mkdir -p "${BACKUP_DIR}"

docker run --rm \
-v ${VOLUME_NAME}:/volume \
-v $(pwd)/${BACKUP_DIR}:/backup \
alpine \
tar czf /backup/${VOLUME_NAME}_${TIME}.tar.gz -C /volume .

echo "backup success: ${BACKUP_DIR}/${VOLUME_NAME}_${TIME}.tar.gz"

十二、profiles:让可选服务按需启动

Profiles 可以让某些服务默认不启动,只有指定 profile 时才启动。

适合:

  • Adminer
  • phpMyAdmin
  • RedisInsight
  • Prometheus
  • Grafana
  • Jaeger
  • 临时 debug 工具
  • 本地 mock 服务

1. 示例:Adminer 只在 debug 模式启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mall

adminer:
image: adminer
profiles:
- debug
ports:
- "8081:8080"
depends_on:
- mysql

默认启动:

1
docker compose up -d

只启动 MySQL,不启动 Adminer。

带 profile 启动:

1
docker compose --profile debug up -d

2. 多 profile

1
2
3
4
5
6
services:
grafana:
image: grafana/grafana
profiles:
- monitor
- debug

启动:

1
docker compose --profile monitor up -d

3. profiles 设计建议

建议把核心服务默认启用,可选工具放 profile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
services:
app:
image: mall-app

mysql:
image: mysql:8.0

redis:
image: redis:7

adminer:
image: adminer
profiles:
- debug

不要把核心服务放 profile,否则别人直接 docker compose up -d 时可能启动不完整。


十三、多 Compose 文件与环境覆盖

实际项目通常有多套环境:

  • dev
  • test
  • staging
  • prod

不建议复制四份完整 Compose 文件。更好的做法是:

1
2
3
4
compose.yaml              # 基础配置
compose.dev.yaml # 开发环境覆盖
compose.test.yaml # 测试环境覆盖
compose.prod.yaml # 生产环境覆盖

1. 基础配置 compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
services:
app:
image: registry.example.com/mall-app:${APP_VERSION:?APP_VERSION required}
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-mall}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD required}
SPRING_DATA_REDIS_HOST: redis
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD required}
MYSQL_DATABASE: ${MYSQL_DATABASE:-mall}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s

redis:
image: redis:7
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

volumes:
mysql-data:
redis-data:

2. 开发环境覆盖 compose.dev.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
volumes:
- ./logs:/app/logs

mysql:
ports:
- "3306:3306"

redis:
ports:
- "6379:6379"

3. 生产环境覆盖 compose.prod.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
services:
app:
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
mem_limit: 768m
cpus: 1.0

mysql:
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

redis:
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

4. 启动不同环境

开发:

1
docker compose -f compose.yaml -f compose.dev.yaml up -d --build

生产:

1
docker compose -f compose.yaml -f compose.prod.yaml up -d

查看最终配置:

1
docker compose -f compose.yaml -f compose.prod.yaml config

5. 多文件合并规则理解

Compose 会按 -f 指定顺序合并文件,后面的文件会覆盖或追加前面的配置。

flowchart LR
    A[compose.yaml 基础配置] --> C[最终配置]
    B[compose.prod.yaml 生产覆盖] --> C
    C --> D[docker compose up -d]

一般原则:

  • 基础文件写所有环境共用内容。
  • dev 文件只写开发环境差异。
  • prod 文件只写生产环境差异。
  • 不要复制粘贴整份配置,否则后期维护很痛苦。

十四、完整实战:Spring Boot + MySQL + Redis + Nginx

1. 项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mall-compose/
├── compose.yaml
├── compose.dev.yaml
├── compose.prod.yaml
├── .env.example
├── Dockerfile
├── nginx/
│ └── conf.d/
│ └── mall.conf
├── scripts/
│ ├── deploy.sh
│ ├── restart-app.sh
│ ├── backup-mysql.sh
│ ├── restore-mysql.sh
│ ├── logs.sh
│ └── health.sh
├── src/
├── pom.xml
└── target/

2. Spring Boot Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

COPY pom.xml .
RUN mvn dependency:go-offline

COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre AS runner

RUN groupadd -r app && useradd -r -g app app

WORKDIR /app

COPY --from=builder /build/target/*.jar app.jar

RUN chown -R app:app /app

USER app

EXPOSE 8080

ENV JAVA_OPTS="-Xms256m -Xmx512m"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

3. .env.example

1
2
3
4
5
6
7
PROJECT_NAME=mall-dev
APP_VERSION=1.0.0
APP_PORT=8080
NGINX_PORT=80
MYSQL_ROOT_PASSWORD=please-change-me
MYSQL_DATABASE=mall
SPRING_PROFILES_ACTIVE=dev

使用时复制:

1
cp .env.example .env

4. compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
name: ${PROJECT_NAME:-mall-dev}

services:
nginx:
image: nginx:1.27-alpine
depends_on:
app:
condition: service_healthy
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
networks:
- frontend
restart: unless-stopped

app:
image: mall-app:${APP_VERSION:?APP_VERSION required}
build:
context: .
dockerfile: Dockerfile
target: runner
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-dev}
JAVA_OPTS: "-Xms256m -Xmx512m"
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE:-mall}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD required}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- frontend
- backend
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep UP || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 30s
restart: unless-stopped

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD required}
MYSQL_DATABASE: ${MYSQL_DATABASE:-mall}
TZ: Asia/Shanghai
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-time-zone=+08:00
volumes:
- mysql-data:/var/lib/mysql
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p${MYSQL_ROOT_PASSWORD} || exit 1"]
interval: 5s
timeout: 3s
retries: 20
start_period: 30s
restart: unless-stopped

redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
restart: unless-stopped

adminer:
image: adminer:latest
profiles:
- debug
ports:
- "8081:8080"
depends_on:
mysql:
condition: service_healthy
networks:
- backend

networks:
frontend:
backend:
internal: false

volumes:
mysql-data:
redis-data:

5. compose.dev.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
services:
nginx:
ports:
- "${NGINX_PORT:-80}:80"

app:
ports:
- "${APP_PORT:-8080}:8080"
volumes:
- ./logs:/app/logs

mysql:
ports:
- "3306:3306"

redis:
ports:
- "6379:6379"

6. compose.prod.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
services:
nginx:
ports:
- "${NGINX_PORT:-80}:80"
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

app:
build: null
image: registry.example.com/mall/mall-app:${APP_VERSION:?APP_VERSION required}
mem_limit: 768m
cpus: 1.0
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

mysql:
ports: []
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

redis:
ports: []
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

说明:

  • 开发环境可以暴露 MySQL 和 Redis,方便本机工具连接。
  • 生产环境不暴露 MySQL 和 Redis,只让 app 通过内部网络访问。
  • 生产环境使用远程镜像仓库的镜像,不在服务器上临时构建。

7. Nginx 配置

nginx/conf.d/mall.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
listen 80;
server_name _;

location / {
proxy_pass http://app:8080;
proxy_http_version 1.1;

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;
}

location /actuator/health {
proxy_pass http://app:8080/actuator/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}

Nginx 访问 app 用的是:

1
http://app:8080

因为它们在同一个 frontend 网络里。

8. 启动开发环境

1
2
3
cp .env.example .env

docker compose -f compose.yaml -f compose.dev.yaml up -d --build

启动 debug 工具:

1
docker compose -f compose.yaml -f compose.dev.yaml --profile debug up -d

访问:

1
2
curl http://localhost:8080/actuator/health
curl http://localhost

9. 启动生产环境

1
2
3
docker compose -f compose.yaml -f compose.prod.yaml pull

docker compose -f compose.yaml -f compose.prod.yaml up -d

更新 app:

1
2
3
docker compose -f compose.yaml -f compose.prod.yaml pull app

docker compose -f compose.yaml -f compose.prod.yaml up -d --no-deps app

十五、生产部署脚本

1. deploy.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILES="-f compose.yaml -f compose.prod.yaml"
APP_VERSION="${1:-}"

if [ -z "${APP_VERSION}" ]; then
echo "Usage: ./scripts/deploy.sh <app-version>"
exit 1
fi

export APP_VERSION="${APP_VERSION}"

echo "==> Checking final compose config"
docker compose ${COMPOSE_FILES} config > /tmp/compose.final.yaml

echo "==> Pulling images"
docker compose ${COMPOSE_FILES} pull

echo "==> Starting services"
docker compose ${COMPOSE_FILES} up -d

echo "==> Service status"
docker compose ${COMPOSE_FILES} ps

echo "==> Recent app logs"
docker compose ${COMPOSE_FILES} logs --tail=100 app

使用:

1
2
chmod +x scripts/deploy.sh
./scripts/deploy.sh 1.0.3

2. restart-app.sh

1
2
3
4
5
6
7
#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILES="-f compose.yaml -f compose.prod.yaml"

docker compose ${COMPOSE_FILES} restart app
docker compose ${COMPOSE_FILES} logs --tail=100 -f app

3. logs.sh

1
2
3
4
5
6
7
#!/usr/bin/env bash
set -euo pipefail

SERVICE="${1:-app}"
TAIL="${2:-200}"

docker compose -f compose.yaml -f compose.prod.yaml logs --tail="${TAIL}" -f "${SERVICE}"

使用:

1
2
./scripts/logs.sh app 300
./scripts/logs.sh mysql 100

4. health.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILES="-f compose.yaml -f compose.prod.yaml"

echo "==> Compose services"
docker compose ${COMPOSE_FILES} ps

echo "==> App health"
curl -fsS http://127.0.0.1:8080/actuator/health || true

echo

echo "==> Docker stats snapshot"
docker stats --no-stream

5. backup-mysql.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILES="-f compose.yaml -f compose.prod.yaml"
BACKUP_DIR="./backup/mysql"
TIME=$(date +%Y%m%d_%H%M%S)
MYSQL_DATABASE="${MYSQL_DATABASE:-mall}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD required}"

mkdir -p "${BACKUP_DIR}"

docker compose ${COMPOSE_FILES} exec -T mysql \
mysqldump -uroot -p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction \
--routines \
--triggers \
--databases "${MYSQL_DATABASE}" \
> "${BACKUP_DIR}/${MYSQL_DATABASE}_${TIME}.sql"

gzip "${BACKUP_DIR}/${MYSQL_DATABASE}_${TIME}.sql"

echo "backup success: ${BACKUP_DIR}/${MYSQL_DATABASE}_${TIME}.sql.gz"

6. rollback.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILES="-f compose.yaml -f compose.prod.yaml"
ROLLBACK_VERSION="${1:-}"

if [ -z "${ROLLBACK_VERSION}" ]; then
echo "Usage: ./scripts/rollback.sh <app-version>"
exit 1
fi

export APP_VERSION="${ROLLBACK_VERSION}"

echo "==> Rolling back to ${APP_VERSION}"
docker compose ${COMPOSE_FILES} pull app
docker compose ${COMPOSE_FILES} up -d --no-deps app
docker compose ${COMPOSE_FILES} logs --tail=100 app

十六、CI 中使用 Compose

Compose 很适合在 CI 中启动依赖服务,跑集成测试。

1. CI 测试环境 compose.test.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
SPRING_PROFILES_ACTIVE: test
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy

mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: mall_test
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p123456"]
interval: 5s
timeout: 3s
retries: 20

redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

2. CI 脚本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash
set -euo pipefail

docker compose -f compose.test.yaml up -d --build

# 等待服务稳定
sleep 10

docker compose -f compose.test.yaml ps

# 执行测试
mvn test

# 清理环境
docker compose -f compose.test.yaml down -v

如果测试失败,建议保留日志:

1
docker compose -f compose.test.yaml logs > compose-test.log

十七、常见问题与排错

1. 变量没有生效

排查:

1
docker compose config

检查:

  • .env 是否在 compose.yaml 同目录?
  • 变量名是否写错?
  • Shell 环境变量是否覆盖了 .env
  • 是否把 env_file 误当成 Compose 插值来源?

2. app 连不上 mysql

检查服务名:

1
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mall

不要写:

1
jdbc:mysql://127.0.0.1:3306/mall

进入 app 容器测试:

1
docker compose exec app sh

如果有 nc:

1
nc -zv mysql 3306

查看 MySQL 日志:

1
docker compose logs -f mysql

3. depends_on 写了还是失败

原因:短语法不保证服务 ready。

使用:

1
2
3
depends_on:
mysql:
condition: service_healthy

并给 mysql 配置 healthcheck。

4. 端口被占用

1
sudo lsof -i :8080

.env

1
APP_PORT=8081

5. 数据库数据丢了

检查是否使用了命名卷:

1
docker volume ls

查看服务挂载:

1
docker compose config

如果你执行过:

1
docker compose down -v

那卷可能已经被删除。这个命令对数据库杀伤力很大,别把它当普通 stop 用。

6. 修改 compose.yaml 后没有生效

重新创建容器:

1
docker compose up -d --force-recreate

如果改了镜像构建内容:

1
docker compose up -d --build

如果只更新 app:

1
docker compose up -d --no-deps --build app

7. YAML 格式错误

YAML 对缩进很敏感。

错误:

1
2
3
services:
app:
image: nginx

建议统一两个空格:

1
2
3
services:
app:
image: nginx

校验:

1
docker compose config

8. profile 服务没有启动

如果服务写了:

1
2
profiles:
- debug

默认不会启动。

需要:

1
docker compose --profile debug up -d

9. container_name 导致冲突

如果写死:

1
container_name: mysql

另一套项目也写 container_name: mysql,就会冲突。

建议删除 container_name,让 Compose 自动命名。

10. Nginx 502

排查顺序:

1
2
3
4
5
6
7
8
9
10
11
12
# app 是否运行
docker compose ps

# app 日志
docker compose logs -f app

# nginx 日志
docker compose logs -f nginx

# nginx 容器里访问 app
docker compose exec nginx sh
wget -qO- http://app:8080/actuator/health

常见原因:

  • Nginx 和 app 不在同一个网络
  • app 服务名写错
  • app 端口写错
  • app 健康检查没过
  • Spring Boot 绑定了错误地址

十八、Compose 生产实践清单

1. 镜像版本要明确

不要:

1
image: mall-app:latest

推荐:

1
image: registry.example.com/mall/mall-app:1.0.3

或者:

1
image: registry.example.com/mall/mall-app:${APP_VERSION:?APP_VERSION required}

2. 生产环境不要挂载源码

开发环境可以:

1
2
volumes:
- .:/app

生产环境不建议。生产环境应该使用构建好的镜像,保证不可变部署。

3. 数据必须持久化

MySQL:

1
2
volumes:
- mysql-data:/var/lib/mysql

Redis AOF:

1
2
3
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data

4. 不暴露不必要的端口

生产环境中,MySQL、Redis 通常不需要:

1
2
ports:
- "3306:3306"

内部服务通过 Compose 网络访问即可。

5. 加 restart 策略

1
restart: unless-stopped

6. 加健康检查

关键服务都建议配置:

1
2
3
4
5
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20

7. 控制日志大小

1
2
3
4
5
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

8. 设置资源限制

1
2
mem_limit: 768m
cpus: 1.0

9. 配置备份脚本

至少要有:

  • MySQL dump 备份
  • volume 备份
  • 备份文件压缩
  • 定期清理旧备份
  • 恢复演练

10. 发布前看最终配置

1
docker compose -f compose.yaml -f compose.prod.yaml config

这是一个非常简单但非常救命的习惯。


十九、Compose 与 Kubernetes 的边界

Compose 适合单机编排,Kubernetes 适合集群编排。

能力 Docker Compose Kubernetes
本地开发 很适合 偏重
单机部署 适合 可以但复杂
多机调度 不擅长 擅长
自动扩缩容 不擅长 擅长
服务发现 单机 DNS 集群 DNS
滚动发布 基础能力弱 成熟
故障迁移
学习成本

一句话:

Compose 适合把一组容器在一台机器上优雅地跑起来;Kubernetes 适合把一组服务在一个集群里长期治理起来。

不要因为 K8s 高级就什么都上 K8s。很多内部小系统,一份 Compose 加几个脚本就够了。技术选型不是比谁更重,而是比谁更合适。


二十、命令速查表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 查看版本
docker compose version

# 启动
docker compose up -d

# 构建并启动
docker compose up -d --build

# 停止
docker compose stop

# 启动已停止服务
docker compose start

# 重启
docker compose restart

# 删除容器和网络
docker compose down

# 删除容器、网络、卷
docker compose down -v

# 查看状态
docker compose ps

# 查看日志
docker compose logs -f

# 查看指定服务日志
docker compose logs -f app

# 进入容器
docker compose exec app sh

# 临时运行命令
docker compose run --rm app sh

# 构建镜像
docker compose build

# 拉取镜像
docker compose pull

# 推送镜像
docker compose push

# 查看最终配置
docker compose config

# 指定项目名
docker compose -p mall-dev up -d

# 指定多个配置文件
docker compose -f compose.yaml -f compose.prod.yaml up -d

# 启用 profile
docker compose --profile debug up -d

# 扩容服务
docker compose up -d --scale app=3

总结

Docker Compose 的核心能力不是“帮你少敲几个命令”,而是把多容器应用变成一份可读、可版本管理、可复现的声明式配置。

你需要真正掌握的不是某一个字段,而是这套模型:

  • Project:一组 Compose 资源的命名空间
  • Service:服务声明
  • Container:服务运行实例
  • Network:服务间通信边界
  • Volume:持久化数据
  • Environment:配置注入
  • Healthcheck:可用性判断
  • Depends_on:依赖顺序控制
  • Profiles:可选服务开关
  • Override files:多环境配置覆盖

实际项目里,建议形成这样的习惯:

  1. 使用现代 docker compose,不要继续依赖老的 docker-compose
  2. 新项目省略顶层 version
  3. 使用 compose.yaml 作为基础配置。
  4. 使用 compose.dev.yamlcompose.prod.yaml 做环境覆盖。
  5. 使用 .env.example 给团队提供配置模板。
  6. 使用服务名进行容器间通信,不要在容器里写 127.0.0.1 访问别的服务。
  7. 数据库必须挂载命名卷。
  8. 生产环境不要暴露 MySQL、Redis 等内部端口。
  9. 核心服务配置 healthcheck 和 restart。
  10. 发布前执行 docker compose config 看最终配置。
  11. 日志必须限制大小。
  12. 数据必须有备份和恢复脚本。

Compose 写得好,开发环境和测试环境会非常顺滑;Compose 写得差,也能把一个小项目编排成“分布式精神内耗现场”。所以别只追求能跑,要追求可维护、可迁移、可排错、可恢复。

参考资料

  • Docker Docs:Docker Compose
  • Docker Docs:Compose file reference
  • Docker Docs:Services top-level element
  • Docker Docs:Networks top-level element
  • Docker Docs:Volumes top-level element
  • Docker Docs:Interpolation
  • Docker Docs:Environment variables in Compose
  • Docker Docs:Control startup and shutdown order in Compose
  • Docker Docs:Use profiles in Docker Compose
  • Docker Docs:Use multiple Compose files
  • Docker Docs:Merge Compose files
  • Docker Docs:Use Compose in production
  • Docker Docs:docker compose CLI reference

Docker Compose 深度实战:从编排模型、网络卷、环境变量到生产部署
https://allendericdalexander.github.io/2026/06/07/devops/management/docker-compose-deep-dive-blog/
作者
AtLuoFu
发布于
2026年6月7日
许可协议