Docker 教程

Docker Compose 构建、运行与管理多容器应用

当你在 docker-compose.yml 文件中精心定义了多容器应用的服务、网络和数据卷之后,下一个关键步骤就是让这些定义“活”起来。

Docker Compose 提供了直观的命令来简化应用栈的整个生命周期管理:从为服务构建自定义镜像,到编排它们的启动、管理运行状态,再到优雅地关闭并清理它们。本章将引导你学习这些核心命令,只需敲击几行简单的代码,就能将声明式的 Compose 文件转化为全面运行的多容器应用,让你轻松实现构建、运行、检查和销毁整个应用栈。

1. 使用 docker compose up 编排应用启动

docker compose up 是运行 docker-compose.yml 中定义的多容器应用的基石命令。当你执行这个命令时,Docker Compose 会读取你的配置,处理必要资源的创建(如不存在的网络和数据卷),构建指定的自定义服务镜像,然后按照正确的顺序启动所有服务,并妥善管理它们之间的依赖关系。

1.1 docker compose up 是如何工作的

  1. 解析 docker-compose.yml:Compose 首先解析你的 YAML 文件,以理解整个应用的结构。
  2. 创建资源:它会检查 YAML 中声明的命名网络或数据卷是否存在。如果不存在,它会自动创建。
  3. 构建镜像:对于任何指定了 build 上下文(意味着它使用 Dockerfile 来创建镜像)的服务,Compose 都会去构建该镜像。如果镜像在本地已存在,且 Dockerfile 或构建上下文没有发生改变,Compose 会直接使用现有镜像,除非你显式要求重新构建。
  4. 创建并启动服务:接着,Compose 为每个服务创建容器,将它们连接到定义的网络,并按照指定的挂载数据卷。它会根据显式的 depends_on 声明和隐式的网络依赖,以智能的顺序启动服务。
  5. 日志记录:默认情况下,docker compose up 会附加到所有运行中服务的输出上,将它们的日志直接流式传输到你的终端。这对于调试和初步观察非常棒。

1.2 docker compose up 的核心选项

  • -d--detach:在后台运行容器,释放你的终端。这对于生产环境部署,或者当你启动应用后还需要继续使用终端时必不可少。
  • --build:强制 Compose 为带有 build 指令的服务重新构建镜像,即使镜像已经存在且未发生改变。当你在构建上下文中修改了 Dockerfile 或应用代码,但 Compose 的缓存可能阻止了重建时,这个选项非常有用。
  • --no-recreate:如果服务的容器已经存在,此选项会阻止 Compose 重新创建它们。它会尝试直接启动现有容器。如果你没有进行需要重新创建的配置更改,这有助于保留容器状态。
  • --force-recreate:强制 Compose 停止并重新创建容器,即使它们的配置没有改变。这有助于确保干净的启动环境,或应用某些可能无法触发常规重建的微小环境更改。
  • --remove-orphans:删除 Compose 文件中不再定义但依然存在的服务容器。当你重构 docker-compose.yml 并删除了旧服务时,这有助于保持环境的整洁。

1.3 示例:启动包含数据库的 Web 应用

让我们考虑一个常见的场景:一个连接到 PostgreSQL 数据库的简单 Web 应用。

项目结构:

my-web-app/
├── docker-compose.yml
├── web/
│   ├── Dockerfile
│   ├── app.py
│   └── requirements.txt

web/Dockerfile:

# 使用官方 Python 运行环境作为基础镜像
FROM python:3.9-slim-buster

# 设置容器内的工作目录
WORKDIR /app

# 复制 requirements.txt 并安装所需的包
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用程序的其余代码
COPY . .

# 暴露 Flask 应用的 5000 端口
EXPOSE 5000

# 定义环境变量
ENV FLASK_APP=app.py

# 运行 Flask 应用程序
CMD ["flask", "run", "--host=0.0.0.0"]

web/requirements.txt:

Flask
psycopg2-binary

web/app.py:

from flask import Flask
import os
import psycopg2

app = Flask(__name__)

@app.route('/')
def hello():
    db_host = os.environ.get('DB_HOST', 'db') # 'db' 是 docker-compose 中的服务名称
    db_name = os.environ.get('POSTGRES_DB', 'mydatabase')
    db_user = os.environ.get('POSTGRES_USER', 'myuser')
    db_password = os.environ.get('POSTGRES_PASSWORD', 'mypassword')

    try:
        conn = psycopg2.connect(host=db_host, database=db_name, user=db_user, password=db_password)
        cur = conn.cursor()
        cur.execute("SELECT version();")
        db_version = cur.fetchone()[0]
        cur.close()
        conn.close()
        return f"Hello from Flask! 成功连接到 PostgreSQL 版本: {db_version}"
    except Exception as e:
        return f"Hello from Flask! 无法连接到数据库: {e}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

