在使用 docker compose 架設開發環境時,會遇到所謂的服務相依性問題(service dependencies), 舉個簡單的例子,通常資料庫的容器(container)ㄧ定要先正常啟動,後端 API server 的容器才能接著啟動,因為 API server 可能在啟動時需要與資料庫連線建立 connection pool 之後,才會提供 API 服務,否則就會發生錯誤並退出執行,導致容器啟動失敗。
在這個情況下, API server 容器依賴資料庫容器的正常運作,這就是服務相依性。如果沒有理解服務相依性的問題,那麼在使用 docker compose 的時候,將會很容易遇到時而啟動成功,時而啟動失敗的問題,因為有時相依的服務會剛好準時或提早起動成功,有時則還沒準備好。
為此, docker compose 也有提供解決服務相依性的設定,那就是 depends_on 設定。
本文將教導如何使用 depends_on
設定。
本文環境
- macOS
- OrbStack
本文範例專案結構
以下是本文範例的專案結構,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
容器先啟動,不過仍有些情況會有問題:
redis
容器啟動之後的執行時間比預期的還要長redis
容器啟動之後,無法正常運作
這些情況都會讓 web
容器啟動失敗,因為 redis
容器沒辦法在 web
容器來進行連線時準備好提供服務。
以圖表示的話,會更清楚:
這種時間差或者容器沒有正常運作的問題,只能改用 long syntax 更明確的指定要等相依容器處於什麼情況再啟動。
long syntax / 長語法
depends_on
的 long 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
區塊的設定解釋如下:
test: ["CMD", "redis-cli", "ping"]
執行檢查 health check 的指令,CMD
代表執行指令的意思, Redis 剛好有指令redis-cli ping
可以用來檢查服務是否正常運作start_period: 10s
代表容器啟動後 10 秒才開始執行檢查的指令timeout: 10s
代表檢查的過程超過 10 秒就算不健康,可以視為服務需要在 10 秒內回應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
容器沒有啟動。
如果 redis
對 web
容器來說有沒有正常執行都沒關係,就可以使用 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
除了 condition
與 required
之外,還有 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!