4. 环境搭建与基础配置:Docker Compose 搭建开发环境

说实话,很多人在搭建实时数据服务目录时,第一步就卡住了。不是技术多难,而是环境太乱。Java 版本冲突、Python 虚拟环境打架、端口被占用……我见过太多团队花了两天时间,最后发现是本地装了个旧版 Zookeeper 在捣乱。

所以这一章,我带你用 Docker Compose 一把梭。把开发环境做成「可复现的基建」,换台机器也能秒级拉起。

4.1 网络规划:别让容器「失联」

先想清楚一件事:我们的服务目录需要跟哪些组件通信?

  • 元数据存储:MySQL / PostgreSQL,存服务定义和版本历史
  • 实时消息通道:Kafka,接收服务注册和变更事件
  • 缓存层:Redis,扛查询压力
  • 服务发现:Consul / Etcd,做健康检查和动态路由
  • 应用本身:Spring Boot 应用,暴露 REST 和 WebSocket 接口

这么多容器,如果各自用默认网络,IP 乱飞,你连调试都找不到门。我个人的习惯是:创建一个独立的 bridge 网络,所有服务都挂上去。

网络规划要点

  • 使用自定义网络,不要用默认的 bridge
  • 服务名即主机名,容器间通过服务名通信
  • 预留一个管理网络段(如 172.20.0.0/16),避免跟宿主机冲突

为什么强调自定义网络?因为默认 bridge 网络不支持容器间的 DNS 解析。你想想看,如果 Kafka 的地址写死了 IP,容器重启后 IP 变了,整个链路就断了。用服务名通信,Docker 会自动做 DNS 解析,这才是真正的「声明式配置」。

4.2 端口映射:内外有别

端口映射的原则很简单:内部用标准端口,外部按需暴露。

举个例子,Kafka 内部监听 9092,但宿主机上可能已经跑了一个 Kafka 实例。这时候把外部端口映射成 9093,既不影响内部通信,也不跟宿主机冲突。

服务 内部端口 外部映射 说明
MySQL 3306 3307 避免跟本地 MySQL 冲突
Redis 6379 6380 同上
Kafka 9092 9093 外部客户端连接用
应用服务 8080 8080 直接暴露给前端
Consul 8500 8500 管理界面

小技巧:Kafka 的 KAFKA_ADVERTISED_LISTENERS 要配置成宿主机地址,否则外部客户端连不上。我曾经在这上面踩过坑,容器内部通信正常,但本地代码死活连不上 Kafka,折腾了半天才发现是 advertised 地址没改。

4.3 基础镜像准备:选对「地基」

镜像选型直接影响构建速度和运行稳定性。我的建议是:优先用官方镜像,版本锁定到小版本。

为什么?官方镜像经过大量验证,安全补丁也及时。锁定小版本是为了避免「昨天还能跑,今天拉了个新镜像就挂了」的尴尬。

4.3.1 应用镜像

我们的服务目录基于 Spring Boot 3.x,JDK 选 17 LTS。基础镜像我推荐用 eclipse-temurin:17-jre-alpine,体积小、安全更新快。

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY target/service-catalog-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

4.3.2 中间件镜像

中间件全部用官方镜像,版本如下:

  • MySQL: mysql:8.0.32 — 注意 8.0 以上默认使用 caching_sha2_password,客户端驱动要兼容
  • Redis: redis:7.0-alpine — 轻量,够用
  • Kafka: confluentinc/cp-kafka:7.4.0 — Confluent 版带 Kraft 模式,可以不用 Zookeeper
  • Consul: consul:1.16.2 — 服务发现和 KV 存储

注意:Kafka 从 3.x 开始支持 Kraft 模式,不再强依赖 Zookeeper。如果你是新项目,直接用 Kraft 模式,少维护一个组件。我去年帮客户迁移时,从 ZK 模式切到 Kraft,运维复杂度降了一半。

4.4 Docker Compose 编排:把一切串起来

好了,网络、端口、镜像都定好了,接下来用 Docker Compose 把它们编排起来。下面是我实际项目中用的精简版配置:

version: '3.8'

networks:
  catalog-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

services:
  mysql:
    image: mysql:8.0.32
    container_name: catalog-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: service_catalog
    ports:
      - "3307:3306"
    networks:
      - catalog-net
    volumes:
      - mysql-data:/var/lib/mysql

  redis:
    image: redis:7.0-alpine
    container_name: catalog-redis
    ports:
      - "6380:6379"
    networks:
      - catalog-net

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    container_name: catalog-kafka
    environment:
      CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk"
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@localhost:9093"
      KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9093
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    ports:
      - "9093:9092"
    networks:
      - catalog-net

  consul:
    image: consul:1.16.2
    container_name: catalog-consul
    command: agent -dev -client=0.0.0.0
    ports:
      - "8500:8500"
    networks:
      - catalog-net

  app:
    build: .
    container_name: catalog-app
    depends_on:
      - mysql
      - redis
      - kafka
      - consul
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/service_catalog
      SPRING_REDIS_HOST: redis
      SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
      SPRING_CLOUD_CONSUL_HOST: consul
    ports:
      - "8080:8080"
    networks:
      - catalog-net

volumes:
  mysql-data:

这里有几个细节要注意:

  • depends_on 只保证容器启动顺序,不保证服务就绪。建议在应用启动脚本里加健康检查等待逻辑
  • volumes 挂载持久化数据,避免容器重启后数据丢失
  • 环境变量 里用服务名(如 mysql)而不是 IP,Docker DNS 会自动解析

4.5 启动与验证

配置写好了,怎么验证它是对的?我一般分三步走:

  1. 启动所有服务:docker compose up -d,观察日志有没有报错
  2. 检查网络连通性:docker exec catalog-app ping mysql,确认 DNS 解析正常
  3. 验证业务端口:用 curl 或浏览器访问 http://localhost:8080/actuator/health,看返回是否 200

避坑指南:我曾经在 Kafka 启动时遇到 TimeoutException,原因是 KAFKA_ADVERTISED_LISTENERS 配成了容器内部 IP。外部客户端(比如你的本地应用)连不上。记住:外部访问用宿主机 IP,内部通信用服务名。

4.6 整体架构图

下面这张图展示了我们搭建的开发环境全貌。每个组件都在自定义网络里,通过服务名通信,端口按需映射到宿主机。

开发环境容器架构图 catalog-net (172.20.0.0/16) MySQL 8.0 3306 → 3307 Redis 7.0 6379 → 6380 Kafka 7.4 9092 → 9093 Consul 1.16 8500 → 8500 服务目录应用 Spring Boot 3.x + JDK 17 8080 → 8080 说明: 所有容器通过服务名(如 mysql、redis)通信,端口映射仅用于宿主机访问。 应用启动时通过环境变量注入中间件地址,无需硬编码 IP。

4.7 小结

这一章我们做了三件事:规划了网络和端口、选定了基础镜像、用 Docker Compose 把整个环境串了起来。说白了,就是给后面的开发工作搭了个「可复现的台子」。

你可能会问:为什么不用 Kubernetes?嗯,开发环境用 K8s 太重了,Docker Compose 足够。等上了生产,再考虑迁移到 K8s 也不迟。