Docker Compose 完全指南:从入门到实战

1. 什么是 Docker Compose?

在深入学习之前,我们必须理解 Docker Compose 是什么以及它解决了什么问题。想象一下,你正在开发一个现代的 Web 应用程序。它通常不是一个单一的、庞大的程序,而是由多个相互协作的“服务”组成的。例如:

* 一个 Web 服务器(如 Nginx)用于处理静态文件和作为反向代理。

* 一个应用后端(用 Python、Java、Go 等编写)。

* 一个数据库(如 MySQL、PostgreSQL)用于存储数据。

* 一个缓存服务(如 Redis)用于提升性能。

在 Docker 出现之前,在你的开发机器上设置所有这些组件并确保它们正确协同工作是一个繁琐且容易出错的过程。你需要安装每个软件,配置它们的连接参数,并手动启动它们。

Docker 容器化技术解决了“环境一致性”的问题,它将每个服务打包成一个独立的、可移植的镜像。但是,管理多个容器仍然是一个挑战。你需要手动使用 docker run 命令来启动每一个容器,并且需要处理容器之间的网络连接、数据卷映射、环境变量等。一个复杂的应用可能需要十条甚至更多的 docker run 命令,这既难以记忆,也难以分享给团队成员。

Docker Compose 就是为了解决这个问题而生的。

Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。你使用一个 YAML 文件(通常命名为 docker-compose.yml)来配置你的应用程序所需的所有服务。然后,使用一个简单的命令,你就可以创建并启动你配置中的所有服务。

核心优势:

声明式配置:在 docker-compose.yml 文件中,你只需要“声明”你想要的服务状态(例如,我需要一个 Web 服务,它使用这个镜像,暴露这个端口),而不是编写一系列命令来“达到”这个状态。

一键启动:通过 docker-compose up 命令,可以一次性启动、构建和连接所有服务。

环境隔离:整个多容器应用可以被看作一个整体,可以轻松地在不同环境(开发、测试、生产)之间迁移。

服务管理:提供了一整套命令来管理整个应用的生命周期,如启动、停止、重启、查看日志等。

2. 安装 Docker Compose

在最新版本的 Docker Desktop (for Mac/Windows) 和 Docker Engine (for Linux) 中,Docker Compose 已经作为一个插件被集成在内,名为 docker-compose-plugin。这意味着你不再需要单独下载和安装它。你只需要确保你的 Docker 是最新版本即可。

你可以通过以下命令来检查 Docker Compose 是否已经安装:

docker compose version

如果看到了版本号输出,例如 Docker Compose version v2.20.2,那么你就可以直接使用了。请注意,新的命令格式是 docker compose(中间没有连字符),而不是旧版的 docker-compose。尽管旧版命令在很多系统上仍然通过别名可用,但推荐使用新的 docker compose 格式。

如果你的系统非常老旧,或者没有这个插件,你可能需要单独安装。对于 Linux 系统,可以通过以下方式安装:

sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

请将 v2.20.2 替换为最新的版本号。

3. 核心概念:服务、网络、卷

在编写 docker-compose.yml 文件之前,理解其三大核心概念至关重要。

服务:一个服务代表一个应用的容器。例如,你的 Web 应用、数据库、缓存等都可以被定义为一个服务。在 docker-compose.yml 中,你为每个服务定义其镜像、端口、环境变量、依赖关系等属性。它是 docker-compose.yml 文件中最顶级的配置项。

网络:默认情况下,Docker Compose 会为你创建一个默认的网络。所有定义在同一个 docker-compose.yml 文件中的服务都会自动加入这个网络,并且它们可以通过服务名作为主机名来相互访问。例如,如果你的 Web 服务名为 web,数据库服务名为 db,那么在 web 容器中,你可以直接用 db 这个主机名来连接数据库容器。这极大地简化了服务间的发现和通信。