docker-compose.yml:

version: '3.8'
services:
  web:
    build: ./web # 指示 Compose 使用 './web' 目录中的 Dockerfile 构建镜像
    ports:
      - "5000:5000" # 将宿主机的 5000 端口映射到容器的 5000 端口
    environment:
      # 这些环境变量将被传递到 web 服务的容器中
      - DB_HOST=db # 主机名 'db' 会在 Docker 网络内解析为 'db' 服务
      - POSTGRES_DB=mydatabase
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypassword
    depends_on:
      # 确保 'db' 服务在 'web' 之前启动。
      # 注意:这仅保证启动顺序,不保证服务已准备就绪。
      - db
    networks:
      - app-network # 将 web 服务连接到 'app-network'

  db:
    image: postgres:13 # 使用 Docker Hub 上官方的 PostgreSQL 13 镜像
    environment:
      # 用于初始化设置的 PostgreSQL 专用环境变量
      POSTGRES_DB: mydatabase
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    volumes:
      # 用于 PostgreSQL 数据的持久化卷,防止容器重建时丢失数据
      - db-data:/var/lib/postgresql/data
    networks:
      - app-network # 将 db 服务连接到 'app-network'

networks:
  app-network: # 为应用服务定义一个自定义桥接网络

volumes:
  db-data: # 为数据库持久化定义一个命名卷

要运行这个应用,请在终端中导航到 my-web-app 目录并执行:

docker compose up

你将在终端中看到来自 webdb 服务的日志不断滚动。如果你想在后台运行它(这在日常开发或部署中更为常见),请加上 -d

docker compose up -d

现在,服务将在后台运行。你可以打开浏览器访问 http://localhost:5000 来查看你的 Web 应用。

2. 使用 docker compose build 显式构建镜像

虽然 docker compose up 会自动处理带有 build 上下文的服务的镜像构建,但在某些场景下,你可能只想显式地构建或重新构建镜像,而不一定要启动整个应用栈。这时就可以使用 docker compose build 命令。

2.1 何时使用 docker compose build

  • CI/CD 预构建:在持续集成/持续部署流水线中,你可能希望将所有镜像的构建作为一个独立的步骤,然后再将它们推送到镜像仓库或进行部署。
  • 调试 Dockerfile:在积极开发和调试 Dockerfile 时,通过仅构建镜像而不等待应用启动,你可以更快地迭代构建过程。
  • 强制重新构建:如果你怀疑缓存镜像存在问题,或者想要确保获得一个全新的构建,docker compose build 能让你拥有直接的控制权。

2.2 docker compose build 的核心选项

  • --no-cache:在镜像构建过程中不使用缓存层。这会强制 Docker 从头开始运行 Dockerfile 中的每一条指令,这对于确保干净的构建或拉取新的基础镜像非常有用。
  • --pull:始终尝试拉取 DockerfileFROM 指令所引用的镜像的最新版本。这能确保你的基础镜像是最新的。
  • [SERVICE...]:你可以指定一个或多个服务名称,以便仅构建这些特定服务的镜像,而不是构建 docker-compose.yml 中定义的所有服务。

2.3 示例:构建特定的服务

继续使用前面的 my-web-app 示例:

如果只想构建 web 服务的镜像:

docker compose build web

如果你想构建 docker-compose.yml 中带有 build 指令的所有服务镜像,并且强制拉取最新的基础镜像且不使用缓存:

docker compose build --no-cache --pull

这条命令会重新构建 web 服务镜像,忽略任何缓存层,并确保如果 Docker Hub 上有更新的版本,它会拉取最新的 python:3.9-slim-buster 基础镜像。

3. 管理运行中的多容器应用

一旦你的应用跑起来了,你就需要使用各种命令来检查其状态、查看日志以及控制其服务的生命周期。Docker Compose 提供了一套完整的命令来完成这些管理任务。

3.1 使用 docker compose ps 检查服务状态

docker compose ps 命令会列出与你的 Compose 应用关联的容器,并显示它们的状态、端口和运行命令。

docker compose ps

预期输出(示例):

