Docker Compose 编排实战:三阶段演进搞定微服务本地开发环境

2026-05-03 21:46 Docker Compose 编排实战:三阶段演进搞定微服务本地开发环境已关闭评论

Docker Compose 编排实战:三阶段演进搞定微服务本地开发环境

接手一个微服务项目,本地要起 6 个服务 + 数据库 + 缓存 + 消息队列。靠手动 go runnpm start 撑了三天后,我决定彻底切到 Docker Compose。

问题是什么

团队项目有 4 个 Go 后端服务、2 个 Node.js 前端应用,依赖 MySQL、Redis、RabbitMQ。原来的本地开发方式是在各自机器上配 hosts 文件加手动启停。每次有人改了端口或连接配置,全组都得跟着更新 .env

更头疼的是新人 onboarding——要花半天装环境、对版本。

我需要在保证开发体验的前提下,把这些服务用 Docker Compose 编排起来,实现 docker compose up 一行搞定。

解决思路

我评估了三种方案:

方案 优点 缺点
纯 Docker Compose 零额外依赖,学习成本低 热重载需要额外配置
Tilt 自动热重载,K8s 友好 引入新工具,团队需要学习
Skaffold Google 出品,CI/CD 集成好 偏部署场景,本地太重

最终选了 Docker Compose + 多阶段构建 + 命名卷。理由:团队没人用过 Tilt,学习成本不可接受。纯 Docker Compose 配合 nodemonair(Go 热重载)可以解决开发体验问题。后期真要上 K8s,再引入 Tilt 不迟。

操作步骤

步骤 1:设计镜像分层策略

核心思路:基础镜像层不动,依赖层按需缓存,源码层最后加载

Go 服务用多阶段构建分离编译和运行环境:

# backend/user-service/Dockerfile
# === 构建阶段 ===
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server

# === 运行阶段 ===
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

前端 Node.js 服务也一样,把 package.json 和源码分层拷贝,利用 Docker 的 layer cache:

# frontend/admin-panel/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

开发环境用 npm ci 而非 npm install,保证团队成员依赖版本完全一致,杜绝"我本地能跑"的经典问题。

步骤 2:编写 docker-compose.yml

这是最核心的一步。我把服务按依赖关系分层编排:

# docker-compose.yml
version: "3.8"

volumes:
  mysql_data:
  redis_data:
  rabbitmq_data:

networks:
  app-net:
    driver: bridge

services:
  # === 基础设施层 ===
  mysql:
    image: mysql:8.0
    container_name: app-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: app_dev
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./infra/mysql/init:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - app-net

  redis:
    image: redis:7-alpine
    container_name: app-redis
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks:
      - app-net

  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: app-rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-net

  # === 业务服务层 ===
  user-service:
    build:
      context: ./backend/user-service
      dockerfile: Dockerfile
    container_name: app-user-svc
    ports:
      - "8080:8080"
    environment:
      DB_DSN: "root:root123@tcp(mysql:3306)/app_dev?charset=utf8mb4&parseTime=True"
      REDIS_ADDR: redis:6379
      RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - app-net

  order-service:
    build:
      context: ./backend/order-service
      dockerfile: Dockerfile
    container_name: app-order-svc
    ports:
      - "8081:8080"
    environment:
      DB_DSN: "root:root123@tcp(mysql:3306)/app_dev?charset=utf8mb4&parseTime=True"
      REDIS_ADDR: redis:6379
      RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-net

  admin-panel:
    build:
      context: ./frontend/admin-panel
      dockerfile: Dockerfile
    container_name: app-admin
    ports:
      - "3000:3000"
    environment:
      API_BASE_URL: http://user-service:8080
    depends_on:
      - user-service
    networks:
      - app-net

注意这里用 condition: service_healthy 而不是裸写 depends_on。前者确保数据库真正就绪后才启动业务服务,后者只保证容器启动了——MySQL 启动到可接受连接通常有 10-20 秒窗口期,这是"编排"最容易踩的坑。

步骤 3:配置开发模式的热重载

生产环境用编译后的二进制,开发环境要改代码即生效。不同语言我配了不同的热重载方案。

Go 服务用 air

# backend/user-service/Dockerfile.dev
FROM golang:1.22-alpine
WORKDIR /app
RUN go install github.com/cosmtrek/air@latest
CMD ["air", "-c", ".air.toml"]

再加一个开发环境的 docker-compose.override.yml,挂载源码卷并覆盖 entrypoint:

# docker-compose.override.yml
services:
  user-service:
    build:
      context: ./backend/user-service
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend/user-service:/app
      - /app/tmp

  order-service:
    build:
      context: ./backend/order-service
      dockerfile: Dockerfile.dev
    volumes:
      - ./backend/order-service:/app
      - /app/tmp

  admin-panel:
    build:
      context: ./frontend/admin-panel
      dockerfile: Dockerfile
    volumes:
      - ./frontend/admin-panel:/app
      - /app/node_modules
    command: ["npx", "nodemon", "--watch", ".", "--ext", "js,hbs,css"]

有个坑:Node.js 的 node_modules 如果不挂载匿名卷排除,容器内的依赖会被宿主机的空目录覆盖,启动直接报 module not found。用 - /app/node_modules 声明匿名卷可以绕过去。

步骤 4:日志和调试配置

所有服务的日志统一走 docker logs。但挨个 docker logs -f 太原始,我加了个日志收集的 sidecar:

# 在 docker-compose.yml 的 services 中添加
  loki:
    image: grafana/loki:2.9
    ports:
      - "3100:3100"
    networks:
      - app-net

  promtail:
    image: grafana/promtail:2.9
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./infra/promtail/config.yml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    depends_on:
      - loki
    networks:
      - app-net

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      GF_AUTH_ANONYMOUS_ENABLED: "true"
    depends_on:
      - loki
    networks:
      - app-net

浏览器打开 http://localhost:3001,通过 Grafana + Loki 就能搜索所有服务的日志,不用再挨个 docker logs -f 了。

步骤 5:一键启动脚本

最后封装一个 Makefile 把常用操作收敛起来:

.PHONY: up down build logs clean

up:
	docker compose up -d
	@echo "所有服务已启动,访问:"
	@echo "  Admin Panel: http://localhost:3000"
	@echo "  Grafana Logs: http://localhost:3001"
	@echo "  RabbitMQ UI: http://localhost:15672 (guest/guest)"

down:
	docker compose down

build:
	docker compose build --no-cache

logs:
	docker compose logs -f

reset:
	docker compose down -v
	docker compose up -d

dev:
	docker compose -f docker-compose.yml -f docker-compose.override.yml up -d

down -v 会删除所有命名卷,包括数据库数据。我加了 reset 目标但不鼓励日常用。

结果与总结

这套方案上线后:

  • **新人 onboarding 从半天缩短到 15 分钟**:装好 Docker Desktop,`git clone` + `make up`,搞定
  • **跨服务调试不再需要 hosts 文件**:所有服务通过容器名互访,`user-service:8080` 在任何机器上都是同一个地址
  • **"我本地能跑" 的问题减少 90%**:依赖环境完全容器化,差异只剩代码版本
  • 踩到的坑:

  • **MySQL 8 的认证插件**:默认 `caching_sha2_password`,老项目用的 `mysql_native_password`,得在环境变量加 `MYSQL_ROOT_HOST: '%'` 或改连接串参数
  • **文件权限**:Go 编译出来的二进制在 Alpine 里跑没问题,但宿主机和容器用户 ID 不一致时,挂载卷的文件权限会乱。解决办法是在 Dockerfile 里 `RUN adduser -D appuser` 并 `USER appuser`
  • **macOS 文件共享性能**:代码挂载到容器后,macOS 上 IO 性能很差(特别是 node_modules)。用 `:delegated` 挂载模式能缓解,更好的方案是前端用 webpack-dev-server 的磁盘缓存
  • 延伸思考

  • **资源占用**:6 个服务 + 3 个中间件 + Grafana 全家桶,在 16GB 内存的 MacBook 上大约吃 6-8GB。可以考虑用 `profiles` 把 Grafana/Loki 设为可选,日常开发不启动
  • **CI/CD 复用**:这套 `docker-compose.yml` 稍作修改就能在 CI 里当集成测试环境用——去掉端口映射,加一个 `test` service 跑 `go test ./...`
  • **下一步**:如果服务继续膨胀到 10+,我会考虑引入 Tilt 管理开发环境,或者转 K8s + Skaffold 做更细粒度的资源控制
  • **调试痛点**:断点调试在容器里比较麻烦。GoLand / VS Code 都支持 Remote Container 调试,但配置成本不低。取巧的办法是保留一个非容器化的本地启动方式,只在需要断点调试时用
  • 你可能感兴趣的文章

    来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
    转载请注明出处: https://teachcourse.cn/4057.html ,谢谢支持!

    资源分享

    分类:Android 标签:
    Python库flask-mail使用完整示例 Python库flask-mail使用完整示例
    如何给WordPress长文章添加分页功能 如何给WordPress长文章添加分页
    Claude Code Skill 技能使用指南 Claude Code Skill 技能使用指
    新版本ADT创建Android项目无法自动生成R文件解决办法 新版本ADT创建Android项目无

    评论已关闭!