談 docker compose 的 depends_on 設定怎麼使用

Posted on  Dec 14, 2023  in  Docker  by  Amo Chen  ‐ 5 min read

在使用 docker compose 架設開發環境時,會遇到所謂的服務相依性問題(service dependencies), 舉個簡單的例子,通常資料庫的容器(container)ㄧ定要先正常啟動,後端 API server 的容器才能接著啟動,因為 API server 可能在啟動時需要與資料庫連線建立 connection pool 之後,才會提供 API 服務,否則就會發生錯誤並退出執行,導致容器啟動失敗。

在這個情況下, API server 容器依賴資料庫容器的正常運作,這就是服務相依性。如果沒有理解服務相依性的問題,那麼在使用 docker compose 的時候,將會很容易遇到時而啟動成功,時而啟動失敗的問題,因為有時相依的服務會剛好準時或提早起動成功,有時則還沒準備好。

為此, docker compose 也有提供解決服務相依性的設定,那就是 depends_on 設定。

本文將教導如何使用 depends_on 設定。

本文環境

本文範例專案結構

以下是本文範例的專案結構,Dockerfile 用以編譯執行 app.py 的容器,compose.yaml 則是架設開發環境的 docker compose file:

.
├── Dockerfile
├── app.py
└── compose.yaml

本文使用 Python Flask 架設 1 個簡單的 Web server 作為模擬之用,如果對 Python 不熟也沒關係,不影響教學進行,以下是 app.py 的內容,該程式碼主要會連線到 Redis 對 hits 這個 key 值做遞增操作,每 GET app.py/ 就做 1 次遞增。

app.py

import time

import redis
from flask import Flask

app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)

def get_hit_count():
    retries = 5
    while True:
        try:
            return cache.incr('hits')
        except redis.exceptions.ConnectionError as exc:
            if retries == 0:
                raise exc
            retries -= 1
            time.sleep(0.5)

@app.route('/')
def hello():
    count = get_hit_count()
    return 'Hit count: {}\n'.format(count)

以下是 Dockerfile 的內容。

Dockerfile

FROM python:3.8-alpine
WORKDIR /app
ENV FLASK_APP=app.py
RUN apk add --no-cache gcc musl-dev linux-headers
RUN pip install flask redis
EXPOSE 5000
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]

以下是 compose.yaml 的內容,可以看到開發環境需要執行 Redis 與我們所編譯的 Web server 。

compose.yaml

services:
  web:
    build: .
    ports:
      - "8000:5000"
  redis:
    image: "redis:alpine"

depends_on 解說

如前文所述, depends_on 是解決服務相依性的設定。

short syntax / 短語法

以本文範例為例, web 容器依賴 redis 容器,以箭頭關係表達就是 web → redis , 所以需要在 web 容器設定加上 depends_on 設定,明確表達 web 容器需要等 redis 容器啟動。

所以 compose.yaml 可以改成下列形式:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      - redis
  redis:
    image: "redis:alpine"

如果有多個相依的話,可以參考下列範例:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      - container_1
      - container_2
  container_1:
    image: "redis:alpine"
  container_2:
    image: "redis:alpine"

前述的 depends_on 設定只有簡單的寫上相依容器名稱,這種方式被稱為 short syntax 1, 它只代表啟動順序,所以 redis 容器會比 web 容器先啟動。

雖然上述設定讓 redis 容器比 web 容器先啟動,不過仍有些情況會有問題:

  1. redis 容器啟動之後的執行時間比預期的還要長
  2. redis 容器啟動之後,無法正常運作

這些情況都會讓 web 容器啟動失敗,因為 redis 容器沒辦法在 web 容器來進行連線時準備好提供服務。

以圖表示的話,會更清楚:

timeline.png

這種時間差或者容器沒有正常運作的問題,只能改用 long syntax 更明確的指定要等相依容器處於什麼情況再啟動。

long syntax / 長語法

depends_onlong syntax 可以更明確的指定相依容器需要符合什麼情況。

condition

