Docker Compose 在启动 Y 容器前等待 X 容器完成

618

我正在使用RabbitMQ和来自这里的简单Python示例,以及Docker Compose。我的问题是,我需要等待RabbitMQ完全启动。从我迄今为止搜索的内容来看,我不知道如何在容器x(在我的情况下是worker)等待y(RabbitMQ)启动。

我找到了这篇博客文章,在其中他检查另一个主机是否在线。我还找到了这个Docker命令

wait

用法:docker wait CONTAINER [CONTAINER...]

阻塞,直到容器停止,然后打印其退出代码。

也许等待容器停止不是我想要的,但如果是,是否可以在docker-compose.yml中使用该命令?到目前为止,我的解决方案是等待几秒钟并检查端口,但这是达成此目的的方法吗?如果我不等待,就会出现错误。

docker-compose.yml

worker:
    build: myapp/.
    volumes:
    - myapp/.:/usr/src/app:ro

    links:
    - rabbitmq
rabbitmq:
    image: rabbitmq:3-management

Python初学示例(rabbit.py):

import pika
import time

import socket

pingcounter = 0
isreachable = False
while isreachable is False and pingcounter < 5:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect(('rabbitmq', 5672))
        isreachable = True
    except socket.error as e:
        time.sleep(2)
        pingcounter += 1
    s.close()

if isreachable:
    connection = pika.BlockingConnection(pika.ConnectionParameters(
            host="rabbitmq"))
    channel = connection.channel()

    channel.queue_declare(queue='hello')

    channel.basic_publish(exchange='',
                          routing_key='hello',
                          body='Hello World!')
    print (" [x] Sent 'Hello World!'")
    connection.close()

工作机器的 Dockerfile:

FROM python:2-onbuild
RUN ["pip", "install", "pika"]

CMD ["python","rabbit.py"]

2015年11月更新:

一个Shell脚本或者在你的程序中等待可能是一种可行的解决方案。但经过查看这个问题后,我正在寻找docker/docker-compose自身的命令或功能。

他们提到了一种实现健康检查的解决方案,这可能是最好的选择。一个开放的TCP连接并不意味着你的服务已经准备好或者可以保持准备状态。除此之外我还需要在我的Dockerfile中更改入口点。

因此,我希望有一个带有docker-compose内置命令的答案,如果他们完成了这个问题,那就太好了。

2016年3月更新:

有一个提议提供一种内置的方法来确定容器是否“存活”。因此,在不久的将来,docker-compose可能会利用它。

2016年6月更新:

看起来健康检查将被集成到docker 1.12.0版本中。

2017年1月更新:

我找到了一个docker-compose的解决方案,参见: Docker Compose等待容器X启动后再启动Y


3
在docker-compose 2.3中,使用“healthchecks”已被弃用,以鼓励构建具备容错能力的分布式系统。详情请参考:https://docs.docker.com/compose/startup-order/。 - Kmaid
我已经多次遇到这个问题。你可以克服它,但是docker-compose会阻碍你的每一个步骤。如果你想要设置测试拆卸容器控制权,最好使用类似conducto的东西。 - MatrixManAtYrService
@Kmaid 更新: healthchecksdepends_on.condition 的支持实际上在v3中已经回归。因此,您确实可以使用它-请参见发布说明(重要的部分是它说v2和v3规范已合并)+这里是最新规范 - aradalvand
20个回答

5
基于这篇博客文章https://8thlight.com/blog/dariusz-pasciak/2016/10/17/docker-compose-wait-for-dependencies.html,我配置了我的docker-compose.yml如下:
version: "3.1"

services:
  rabbitmq:
    image: rabbitmq:3.7.2-management-alpine
    restart: always
    environment:
      RABBITMQ_HIPE_COMPILE: 1
      RABBITMQ_MANAGEMENT: 1
      RABBITMQ_VM_MEMORY_HIGH_WATERMARK: 0.2
      RABBITMQ_DEFAULT_USER: "rabbitmq"
      RABBITMQ_DEFAULT_PASS: "rabbitmq"
    ports:
      - "15672:15672"
      - "5672:5672"
    volumes:
      - data:/var/lib/rabbitmq:rw

  start_dependencies:
    image: alpine:latest
    links:
      - rabbitmq
    command: >
      /bin/sh -c "
        echo Waiting for rabbitmq service start...;
        while ! nc -z rabbitmq 5672;
        do
          sleep 1;
        done;
        echo Connected!;
      "

volumes:
  data: {}

然后我运行以下命令 =>:

docker-compose up start_dependencies

rabbitmq 服务将以守护进程模式启动,start_dependencies 将完成工作。


