使用 docker-compose.yml 定义多容器应用程序

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

在本指南中,步骤 4 中引入了 docker-compose.yml 文件 。生成多容器 Docker 应用程序时,请在docker-compose.yml中定义服务。 但是,还有其他一些使用 docker-compose 文件的方法,值得进一步详细探索。

例如,可以显式描述如何在 docker-compose.yml 文件中部署多容器应用程序。 (可选)还可以描述如何生成自定义 Docker 映像。 (也可以使用 Docker CLI 生成自定义 Docker 映像。

基本上,定义要部署的每个容器以及每个容器部署的特定特征。 有了多容器部署说明文件后,可以在 docker-compose up CLI 命令协调的单个作中部署整个解决方案,也可以从 Visual Studio 以透明方式部署该解决方案。 否则,需要在命令行中使用 docker run 命令,通过 Docker CLI 经多个步骤逐个部署容器。 因此,docker-compose.yml 中定义的每个服务都必须指定一个映像或生成映像。 其他键是可选的,类似于其 docker run 命令行对应项。

以下 YAML 代码是 eShopOnContainers 示例的单个 docker-compose.yml 文件的定义,文件可能是全局文件。 此代码不是 eShopOnContainers 中的实际 docker-compose 文件。 相反,它是单个文件中简化和合并的版本,这不是使用 docker-compose 文件的最佳方式,稍后将对此进行说明。

version: '3.4'

services:
  webmvc:
    image: eshop/webmvc
    environment:
      - CatalogUrl=http://catalog-api
      - OrderingUrl=http://ordering-api
      - BasketUrl=http://basket-api
    ports:
      - "5100:80"
    depends_on:
      - catalog-api
      - ordering-api
      - basket-api

  catalog-api:
    image: eshop/catalog-api
    environment:
      - ConnectionString=Server=sqldata;Initial Catalog=CatalogData;User Id=sa;Password=[PLACEHOLDER]
    expose:
      - "80"
    ports:
      - "5101:80"
    #extra hosts can be used for standalone SQL Server or services at the dev PC
    extra_hosts:
      - "CESARDLSURFBOOK:10.0.75.1"
    depends_on:
      - sqldata

  ordering-api:
    image: eshop/ordering-api
    environment:
      - ConnectionString=Server=sqldata;Database=Services.OrderingDb;User Id=sa;Password=[PLACEHOLDER]
    ports:
      - "5102:80"
    #extra hosts can be used for standalone SQL Server or services at the dev PC
    extra_hosts:
      - "CESARDLSURFBOOK:10.0.75.1"
    depends_on:
      - sqldata

  basket-api:
    image: eshop/basket-api
    environment:
      - ConnectionString=sqldata
    ports:
      - "5103:80"
    depends_on:
      - sqldata

  sqldata:
    environment:
      - SA_PASSWORD=[PLACEHOLDER]
      - ACCEPT_EULA=Y
    ports:
      - "5434:1433"

  basketdata:
    image: redis

此文件中的根密钥是服务。 在该密钥下,您定义了要部署和运行的服务,这些服务将在您执行 docker-compose up 命令时或在使用此 docker-compose.yml 文件从 Visual Studio 部署时启动。 在这种情况下,docker-compose.yml文件定义了多个服务,如下表所述。

服务名称 DESCRIPTION
webmvc 容器,包括从服务器端 C# 使用微服务的 ASP.NET Core MVC 应用程序
catalog-api 包含目录 ASP.NET 核心 Web API 微服务的容器
ordering-api 容器,包括 Ordering ASP.NET Core Web API 微服务
sqldata 运行适用于 Linux 的 SQL Server 的容器,保存微服务数据库
basket-api 包含购物篮 ASP.NET 核心 Web API 微服务的容器
basketdata 运行 REDIS 缓存服务的容器,将篮数据库用作 REDIS 缓存

简单的 Web 服务 API 容器

专注于单个容器,catalog-api container-microservice 的定义很简单。

  catalog-api:
    image: eshop/catalog-api
    environment:
      - ConnectionString=Server=sqldata;Initial Catalog=CatalogData;User Id=sa;Password=[PLACEHOLDER]
    expose:
      - "80"
    ports:
      - "5101:80"
    #extra hosts can be used for standalone SQL Server or services at the dev PC
    extra_hosts:
      - "CESARDLSURFBOOK:10.0.75.1"
    depends_on:
      - sqldata

此容器化服务具有以下基本配置:

  • 它基于自定义 eshop/catalog-api 映像。 为简便起见,文件中没有 build: key 设置。 这意味着映像必须以前生成(使用 docker 生成)或已从任何 Docker 注册表下载(使用 docker 拉取命令)。

  • 该定义了一个名为 ConnectionString 的环境变量,其中包含 Entity Framework 用于访问包含目录数据模型的 SQL Server 实例的连接字符串。 在这种情况下,同一 SQL Server 容器保存多个数据库。 因此,在用于 Docker 的开发计算机中,需要更少的内存。 但是,还可以为每个微服务数据库部署一个 SQL Server 容器。

  • SQL Server 名称是 sqldata,它与运行适用于 Linux 的 SQL Server 实例的容器使用相同的名称。 这非常便利;使用此域名解析(在 Docker 主机内部)可以解析网络地址,因此您无需知道从其他容器访问的容器的内部 IP。

由于连接字符串是由环境变量定义的,因此可以通过不同的机制和不同的时间设置该变量。 例如,您可以在最终主机中部署到生产环境时设置不同的连接字符串,或者在您的 Azure DevOps Services 或首选的 DevOps 系统中的 CI/CD 管道中执行此操作。

  • 它公开端口 80,供内部访问 Docker 主机中的 catalog-api 服务。 主机当前是 Linux VM,因为它基于适用于 Linux 的 Docker 映像,但可以将容器配置为改为在 Windows 映像上运行。

  • 它将容器上的公开端口 80 转发到 Docker 主机(Linux VM)上的端口 5101。

  • 它将 Web 服务链接到 sqldata 服务(容器中运行的 Linux 数据库的 SQL Server 实例)。 指定此依赖项时,在 sqldata 容器已启动之前,catalog-api 容器将不会启动;这一方面很重要,因为 catalog-api 需要首先启动并运行 SQL Server 数据库。 但是,在许多情况下,这种容器依赖项是不够的,因为 Docker 仅在容器级别进行检查。 有时服务(在本例中,SQL Server)可能尚未准备就绪,因此建议在客户端微服务中使用指数退避实现重试逻辑。 这样一来,如果依赖项容器在短时间内未准备就绪,应用程序仍将具有复原能力。

  • 它配置为允许访问外部服务器:extra_hosts 设置会让您能够访问 Docker 主机之外的服务器或计算机(也就是说,访问默认的用于开发的 Linux 虚拟机之外的设备),例如您开发电脑上的本地 SQL Server 实例。

下面各节还介绍了其他更高级 docker-compose.yml 的设置。

使用 docker-compose 文件以目标多个环境

这些文件 docker-compose.*.yml 是定义文件,可供多个理解该格式的基础结构使用。 最简单的工具是 docker-compose 命令。

因此,通过使用 docker-compose 命令,可以针对以下主要方案。

开发环境

开发应用程序时,必须能够在隔离的开发环境中运行应用程序。 可以使用 docker-compose CLI 命令或 Visual Studio 来创建该环境,后者在后台运行 docker-compose。

docker-compose.yml文件允许配置和记录应用程序的所有服务依赖项(其他服务、缓存、数据库、队列等)。 使用 docker-compose CLI 命令,可以使用单个命令(docker-compose up)为每个依赖项创建和启动一个或多个容器。

docker-compose.yml文件是由 Docker 引擎解释的配置文件,但也可用作有关多容器应用程序的构成的便捷文档文件。

测试环境

任何持续部署(CD)或持续集成(CI)过程的重要组成部分是单元测试和集成测试。 这些自动测试需要隔离的环境,因此它们不受用户或应用程序数据的任何其他更改的影响。

使用 Docker Compose,可以在命令提示符或脚本的几个命令中轻松创建和销毁隔离的环境,如以下命令:

docker-compose -f docker-compose.yml -f docker-compose-test.override.yml up -d
./run_unit_tests
docker-compose -f docker-compose.yml -f docker-compose-test.override.yml down

生产部署

还可以使用 Compose 部署到远程 Docker 引擎。 典型情况是部署到单个 Docker 主机实例。

如果使用任何其他编排器(例如 Azure Service Fabric 或 Kubernetes),则可能需要添加配置和元数据设置,类似于 docker-compose.yml 中的那些设置,但采用其他编排器所需的格式。

在任何情况下,docker-compose 都是用于开发、测试和生产工作流的便捷工具和元数据格式,尽管生产工作流可能因所使用的业务流程协调程序而异。

使用多个 docker-compose 文件来处理多个环境

在针对不同环境时,应使用多个 Compose 文件。 此方法允许根据环境创建多个配置变体。

重写基本 docker-compose 文件

可以使用单个docker-compose.yml文件,如前面部分所示的简化示例所示。 但是,对于大多数应用程序,不建议这样做。

默认情况下,Compose 读取两个文件:一个docker-compose.yml和一个可选的docker-compose.override.yml文件。 如图 6-11 所示,使用 Visual Studio 并启用 Docker 支持时,Visual Studio 还会创建一个用于调试应用程序的其他docker-compose.vs.debug.g.yml文件,可以在主解决方案文件夹中的文件夹中查看此文件 obj\Docker\ 。

docker compose 项目中的文件。

图 6-11. Visual Studio 2019 中的 docker-compose 文件

docker-compose 项目文件结构:

  • .dockerignore - 用于忽略文件
  • docker-compose.yml - 用于撰写微服务
  • docker-compose.override.yml - 用于配置微服务环境

可以使用任何编辑器(如 Visual Studio Code 或 Sublime)编辑 docker-compose 文件,并使用 docker-compose up 命令运行应用程序。

按照约定,docker-compose.yml文件包含基本配置和其他静态设置。 这意味着服务配置不应根据目标部署环境而更改。

顾名思义,docker-compose.override.yml 文件包含替代基本配置的配置设置,例如依赖于部署环境的配置。 用户也可以拥有具有不同名称的多个重写文件。 重写文件通常包含应用程序所需的其他信息,但特定于环境或部署。

针对多个环境

典型的用例是定义多个撰写文件,以便可以面向多个环境,例如生产环境、过渡环境、CI 或开发环境。 若要支持这些差异,可以将 Compose 配置拆分为多个文件,如图 6-12 所示。

三个 docker-compose 文件设置为替代基文件的关系图。

图 6-12. 重写基本 docker-compose.yml 文件中的值的多个 docker-compose 文件

可以合并多个 docker-compose*.yml 文件来处理不同的环境。 从基本docker-compose.yml文件开始。 此基文件包含不根据环境更改的基本或静态配置设置。 例如,eShopOnContainers 应用将以下docker-compose.yml文件(使用更少的服务简化)作为基本文件。

#docker-compose.yml (Base)
version: '3.4'
services:
  basket-api:
    image: eshop/basket-api:${TAG:-latest}
    build:
      context: .
      dockerfile: src/Services/Basket/Basket.API/Dockerfile
    depends_on:
      - basketdata
      - identity-api
      - rabbitmq

  catalog-api:
    image: eshop/catalog-api:${TAG:-latest}
    build:
      context: .
      dockerfile: src/Services/Catalog/Catalog.API/Dockerfile
    depends_on:
      - sqldata
      - rabbitmq

  marketing-api:
    image: eshop/marketing-api:${TAG:-latest}
    build:
      context: .
      dockerfile: src/Services/Marketing/Marketing.API/Dockerfile
    depends_on:
      - sqldata
      - nosqldata
      - identity-api
      - rabbitmq

  webmvc:
    image: eshop/webmvc:${TAG:-latest}
    build:
      context: .
      dockerfile: src/Web/WebMVC/Dockerfile
    depends_on:
      - catalog-api
      - ordering-api
      - identity-api
      - basket-api
      - marketing-api

  sqldata:
    image: mcr.microsoft.com/mssql/server:2019-latest

  nosqldata:
    image: mongo

  basketdata:
    image: redis

  rabbitmq:
    image: rabbitmq:3-management

基docker-compose.yml文件中的值不应因不同的目标部署环境而更改。

例如,如果专注于 Webmvc 服务定义,则无论目标环境如何,该信息都完全相同。 你有以下信息:

  • 服务名称:webmvc。

  • 容器的自定义映像:eshop/webmvc。

  • 用于生成自定义 Docker 映像的命令,指示要使用的 Dockerfile。

  • 依赖于其他服务,因此此容器在启动其他依赖项容器之前不会启动。

可以进行其他配置,但要点是,在基本docker-compose.yml文件中,你只想设置跨环境通用的信息。 然后,在 docker-compose.override.yml 或用于生产或暂存的类似文件中,应配置特定于每个环境的配置。

通常,docker-compose.override.yml 适用于开发环境,如 eShopOnContainers 中的以下示例所示:

#docker-compose.override.yml (Extended config for DEVELOPMENT env.)
version: '3.4'

services:
# Simplified number of services here:

  basket-api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - ConnectionString=${ESHOP_AZURE_REDIS_BASKET_DB:-basketdata}
      - identityUrl=http://identity-api
      - IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
      - EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
      - EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
      - EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
      - AzureServiceBusEnabled=False
      - ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
      - OrchestratorType=${ORCHESTRATOR_TYPE}
      - UseLoadTest=${USE_LOADTEST:-False}

    ports:
      - "5103:80"

  catalog-api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - ConnectionString=${ESHOP_AZURE_CATALOG_DB:-Server=sqldata;Database=Microsoft.eShopOnContainers.Services.CatalogDb;User Id=sa;Password=[PLACEHOLDER]}
      - PicBaseUrl=${ESHOP_AZURE_STORAGE_CATALOG_URL:-http://host.docker.internal:5202/api/v1/catalog/items/[0]/pic/}
      - EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
      - EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
      - EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
      - AzureStorageAccountName=${ESHOP_AZURE_STORAGE_CATALOG_NAME}
      - AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_CATALOG_KEY}
      - UseCustomizationData=True
      - AzureServiceBusEnabled=False
      - AzureStorageEnabled=False
      - ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
      - OrchestratorType=${ORCHESTRATOR_TYPE}
    ports:
      - "5101:80"

  marketing-api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - ConnectionString=${ESHOP_AZURE_MARKETING_DB:-Server=sqldata;Database=Microsoft.eShopOnContainers.Services.MarketingDb;User Id=sa;Password=[PLACEHOLDER]}
      - MongoConnectionString=${ESHOP_AZURE_COSMOSDB:-mongodb://nosqldata}
      - MongoDatabase=MarketingDb
      - EventBusConnection=${ESHOP_AZURE_SERVICE_BUS:-rabbitmq}
      - EventBusUserName=${ESHOP_SERVICE_BUS_USERNAME}
      - EventBusPassword=${ESHOP_SERVICE_BUS_PASSWORD}
      - identityUrl=http://identity-api
      - IdentityUrlExternal=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105
      - CampaignDetailFunctionUri=${ESHOP_AZUREFUNC_CAMPAIGN_DETAILS_URI}
      - PicBaseUrl=${ESHOP_AZURE_STORAGE_MARKETING_URL:-http://host.docker.internal:5110/api/v1/campaigns/[0]/pic/}
      - AzureStorageAccountName=${ESHOP_AZURE_STORAGE_MARKETING_NAME}
      - AzureStorageAccountKey=${ESHOP_AZURE_STORAGE_MARKETING_KEY}
      - AzureServiceBusEnabled=False
      - AzureStorageEnabled=False
      - ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
      - OrchestratorType=${ORCHESTRATOR_TYPE}
      - UseLoadTest=${USE_LOADTEST:-False}
    ports:
      - "5110:80"

  webmvc:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
      - PurchaseUrl=http://webshoppingapigw
      - IdentityUrl=http://10.0.75.1:5105
      - MarketingUrl=http://webmarketingapigw
      - CatalogUrlHC=http://catalog-api/hc
      - OrderingUrlHC=http://ordering-api/hc
      - IdentityUrlHC=http://identity-api/hc
      - BasketUrlHC=http://basket-api/hc
      - MarketingUrlHC=http://marketing-api/hc
      - PaymentUrlHC=http://payment-api/hc
      - SignalrHubUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5202
      - UseCustomizationData=True
      - ApplicationInsights__InstrumentationKey=${INSTRUMENTATION_KEY}
      - OrchestratorType=${ORCHESTRATOR_TYPE}
      - UseLoadTest=${USE_LOADTEST:-False}
    ports:
      - "5100:80"
  sqldata:
    environment:
      - SA_PASSWORD=[PLACEHOLDER]
      - ACCEPT_EULA=Y
    ports:
      - "5433:1433"
  nosqldata:
    ports:
      - "27017:27017"
  basketdata:
    ports:
      - "6379:6379"
  rabbitmq:
    ports:
      - "15672:15672"
      - "5672:5672"

在此示例中,开发重写配置向主机公开一些端口,使用重定向 URL 定义环境变量,并为开发环境指定连接字符串。 这些设置都仅适用于开发环境。

运行 docker-compose up(或从 Visual Studio 启动它时)时,该命令会自动读取替代内容,就像它已合并这两个文件。

假设需要生产环境的另一个 Compose 文件,其中包含不同的配置值、端口或连接字符串。 您可以创建另一个覆盖文件,例如命名为 docker-compose.prod.yml 的文件,用于不同的设置和环境变量。 该文件可能存储在不同的 Git 存储库中,或者由其他团队管理和保护。

如何使用特定的替代文件进行部署

若要使用多个替代文件或具有不同名称的替代文件,可以将 -f 选项与 docker-compose 命令一起使用并指定文件。 Compose 会按照在命令行指定的顺序合并文件。 以下示例演示如何使用替代文件进行部署。

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

在 docker-compose 文件中使用环境变量

方便,尤其是在生产环境中,能够从环境变量获取配置信息,如前面的示例所示。 可以使用语法 ${MY_VAR} 在 docker-compose 文件中引用环境变量。 docker-compose.prod.yml文件中的以下行演示如何引用环境变量的值。

IdentityUrl=http://${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}:5105

根据主机环境(Linux、Windows、云群集等),以不同的方式创建和初始化环境变量。 但是,一种方便的方法是使用 .env 文件。 docker-compose 文件支持在 .env 文件中声明默认环境变量。 环境变量的这些值是默认值。 但是,可以通过你可能在每个环境中定义的值(群集中的主机 OS 或环境变量)来替代它们。 将此 .env 文件放置在从中执行 docker-compose 命令的文件夹。

以下示例显示了一个 .env 文件,如 eShopOnContainers 应用程序的 .env 文件。

# .env file

ESHOP_EXTERNAL_DNS_NAME_OR_IP=host.docker.internal

ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP=10.121.122.92

Docker-compose 要求 .env 文件中的每一行都采用格式 <variable>=<value>。

在运行时环境中设置的值始终替代 .env 文件中定义的值。 同样,通过命令行参数传递的值也会重写 .env 文件中设置的默认值。

其他资源

构建优化的 ASP.NET Core Docker 映像

如果你在互联网资源中探索 Docker 和 .NET,你会发现有一些 Dockerfiles 演示了通过将你的资源复制到容器中来构建 Docker镜像的简单性。 这些示例表明,通过使用简单的配置,可以将 Docker 映像与应用程序打包的环境配合使用。 以下示例显示在此情况下的简单 Dockerfile。

FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
ENV ASPNETCORE_URLS http://+:80
EXPOSE 80
COPY . .
RUN dotnet restore
ENTRYPOINT ["dotnet", "run"]

这样的 Dockerfile 为有效 Dockerfile。 但开发人员可以大幅优化映像,尤其是生产映像。

在容器和微服务模型中,会不断启动容器。 使用容器时,通常不会重启睡眠容器,因为该容器为一次性容器。 业务流程协调程序(如 Kubernetes 和 Azure Service Fabric)创建新的映像实例。 这意味着,在生成应用程序时需要通过预编译应用程序来优化,以便实例化过程会更快。 启动容器后,应准备好运行。 请勿使用 dotnet restoredotnet build CLI 命令在运行时还原和编译,如有关 .NET 和 Docker 的博客文章中所示。

.NET 团队一直在做重要工作,使 .NET 和 ASP.NET Core 成为容器优化的框架。 .NET 不仅是一个内存占用量较小的轻型框架;团队专注于针对三个主要方案的优化 Docker 映像,并在 dotnet/的 Docker 中心注册表中发布这些映像,从版本 2.1 开始:

  • 开发:优先级是快速迭代和调试更改,大小则是次要因素。
  • 生成:编译应用程序是优先事项,此映像包括二进制文件和其他可优化二进制文件的依赖项。
  • 生产:重点是快速部署和启动容器,因此这些映像仅限于运行应用程序所需的二进制文件和内容。

.NET 团队在 dotnet/中提供了一些基本变体,例如:

  • sdk:用于开发和生成方案
  • aspnet:适用于 ASP.NET 生产方案
  • 运行时:适用于 .NET 生产方案
  • runtime-deps:用于自包含应用程序的生产方案

为了更快地启动,运行时映像还会自动将 aspnetcore_urls 设置为端口 80,并使用 Ngen 创建程序集的本地映像缓存。

其他资源