NAME                    IMAGE                         COMMAND                  SERVICE    CREATED        STATUS        PORTS
my-web-app-db-1         postgres:13                   "docker-entrypoint.s…"   db         2 minutes ago   Up 2 minutes   5432/tcp
my-web-app-web-1        my-web-app-web                "flask run --host=0.…"   web        2 minutes ago   Up 2 minutes   0.0.0.0:5000->5000/tcp

这个输出清晰地表明 dbweb 服务都在正常运行(Up),同时也展示了它们的镜像、命令以及端口映射情况。

3.2 使用 docker compose logs 查看日志

docker compose logs 命令用于显示服务的日志输出。默认情况下,它会显示所有服务的日志。

docker compose logs

核心选项:

  • -f--follow:跟随日志输出,实时流式传输新日志(类似 tail -f)。
  • --tail N:每个容器仅显示最后 N 行日志。
  • [SERVICE...]:指定一个或多个服务名称,以仅查看特定服务的日志。

如果想专门查看 web 服务的日志并实时跟踪:

docker compose logs -f web

这对于实时调试和监控极其有用。

3.3 使用 docker compose stop 停止服务

docker compose stop 命令会优雅地停止正在运行的服务容器,但不会删除它们。稍后你可以使用 docker compose start 重新启动这些容器。

docker compose stop

你也可以指定停止特定的服务:

docker compose stop web

3.4 使用 docker compose start 启动已停止的服务

如果你使用了 docker compose stop 停止了服务,你可以使用 docker compose start 重新启动它们(前提是容器仍然存在)。

docker compose start

或者启动特定服务:

docker compose start web

3.5 使用 docker compose restart 重启服务

docker compose restart 命令会先停止然后再启动服务。这是应用那些需要服务重启才能生效的更改的便捷方式。

docker compose restart

或者重启特定服务:

docker compose restart web

3.6 使用 docker compose exec 在服务内执行命令

docker compose exec 命令允许你在正在运行的服务容器内执行任意命令。这对于调试、运行数据库迁移或执行管理任务来说具有不可估量的价值。

docker compose exec [SERVICE] [COMMAND]

如果想在 web 服务容器内打开一个交互式的 bash 终端:

docker compose exec web bash

进入容器后,你可以检查文件、查看环境变量或运行特定于应用程序的命令。输入 exit 即可退出容器终端。

如果想在 db 服务容器内运行 SQL 客户端:

docker compose exec db psql -U myuser mydatabase

这允许你直接与 PostgreSQL 数据库进行交互。

3.7 使用 docker compose down 销毁应用

docker compose down 命令会停止运行中的容器,并删除由 docker compose up 创建的容器、网络以及(默认配置下的)默认网络。

docker compose down

核心选项:

  • --volumes-v:删除在 docker-compose.ymlvolumes 部分声明的命名卷以及任何匿名卷。谨慎使用此选项,尤其是对于数据库,因为它会永久删除你的持久化数据!
  • --remove-orphans:删除 Compose 文件中不再定义但可能仍在运行的服务的容器。
  • --rmi all:删除被任何服务使用的所有镜像,即使是缓存的镜像。(--rmi local 仅删除没有自定义标签的本地镜像)。

如果要销毁应用并同时删除 db-data 命名卷(这会删除数据库数据):

docker compose down --volumes

强烈注意:不带 --volumes 选项的 docker compose down保留你的命名卷(如 db-data)。这意味着如果你再次运行 docker compose up,你的数据库数据依然存在。这是开发中常见且推荐的实践。只有当你明确想要重置为全新的数据状态时,才使用 --volumes

4. 应用依赖与编排

docker-compose.yml 中的 depends_on 关键字在编排服务的启动顺序中扮演着至关重要的角色。当你运行 docker compose up 时,Compose 会利用这个信息,确保在启动目标服务之前,先启动它所依赖的服务。

在我们的例子中:

services:
  web:
    # ...
    depends_on:
      - db

这告诉 Docker Compose 必须先启动 db 服务,只有当 db 正在运行(意味着其容器已经启动)后,它才会继续启动 web 服务。

4.1 理解 depends_on 的局限性

必须理解 depends_on 的一个关键局限:它仅保证作为依赖项的容器已经启动。它不会等待容器内部的应用程序准备就绪、变为健康状态或开始监听连接。

例如,depends_on: - db 确保了 PostgreSQL 容器正在运行,但它不能保证该容器内部的 PostgreSQL 服务器已经完全初始化、创建了数据库并准备好接受客户端连接。