卷:卷用于持久化和共享数据。容器本身是临时的,当容器被删除时,其内部的文件系统也会被销毁。对于数据库、用户上传的文件等需要长期保存的数据,我们必须使用卷将数据存储在容器外部,挂载到容器内部。Docker Compose 允许你定义两种主要的卷:

* 命名卷:由 Docker 管理,存储在 Docker 的特定目录下。优点是易于备份和管理,与宿主机的文件系统解耦。

* 绑定挂载:将宿主机上的一个文件或目录直接挂载到容器中。优点是直观,可以直接在宿主机上编辑文件并立即在容器内生效,非常适合开发场景。

4. 实战演练:构建一个多容器 Web 应用

我们将通过一个完整的例子来学习 Docker Compose 的用法。这个应用包含三个部分:

Web App:一个简单的 Python Flask 应用,它会记录页面访问次数到 Redis。

Redis:一个内存数据库,用作缓存。

Nginx:一个反向代理,将外部的请求转发到我们的 Flask 应用。

4.1. 第一步:创建项目结构

首先,创建一个项目目录,并组织好文件结构。

mkdir my_flask_app
cd my_flask_app

touch docker-compose.yml
touch Dockerfile
touch app.py
touch requirements.txt

mkdir nginx
touch nginx/nginx.conf

最终的目录结构如下:

my_flask_app/
├── app.py
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── nginx/
    └── nginx.conf

4.2. 第二步:编写 Flask 应用 (app.py)

这是一个非常简单的 Flask 应用,它连接到 Redis 服务,每次访问根路径时,都会增加一个计数器的值并显示。

# app.py
from flask import Flask
from redis import Redis
import os

app = Flask(__name__)
# 使用服务名 'redis' 作为主机名连接 Redis 容器
# Redis 的默认端口是 6379
redis_host = os.environ.get('REDIS_HOST', 'redis')
redis_client = Redis(host=redis_host, port=6379, db=0)

@app.route('/')
def hello():
    # 尝试增加计数器
    count = redis_client.incr('visits')
    return f'Hello! This page has been visited {count} times.\n'

if __name__ == '__main__':
    # 监听在 0.0.0.0 上,使其在容器外部可以访问
    app.run(host='0.0.0.0', port=5000)

4.3. 第三步:创建依赖文件和 Dockerfile

requirements.txt:

这个文件列出了 Python 应用需要的依赖库。

# requirements.txt
Flask==2.2.2
redis==4.3.4

Dockerfile:

这个文件描述了如何构建我们 Flask 应用的 Docker 镜像。

# Dockerfile
# 使用官方的 Python 3.9 slim 镜像作为基础镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件到工作目录
COPY requirements.txt .

# 安装依赖
# --no-cache-console 减小镜像大小
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码到工作目录
COPY . .

# 声明容器暴露的端口(仅为说明,实际映射由 docker-compose 决定)
EXPOSE 5000

# 启动应用的命令
CMD ["python", "app.py"]

4.4. 第四步:配置 Nginx (nginx/nginx.conf)

我们将配置 Nginx 作为反向代理,监听 80 端口,并将所有请求转发到内部的 Flask 应用(在 5000 端口)。

# nginx/nginx.conf

server {
    listen 80;
    server_name localhost;

    location / {
        # 设置代理头,让 Flask 应用知道原始请求信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 将请求转发到名为 'web' 的服务的 5000 端口
        proxy_pass http://web:5000;
    }
}

4.5. 第五步:编写 docker-compose.yml 文件

这是整个项目的核心。我们将在这里定义所有服务。

# docker-compose.yml
version: '3.8'

