Docker 教程

Docker Compose 实战:定义服务、网络与数据卷

Docker Compose 允许你定义和运行多容器 Docker 应用程序。Compose 的核心是一个 YAML 文件(通常名为 docker-compose.yml),用于配置你的应用程序的服务、网络和数据卷。这个文件就像一张蓝图,描述了应用程序的各个组件将如何交互以及如何持久化数据。

1. 定义服务 (Services)

在 Docker Compose 中,一个服务 (Service) 代表运行应用程序特定部分的一个独立容器。例如,一个 Web 应用程序可能有一个服务用于 Web 服务器(如 Nginx 或 Apache),另一个服务用于应用程序代码(如 Python Flask 应用或 Node.js 应用),还有一个服务用于数据库(如 PostgreSQL 或 MongoDB)。每个服务都在 docker-compose.yml 文件的 services 键下进行定义。

每个服务定义都包含类似于你在 docker run 命令行上提供的配置选项。核心选项包括 image(镜像)、build(构建)、ports(端口)、volumes(数据卷)和 networks(网络)。

1.1 基础服务定义

来看一个由 Nginx Web 服务器组成的简单 Web 应用程序:

version: '3.8' # 指定 Compose 文件格式版本
services:
  web: # 服务的名称
    image: nginx:latest # 该服务使用的 Docker 镜像
    ports:
      - "80:80" # 将宿主机的 80 端口映射到容器的 80 端口

在这个例子中:

  • version: '3.8':指定了 Compose 文件的格式版本。使用像 3.8 这样的现代版本可以使用最新的功能和最佳实践。
  • services::是定义所有应用程序服务的顶级键。
  • web::是我们第一个服务的名称。Compose 在内部网络中使用此名称进行 DNS 解析,允许服务通过它们的名称进行通信。
  • image: nginx:latest:告诉 Compose 从 Docker Hub 使用 nginx:latest Docker 镜像。如果本地找不到该镜像,Compose 会自动拉取它。
  • ports::在宿主机和容器之间映射端口。"80:80" 将宿主机的 80 端口映射到容器的 80 端口,使得 Nginx 服务器可以通过宿主机的网络接口被访问。

1.2 使用自定义镜像定义服务

通常,你需要为自己的应用程序代码构建自定义 Docker 镜像。build 选项用于指定 Dockerfile 的位置。

version: '3.8'
services:
  app:
    build: ./app # 指定包含 Dockerfile 的目录路径
    ports:
      - "5000:5000"
    command: python app.py # 覆盖 Dockerfile 中的默认命令
  web:
    image: nginx:latest
    ports:
      - "80:80"
    depends_on: # 确保 'app' 服务在 'web' 之前启动(不会等待 'app' 达到健康状态)
      - app

在这里,app 服务是从位于 ./app 目录中的 Dockerfile 构建的。command 选项覆盖了此服务在 Dockerfile 中指定的默认命令,确保执行 python app.pydepends_on 关键字用于表达服务之间的依赖关系,表明 web 服务应在 app 服务之后启动。请注意,虽然 depends_on 确保了启动顺序,但它不会等待依赖服务“就绪”或“健康”;它只等待其容器被启动。

1.3 环境变量与重启策略

你可以将环境变量注入到你的服务中,并定义容器在退出时的行为。

version: '3.8'
services:
  database:
    image: postgres:13
    environment: # 在容器内部设置环境变量
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    restart: always # 如果容器停止,总是重新启动它
  backend:
    build: ./backend
    environment:
      DATABASE_HOST: database # 引用了名为 'database' 的服务名称
      DATABASE_PORT: 5432
    ports:
      - "8080:8080"
    restart: on-failure # 仅当容器以非零状态码(错误)退出时才重启

在此配置中,database 服务使用 postgres:13 并设置了用于数据库配置的关键环境变量。restart: always 策略确保如果 PostgreSQL 容器因任何原因停止,Compose 将尝试重启它。backend 服务从本地的 ./backend 目录构建,使用环境变量连接到 database 服务,并使用 restart: on-failure,它只在容器意外报错退出时重启。

2. 定义网络 (Networks)

Docker Compose 会自动为你的应用程序创建一个默认的桥接网络(Bridge Network),允许所有服务使用它们的服务名称相互通信。但是,你可以定义自定义网络来隔离应用程序的流量或连接到外部网络。定义自定义网络可以更好地控制服务间的通信并提升安全性。

2.1 隐式的默认网络

当你没有显式定义任何网络时,Compose 会为你的应用创建一个默认网络。所有服务都会自动连接到这个网络。

回顾我们之前的例子:

version: '3.8'
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
  app:
    build: ./app
    ports:
      - "5000:5000"

