如何在Docker Compose中等待MSSQL?

32

我有一个依赖于MSSQL的服务(一个ASP.NET Core Web应用程序)。这些服务使用Docker compose进行编排,我希望docker compose在运行我的服务之前首先启动数据库并等待其准备就绪。为此,我正在定义docker-compose.yml

version: '3.7'

services:

  sql.data:
    container_name: db_service
    image: microsoft/mssql-server-linux:2017-latest
    healthcheck:
      test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "http://localhost:1433", "-U", "sa", "-P", "Pass_word", "-Q", "SELECT 1", "||", "exit 1"]

  my_service:
    container_name: my_service_container
    image: ${DOCKER_REGISTRY-}my_service
    build:
      context: .
      dockerfile: MyService/Dockerfile
    depends_on:
      - sql.data

通过这个健康检查,Docker Compose 不会等待数据库服务准备就绪,而是立即启动my_service。如预期的那样,my_service在连接数据库时失败。其中一部分日志如下:

Recreating db_service ... done
Recreating my_service_container ... done
Attaching to db_service, my_service_container 
my_service_container | info: ...Context[0]
my_service_container |       Migrating database associated with context Context
my_service_container | info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
my_service_container |       Entity Framework Core 3.1.1 initialized 'Context' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: MigrationsAssembly=MyService
my_service_container | fail: Context[0]
my_service_container |       An error occurred while migrating the database used on context Context
my_service_container | Microsoft.Data.SqlClient.SqlException (0x80131904): A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: TCP Provider, error: 40 - Could not open a connection to SQL Server)
...
exception details
...
my_service_container | ClientConnectionId:00000000-0000-0000-0000-000000000000
my_service_container exited with code 0
db_service | 2020-03-05 05:45:51.82 Server      Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64)
        Nov 30 2018 12:57:58
        Copyright (C) 2017 Microsoft Corporation
        Developer Edition (64-bit) on Linux (Ubuntu 16.04.5 LTS)
2020-03-05 05:45:51.82 Server      UTC adjustment: 0:00
2020-03-05 05:45:51.82 Server      (c) Microsoft Corporation.
2020-03-05 05:45:51.82 Server      All rights reserved.
2020-03-05 05:45:51.82 Server      Server process ID is 4120.
2020-03-05 05:45:51.82 Server      Logging SQL Server messages in file '/var/opt/mssql/log/errorlog'.
2020-03-05 05:45:51.82 Server      Registry startup parameters:
         -d /var/opt/mssql/data/master.mdf
         -l /var/opt/mssql/data/mastlog.ldf
         -e /var/opt/mssql/log/errorlog

根据日志显示,Docker Compose首先启动了数据库,但在运行我的服务之前并没有等待它变为就绪。

我尝试了不同的语法来使用 healthcheck,例如:

test: /opt/mssql-tools/bin/sqlcmd -S http://localhost:1433 -U sa -P ${SA_PASSWORD} -Q "SELECT 1" || exit 1

但是两者都没有按预期工作。

我已在以下在线来源中检查,但使用任何一种都无法解决问题:

这个功能在版本3.7中是否得到支持?因为这个令人困惑的评论


问题

有什么好的方法可以等待MSSQL服务启动吗?

8个回答

16

在尝试了许多不同的方案后,我成功地通过以下composer文件添加了等待。这是针对asp.net核心解决方案的。关键是如果dockerfile中指定了entrypoint,则必须覆盖它。另外,你需要确保将“wait-for-it.sh”的行尾LF保存为而不是CRLF,否则你会收到file not found错误。

dockerfile应该包含以下内容(从此处下载: https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh,请确保保存文件):

COPY ./wait-for-it.sh /wait-for-it.sh
RUN chmod +x wait-for-it.sh

docker-compose.yml

的翻译是:

docker-compose.yml

version: '3.7'