通过使用"curl", "-f", "http://localhost:15672"进行查询,需要安装management插件并使用已经过时的healthcheck - 这是最佳答案。如果使用nc进行检查的简单工作示例,则会被downvote。哈,好吧... - Igor Komar
1
答案并没有使用 Docker 的本地功能,因此使用 curl、nc 或其他工具都是无关紧要的。while!nc 与其他答案中已发布的相同。 - svenhornberg
1
本地 Docker 功能:
  1. https://docs.docker.com/compose/startup-order/
  2. https://github.com/docker/compose/issues/5007
  3. https://github.com/docker/compose/issues/374
- Igor Komar
1
@IgorKomar,谢谢你,你救了我的一天!:3 我使用了几乎相同的机制来检查mysql服务器是否准备好,然后才启动实际应用程序。;) 我正在传递类似的命令给 docker-compose run --name app-test --rm "app" bash -l -c 'echo 等待mysql服务启动... && while ! nc -z db-server 3306; do sleep 1; done && echo 已连接! && /bin/bash /script/ci_tests.sh' - TooroSan

5

在尝试了几种方法后,我认为最简单和最优雅的选择是使用jwilder/dockerize实用程序镜像(由@Henrik Sachse提到,但他没有展示具体的例子),并使用其-wait标志。下面是一个简单的例子,在这个例子中,我需要在启动我的应用程序之前准备好RabbitMQ:

version: "3.8"
services:
  # Start RabbitMQ.
  rabbit:
    image: rabbitmq

  # Wait for RabbitMQ to be joinable.
  check-rabbit-started: 
    image: jwilder/dockerize:0.6.1
    depends_on:
      - rabbit
    command: 'dockerize -wait=tcp://rabbit:5672'
  
  # Only start myapp once RabbitMQ is joinable.
  myapp:
    image: myapp:latest
    depends_on:
      - check-rabbit-started

这是一个非常好的解决方案!对我非常有效。谢谢! - erwin

5

有一个名为 "docker-wait" 的实用工具可用于等待使用。


1
谢谢,但它只是一个shell脚本,所以就像h3nrik的回答或在Python中等待一样。这不是docker-compose本身的功能。您可以查看https://github.com/docker/compose/issues/374,他们计划实现健康检查,这将是最好的方法。打开TCP连接并不意味着您的服务已准备好或可能保持准备状态。除此之外,我还需要更改我的dockerfile中的入口点。 - svenhornberg

4
在Docker Compose文件的版本3中,您可以使用RESTART。例如:

docker-compose.yml

worker:
    build: myapp/.
    volumes:
    - myapp/.:/usr/src/app:ro
    restart: on-failure
    depends_on:
    - rabbitmq
rabbitmq:
    image: rabbitmq:3-management

请注意,我使用了 depends_on而不是 links,因为后者在版本3中已被弃用。
尽管它可以工作,但它可能不是理想的解决方案,因为您需要在每次失败时重新启动Docker容器。
还要查看 RESTART_POLICY。它可以让您对重启策略进行精细调整。
当您在生产中使用Compose时,最好实践使用重启策略:

指定重启策略,例如restart: always,以避免停机时间


3

不建议在严肃的部署中使用,但这里基本上是一个“等待x秒”的命令。

使用 docker-compose 版本 3.4已添加start_period指令到healthcheck。这意味着我们可以执行以下操作:

docker-compose.yml:

version: "3.4"
services:
  # your server docker container
  zmq_server:
    build:
      context: ./server_router_router
      dockerfile: Dockerfile

  # container that has to wait
  zmq_client:
    build:
      context: ./client_dealer/
      dockerfile: Dockerfile
    depends_on:
      - zmq_server
    healthcheck:
      test: "sh status.sh"
      start_period: 5s

status.sh:

#!/bin/sh

exit 0

这里发生的情况是,在5秒后调用了healthcheck。这会调用status.sh脚本,该脚本始终返回"No problem"。 我们只是让zmq_client容器在启动前等待5秒钟! 注意:重要的是你有version: "3.4"。如果没有.4,docker-compose会抱怨。

2
作为一个天真的“等待5秒钟”的解决方案,这个相当巧妙。我会点赞的,但我不会因为这在类似生产环境的设置中并不真正起作用,而且我担心有人会看投票数而不是仔细阅读。尽管如此,我还是想说“哇,这太聪明了” ;) - Filip Malczak
PS. 对于更复杂的解决方案,请参考Evereq的答案。 - Filip Malczak
1
这并不是 start_period 的作用。该配置意味着有一个宽限期,在此期间失败的健康检查不计入重试次数。如果早期成功,则被视为健康。在开始期之后,失败将计为重试。请参阅 https://docs.docker.com/engine/reference/builder/#healthcheck - Capi Etheriel