同樣舉本文為例,我們需要 redis 容器正常運作後才能接著啟動 web 容器,所以可以用 condidtion 設定額外指定要等 redis 處於 service_healthy 狀態,才啟動 web 容器,因此 compose.yaml 可以進一步改成下列形式:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      redis:
        condition: service_healthy
  redis:
    image: "redis:alpine"

condition 設定有 3 種可設定的值:

  • service_started , 作用與 short syntax 相同,只要相依容器啟動就算通過條件
  • service_completed_successfully , 需要等相依容器正常結束執行,也就是 exit code 必須為 0 才算符合情況,否則就算不符合相依情況,通常這個值也比較少使用
  • service_healthy , 需要相依容器處於健康(healthy)狀態,這種設定需要搭配編譯時有指定 HEALTHCHECK 的容器或者在 docker compose file 額外寫上 healthcheck 設定,否則就會出現類似 dependency failed to start: container myrepo-redis-1 has no healthcheck configured 的錯誤訊息

由於 redis 官方提供的 Docker image 編譯時沒有設定 HEALTHCHECK ,所以我們需要自己添加 healthcheck 設定在 compose.yaml 內:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      redis:
        condition: service_healthy
  redis:
    image: "redis:alpine"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      timeout: 10s
      retries: 3
      start_period: 10s

上述 healthcheck 區塊的設定解釋如下:

  1. test: ["CMD", "redis-cli", "ping"] 執行檢查 health check 的指令,CMD 代表執行指令的意思, Redis 剛好有指令 redis-cli ping 可以用來檢查服務是否正常運作
  2. start_period: 10s 代表容器啟動後 10 秒才開始執行檢查的指令
  3. timeout: 10s 代表檢查的過程超過 10 秒就算不健康,可以視為服務需要在 10 秒內回應
  4. retries: 3 如果不健康的話,可以重試 3 次。只要有 1 次正常,就結束檢查的指令,視為健康容器;如果 3 次重試都不健康,就視容器為不健康的容器。

上述 Docker compose file 可以用以下指令測試,將會發現 redis 容器啟動之後會停個約 10 秒,才讓 web 容器啟動:

$ docker compose up --build

更多關於 HEALTHCHECK 的解說請看文件

required

除了 condition 之外,還有 required 可以使用,如果想指定某個容器可有可無都沒關係,可以設定為 required: false

原本需要 redis 容器在健康的情況,才能啟動 web 容器:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      redis:
        condition: service_healthy
  redis:
    image: "redis:alpine"

可是 redis 容器並沒有設定 healthcheck ,因此導致 dependency failed to start 錯誤發生,最終讓 web 容器沒有啟動。

如果 redisweb 容器來說有沒有正常執行都沒關係,就可以使用 required: false 設定,如此一來就算 redis 容器沒有正常啟動,也可以看到 web 容器被啟動:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      redis:
        condition: service_healthy
        required: false
  redis:
    image: "redis:alpine"

上述 docker compose file 同樣可以用以下指令執行:

$ docker compose up --build
restart

除了 conditionrequired 之外,還有 restart 可以設定,這個設定需要同時設定 condition

restart 設定的作用在於相依容器如果有重新啟動或者更新,相依於它的其他容器也會跟著重新啟動。

舉本文範例為例:

services:
  web:
    build: .
    ports:
      - "8000:5000"
    depends_on:
      redis:
        condition: service_started
        restart: true
  redis:
    image: "redis:alpine"

如果 redis 設定為 restart: true , 當它用以下指令重新啟動時:

$ docker compose restart redis

其執行結果就會發現 web 容器也跟著重新啟動:

 Restarting 2/2
 ✔ Container myrepo-redis-1  Started
 ✔ Container myrepo-web-1    Started

總結

depends_on 是 docker compose file 的重要設定,原因在於現代開發環境經常需要多個容器進行互動,例如常見的 Web API server 與資料庫、快取伺服器等等,因此會有相依問題存在,相依問題可能導致 docker compose 運作不如預期,因為有些容器需要相依其他容器才能夠正常運作。

學會 depends_on 可以解決相依問題,寫出更好的 docker compose file, 做出更穩定的開發環境 。

以上!

Enjoy!

References

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!