一个基于 Docker Compose 编排的四服务多容器应用:

  • Nginx 作为统一入口,以轮询策略将请求分发到两个 Node.js 实例。

  • 两个 Node.js 实例(web1 / web2)运行相同的 Express 应用,实现水平扩展。

  • Redis 作为共享存储,保证跨实例的访问计数全局一致。

准备工作:

  1. 安装 Docker DeskTop。

  2. 登录 Docker DeskTop。

用户只需访问 http://localhost:80,即可观察到请求被交替分配到不同主机名的实例,同时计数器持续递增。正如 Docker 官方文档所指出的,容器的最佳实践是每个容器只做一件事,并把它做好。本项目的四个服务正是对这一原则的实践。

一、架构全景

                         ┌──────────────────┐
浏览器 ──► :80 ───►│      Nginx                        │  反向代理 + 负载均衡
                         │  (Round-Robin)                     │
                         └───────────┬──────┘
                                                 │ 轮询分发
                     ┌─────────────┴───────────┐
                     ▼                                                  ▼
              ┌──────────────┐            ┌──────────────┐
              │     web1                   │            │     web2                   │
              │  Node.js                   │            │  Node.js                   │
              │  Express                   │            │  Express                   │
              │  :5000                     │            │  :5000                     │
              └──────┬───────┘            └──────┬───────┘
                            │                                          │
                            └─────────┬───────────┘
                                                ▼
                                ┌──────────────┐
                                │    Redis                   │  共享计数器
                                │   :6379                    │
                                └──────────────┘

服务

技术栈

主机端口 → 容器端口

职责

nginx

Nginx

80 → 80

反向代理 + 负载均衡入口

web1

Node.js + Express

81 → 5000

Web 应用实例 #1

web2

Node.js + Express

82 → 5000

Web 应用实例 #2

redis

Redis

63790 → 6379

共享访问计数存储

端口说明:81 和 82 端口仅用于单独调试各实例,正常使用只需访问 :80。Redis 映射到 63790 而非标准 6379,是为了避免与主机上可能存在的其他 Redis 实例冲突。

二、工程结构

.
├── compose.yml          # Docker Compose 编排文件(核心)
├── nginx/
│   ├── Dockerfile       # Nginx 镜像构建
│   └── nginx.conf       # Nginx 配置(负载均衡 + 反向代理)
├── web/
│   ├── Dockerfile       # Node.js 镜像构建
│   ├── package.json     # Node.js 依赖声明
│   ├── package-lock.json
│   └── server.js        # Express 应用入口
├── LICENSE
└── README.md

三、从 docker run 到 Compose:多容器编排的必然选择

3.1 手动编排的困境

在引入 Compose 之前,让我们先理解为什么需要它。如果不使用 Compose,你需要为每个服务执行一条 docker run 命令,并手动创建网络、指定容器名、配置端口映射。Docker 官方文档对此有精准的概括:

Imagine running several docker run commands (frontend, backend, and database) with different configurations for development, testing, and production environments. It's error-prone and time-consuming.

具体来说,手动方式面临以下挑战:

  • 命令冗长:每个容器都需要记住完整的 docker run 参数。

  • 网络配置繁琐:需要手动创建 bridge 网络并将每个容器加入。

  • 启动顺序难以保证:Nginx 必须在 web1/web2 之后启动,否则 DNS 解析会失败。

  • 扩展困难:增加第三个实例意味着重复大量配置。

  • 环境变量管理分散:每个容器的环境变量在各自的命令中设置,难以统一维护。

3.2 手动方式(理解原理)

为了深入理解多容器协作的底层机制,我们先看看手动 docker run 方式的完整命令:

# 1. 创建专用网络
docker network create counter-net

# 2. 启动 Redis
docker container run -d --name redis --network counter-net redis:6-alpine

# 3. 启动 web1 (需要先构建镜像 my-web,cd web/目录下,运行:docker image build -t my-web .)
docker container run -d --name web1 --hostname web1 --network counter-net -p 81:5000 my-web

# 4. 启动 web2
docker container run -d --name web2 --hostname web2 --network counter-net -p 82:5000 my-web

# 5. 启动 Nginx(需先构建镜像,cd nginx/目录下,运行:docker image build -t my-nginx .)
docker container run -d --name nginx --network counter-net -p 80:80 my-nginx

这五条命令才能完成与一份 compose.yml 等效的部署。Docker 官方文档也推荐先通过手动方式理解原理,再用 Compose 简化整个部署过程。

