纲要
- 引言:微服务容器化的挑战与
Docker Compose的定位 - 容器网络模式
none模式bridge桥接模式host模式
- 容器间通信的三种方式
- 宿主机端口绑定
- 同一桥接网络下的 IP 直连
--link与容器名解析
- 实战:手动部署
go-zero微服务- 项目结构
- 编写
Dockerfile - 启动
ETCD - 启动
API与RPC服务 - 常见问题与日志诊断
- 使用
Docker Compose统一编排- 安装
Docker Compose - 编写
docker-compose.yml - 自定义网络与固定 IP
- 服务依赖与健康检查
- 一键启动、停止与清理
- 安装
- 小结
引言
在微服务架构中,一个应用通常由多个独立服务组成,例如 API 网关、RPC 服务、配置中心 ETCD、缓存 Redis、数据库 MySQL 等。如果每个服务都手动通过 docker run 启动,并逐个管理网络、端口、依赖关系,不仅效率低下,而且极易出错。Docker Compose 正是为解决这一问题而生------它允许通过一个 YAML 文件定义所有服务、网络和卷,实现"一键编排"。
本文将以 go-zero 框架生成的微服务为例,从容器网络原理入手,逐步演示如何手动部署多个容器,并最终迁移到 Docker Compose 进行统一管理。文中的 go-zero 代码均基于 go-zero@latest 生成,确保与最新版本兼容。
容器网络模式
理解 Docker 网络是进行多容器通信的前提。Docker 提供三种基本网络模式:
-
none模式容器拥有独立的网络栈,但
Docker不会为其分配 IP、网卡等网络资源。此时容器完全隔离,无法与外界通信,适用于无需网络的特殊场景。 -
bridge桥接模式 (默认)Docker在宿主机上创建一个虚拟网桥(通常为docker0),所有未指定网络的容器默认接入该网桥。每个容器获得一个私有 IP,宿主机通过 NAT 与容器通信。这是开发中最常用的模式。下图展示了桥接网络的工作原理:
#mermaid-svg-MVBpXRX4LAV2nsVH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MVBpXRX4LAV2nsVH .error-icon{fill:#552222;}#mermaid-svg-MVBpXRX4LAV2nsVH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MVBpXRX4LAV2nsVH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MVBpXRX4LAV2nsVH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MVBpXRX4LAV2nsVH .marker.cross{stroke:#333333;}#mermaid-svg-MVBpXRX4LAV2nsVH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MVBpXRX4LAV2nsVH p{margin:0;}#mermaid-svg-MVBpXRX4LAV2nsVH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster-label text{fill:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster-label span{color:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster-label span p{background-color:transparent;}#mermaid-svg-MVBpXRX4LAV2nsVH .label text,#mermaid-svg-MVBpXRX4LAV2nsVH span{fill:#333;color:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH .node rect,#mermaid-svg-MVBpXRX4LAV2nsVH .node circle,#mermaid-svg-MVBpXRX4LAV2nsVH .node ellipse,#mermaid-svg-MVBpXRX4LAV2nsVH .node polygon,#mermaid-svg-MVBpXRX4LAV2nsVH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MVBpXRX4LAV2nsVH .rough-node .label text,#mermaid-svg-MVBpXRX4LAV2nsVH .node .label text,#mermaid-svg-MVBpXRX4LAV2nsVH .image-shape .label,#mermaid-svg-MVBpXRX4LAV2nsVH .icon-shape .label{text-anchor:middle;}#mermaid-svg-MVBpXRX4LAV2nsVH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MVBpXRX4LAV2nsVH .rough-node .label,#mermaid-svg-MVBpXRX4LAV2nsVH .node .label,#mermaid-svg-MVBpXRX4LAV2nsVH .image-shape .label,#mermaid-svg-MVBpXRX4LAV2nsVH .icon-shape .label{text-align:center;}#mermaid-svg-MVBpXRX4LAV2nsVH .node.clickable{cursor:pointer;}#mermaid-svg-MVBpXRX4LAV2nsVH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MVBpXRX4LAV2nsVH .arrowheadPath{fill:#333333;}#mermaid-svg-MVBpXRX4LAV2nsVH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MVBpXRX4LAV2nsVH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MVBpXRX4LAV2nsVH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MVBpXRX4LAV2nsVH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MVBpXRX4LAV2nsVH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MVBpXRX4LAV2nsVH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster text{fill:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH .cluster span{color:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MVBpXRX4LAV2nsVH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MVBpXRX4LAV2nsVH rect.text{fill:none;stroke-width:0;}#mermaid-svg-MVBpXRX4LAV2nsVH .icon-shape,#mermaid-svg-MVBpXRX4LAV2nsVH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MVBpXRX4LAV2nsVH .icon-shape p,#mermaid-svg-MVBpXRX4LAV2nsVH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MVBpXRX4LAV2nsVH .icon-shape .label rect,#mermaid-svg-MVBpXRX4LAV2nsVH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MVBpXRX4LAV2nsVH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MVBpXRX4LAV2nsVH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MVBpXRX4LAV2nsVH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NAT
端口映射 8080:8080
端口映射 6379:6379
宿主机网卡 eth0
docker0 虚拟网桥
容器1: 172.17.0.2
容器2: 172.17.0.3
host模式
容器直接使用宿主机的网络栈,共享宿主机的 IP 和端口。这意味着容器内监听的端口会直接暴露在宿主机上,省略了端口映射的步骤。但该模式容易与宿主机其他程序发生端口冲突,不推荐在生产环境滥用。
容器间通信的三种方式
假设宿主机上运行了两个容器:go-zero-api(提供 HTTP 服务)和 redis(缓存),我们需要让它们互相访问。
#mermaid-svg-D8A8tIFQuktst1KE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D8A8tIFQuktst1KE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D8A8tIFQuktst1KE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D8A8tIFQuktst1KE .error-icon{fill:#552222;}#mermaid-svg-D8A8tIFQuktst1KE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D8A8tIFQuktst1KE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D8A8tIFQuktst1KE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D8A8tIFQuktst1KE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D8A8tIFQuktst1KE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D8A8tIFQuktst1KE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D8A8tIFQuktst1KE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D8A8tIFQuktst1KE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D8A8tIFQuktst1KE .marker.cross{stroke:#333333;}#mermaid-svg-D8A8tIFQuktst1KE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D8A8tIFQuktst1KE p{margin:0;}#mermaid-svg-D8A8tIFQuktst1KE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D8A8tIFQuktst1KE .cluster-label text{fill:#333;}#mermaid-svg-D8A8tIFQuktst1KE .cluster-label span{color:#333;}#mermaid-svg-D8A8tIFQuktst1KE .cluster-label span p{background-color:transparent;}#mermaid-svg-D8A8tIFQuktst1KE .label text,#mermaid-svg-D8A8tIFQuktst1KE span{fill:#333;color:#333;}#mermaid-svg-D8A8tIFQuktst1KE .node rect,#mermaid-svg-D8A8tIFQuktst1KE .node circle,#mermaid-svg-D8A8tIFQuktst1KE .node ellipse,#mermaid-svg-D8A8tIFQuktst1KE .node polygon,#mermaid-svg-D8A8tIFQuktst1KE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D8A8tIFQuktst1KE .rough-node .label text,#mermaid-svg-D8A8tIFQuktst1KE .node .label text,#mermaid-svg-D8A8tIFQuktst1KE .image-shape .label,#mermaid-svg-D8A8tIFQuktst1KE .icon-shape .label{text-anchor:middle;}#mermaid-svg-D8A8tIFQuktst1KE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D8A8tIFQuktst1KE .rough-node .label,#mermaid-svg-D8A8tIFQuktst1KE .node .label,#mermaid-svg-D8A8tIFQuktst1KE .image-shape .label,#mermaid-svg-D8A8tIFQuktst1KE .icon-shape .label{text-align:center;}#mermaid-svg-D8A8tIFQuktst1KE .node.clickable{cursor:pointer;}#mermaid-svg-D8A8tIFQuktst1KE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D8A8tIFQuktst1KE .arrowheadPath{fill:#333333;}#mermaid-svg-D8A8tIFQuktst1KE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D8A8tIFQuktst1KE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D8A8tIFQuktst1KE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8A8tIFQuktst1KE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D8A8tIFQuktst1KE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8A8tIFQuktst1KE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D8A8tIFQuktst1KE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D8A8tIFQuktst1KE .cluster text{fill:#333;}#mermaid-svg-D8A8tIFQuktst1KE .cluster span{color:#333;}#mermaid-svg-D8A8tIFQuktst1KE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D8A8tIFQuktst1KE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D8A8tIFQuktst1KE rect.text{fill:none;stroke-width:0;}#mermaid-svg-D8A8tIFQuktst1KE .icon-shape,#mermaid-svg-D8A8tIFQuktst1KE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D8A8tIFQuktst1KE .icon-shape p,#mermaid-svg-D8A8tIFQuktst1KE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D8A8tIFQuktst1KE .icon-shape .label rect,#mermaid-svg-D8A8tIFQuktst1KE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D8A8tIFQuktst1KE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D8A8tIFQuktst1KE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D8A8tIFQuktst1KE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Host
映射
宿主机端口 8080
API 容器
Redis 容器
方式一:通过宿主机端口绑定访问
将 go-zero-api 的 8080 端口映射到宿主机,redis 容器通过访问宿主机 IP 加端口的方式间接调用 API。
示例:
bash
# Redis 容器内执行
curl http://192.168.1.100:8080/api/login
该方式简单直接,但调用方必须知道宿主机的 IP 和映射端口,耦合度较高。
方式二:基于同一桥接网络 IP 直连
当两个容器处于同一个 bridge 网络时,可以直接使用对方的私有 IP 进行通信。
查看网络信息:
bash
docker network inspect bridge
输出会显示每个容器分配的 IP,例如 go-zero-api 为 172.17.0.2,redis 为 172.17.0.3。
在 redis 容器中直接访问:
bash
curl http://172.17.0.2:8080/api/login
这种方式依赖固定 IP,但在容器重启后 IP 可能发生变化。
方式三:通过 --link 实现容器名解析
在启动容器时,使用 --link 参数可以将目标容器的名称或 ID 注入到当前容器的 /etc/hosts 中,从而实现"通过容器名访问"。
例如,先启动 redis,再启动 api 并链接到 redis:
bash
docker run -d --name redis redis:alpine
docker run -d -p 8080:8080 --name go-zero-api --link redis go-zero-api:latest
此时在 go-zero-api 容器内部,可以直接 ping redis 并解析到 redis 容器的 IP。注意 --link 是单向的,且官方已推荐使用自定义网络替代。
实战:手动部署 go-zero 微服务
假设我们已经用 go-zero@latest 生成了一个简单的微服务项目,包含 api 服务和 rpc 服务,它们都依赖 ETCD 作为服务注册中心。
项目结构概览:
dir
demo/
├── api/
│ ├── Dockerfile
│ └── api.go
├── rpc/
│ ├── Dockerfile
│ └── rpc.go
└── etcd/
└── Dockerfile
为了使部署简单化,我们只保留核心通信逻辑:api 通过 ETCD 发现 rpc 服务并调用它。为了纯容器化,暂时注释掉数据库等非必要依赖。
编写 API 服务的 Dockerfile:
dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o api-server api.go
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/api-server .
COPY etc/api.yaml .
EXPOSE 8080
CMD ["./api-server"]
编写 RPC 服务的 Dockerfile(与 API 类似,仅服务名不同):
dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o rpc-server rpc.go
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/rpc-server .
COPY etc/rpc.yaml .
EXPOSE 8081
CMD ["./rpc-server"]
构建镜像:
bash
docker build -t go-zero-api -f api/Dockerfile .
docker build -t go-zero-rpc -f rpc/Dockerfile .
启动 ETCD 容器
首先确保 ETCD 正常运行,因为它是服务注册发现的基础。
bash
docker build -t etcd -f etcd/Dockerfile .
docker run -d --name etcd \
-p 2379:2379 -p 2380:2380 \
--network mynet --ip 192.168.10.20 \
etcd
这里我们事先创建了一个自定义网络 mynet:
bash
docker network create --subnet=192.168.10.0/24 mynet
启动 RPC 服务,并指定 ETCD 地址:
bash
docker run -d --name go-zero-rpc \
--network mynet \
-e ETCD_HOST=192.168.10.20:2379 \
go-zero-rpc
启动 API 服务:
bash
docker run -d --name go-zero-api \
-p 8080:8080 \
--network mynet \
-e ETCD_HOST=192.168.10.20:2379 \
go-zero-api
如果容器启动后立即退出,或调用时报错,务必查看日志定位问题:
bash
docker logs go-zero-api
常见错误包括 ETCD 地址配置错误、端口冲突或环境变量未正确传递。通过日志可以快速发现究竟是连接被拒绝还是配置项缺失。
使用 Docker Compose 统一编排
手动运行虽能工作,但每次修改配置都需要停止容器、删除、重新构建镜像、再启动,过程繁琐。Docker Compose 允许将上述所有步骤固化在一个 YAML 文件中。
安装 Docker Compose(以 Linux 为例):
bash
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
编写 docker-compose.yml
在项目根目录创建该文件,内容如下(根据实际环境调整版本号和镜像名称):
yaml
version: '3.8'
networks:
go-zero-net:
driver: bridge
ipam:
config:
- subnet: 192.168.10.0/24
services:
etcd:
image: bitnami/etcd:latest
container_name: etcd
ports:
- "2379:2379"
- "2380:2380"
networks:
go-zero-net:
ipv4_address: 192.168.10.20
environment:
- ALLOW_NONE_AUTHENTICATION=yes
volumes:
- ./etcd/data:/bitnami/etcd
go-zero-rpc:
build:
context: ./rpc
dockerfile: Dockerfile
image: go-zero-rpc:latest
container_name: go-zero-rpc
networks:
- go-zero-net
environment:
- ETCD_HOST=192.168.10.20:2379
depends_on:
- etcd
go-zero-api:
build:
context: ./api
dockerfile: Dockerfile
image: go-zero-api:latest
container_name: go-zero-api
ports:
- "8080:8080"
networks:
- go-zero-net
environment:
- ETCD_HOST=192.168.10.20:2379
depends_on:
- etcd
- go-zero-rpc
上述配置做了几件重要的事:
- 自定义网络 :
go-zero-net使用固定的子网,并为etcd分配了静态 IP,rpc和api会自动分配剩余地址。 - 服务依赖 :
depends_on确保etcd先于其他服务启动。但对于需要"等待服务就绪"的场景,还需配合healthcheck,这里简单起见未展开。 - 构建策略 :若本地不存在对应镜像,
docker-compose up会自动根据build指令构建镜像,避免手动执行docker build。 - 数据卷 :
etcd的数据目录映射到宿主机,防止数据丢失。
启动所有服务:
bash
docker-compose up -d
该命令会以后台方式启动所有定义的服务。使用 docker-compose ps 可以查看状态。
查看服务日志:
bash
docker-compose logs -f go-zero-api
停止并清理所有资源:
bash
docker-compose down
这条命令会停止容器、删除容器,同时移除自动创建的网络,彻底还原环境。
小结
本文从 Docker 网络的基础原理出发,介绍了容器通信的三种模式,并以 go-zero 微服务为例,演示了从手动部署到 Docker Compose 统一编排的演进过程。Docker Compose 通过声明式的配置文件,极大简化了多容器应用的管理,特别是对于服务依赖、网络划分、环境变量注入等高频操作,它提供了清晰且可复用的解决方案。
在实际开发中,建议将 docker-compose.yml 与源代码一同纳入版本控制,这样任何团队成员都能一键拉起完整的开发环境。后续还可以结合 Docker Swarm 或 Kubernetes 进行生产级编排,但 Docker Compose 依然是本地开发和小规模部署的首选工具。