第5章:Docker 网络 - 容器的沟通桥梁

104 阅读7分钟

第5章:Docker 网络 - 容器的沟通桥梁

到目前为止,我们已经学会了如何运行单个容器,并为它保存数据。但一个真正的应用,往往是由多个服务(容器)组成的,比如一个 Web 应用容器需要连接一个数据库容器。

这就引出了一个新问题:容器之间如何进行通信?

默认情况下,Docker 会为每个容器分配一个独立的 IP 地址,但这些 IP 地址是动态的,每次容器重启都可能改变。我们总不能每次都手动去查找 IP,然后在代码里写死吧?

本章,我们将深入 Docker 的网络模型,学习如何为我们的多个容器创建一个稳定、可靠的“局域网”,让它们可以通过服务名轻松地互相发现和通信。

5.1 Docker 的几种网络模式

Docker 内置了多种网络驱动(drivers),来满足不同的通信需求。我们重点关注最常见的三种:

  1. bridge (桥接网络)

    • 这是默认模式。 当你启动一个容器而没有指定任何网络时,它就会被连接到一个名为 bridge 的默认桥接网络上。
    • 工作原理:Docker 会创建一个虚拟的网桥(可以想象成一个虚拟路由器),所有连接到这个网络的容器,都会获得一个内部 IP 地址。它们之间可以通过 IP 地址互相访问,但 Docker 不推荐这样做,因为默认桥接网络在服务发现方面有很大局限性。
    • 与外界通信:容器可以通过这个网桥访问外部世界(比如 ping baidu.com)。但外界无法直接访问容器,除非做了端口映射 (-p 参数)。
  2. host (主机网络)

    • 是什么:容器将不再拥有自己独立的网络空间,而是直接共享主机的网络。
    • 优缺点:优点是网络性能极高,因为没有了虚拟化层的开销。缺点是破坏了容器的网络隔离性,容器内监听的端口会直接占用主机的端口,容易引起冲突。使用场景较少。
  3. none (无网络)

    • 是什么:容器将拥有自己的网络空间,但不进行任何网络配置。它就像一个被拔掉网线的电脑,完全与世隔绝。适合那些完全不需要网络连接的、纯计算型任务。

用户自定义桥接网络 (User-defined Bridge Network)

虽然 Docker 有一个默认的 bridge 网络,但官方强烈推荐我们为自己的应用创建自定义的桥接网络

为什么?因为自定义桥接网络提供了两个杀手级特性:

  • 自动服务发现 (Automatic Service Discovery):在同一个自定义网络中,容器之间可以直接通过容器名作为主机名 (hostname) 来互相访问!Docker 内置了一个 DNS 服务器来负责解析。
  • 更好的隔离性:自定义网络能提供更好的隔离。只有连接在同一个自定义网络上的容器才能互相通信,连接在不同网络上的容器则默认是隔离的。

5.2 容器间如何“对话”?

现在,我们知道了最佳实践是使用用户自定义桥接网络。让我们通过一个实战,来体验一下它神奇的“自动服务发现”功能。

第一步:创建一个新的网络

我们可以使用 docker network create 命令来创建一个新的桥接网络。我们给它起个名字叫 my-app-net

docker network create my-app-net

你可以用 docker network ls 来查看你主机上所有的 Docker 网络,应该能看到我们刚创建的 my-app-net

第二步:将容器连接到这个网络

现在,我们启动两个容器,并在启动时使用 --network 参数,将它们都连接到 my-app-net

我们启动一个 postgres 数据库容器,并给它起个名字叫 db

docker run -d --name db \
  --network my-app-net \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -v pg-data:/var/lib/postgresql/data \
  postgres:15-alpine

接着,我们启动一个普通的 alpine 容器(一个极简的 Linux 镜像),并进入它的 shell,用来模拟我们的应用容器。我们给它起个名字叫 client

docker run -it --rm --name client \
  --network my-app-net \
  docker.m.daocloud.io/alpine sh
  • --rm: 这个参数表示容器退出后自动删除它,很适合做一次性测试。

第三步:见证奇迹的时刻

现在你已经进入了 client 容器的 shell。在这个 shell 里,我们来尝试 ping 一下名为 db 的容器。

# 在 client 容器的 shell 中执行
ping db