3.3 Compose 方案(推荐)

使用 Docker Compose,上述所有步骤被浓缩为一份声明式配置文件和一个命令。Compose 将整个多容器应用定义在一个名为 compose.yml 的 YAML 文件中,指定所有容器的配置、依赖关系、环境变量,甚至数据卷和网络。核心优势包括:

  • 配置集中化:一个文件替代多条命令。

  • 启动顺序可控:通过 depends_on 管理依赖关系。

  • 独立伸缩:可以单独扩展某个服务,而非整个应用。

  • 持久化卷:轻松实现数据持久化。

  • 环境变量统一管理:一次定义,全局使用。

四、核心配置逐文件详解

4.1 compose.yml — 编排中枢
services:
  redis:
    image: redis:6-alpine
    ports:
      - '63790:6379'
  web1:
    restart: on-failure
    build: ./web
    hostname: web1
    ports:
      - '81:5000'
  web2:
    restart: on-failure
    build: ./web
    hostname: web2
    ports:
      - '82:5000'
  nginx:
    build: ./nginx
    ports:
      - '80:80'
    depends_on:
      - web1
      - web2

关键决策解析:

配置项

值

说明

image: redis:6-alpine

官方镜像

Redis 无需定制,直接拉取即可

build: ./web

本地构建

web1 和 web2 共用同一份源码和 Dockerfile

hostname: web1/web2

显式主机名

三重作用

restart: on-failure

异常重启

仅在进程非正常退出时重启(如崩溃)

depends_on: [web1, web2]

启动顺序

保证 nginx 在 web1/web2 之后启动,但不保证服务就绪

ports: '63790:6379'

非标准端口

避免与主机已有 Redis 冲突;容器间通信仍用 redis:6379,不受影响

三重作用:

  • Docker DNS 可解析。

  • nginx.conf 中 server web1:5000 能找到目标。

  • server.js 中 os.hostname() 返回正确标识。

Docker 内部网络机制:

docker compose up 时,Docker 自动创建名为 项目名_default 的 bridge 网络,所有服务加入其中。Docker 的嵌入式 DNS 会自动将这些服务名解析为容器 IP。容器间通过 服务名 DNS 解析互相访问:

nginx 容器内 → ping web1  解析到 web1 容器 IP
web1 容器内  → ping redis 解析到 redis 容器 IP

这就是为什么 nginx.conf 写 server web1:5000 而非硬编码 IP——容器重启后 IP 会变,但 DNS 名不变。

ping: not found 解决:nginx 容器内 或 web1/2 容器内运行:

  1. apt update

  2. apt install -y iputils-ping

4.2 nginx/nginx.conf — 负载均衡核心
# 定义了一个名为 loadbalancer 的上游服务器组,这是 Nginx 实现负载均衡的核心机制
# Nginx upstream 默认使用 轮询(Round-Robin) 策略:请求依次分配给 web1、web2、web1、web2…… 确保两个 Node.js 实例分担流量。
upstream loadbalancer {
  # 引用 Docker Compose 中的服务名 web1,端口 5000(Node.js 应用监听端口)
  server web1:5000;
  # 引用 Docker Compose 中的服务名 web2,端口 5000(Node.js 应用监听端口)
  server web2:5000;
}

# HTTP 虚拟主机,监听端口 80,将所有请求代理到 loadbalancer 上游服务器组
server {
  # 监听容器内的 80 端口(对应 compose.yml 中映射的 80:80)
  listen 80;
  # 匹配请求的 Host 头,这里用 localhost 表示本机访问
  server_name localhost;
  # 匹配所有以 / 开头的请求(即所有请求路径)
  location / {
    # 核心指令 —— 将请求反向代理到上面定义的 upstream 组,由 Nginx 负责在 web1/web2 之间分发请求
    proxy_pass http://loadbalancer;
  }
}
4.3 nginx/Dockerfile — nginx镜像构建
FROM nginx:1.29-alpine
# 删除默认配置,避免与自定义配置冲突或产生意外行为
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
4.4 web/server.js — Express 应用
const os = require('os');
const express = require('express');
const app = express();
const redis = require('redis');
const redisClient = redis.createClient({
  host: 'redis',
  port: 6379
});

app.get('/', function(req, res) {
    redisClient.incr('numVisits', (err, count) => {
        res.send(os.hostname() + ': Number of visits is: ' + count);
    });
});

app.listen(5000, function() {
    console.log('Web application is listening on port 5000');
});
4.5 web/Dockerfile — Node.js 镜像构建
FROM node:21

