1. 概述

在使用 Docker 的过程中,我们通常会构建多个容器,这些容器通过隔离的网络进行通信。但在某些场景下,我们希望这些容器运行在同一台主机上并能相互通信,例如在进行集成测试时。Docker 提供了多种网络配置方式来支持这种需求,比如通过容器名、静态 IP 或直接访问主机。

本教程将演示如何使用 Docker Compose 来实现同一台主机上两个容器之间的通信。


2. Docker 环境准备

我们以两个基于 Alpine 镜像的容器为例,验证它们是否可以通过网络互相通信。

首先我们准备一个自定义的 Dockerfile,添加一些常用的调试命令,如 pingbash

FROM alpine:latest
MAINTAINER baeldung.com
RUN apk update && apk add bash && apk add iputils

然后我们使用 docker-compose 命令来启动和管理容器:

# 启动容器
docker-compose -f yaml-file up -d

# 停止并删除容器
docker-compose -f yaml-file down

# 清理未使用的网络(推荐在开始前执行)
docker network prune

Docker 会根据 docker-compose.yml 所在目录名自动生成网络名称,例如 目录名_default。若未显式指定网络名,默认使用 default


3. 使用 DNS 通信

Docker 内置了一个 DNS 服务,它将容器名等别名映射到对应的 IP 地址。即使 IP 地址发生变化,容器仍可通过别名访问。

我们创建一个 docker-compose.yml 文件如下:

services:
  alpine-app-1:
    container_name: alpine-app-1
    image: alpine-app-1
    build:
      context: ..
      dockerfile: Dockerfile
    tty: true
  
  alpine-app-2:
    container_name: alpine-app-2
    image: alpine-app-2
    build:
      context: ..
      dockerfile: Dockerfile
    tty: true

启动容器后,使用 docker network inspect 查看网络信息:

docker network inspect dns_default

输出中会显示两个容器的 IP 地址,例如:

"IPv4Address": "172.25.0.2/16"
"IPv4Address": "172.25.0.3/16"

我们可以通过 IP、容器名或容器 ID 进行 ping 测试:

docker exec -it alpine-app-1 ping 172.25.0.3
docker exec -it alpine-app-1 ping alpine-app-2
docker exec -it alpine-app-1 ping 577c6ac4aae4

所有方式都能成功通信,响应如下:

64 bytes from alpine-app-2.dns_default (172.25.0.3): icmp_seq=1 ttl=64 time=0.152 ms

小结:Docker 的 DNS 服务使得容器可以通过名称或 ID 直接通信,非常适合本地服务间调用或集成测试。


4. 使用静态 IP 地址

虽然 Docker 默认会自动分配 IP 地址,但有时我们希望为容器分配固定的 IP,以确保服务地址的稳定性。

4.1. 使用 Bridge 网络配置静态 IP

我们显式定义一个自定义的 bridge 网络,并为每个容器指定 IP:

services:
  alpine-app-1:
    container_name: alpine-app-1
    build:
      context: ..
      dockerfile: Dockerfile
    image: alpine-app-1
    tty: true
    networks:
      network-example:
        ipv4_address: 10.5.0.2

  alpine-app-2:
    container_name: alpine-app-2
    build:
      context: ..
      dockerfile: Dockerfile
    image: alpine-app-2
    tty: true
    networks:
      network-example:
        ipv4_address: 10.5.0.3

networks:
  network-example:
    driver: bridge
    ipam:
      config:
        - subnet: 10.5.0.0/16
          gateway: 10.5.0.1

启动后,我们可以通过 ifconfig 在主机上看到新增的桥接网络接口:

br-2fda6ab68472: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
    inet 10.5.0.1  netmask 255.255.0.0  broadcast 10.5.255.255

容器之间依然可以通过 IP、名称或 ID 通信:

docker exec -it alpine-app-1 ping 10.5.0.3
docker exec -it alpine-app-1 ping alpine-app-2

小结:使用静态 IP 可以让服务地址更稳定,适用于生产环境或需要固定 IP 的服务部署。