1
我目前也有等待某些服务启动后再启动其他服务的要求。我阅读了这里和其他一些地方的建议,但大部分都需要在docker-compose.yml上进行一些修改。 因此,我开始着手解决这个问题,考虑在docker-compose本身周围构建一个编排层,并最终编写了一个名为docker-compose-profile的shell脚本。 即使服务没有直接向主机公开任何端口,它也可以等待与某个容器的tcp连接。我使用的技巧是在堆栈内部启动另一个docker容器,从那里我可以(通常)连接到每个服务(只要没有应用其他网络配置)。 还有一种等待方法,可以监视特定的日志消息。 服务可以分组在单个步骤中启动,然后触发另一个步骤来启动。 您还可以将某些服务排除在外,而不必列出所有其他要启动的服务(例如可用服务的集合减去一些排除的服务)。 这种配置可以捆绑到一个配置文件中。 有一个名为dcp.yml的yaml配置文件,现在必须放在您的docker-compose.yml文件旁边。 对于您的问题,这看起来像:
command:
  aliases:
    upd:
      command: "up -d"
      description: |
        Create and start container. Detach afterword.

profiles:
  default:
    description: |
      Wait for rabbitmq before starting worker.
    command: upd
    steps:
      - label: only-rabbitmq
        only: [ rabbitmq ]
        wait:
          - 5@tcp://rabbitmq:5432
      - label: all-others

您现在可以通过调用以下命令来启动您的堆栈:

dcp -p default upd

甚至只需通过

dcp

由于只有一个默认配置文件可以运行up -d命令。

有一个小问题。 我目前的版本不支持像您需要的那样的特殊等待条件。因此,没有测试向rabbit发送消息。

我已经考虑了一种进一步的等待方法,可以在主机上或作为docker容器运行某个命令。然后我们可以通过类似的方式扩展该工具。

...
        wait:
          - service: rabbitmq
            method: container
            timeout: 5
            image: python-test-rabbit
...

有一个名为python-test-rabbit的Docker镜像,用于进行检查。
这样做的好处是不再需要将等待部分带到您的工作程序中。它将被隔离并留在编排层内。
也许有人会发现这很有用。欢迎提出任何建议。
您可以在https://gitlab.com/michapoe/docker-compose-profile找到此工具。

1

这也是我的建议。虽然黑客技巧可以解决问题,但稍微花点时间学习k8s会让你获得更多的收益。 - Jose Gleeson

0
这是一个关于编程的例子,main 容器等待 worker 开始响应ping请求时的情况:
version: '3'
services:
  main:
    image: bash
    depends_on:
     - worker
    command: bash -c "sleep 2 && until ping -qc1 worker; do sleep 1; done &>/dev/null"
    networks:
      intra:
        ipv4_address: 172.10.0.254
  worker:
    image: bash
    hostname: test01
    command: bash -c "ip route && sleep 10"
    networks:
      intra:
        ipv4_address: 172.10.0.11
networks:
  intra:
    driver: bridge
    ipam:
      config:
      - subnet: 172.10.0.0/24

然而,正确的方法是使用 healthcheck (>=2.1)。


0

我猜Docker的人真的希望我们使用自己镜像中的代码等待服务。但我仍然想在docker-compose.yml中配置服务等待。如果你愿意使用入口脚本,这里有一种方法。

将此循环添加到您的入口脚本中,使用您选择的包含在镜像中的wait-for-it工具。我正在使用https://github.com/vishnubob/wait-for-it/。如果您没有传递任何服务,则循环不执行任何操作。

for service in "$@"; do
    echo "$0: wait for service $service"
    if ! wait-for-it "$service"; then
        echo "$0: failed on service $service"
        exit 1
    fi
done

docker-compose.yml 中为容器传递所需的服务:

    command: ["my-data-svc:5000"]

这取决于将docker命令作为参数传递给entrypoint脚本的行为。你可能会认为我在滥用docker命令的意图。但我不会因此而争论,因为它对我有效。


这个解决方案缺乏上下文。 - Levijatanu
@Levijatanu 我很乐意扩展,请建议在这里添加什么上下文。 - chrisinmtown

-6
我只有 2 个 compose 文件,先启动第一个,然后再启动第二个。我的脚本如下:
#!/bin/bash
#before i build my docker files
#when done i start my build docker-compose
docker-compose -f docker-compose.build.yaml up
#now i start other docker-compose which needs the image of the first
docker-compose -f docker-compose.prod.yml up

这不被认为是一个好的实践。你不能从一个compose文件中交付由多个容器组成的解决方案。 - juergi

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