对于生产级别的应用,你通常需要更健壮的就绪性检查(Readiness Checks)。虽然本章侧重于基础的构建和运行,但你要知道,要构建真正具有弹性的多容器应用,通常需要使用像 healthcheck(健康检查,属于进阶的 Docker Compose 主题)或轮询依赖项是否就绪的自定义入口点脚本(Entrypoint Scripts)。不过,对于许多开发场景而言,depends_on 提供的基础编排已经足够了。

5. 实战演示

让我们用刚学到的命令,走一遍 my-web-app 的完整生命周期。

步骤 1: 首次设置并运行(全新启动)

假设你拥有前面描述的包含 docker-compose.ymlweb/Dockerfileweb/app.pyweb/requirements.txt 文件的 my-web-app 项目目录。

导航到该目录:

cd my-web-app

在后台模式下启动应用。这将会构建 web 镜像,创建 app-network 网络和 db-data 数据卷,然后启动 dbweb 容器。

docker compose up -d

你应该会看到指示网络、数据卷和容器创建的输出,接着是类似 Started 的消息。

验证服务是否正在运行:

docker compose ps

两个服务的状态都应显示为 Up

检查日志确保一切启动正常:

docker compose logs

寻找表明 PostgreSQL 服务器已就绪以及 Flask 应用正在 5000 端口上运行的消息。

在浏览器中访问 http://localhost:5000。你应该能看到 "Hello from Flask! 成功连接到 PostgreSQL 版本: ..."。

步骤 2: 修改代码并重新构建

让我们修改 app.py 更改显示的信息。

编辑 web/app.py

# ... (保留原有的导入和应用定义)
@app.route('/')
def hello():
    # ... (保留数据库连接参数)
    try:
        # ... (保留数据库连接逻辑)
        return f"来自*修改后* Flask 应用的问候!成功连接到 PostgreSQL 版本: {db_version}" # 在此处修改了消息
    except Exception as e:
        return f"来自*修改后* Flask 应用的问候!无法连接到数据库: {e}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

现在,因为我们修改了应用代码,我们需要重新构建 web 服务的镜像并重启它的容器。

首先,停止 web 服务(db 服务可以保持运行,因为它的镜像没变):

docker compose stop web

然后,重新构建 web 镜像。使用 --no-cache 确保最新的 app.py 被包含到镜像中。

docker compose build --no-cache web

最后,重新启动 web 服务。简单来说,在构建之后再次对 web 服务执行 up 命令即可。

docker compose up -d web

(或者,在构建镜像后,一个更简单的应用更改的方法是直接重启服务:docker compose restart web)

刷新浏览器 http://localhost:5000。你现在应该能看到更新后的消息:“来自修改后 Flask 应用的问候!...”

步骤 3: 通过 exec 执行数据库操作

使用 docker compose exec 与数据库交互。我们将连接到 PostgreSQL shell 并创建一个简单的表。

确保服务仍在运行 (docker compose ps)。

进入 db 容器的 psql 终端:

docker compose exec db psql -U myuser mydatabase

你现在位于 PostgreSQL 客户端内。执行一些 SQL 命令:

CREATE TABLE messages (id SERIAL PRIMARY KEY, text VARCHAR(255));
INSERT INTO messages (text) VALUES ('来自数据库的问候!');
SELECT * FROM messages;
\q

\q 命令会退出 psql 终端。你现在回到了宿主机的终端。

这演示了 docker compose exec 如何让你与特定服务进行交互,以实现管理或调试目的,而无需直接登录到宿主机或容器内。

步骤 4: 销毁应用(保留数据)

当你完成开发或者想停止应用但为下次保留数据库数据时:

docker compose down

这会停止并删除 webdb 容器以及 app-network 网络。最关键的是,它不会删除 db-data 命名卷,从而保留了你的 messages 表数据。

验证容器是否已消失:

docker compose ps

不应该列出任何服务。

步骤 5: 重启应用(携带持久化数据)

现在,让我们再次启动应用。因为我们在 down 时没有使用 --volumes,我们的 db-data 卷依然存在。

docker compose up -d

Docker Compose 将重新创建容器和网络。db 服务会找到其现有的 db-data 卷并使用它。如果你应用设计为展示该数据,那么你之前插入的数据依然会在那里。

步骤 6: 彻底销毁并清除所有数据

如果你想要一个完全干净的环境,包括删除所有持久化数据:

docker compose down --volumes

这会停止并删除容器、网络以及 db-data 卷。你的 messages 表和任何其他数据库数据将被永久删除

验证卷是否已消失:

docker volume ls

你应该再也看不到 my-web-app_db-data 这个卷了。