你会惊奇地发现,ping 命令成功了!它会自动解析 db 这个名字,并找到 postgres 容器的内部 IP 地址。

这就是 Docker 的内置 DNS 发挥的作用。在同一个自定义网络中,容器名就是它们互相访问的地址。

chapter_05_01.png

5.3 [实战] 让应用容器连接到数据库容器

理论验证完毕,我们来动真格的。修改我们的 Node.js 应用代码,让它去连接 postgres 数据库。

注意:为了演示,我们这里只做连接测试,实际的项目代码会更复杂。你需要先在 my-app 目录下安装 pg 驱动:npm install pg,然后重新构建 my-app:1.1 镜像。

修改 my-app/app.js 文件:

const express = require('express');
const { Pool } = require('pg');

// Constants
const PORT = 8080;
const HOST = '0.0.0.0';

// App
const app = express();

// Postgres Client Setup
const pgClient = new Pool({
  // 关键在这里!我们直接使用容器名 'db' 作为主机地址
  host: 'db',
  port: 5432,
  user: 'postgres', // 默认用户
  password: 'mysecretpassword', // 我们启动db容器时设置的环境变量
  database: 'postgres' // 默认数据库
});

app.get('/', (req, res) => {
  res.send('Hello, Docker World!');
});

app.get('/db', async (req, res) => {
  try {
    const result = await pgClient.query('SELECT NOW()');
    res.send(`Database time is: ${result.rows[0].now}`);
  } catch (err) {
    res.status(500).send(`Error connecting to database: ${err}`);
  }
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

重新构建镜像

# 在 my-app 目录下
docker build -t my-app:1.1 .

启动所有服务: 确保你已经创建了 my-app-net 网络,并启动了 db 容器。

# 如果之前的 db 还在,先停掉并删除
docker rm -f db

# 启动数据库
docker run -d --name db \
  --network my-app-net \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -v pg-data:/var/lib/postgresql/data \
  postgres:15-alpine

# 启动我们的应用
docker run -d --name my-web-app -p 4000:8080 \
  --network my-app-net \
  my-app:1.1

现在,打开浏览器:

  • 访问 http://localhost:4000,应该能看到 "Hello, Docker World!"。
  • 访问 http://localhost:4000/db,你应该能看到页面成功地显示出了数据库的当前时间!

这证明我们的 my-web-app 容器,已经通过 db 这个名字,成功地连接上了 db 容器。

chapter_05_02.png

5.4 本章小结 & 避坑指南

你已经打通了容器之间任督二脉,构建多服务应用的基础已经牢固。

  • 本章回顾

    • 我们了解了 Docker 的 bridge, host, none 三种网络模式。
    • 我们掌握了最佳实践:使用用户自定义桥接网络
    • 我们利用了自定义网络的核心优势——自动服务发现,实现了通过容器名进行通信。
    • 我们通过实战,成功让应用容器连接上了数据库容器。
  • 避坑指南:容器无法访问外部网络怎么办?

    • 问题:在容器里 ping baidu.comnpm install 时,提示 Temporary failure in name resolution 或网络超时。
    • 原因:这通常是 Docker 的 DNS 配置问题,或者主机防火墙的限制。
    • 排查与解决
      1. 检查 Docker DNS:Docker 默认会使用主机的 DNS 设置。但有时可能会出问题。你可以尝试在 Docker 的配置文件(Docker Desktop 在 Settings -> Docker Engine 的 JSON 里,Linux 在 /etc/docker/daemon.json)中,强制指定一个公共 DNS 服务器,比如 Google 的 8.8.8.8 或 Cloudflare 的 1.1.1.1
        {
          "dns": ["8.8.8.8", "1.1.1.1"]
        }
        
        修改后需要重启 Docker 服务。
      2. 检查防火墙:确保你的主机防火墙没有阻止 Docker 的网络流量。特别是要允许 IP 转发。
      3. 重启大法:有时候,简单地重启 Docker 服务或重启电脑就能解决一些奇怪的网络问题。

虽然我们已经能让多个容器协同工作了,但每次都要手动敲那么多 docker run 命令,还要注意启动顺序,实在太麻烦了。在下一章,我们将学习一个编排神器——Docker Compose,让你用一个文件,一键启停整个应用!