Docker Compose 编排实战:三阶段演进搞定微服务本地开发环境
接手一个微服务项目,本地要起 6 个服务 + 数据库 + 缓存 + 消息队列。靠手动
go run和npm 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 配合 nodemon 和 air(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目标但不鼓励日常用。
结果与总结
这套方案上线后:
踩到的坑:
延伸思考

评论已关闭!