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 也支持 configs 和 secrets,用于挂载配置和敏感文件。

不过要注意:在普通单机 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-compose 和 docker 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

这表示 mysql 和 redis 会先创建,再创建 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

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

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

exec 和 run 的区别:

命令 作用
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. environment 和 env_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 在内部网络中通过 mysql、redis 访问即可。

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.yaml、compose.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日
许可协议