services:
  # Nginx 服务
  nginx:
    # 从当前目录下的 'nginx' 目录构建镜像
    build: ./nginx
    # 将宿主机的 8080 端口映射到容器的 80 端口
    ports:
      - "8080:80"
    # 依赖于 'web' 服务,确保 'web' 服务在 'nginx' 之前启动
    depends_on:
      - web
    # 将服务连接到自定义网络 'app-network'
    networks:
      - app-network

  # Flask Web 应用服务
  web:
    # 从当前目录构建镜像
    build: .
    # 设置环境变量,告诉 Flask 应用 Redis 的主机名
    environment:
      - REDIS_HOST=redis
    # 依赖于 'redis' 服务
    depends_on:
      - redis
    # 将服务连接到自定义网络 'app-network'
    networks:
      - app-network

  # Redis 服务
  redis:
    # 使用官方的 Redis latest 镜像
    image: redis:latest
    # 将服务连接到自定义网络 'app-network'
    networks:
      - app-network

# 定义顶级的网络
networks:
  app-network:
    # 使用 bridge 驱动
    driver: bridge

详细解释:

* version: '3.8': 指定 docker-compose.yml 文件的语法版本。推荐使用较新的 3.x 版本。

* services: 定义了三个服务:nginx, web, redis

* nginx 服务:

* build: ./nginx: Compose 会在 ./nginx 目录下寻找 Dockerfile 并构建镜像。我们还需要为 Nginx 创建一个简单的 Dockerfile,它只需复制配置文件。

* ports: - "8080:80": 将你本地机器的 8080 端口映射到 Nginx 容器的 80 端口。

* depends_on: - web: 声明 nginx 服务依赖于 web 服务。这保证了在 nginx 启动前,web 服务已经启动。

* web 服务:

* build: .: 在当前目录下寻找 Dockerfile 来构建镜像。

* environment: - REDIS_HOST=redis: 设置一个环境变量。我们的 app.py 会读取这个变量来知道 Redis 的地址是 redis(即服务名)。

* depends_on: - redis: web 服务依赖于 redis 服务。

* redis 服务:

* image: redis:latest: 直接使用 Docker Hub 上官方的 redis 镜像,无需自己构建。

* networks:

* 我们定义了一个名为 app-network 的自定义网络。

* 所有服务都通过 networks: - app-network 声明加入了这个网络。

* 虽然不定义自定义网络,Compose 也会创建一个默认网络,但显式定义的好处是网络命名更清晰,并且可以配置更高级的选项。

我们需要为 Nginx 创建一个 Dockerfile (nginx/Dockerfile):

# nginx/Dockerfile
FROM nginx:latest
# 删除 Nginx 的默认配置
RUN rm /etc/nginx/conf.d/default.conf
# 复制我们的自定义配置文件
COPY nginx.conf /etc/nginx/conf.d/

4.6. 第六步:启动和运行应用

现在,万事俱备。在 my_flask_app 根目录下,运行以下命令:

docker compose up --build

* up: 创建并启动容器。

* --build: 在启动前,强制重新构建镜像。如果你修改了 Dockerfile 或源代码,这个标志是必需的。

你会看到一连串的日志输出,显示 Compose 正在构建镜像和启动容器。如果一切顺利,最后你会看到来自 Flask、Nginx 和 Redis 的日志混合在一起。

打开你的浏览器,访问 http://localhost:8080。你应该能看到类似 "Hello! This page has been visited 1 times." 的消息。刷新页面,你会发现访问次数在增加,证明 Redis 正在正常工作,并且 Nginx 成功地将请求代理到了我们的 Flask 应用。

Ctrl + C 可以停止并移除所有容器。

5. Docker Compose 常用命令详解

docker compose 提供了一套完整的命令集来管理你的应用。

* docker compose up -d

* -d--detach 表示在“后台”运行。这非常常用,这样容器就不会占用你的终端。你可以继续使用终端做其他事情。

* docker compose down

* 停止并删除由 docker compose up 创建的容器、网络。

* 如果你使用了命名卷,并且希望同时删除它们,可以使用 docker compose down -v。注意:这会永久删除数据!

* docker compose ps

* 列出项目中当前正在运行的容器,类似于 docker ps,但会按服务分组。

* docker compose logs

* 查看所有服务的日志。

* docker compose logs -f web: 实时跟踪 web 服务的日志。-f 表示 follow,类似 tail -f