在这种情况下,Compose 创建了一个默认网络(例如,如果你的项目目录是 myproject,网络名可能是 myproject_default)。webapp 服务都附加到这个网络,允许 web 通过主机名 app 在端口 5000 上访问 app(例如请求 http://app:5000)。

2.2 显式的自定义网络

你可以在顶级的 networks 键下定义自定义网络,然后在每个服务定义中使用 networks 选项将服务分配给这些网络。

version: '3.8'
services:
  frontend:
    image: frontend-app:latest
    ports:
      - "80:80"
    networks:
      - frontend-network # 连接到 'frontend-network'
  backend:
    image: backend-app:latest
    networks:
      - frontend-network # 连接到 'frontend-network'
      - backend-network # 连接到 'backend-network'
  database:
    image: postgres:13
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    networks:
      - backend-network # 连接到 'backend-network'

networks: # 定义网络的顶级键
  frontend-network: # 名为 'frontend-network' 的自定义网络
    driver: bridge # 指定网络驱动程序,'bridge' 是默认值
  backend-network: # 名为 'backend-network' 的自定义网络
    driver: bridge

在这个设置中:

  • frontend 服务仅连接到 frontend-network
  • backend 服务连接到两个网络:frontend-networkbackend-network。这允许它接收来自前端的请求,同时能连接到数据库。
  • database 服务仅连接到 backend-network

这种隔离意味着 frontend 服务无法直接与数据库通信,因为它们不共享同一个网络。这极大地增强了安全性并隔离了通信路径。

2.3 外部网络

你也可以将 Compose 应用程序连接到在 docker-compose.yml 文件之外创建的网络,例如使用 docker network create 手动创建的网络。

version: '3.8'
services:
  my-app:
    image: my-custom-app:latest
    networks:
      - existing-shared-network # 连接到一个已存在的外部网络

networks:
  existing-shared-network:
    external: true # 声明这是一个外部网络
    name: my-global-app-network # 指定外部网络的真实名称

在这里,my-app 连接到一个名为 my-global-app-network 的现有网络。external: true 标志告诉 Compose 不要去创建这个网络,而是去寻找一个已经存在的网络。这对于将你的 Compose 应用程序与运行在共享网络上的其他 Docker 容器或服务集成非常有用。

3. 定义数据卷 (Volumes)

数据卷是持久化 Docker 容器生成和使用的数据的首选机制。它们允许数据在容器的生命周期之外继续存活。在 Compose 中,你可以定义由 Docker 管理的命名卷(Named Volumes),然后将它们挂载到你的服务中。

3.1 命名卷 (Named Volumes)

命名卷在顶级的 volumes 键下定义,然后可以被各个服务引用。

version: '3.8'
services:
  database:
    image: postgres:13
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - db-data:/var/lib/postgresql/data # 将命名卷 'db-data' 挂载到容器内部
    networks:
      - backend-network
  backend:
    build: ./backend
    ports:
      - "8080:8080"
    networks:
      - backend-network

volumes: # 定义数据卷的顶级键
  db-data: # 命名卷的名称

在这个例子中:

  • db-data: 定义了一个命名卷。Compose 将创建并管理这个卷。
  • database 服务使用了 db-data:/var/lib/postgresql/data。这会将 db-data 卷挂载到 PostgreSQL 容器内的 /var/lib/postgresql/data 路径下,这是 PostgreSQL 存储其数据的默认目录。这确保了即使 database 容器被删除,数据依然安全地保存在 db-data 卷中。当使用此 Compose 文件启动新的 database 容器时,它将重新使用现有的 db-data 卷,从而找回以前存储的数据。

3.2 带有驱动选项的数据卷

你可以为数据卷指定 driver(驱动),允许你使用不同的存储后端,例如网络文件系统 (NFS) 或云存储插件。

version: '3.8'
services:
  web:
    image: nginx:latest
    volumes:
      - web-content:/usr/share/nginx/html # 挂载 'web-content' 卷
    ports:
      - "80:80"

volumes:
  web-content:
    driver: local # 显式定义使用 'local' 本地驱动(默认)
    driver_opts: # 针对该驱动的具体选项
      type: "nfs"
      o: "addr=192.168.1.100,rw" # NFS 选项示例
      device: ":/path/to/nfs/share"

在这里,web-content 卷配置了 driver: localdriver_opts 来模拟 NFS 挂载。这是一个更高级的用例,通常出现在需要共享存储的生产环境中。

3.3 主机挂载 / 绑定挂载 (Bind Mounts)

虽然在生产环境中通常首选命名卷进行数据持久化,但绑定挂载在开发环境中非常有用,它允许你将宿主机上的本地目录直接挂载到容器中。这对于代码开发特别方便,因为你在宿主机上所做的更改会立即反映在容器中,而无需重新构建镜像。

version: '3.8'
services:
  app:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - ./app:/usr/src/app # 将本地的 './app' 目录挂载到容器的 '/usr/src/app'
    environment:
      PYTHONUNBUFFERED: 1 # Python 开发环境的示例环境变量

在这个设置中,本地 ./app 目录中的代码被挂载到了容器的 /usr/src/app 中。这意味着在宿主机的 ./app 中对文件所做的任何更改,都会立刻在运行中的 app 容器内部生效,实现了极速的开发迭代。

4. 综合实战案例

让我们将服务、网络和数据卷结合到一个更完整的应用程序中:一个简单的博客系统,使用 Node.js API、React 前端和 MongoDB 数据库。

项目目录结构

.
├── docker-compose.yml
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       └── App.js
├── backend/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       └── app.js
└── data/ # 为潜在的本地数据库文件保留的占位符(尽管本例 MongoDB 使用的是数据卷)

docker-compose.yml 配置解析

version: '3.8'
services:
  frontend:
    build: ./frontend # 从 ./frontend/Dockerfile 构建前端镜像
    ports:
      - "3000:3000" # 将宿主机端口 3000 映射到容器端口 3000
    volumes:
      - ./frontend:/app # 开发用绑定挂载:将本地代码挂载到容器的 /app
      - /app/node_modules # 匿名卷:用于向容器隐藏宿主机的 node_modules
    environment:
      REACT_APP_API_URL: http://localhost:8080 # 指向可从浏览器访问的后端 API
    networks:
      - app-network # 连接到主应用网络
    depends_on:
      - backend # 前端依赖后端进行 API 调用(启动顺序)

  backend:
    build: ./backend # 从 ./backend/Dockerfile 构建后端镜像
    ports:
      - "8080:8080" # 将宿主机端口 8080 映射到容器端口 8080
    volumes:
      - ./backend:/app # 开发用绑定挂载:将本地代码挂载到容器的 /app
      - /app/node_modules # 匿名卷:用于向容器隐藏宿主机的 node_modules
    environment:
      NODE_ENV: development
      MONGO_URI: mongodb://database:27017/blogdb # 连接到 'app-network' 内的 'database' 服务
    networks:
      - app-network # 连接到主应用网络
    depends_on:
      - database # 后端依赖数据库

  database:
    image: mongo:latest # 使用官方的 MongoDB 镜像
    ports:
      - "27017:27017" # 暴露 MongoDB 以供潜在的外部访问(例如使用 MongoDB Compass)
    volumes:
      - mongo-data:/data/db # 用于持久化 MongoDB 数据的命名卷
    networks:
      - app-network # 连接到主应用网络

networks:
  app-network: # 为应用程序组件自定义的桥接网络
    driver: bridge

volumes:
  mongo-data: # 用于 MongoDB 数据持久化的命名卷

附带的 Dockerfile 示例

frontend/Dockerfile

# 使用 Node.js 基础镜像进行构建和运行
FROM node:18-alpine
# 设置容器内的工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json 以安装依赖
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制应用程序的其余代码
COPY . .
# 暴露应用运行的端口
EXPOSE 3000
# 运行应用程序的命令
CMD ["npm", "start"]

backend/Dockerfile

# 使用 Node.js 基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制应用程序的其余代码
COPY . .
# 暴露 API 运行的端口
EXPOSE 8080
# 运行应用程序的命令
CMD ["npm", "start"]

案例原理详解:

案例原理详解:

  1. 服务 (Services):
    • frontend: 构建 React 应用。它将宿主机端口 3000 绑定到容器端口 3000。它使用 ./frontend 目录的绑定挂载进行实时开发,并使用了一个匿名卷 (/app/node_modules) 以确保容器内的 node_modules 目录不会被宿主机的 node_modules 覆盖(它们可能具有不同的操作系统架构)。
    • backend: 构建 Node.js API 应用。它将宿主机端口 8080 绑定到容器端口 8080,并且类似地使用了绑定挂载和匿名卷。它设置了 MONGO_URI 以连接到 database 服务。
    • database: 使用 mongo:latest 镜像。它暴露了 27017 端口,并且至关重要的一点是,它使用了一个名为 mongo-data 的命名卷将其数据持久化在 /data/db 路径下。
  2. 网络 (Networks):
    • app-network: 定义了一个单一的自定义桥接网络。所有三个服务(frontendbackenddatabase)都连接到这个网络。这使得它们可以使用各自的服务名称相互通信(例如,backend 使用 mongodb://database:27017 连接到 database)。
  3. 数据卷 (Volumes):
    • mongo-data:volumes 部分显式定义的一个命名卷。此卷由 Docker 管理,并确保即使 database 容器被停止、删除或重新创建,MongoDB 的数据依然得以保留。
    • ./frontend:/app./backend:/app: 这些是用于开发的绑定挂载。它们将本地项目代码与容器同步,允许实时修改代码而无需重新构建镜像。
    • /app/node_modules: 这些是匿名卷 (Anonymous Volumes)。它们用于防止宿主机的 node_modules 目录干扰容器内的 node_modules。Docker 会在容器内的 /app/node_modules 处创建一个匿名卷,对于该特定路径,匿名卷的优先级将高于绑定挂载。当在代码源码上使用绑定挂载时,这是 Node.js 项目中极其常见的一种模式。

这个全面的 docker-compose.yml 文件定义了一个完整的多容器应用程序,清晰地规定了每个组件应该如何构建、运行、通信以及持久化数据。