4.2. 使用 Macvlan 网络

如果希望容器拥有独立的 MAC 地址,并直接连接物理网络,可以使用 Macvlan 网络。这种方式只适用于 Linux 系统。

services:
  alpine-app-1:
    container_name: alpine-app-1
    build:
      context: ..
      dockerfile: Dockerfile
    image: alpine-app-1
    tty: true
    networks:
      network-example:
        ipv4_address: 192.168.2.2

  alpine-app-2:
    container_name: alpine-app-2
    build:
      context: ..
      dockerfile: Dockerfile
    image: alpine-app-2
    tty: true
    networks:
      network-example:
        ipv4_address: 192.168.2.3

networks:
  network-example:
    driver: macvlan
    driver_opts:
      parent: enp0s3
    ipam:
      config:
        - subnet: 192.168.2.0/24
          gateway: 192.168.2.1

⚠️ 注意:使用 Macvlan 时,主机无法直接访问容器,需要手动配置路由规则:

sudo ip link add macvlan-net link enp0s3 type macvlan mode bridge \
&& sudo ip addr add 192.168.2.50/32 dev macvlan-net \
&& sudo ip link set macvlan-net up

sudo ip route add 192.168.2.0/24 dev macvlan-net

小结:Macvlan 是实现容器直接接入物理网络的理想选择,适合对网络性能和拓扑结构有较高要求的场景。


5. 使用 host.docker.internal 访问主机

当容器需要访问运行在主机上的服务时,可以使用 host.docker.internal 这个特殊 DNS 名称。它指向 Docker 主机的默认网关地址。

5.1. 创建一个 Node.js 接口服务

我们创建一个简单的 Node.js 服务,监听 8080 端口并返回 “Hello World”:

var express = require('express')
var app = express()

app.get('/', function (req, res) {
    res.send('Hello World!')
})

app.listen(8080, function () {
    console.log('app listening on port 8080!')
})

对应的 Dockerfile

FROM node:8.16.1-alpine
WORKDIR /app
COPY host_docker_internal/package.json /app
COPY host_docker_internal/index.js /app
RUN npm install
CMD node index.js
EXPOSE 8080

5.2. 配置 Alpine 容器访问主机服务

我们在 docker-compose.yml 中为 Alpine 容器添加 host.docker.internal 的解析(仅适用于 Linux):

services:
  alpine-app-1:
    container_name: alpine-app-1
    extra_hosts:
      - host.docker.internal:host-gateway
    build:
      context: ..
      dockerfile: Dockerfile
    image: alpine-app-1
    tty: true
    networks:
      - first-network

  node-app:
    container_name: node-app
    build:
      context: ..
      dockerfile: Dockerfile.node
    image: node-app
    ports:
      - 8080:8080
    networks:
      - second-network

networks:
  first-network:
    driver: bridge
  second-network:
    driver: bridge

然后从 Alpine 容器中访问 Node.js 接口:

docker exec -it alpine-app-1 curl -i -X GET http://host.docker.internal:8080

返回结果:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 12
ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"
Date: Tue, 24 Jan 2023 18:33:38 GMT
Connection: keep-alive

Hello World!

我们可以查看 /etc/hosts 文件确认解析:

172.17.0.1      host.docker.internal

小结host.docker.internal 是容器访问主机服务的便捷方式,尤其适用于本地开发调试。


6. 总结

本文介绍了几种在同一台机器上实现 Docker 容器间通信的方式:

通信方式 说明 适用场景
DNS 通信 容器通过名称或 ID 相互访问 本地服务间通信、集成测试
静态 IP 显式分配 IP,适合地址固定的服务 生产环境、网络地址固定需求
Macvlan 容器拥有独立 MAC 地址,接入物理网络 高性能网络、直接接入 LAN
host.docker.internal 容器访问主机服务 本地开发调试、服务依赖主机

如需完整代码示例,可参考 GitHub 仓库


原始标题:Communicating With Docker Containers on the Same Machine