前言

上一篇我们学会了用 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、.dockerignorenginx.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 网络原理——容器之间怎么通信,bridgehostoverlay 有什么区别 🚀


📌 觉得有帮助点个赞!有问题欢迎评论区交流 👇

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