WORKDIR /usr/src/app

COPY package.json package-lock.json ./
RUN npm ci
COPY ./server.js ./

CMD ["npm","start"]

RUN npm ci :安装依赖(ci 比 install 更严格,按 lock 文件安装)。

4.6 web/package.json — 依赖声明
{
  "name": "web",
  "version": "1.0.0",
  "description": "Running Node.js and Express.js on Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.2",
    "redis": "3.1.2"
  },
  "author": "",
  "license": "MIT"
}

五、部署与验证

5.1 启动应用
# 先cd 到 compoose.yaml所在目录
docker compose up -d

预期输出:

[+] up 8/8                                                                                                                                                                                                                                                                                                          
 ✔ Image 202609-nginx-node-redis-multi-container-app-web1        Built                                                                                                                                                                                                                                         11.2s
 ✔ Image 202609-nginx-node-redis-multi-container-app-web2        Built                                                                                                                                                                                                                                         11.2s
 ✔ Image 202609-nginx-node-redis-multi-container-app-nginx       Built                                                                                                                                                                                                                                         11.3s
 ✔ Network 202609-nginx-node-redis-multi-container-app_default   Created                                                                                                                                                                                                                                        0.0s
 ✔ Container 202609-nginx-node-redis-multi-container-app-web1-1  Started                                                                                                                                                                                                                                        0.9s
 ✔ Container 202609-nginx-node-redis-multi-container-app-web2-1  Started                                                                                                                                                                                                                                        0.5s
 ✔ Container 202609-nginx-node-redis-multi-container-app-redis-1 Started                                                                                                                                                                                                                                        0.7s
 ✔ Container 202609-nginx-node-redis-multi-container-app-nginx-1 Started  
5.2 查看容器状态
# 先cd 到 compoose.yaml所在目录
docker compose ps

预期看到 4 个容器均为 running 状态,端口映射如下:

NAME                          SERVICE   CREATED         STATUS         PORTS
multi-container-app-nginx-1   nginx     2 minutes ago   Up 2 minutes   0.0.0.0:80->80/tcp, [::]:80->80/tcp
multi-container-app-redis-1   redis     2 minutes ago   Up 2 minutes   0.0.0.0:63790->6379/tcp, [::]:63790->6379/tcp
multi-container-app-web1-1    web1      2 minutes ago   Up 2 minutes   0.0.0.0:81->5000/tcp, [::]:81->5000/tcp
multi-container-app-web2-1    web2      2 minutes ago   Up 2 minutes   0.0.0.0:82->5000/tcp, [::]:82->5000/tcp
5.3 测试负载均衡

连续请求,观察主机名交替变化、计数持续递增:

$ curl localhost:80
web1: Number of visits is: 1

$ curl localhost:80
web2: Number of visits is: 2

$ curl localhost:80
web1: Number of visits is: 3

$ curl localhost:80
web2: Number of visits is: 4

可直接打开浏览器访问:http:localhost

  1. web1 / web2 交替出现 → 负载均衡生效。

  2. 计数连续递增 → Redis 共享状态生效。

5.4 单独调试实例
curl localhost:81    # 直接访问 web1
curl localhost:82    # 直接访问 web2
5.5 外部连接 Redis
redis-cli -p 63790
> GET numVisits
"4"
> INCR numVisits
(integer) 5

需要在宿主机上安装:redis-cli

5.6 停止并清理
docker compose down        # 删除容器,保留数据卷
docker compose down -v     # 删除容器,同时删除数据卷(Redis 数据会丢失)

七、总结

本文通过一个访问计数器应用,完整拆解了 Docker Compose 多容器编排的核心模式:

核心概念

本文体现

服务发现

nginx.conf 用 Docker DNS 名(web1、redis)代替 IP

负载均衡

Nginx upstream 轮询分发到多个 Node.js 实例

共享状态

两个无状态实例通过 Redis 共享计数器

水平扩展

新增实例只需加 compose 服务 + upstream server

分层缓存

Dockerfile 先 COPY 依赖再 COPY 源码,加速重建

端口隔离

对外仅暴露 :80,内部服务通过 Docker 网络互通

Docker Compose 的本质不是“多容器运行”,而是应用级的协同编排。DNS 发现、启动依赖、网络隔离,让分布式容器拥有单机般的协作体验。从 docker run 到 Compose,是从“操作容器”到“描述应用”的思维跃迁。掌握它,即掌握了微服务架构的基石。