services:

  vc-db:
    image: mcr.microsoft.com/mssql/server:latest
    ports:
      - "${DOCKER_SQL_PORT:-1433}:1433"
    expose:  
      - 1433  
    environment: 
      - ACCEPT_EULA=Y
      - MSSQL_PID=Express
      - SA_PASSWORD=v!rto_Labs!
    networks:
      - virto

  vc-platform-web:
    image: virtocommerce/platform:${DOCKER_TAG:-latest}
    ports:
      - "${DOCKER_PLATFORM_PORT:-8090}:80"
    environment:
      - ASPNETCORE_URLS=http://+
    depends_on:
      - vc-db
    entrypoint: ["/wait-for-it.sh", "vc-db:1433", "-t", "120", "--", "dotnet", "VirtoCommerce.Platform.Web.dll"]
    networks:
      - virto

如何通过Docker连接内部MSSQL数据库? 当Docker运行时,我有一个带有MSSQL数据库的服务器与主机在同一网络中。在我的简单Flask应用程序中,我使用pyodbc连接到数据库。 在另一个应用程序中,我使用Docker的sqlite数据库文件,连接起来很简单。 - Andrew Nos
我认为这种“等待”和类似的方法只是半个解决方案:如果由于错误或任何原因,容器/SQL容器无法启动,我们该怎么办?我们会一直等下去吗?或者如果我们使用超时,如何确保在特定环境中这个超时足够? - Evgeny Gorbovoy
@EugeneGorbovoy 我认为等待相关服务变得可用是合理的,此时调度程序的工作就是抛出错误。超时是一种hack,因为我们真的不知道服务启动需要多长时间。 - Woland

9
当您使用depends_on时,docker-compose仅以更高的优先级启动基础服务,并永远不会等待启动服务。
有一些有用的外部程序可以帮助您等待特定服务(端口),然后运行另一个服务。 vishnubob/wait-for-it是其中之一,它会阻塞执行流程,直到您的特定端口准备就绪。
另一个很好的选择是eficode/wait-for,它已经为docker-compose做好了准备。
示例用法(根据eficode/wait-for文档)
version: '2'

services:
  db:
    image: postgres:9.4

  backend:
    build: backend
    # Blocks execution flow util db:5432 is ready (Or you can use localhost instead)
    command: sh -c './wait-for db:5432 -- npm start'
    depends_on:
      - db

-- 更新 --

假设你有一个依赖于数据库(如PostgreSQL)的Python应用程序,并且你的应用程序将通过以下命令运行:python app.py
根据Docker官方文档,在你的镜像中(与其他项目文件一起,如app.py),添加vishnubob/wait-for-it

现在只需将以下内容放在你的docker-compose.yml文件中:

version: "3"
services:
  web:
    build: .
    ports:
      - "80:8000"
    depends_on:
      - "db"
    # This command waits until `db:5432` respond (5432 is default PostgreSQL port)
    # then runs our application by this command: `python app.py`
    command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
  db:
    image: postgres

注意:不要忘记将这些命令放在你的镜像文件中的 Dockerfile 中:
# Copy wait-for-it.sh into our image
COPY wait-for-it.sh wait-for-it.sh
# Make it executable, in Linux
RUN chmod +x wait-for-it.sh

1
你能提供一个完整的功能示例吗?例如,./wait-for 命令应该安装在 db 镜像上吗? - Dr. Strangelove
我在镜像文件夹的根目录中添加了wait-for-it.sh文件,但在构建时,Docker 20.10.2显示:在Windows 10 19.09上chmod: wait-for-it.sh: 操作不允许。有什么想法吗? - JoanComasFdz
1
@JoanComasFdz 是你的容器镜像 Windows,chmod 是一个 Linux 命令。 - Ajay Bhasy
@AjayBhasy 我正在使用 Docker Desktop 在 Windows 10 1909 上运行 Linux 容器。 - JoanComasFdz
@JoanComasFdz 我将脚本文件添加到了我的项目中,这导致它被复制到发布文件夹中。然后在 Dockerfile 的发布部分,在 EntryPoint 之前运行了这个命令,它成功地运行了。 - Ajay Bhasy

6