* docker compose logs --tail=100 redis: 查看 redis 服务最近的 100 行日志。

* docker compose stop

* 停止所有服务,但不会删除容器。你可以用 docker compose start 重新启动它们。

* docker compose start

* 启动已经存在的、被停止的容器。

* docker compose restart

* 重启所有服务。

* docker compose exec

* 在一个正在运行的服务的容器内执行命令。这对于调试非常有用。

* 例如:docker compose exec redis redis-cli 可以进入 Redis 的命令行界面。

* 例如:docker compose exec web bash 可以在 Flask 应用的容器里启动一个 bash shell。

* docker compose build

* 重新构建服务的镜像。当你修改了 Dockerfile 或构建上下文中的文件后,需要运行此命令。up --build 也是同样的效果。

6. 进阶概念

6.1. 环境变量

硬编码配置(如数据库密码)是不安全的,也不便于在不同环境间切换。Docker Compose 提供了多种方式来使用环境变量。

.env 文件

在项目根目录创建一个名为 .env 的文件,Docker Compose 会自动加载它。

.env 文件示例:

# .env
REDIS_HOST=myredis
REDIS_PASSWORD=supersecret
NGINX_PORT=8081

然后,你可以在 docker-compose.yml 中使用这些变量:

# docker-compose.yml (部分)
services:
  web:
    build: .
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PASSWORD=${REDIS_PASSWORD}
    ...
  nginx:
    build: ./nginx
    ports:
      - "${NGINX_PORT}:80"
    ...
  redis:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    ...

说明: 这里我们不仅将密码传给了 web 服务,还通过 command 指令传给了 redis 服务,设置了 Redis 的访问密码。

Shell 环境变量

你也可以直接在 Shell 中设置环境变量,Compose 会读取它们。

export NGINX_PORT=9090
docker compose up

docker-compose.yml 文件中仍然使用 ${NGINX_PORT} 来引用。

6.2. 数据持久化

让我们修改之前的例子,为 Redis 添加一个命名卷来持久化数据,即使容器被删除,数据也不会丢失。

修改 docker-compose.yml

# docker-compose.yml (部分)
services:
  redis:
    image: redis:latest
    volumes:
      - redis_data:/data
    networks:
      - app-network

# 在顶级定义 volumes
volumes:
  redis_data:

解释:

* volumes: - redis_data:/data: 在 redis 服务内部,我们将一个名为 redis_data 的命名卷挂载到 Redis 容器内的 /data 目录。Redis 默认将数据保存在 /data 目录。

* volumes: redis_data:: 在顶级 volumes 下,我们声明了这个命名卷。Docker 会自动创建和管理它。下次 docker-compose up 时,这个卷会被重新挂载,数据得以保留。

如果你想使用绑定挂载,可以像这样写:

volumes: - ./my-local-data:/data (将本地的 my-local-data 目录挂载到容器的 /data)。

7. 总结

Docker Compose 是一个极其强大的工具,它极大地简化了多容器应用的开发、部署和管理流程。通过一个简单的 docker-compose.yml 文件,我们能够用声明式的方式定义复杂的应用架构,包括服务、网络和数据持久化。

本教程从 Docker Compose 的基本概念入手,通过一个包含 Nginx、Flask 和 Redis 的完整实战案例,详细讲解了如何编写 Dockerfiledocker-compose.yml,如何构建、运行和管理整个应用生命周期。此外,我们还探讨了环境变量管理和数据持久化等进阶话题,这些都是将 Docker Compose 应用于实际生产环境的关键技能。

掌握 Docker Compose,意味着你已经从使用单个 Docker 容器,迈入了使用容器编排的现代开发阶段。它不仅是开发者的得力助手,也是通往更复杂的容器编排工具(如 Kubernetes)的重要基石。希望这篇详细的教程能帮助你彻底理解和熟练运用 Docker Compose,从而更高效地构建你的应用。

发表回复

后才能评论