[特殊字符] Docker Compose 实战:从核心概念到真实微服务项目全解析
前言
上一篇我们学会了用 docker run 跑单个容器。但真实项目哪有只跑一个容器的——你需要后端服务、数据库、缓存、消息队列……难道每个都手动 docker run 一遍?
这就是 Docker Compose 存在的意义。
本篇将结合一个真实的 Go 微服务项目进行讲解,建议拉取源码对照学习。
第一步:克隆项目
bash
git clone https://github.com/twitter-learn-cloud-development/backend-services.git
cd backend-services
```
**第二步:创建 `.env` 文件**
项目根目录下新建 `.env` 文件,内容如下:
```
DB_PASSWORD=你的数据库密码
⚠️ 这一步不能跳过。
docker-compose.yaml里 MySQL 的密码通过${DB_PASSWORD}从这个文件读取,如果没有.env或者值为空,MySQL 容器启动会直接报错导致所有依赖它的服务全部无法启动。
第三步:一键启动
bash
# 第一次启动,需要构建镜像
docker compose up --build -d
# 之后代码没有改动,直接启动即可
docker compose up -d
项目文件结构说明:
- 后端各服务的 Dockerfile 均在
deploy/docker/目录下 - 后端服务的
.dockerignore在项目根目录 - 前端的 Dockerfile、
.dockerignore、nginx.conf均在web/目录下 docker-compose.yaml在项目根目录
一、Docker Compose 是什么
Docker Compose 是一个用于定义和运行多容器应用的工具。你只需要写一个 docker-compose.yaml 文件,描述清楚你需要哪些服务、怎么配置、启动顺序是什么,然后一行命令全部拉起来:
bash
docker compose up -d
想关掉所有服务也只需要一行:
bash
docker compose down
二、核心标签逐一讲解
services
整个文件的核心,每个子项就是一个容器服务:
yaml
services:
mysql: # 服务名,自己起
...
redis:
...
image vs build
两种方式指定镜像来源:
yaml
# 方式一:直接用现成镜像(官方的 mysql、redis 等)
image: mysql:8.0
# 方式二:用 Dockerfile 自己构建(自己写的后端/前端服务)
build:
context: . # 构建上下文目录
dockerfile: deploy/docker/user.dockerfile # Dockerfile 路径
注意 mysql:8.0 后面的 :8.0 就是版本号——锁定版本非常重要。不写版本默认拉 latest,今天能跑,三个月后同事拉到新版本可能就挂了。这就是为什么公司项目必须写死版本。
ports
端口映射,格式是 宿主机端口:容器端口:
yaml
ports:
- "3307:3306" # 宿主机 3307 → 容器内 MySQL 的 3306
- "5173:5556" # 宿主机 5173 → 容器内 nginx 的 5556
右边的容器端口必须和服务实际监听的端口一致,左边宿主机端口可以随意,只要不冲突。
volumes
数据挂载,解决容器删了数据也跟着消失的问题:
yaml
volumes:
- mysql_data:/var/lib/mysql # 具名卷,数据存在宿主机,容器重建数据还在
- ./uploads:/app/uploads # 绑定挂载,直接映射宿主机目录
environment
向容器注入环境变量:
yaml
environment:
- DB_HOST=mysql # 直接写服务名,Compose 内部自动 DNS 解析
- DB_PASSWORD=${DB_PASSWORD} # 从 .env 文件读取,敏感信息不硬编码
同一个 Compose 文件里的服务之间可以直接用服务名互相访问,Docker 内部帮你做了 DNS 解析,所以 DB_HOST=mysql 就能找到 mysql 容器。
depends_on
控制启动顺序:
yaml
depends_on:
mysql:
condition: service_healthy # 等 mysql 健康检查通过才启动
redis:
condition: service_healthy
不加 condition 只保证容器启动了,但进程不一定初始化完毕。加 service_healthy 才是真正等服务可用。
healthcheck
定义怎么检测服务是否健康:
yaml
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s # 每 5 秒检查一次
timeout: 5s # 单次超时
retries: 10 # 连续失败 10 次才判定不健康
restart
容器异常退出后的重启策略:
| restart 值 | 含义 |
|---|---|
no |
不自动重启(默认) |
on-failure |
异常退出才重启 |
always |
无论何种原因退出都重启 |
yaml
restart: on-failure # 异常退出自动重启,正常退出不重启
三、Dockerfile 最佳实践:多阶段构建
项目里所有服务的 Dockerfile 都用了多阶段构建,以前端为例:
dockerfile
# ===== 第一阶段:构建 =====
FROM node:22-alpine AS builder # 用完整的 Node 编译环境,命名为 builder
WORKDIR /app
RUN npm install -g pnpm # 安装 pnpm 包管理器
COPY package.json pnpm-lock.yaml ./
RUN pnpm install # 单独安装依赖——这层会被 Docker 缓存
# 只要依赖没变,下次构建直接跳过,大幅提速
COPY . .
RUN pnpm run build # 打包出静态文件到 /app/dist
# ===== 第二阶段:运行 =====
FROM nginx:alpine # 只用极简的 nginx,不需要 Node 环境
COPY --from=builder /app/dist /usr/share/nginx/html # 只复制构建产物
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 5556
CMD ["nginx", "-g", "daemon off;"]
为什么要分两阶段?
node:alpine 镜像约 180MB,nginx:alpine 只有约 40MB。分两阶段后最终镜像只包含静态文件 + nginx,体积极小,构建产物也更干净。
Go 后端服务同理,golang:alpine 约 300MB,编译完只需要一个几MB的二进制文件,最终镜像用 alpine:latest 承载即可。
四、前端 nginx.conf 说明
nginx
server {
listen 5556; # nginx 监听 5556,对应 docker-compose ports 右边的容器端口
location / {
root /usr/share/nginx/html; # 静态文件目录,就是 dist 打包产物
index index.html;
try_files $uri $uri/ /index.html; # 关键:前端路由刷新不 404
# 找不到文件就返回 index.html,让 Vue Router 接管
}
location /api/ {
proxy_pass http://gateway:9638; # /api/ 开头的请求转发到后端网关
# gateway 是服务名,Compose 内部直接解析
}
}
五、完整注释版 docker-compose.yml
yaml
version 字段是 Docker Compose V1 时代的产物,用于声明 Compose 文件格式版本。从 Docker Compose V2(2020 年起)开始引入 Compose Specification 统一规范后,该字段已被标记为废弃——写不写都能运行,但保留它会看到警告:the attribute version is obsolete。看到这条警告直接忽略即可,不影响使用。建议新项目直接删掉这行。
version: '3.8'
services:
# ==================== 基础设施 ====================
mysql:
image: mysql:8.0 # 锁定版本,保证所有人环境一致
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} # 密码从 .env 读取,不硬编码
MYSQL_DATABASE: twitter # 自动创建数据库
ports:
- "3307:3306" # 宿主机用 3307,避免和本机 MySQL 冲突
volumes:
- mysql_data:/var/lib/mysql # 数据持久化,容器删了数据还在
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10 # MySQL 启动慢,多重试几次
redis:
image: redis:alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
rabbitmq:
image: rabbitmq:3-management # management 版本自带 Web 管理界面
ports:
- "5672:5672" # AMQP 协议端口,服务间通信用
- "15672:15672" # Web 管理界面,浏览器可直接查看队列状态
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_running"]
interval: 10s
timeout: 5s
retries: 10
consul:
image: consul:1.15 # 服务注册与发现,微服务互相找到对方靠它
command: "agent -dev -client=0.0.0.0" # dev 模式,单节点开发用
ports:
- "8500:8500" # HTTP API + Web UI
- "8600:8600/udp" # DNS 端口
jaeger:
image: jaegertracing/all-in-one:1.37 # 链路追踪,可以看每个请求经过哪些服务
ports:
- "16686:16686" # Jaeger Web UI
- "6831:6831/udp" # 接收 span 数据
environment:
- COLLECTOR_OTLP_ENABLED=true # 开启 OpenTelemetry 协议支持
prometheus:
image: prom/prometheus:v2.45.0
ports:
- "9090:9090"
volumes:
- ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml # 挂载采集配置
- prometheus_data:/prometheus # 监控数据持久化
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
grafana:
image: grafana/grafana:10.0.0 # 监控数据可视化面板
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin # 初始密码,生产环境记得修改
depends_on:
- prometheus
# ==================== 业务服务 ====================
user-service:
build:
context: .
dockerfile: deploy/docker/user.dockerfile # 用 Dockerfile 自己构建镜像
environment:
- DB_HOST=mysql # 直接写服务名,Compose 内部自动 DNS 解析
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- SERVICE_NAME=user-service
- SERVICE_ADDR=user-service
- SERVICE_PORT=9091
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
- JWT_SECRET=twitter_clone_secret_key_2023
# ports 注释掉——业务服务不直接对外暴露
# 外部请求统一走 gateway,服务间通过服务名直接通信
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
tweet-service:
build:
context: .
dockerfile: deploy/docker/tweet.dockerfile
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- MQ_HOST=rabbitmq # 发推需要通过消息队列异步更新 timeline
- MQ_PORT=5672
- MQ_USER=guest
- MQ_PASSWORD=guest
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- SERVICE_NAME=tweet-service
- SERVICE_PORT=9092
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy # 依赖 MQ,必须等 RabbitMQ 健康
follow-service:
build:
context: .
dockerfile: deploy/docker/follow.dockerfile
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- MQ_HOST=rabbitmq
- MQ_PORT=5672
- MQ_USER=guest
- MQ_PASSWORD=guest
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- SERVICE_NAME=follow-service
- SERVICE_PORT=9093
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
consumer:
build:
context: .
dockerfile: deploy/docker/consumer.dockerfile
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- MQ_HOST=rabbitmq
- MQ_PORT=5672
- MQ_USER=guest
- MQ_PASSWORD=guest
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
# 没有 ports——纯后台消费者,消费 MQ 消息更新 timeline,不对外提供接口
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
notification-service:
build:
context: .
dockerfile: deploy/docker/notification.dockerfile
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- MQ_HOST=rabbitmq
- MQ_PORT=5672
- MQ_USER=guest
- MQ_PASSWORD=guest
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- SERVICE_NAME=notification-service
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
# 同 consumer,纯后台 worker,不暴露端口
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
messenger-service:
build:
context: .
dockerfile: deploy/docker/messenger.dockerfile
environment:
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- REDIS_HOST=redis
- REDIS_PORT=6379
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- SERVICE_NAME=messenger-service
- SERVICE_PORT=9094
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
restart: on-failure
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
# ==================== 网关 ====================
gateway:
build:
context: .
dockerfile: deploy/docker/gateway.dockerfile
environment:
- GATEWAY_PORT=9638
- USER_SERVICE_ADDR=user-service:9091 # 服务名:端口,直接访问内部服务
- TWEET_SERVICE_ADDR=tweet-service:9092
- FOLLOW_SERVICE_ADDR=follow-service:9093
- MESSENGER_SERVICE_ADDR=messenger-service:9094
- REDIS_HOST=redis
- REDIS_PORT=6379
- DB_HOST=mysql
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=twitter
- CONSUL_HOST=consul
- CONSUL_PORT=8500
- JAEGER_COLLECTOR_ENDPOINT=http://jaeger:14268/api/traces
- JWT_SECRET=twitter_clone_secret_key_2023
ports:
- "9638:9638" # 只有 gateway 对外暴露,是整个后端系统唯一入口
restart: on-failure
depends_on:
- user-service
- tweet-service
- follow-service
- notification-service
volumes:
- ./uploads:/app/uploads # 用户上传文件持久化
sentinel:
image: bladex/sentinel-dashboard # 流量控制 + 熔断降级可视化面板
ports:
- "8858:8858"
restart: always
# ==================== 前端 ====================
frontend:
build:
context: ./web # 前端项目目录
dockerfile: Dockerfile
ports:
- "5173:5556" # 宿主机 5173 → 容器内 nginx 监听的 5556
depends_on:
- gateway # 等 gateway 启动后再起前端
# ==================== 数据卷声明 ====================
volumes:
mysql_data: # MySQL 数据持久化
prometheus_data: # Prometheus 监控数据持久化
六、一键启动
在项目根目录创建 .env 文件:
bash
DB_PASSWORD=your_password
然后一行命令启动所有服务(注意需要到docker-compose.yaml文件所在目录下使用启动命令):
bash
# 第一次启动(需要构建镜像)
docker compose up --build -d
# 之后代码无改动直接启动
docker compose up -d
启动后可以访问:
| 服务 | 地址 |
|---|---|
| 前端页面 | http://localhost:5173 |
| API 网关 | http://localhost:9638 |
| RabbitMQ 管理界面 | http://localhost:15672 |
| Prometheus | http://localhost:9090 |
| Grafana 监控面板 | http://localhost:3000 |
| Jaeger 链路追踪 | http://localhost:16686 |
| Consul 服务注册 | http://localhost:8500 |
| Sentinel 熔断面板 | http://localhost:8858 |
总结
| 标签 | 作用 |
|---|---|
image |
指定现成镜像,记得锁版本 |
build |
用 Dockerfile 自己构建 |
ports |
宿主机端口:容器端口,右边必须和服务监听端口一致 |
volumes |
数据持久化 |
environment |
注入配置,敏感信息用 .env |
depends_on |
控制启动顺序,加 healthcheck 才稳 |
restart |
异常自动重启 |
完整项目源码:https://github.com/twitter-learn-cloud-development/backend-services
下一篇:Docker 网络原理——容器之间怎么通信,bridge、host、overlay 有什么区别 🚀
📌 觉得有帮助点个赞!有问题欢迎评论区交流 👇
更多推荐



所有评论(0)