创建两个不同的dockerfile(例如):

  1. Mssql.Dockerfile
  2. App.Dockerfile

在docker-compose.yml中设置顺序。

Mssql.Dockerfile

FROM mcr.microsoft.com/mssql/server AS base

ENV ACCEPT_EULA=Y
ENV SA_PASSWORD=Password123

COPY . .
COPY ["Db/Scripts/*", "Db/Scripts/"]
VOLUME ./Db:/var/opt/mssql/data

HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=10 \
    CMD /opt/mssql-tools/bin/sqlcmd -S . -U sa -P Password123 -i Db/Scripts/SetupDb.sql || exit 1

App.Dockerfile:

    FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
    WORKDIR /app
    EXPOSE 80
    EXPOSE 443

    FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
    WORKDIR /src
    COPY ["AspNetCoreWebApplication/AspNetCoreWebApplication.csproj", "AspNetCoreWebApplication/"]
    COPY ["WebApp.Data.EF/WebApp.Data.EF.csproj", "WebApp.Data.EF/"]
    COPY ["WebApp.Service/WebApp.Service.csproj", "WebApp.Service/"]

    RUN dotnet restore "AspNetCoreWebApplication/AspNetCoreWebApplication.csproj"
    COPY . .
    WORKDIR "/src/AspNetCoreWebApplication"
    RUN dotnet build "AspNetCoreWebApplication.csproj" -c Release -o /app/build
    FROM build AS publish
    RUN dotnet publish "AspNetCoreWebApplication.csproj" -c Release -o /app/publish

    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "AspNetCoreWebApplication.dll"]

Docker-compose.yml:

version: '3.7'

services:
    api:
        image: aspnetcore/mentoring_api
        container_name: mentoring_api
        build:
            context: .
            dockerfile: App.Dockerfile
        ports:
            - 8081:80
        expose: 
            - 8081
        environment:
            ASPNETCORE_ENVIRONMENT: Development
        depends_on:
            - sqlserver
    sqlserver:
        image: aspnetcore/mentoring_db
        container_name: mentoring_db
        build:
            context: .
            dockerfile: Mssql.Dockerfile
        ports:
            - "1433:1433"
        expose: 
            - 1433
        environment:
            - ACCEPT_EULA=Y
            - SA_PASSWORD=Password123
        volumes:
            - ./Db:/var/opt/mssql/data

注意: 连接字符串的格式如下:"Server=sqlserver;Database=Northwind;Trusted_Connection=False;User Id=sa;Password=Password123;MultipleActiveResultSets=true"


你设置了 ENV SA_PASSWORD 但是在 HEALTHCHECK 中又将密码写出来。 - caduceus
嘿 @caduceus,这是什么问题? - Shahar Shokrani
@ShaharShokrani 如果你不打算使用变量,为什么要设置它呢? - caduceus
在您之前的评论中,您是打算说在ENV还是在CMD中密码不是必要的? - Shahar Shokrani
不确定健康检查是否能在没有通过健康检查的 CMD 提供密码的情况下正常工作,也许我错了,但我认为两者都是必要的,值得检查一下。 - Shahar Shokrani

6

我认为你的初步尝试实际上并不远离成功。使用健康检查似乎是最合适的方法,因此我会继续采用这种方法,但是你需要利用 depends_oncondition 功能。使用它,你可以使用 service_healthy 条件,它将等待 SQL Server 健康检查报告正常。

请参阅 Docker 网站上提到这一点的文章: https://docs.docker.com/compose/startup-order/

你的 docker-compose.yml 将如下所示:

version: '3.7'

services:

  sql.data:
    container_name: db_service
    image: microsoft/mssql-server-linux:2017-latest
    healthcheck:
      test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "http://localhost:1433", "-U", "sa", "-P", "Pass_word", "-Q", "SELECT 1", "||", "exit 1"]

  my_service:
    container_name: my_service_container
    image: ${DOCKER_REGISTRY-}my_service
    build:
      context: .
      dockerfile: MyService/Dockerfile
    depends_on:
      sql.data:
        condition: service_healthy

你也可以利用以下的healthcheck选项,以及稍微更整洁的语法:

version: '3.7'

services:

  sql.data:
    container_name: db_service
    image: microsoft/mssql-server-linux:2017-latest
    healthcheck:
      test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "Pass_word" -Q "SELECT 1" -b -o /dev/null
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 10s

  my_service:
    container_name: my_service_container
    image: ${DOCKER_REGISTRY-}my_service
    build:
      context: .
      dockerfile: MyService/Dockerfile
    depends_on:
      sql.data:
        condition: service_healthy

这对我来说似乎是最干净的方法(除了Kubernetes解决方案之外)!更多信息请参阅docker compose文档 - CodeFox
请参阅功能请求#133,其中包含有关一些边缘情况的其他示例和讨论。 - CodeFox

5

这里有一个完整的示例

version: "3.8"

services:
  ms-db-server:
    image: mcr.microsoft.com/mssql/server
    environment: 
      - SA_PASSWORD=P@ssw0rd
      - ACCEPT_EULA=Y
    volumes:
      - ./data/db/mssql/scripts:/scripts/
    ports:
      - "1433:1433"
    #entrypoint: /bin/bash
    command:
      - /bin/bash
      - -c
      - |
        /opt/mssql/bin/sqlservr &
        pid=$$!

        echo "Waiting for MS SQL to be available ⏳"
        /opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -h-1 -V1 -U sa -P $$SA_PASSWORD -Q "SET NOCOUNT ON SELECT \"YAY WE ARE UP\" , @@servername"
        is_up=$$?
        while [ $$is_up -ne 0 ] ; do
          echo -e $$(date)
          /opt/mssql-tools/bin/sqlcmd -l 30 -S localhost -h-1 -V1 -U sa -P $$SA_PASSWORD -Q "SET NOCOUNT ON SELECT \"YAY WE ARE UP\" , @@servername"
          is_up=$$?
          sleep 5
        done

        for foo in /scripts/*.sql
          do /opt/mssql-tools/bin/sqlcmd -U sa -P $$SA_PASSWORD -l 30 -e -i $$foo
        done
        echo "All scripts have been executed. Waiting for MS SQL(pid $$pid) to terminate."

        wait $$pid

  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./etc/tempo-local.yaml:/etc/tempo.yaml
      - ./data/tempo-data:/tmp/tempo
    ports:
      - "14268"      # jaeger ingest, Jaeger - Thrift HTTP
      - "14250"      # Jaeger - GRPC
      - "55680"      # OpenTelemetry
      - "3100"       # tempo
      - "6831/udp"   # Jaeger - Thrift Compact
      - "6832/udp"   # Jaeger - Thrift Binary   

  tempo-query:
    image: grafana/tempo-query:latest
    command: ["--grpc-storage-plugin.configuration-file=/etc/tempo-query.yaml"]
    volumes:
      - ./etc/tempo-query.yaml:/etc/tempo-query.yaml
    ports:
      - "16686:16686"  # jaeger-ui
    depends_on:
      - tempo

  loki:
    image: grafana/loki:2.1.0
    command: -config.file=/etc/loki/loki-local.yaml
    ports:
      - "3101:3100"                                   # loki needs to be exposed so it receives logs
    environment:
      - JAEGER_AGENT_HOST=tempo
      - JAEGER_ENDPOINT=http://tempo:14268/api/traces # send traces to Tempo
      - JAEGER_SAMPLER_TYPE=const
      - JAEGER_SAMPLER_PARAM=1
    volumes:
      - ./etc/loki-local.yaml:/etc/loki/loki-local.yaml
      - ./data/loki-data:/tmp/loki

  nodejs-otel-tempo-api:
    build: .
    command: './wait-for.sh ms-db-server:1433 -- node ./dist/server.js'
    ports:
      - "5555:5555"
    environment:
      - OTEL_EXPORTER_JAEGER_ENDPOINT=http://tempo:14268/api/traces
      - OTEL_SERVICE_NAME=nodejs-opentelemetry-tempo
      - LOG_FILE_NAME=/app/logs/nodejs-opentelemetry-tempo.log
      - DB_USER=sa
      - DB_PASS=P@ssw0rd
      - DB_SERVER=ms-db-server
      - DB_NAME=OtelTempo
    volumes:
      - ./data/logs:/app/logs
      - ./etc/wait-for.sh:/app/bin/wait-for.sh   #https://github.com/eficode/wait-for
    depends_on:
      - ms-db-server
      - tempo-query

  promtail:
    image: grafana/promtail:master-ee9c629
    command: -config.file=/etc/promtail/promtail-local.yaml
    volumes:
      - ./etc/promtail-local.yaml:/etc/promtail/promtail-local.yaml
      - ./data/logs:/app/logs
    depends_on:
      - nodejs-otel-tempo-api
      - loki

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./etc/prometheus.yaml:/etc/prometheus.yaml
    entrypoint:
      - /bin/prometheus
      - --config.file=/etc/prometheus.yaml
    ports:
      - "9090:9090"
    depends_on:
      - nodejs-otel-tempo-api

  grafana:
    image: grafana/grafana:7.4.0-ubuntu
    volumes:
      - ./data/grafana-data/datasources:/etc/grafana/provisioning/datasources
      - ./data/grafana-data/dashboards-provisioning:/etc/grafana/provisioning/dashboards
      - ./data/grafana-data/dashboards:/var/lib/grafana/dashboards
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
      - GF_AUTH_DISABLE_LOGIN_FORM=true
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
      - tempo-query
      - loki

0

你也可以延迟 Docker 启动,直到 MSSQL 启动完成:

docker-compose.yaml

  mssql:
    image: mcr.microsoft.com/mssql/server:2017-latest
    ports:
      - 1433:1433
    environment:
      SA_PASSWORD: "t9D4:EHfU6Xgccs-"
      ACCEPT_EULA: "Y"
    networks:
      - backend
    command:
      - /bin/bash
      - -c
      - |
        /opt/mssql/bin/sqlservr
        curl -s https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh | bash /dev/stdin localhost:1433


0
使用Kubernetes部署,以下探针成功地识别了就绪状态:
      containers:
        - name: mssql
          image: mcr.microsoft.com/mssql/server:2019-latest
          ports:
            - containerPort: 1433
          env:
            - name: ACCEPT_EULA
              value: 'Y'
          startupProbe:
            exec:
              command:
                - /bin/sh
                - '-c'
                - '/opt/mssql-tools/bin/sqlcmd -U sa -P "${SA_PASSWORD}" -Q "SELECT \"READY\"" | grep -q "READY"'
            failureThreshold: 15
            periodSeconds: 10

如果您的Kubernetes版本是<1.20,您可以将其编码为带有initialDelay的livenessProbe。

解释: 无论查询是否返回任何内容,sqlcmd都会返回“0”状态。 但是,grep -q根据单词“READY”的存在与否返回0或1。

我没有使用docker-compose,但我怀疑这个命令将作为健康检查测试(假设SA_PASSWORD被注入到环境中):

    healthcheck:
      test:
        - /bin/sh
        - '-c'
        - '/opt/mssql-tools/bin/sqlcmd -U sa -P "${SA_PASSWORD}" -Q "SELECT \"READY\"" | grep -q "READY"'

0

您可以编写一个简单的脚本,它将在包含您的应用程序的容器中启动。例如,您可以设置延迟,使用sleep N(其中N是启动您的DB所需的时间),或者您可以在其中使用until循环来尝试连接到您的DB,当可能时,然后您可以启动您的应用程序。

我知道这不是一个完美的解决方案,但当我遇到类似的问题时,它帮助了我。


1
检查我们应用程序内部的连接是个好主意,但是Docker文档本身建议使用wait-for-it和其他替代脚本。 - user12958509
1
当然,我不会反驳这个观点,我只是提供了一个简单(并非完美)的解决方案。 - Yurii Paneiko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接