上一篇初步認識了 Docker Compose,並以現有的映像檔為練習對象。本文將介紹深入一點的配置,包含使用 Dockerfile、掛載,並設計服務之間的相依,過程中也會定義健康狀態的檢測方式。
此篇亦轉載到個人部落格。
在 docker-compose.yml 配置檔中,也可將事先寫好的 Dockerfile 作為映像檔來源。
會使用這種做法,筆者認為有兩種情況。第一種是想針對 Docker Hub 上或本地現有的映像檔作客製化。第二種是我們自己在本地有一些程式成品(例如專案原始碼、JAR 檔等),想先透過 Dockerfile 打包為映像檔跑起來。
在 Day 36,我們將 Spring Boot 後端程式的專案,透過 Dockerfile 製作成映像檔。
該程式專案的結構節錄如下。
backend
|_ target
| |_ backend-app.jar
|_ env-config
| |_ test
| | |_ application.properties
| |_ prod
| |_ application.properties
|_ Dockerfile
其中「application.properties」檔案是用來提供一些如 DB 的連線字串、帳號密碼等配置。JAR 檔為程式的執行檔,需根據要部署的環境(如測試、生產),選擇其中一個配置檔來讀取。
Dockerfile 的部份內容如下。
# ...
ARG SERVER_TYPE
# ...
COPY ["./target/backend-app.jar", ...]
COPY ["./env-config/${SERVER_TYPE}", ...]
# ...
建立映像檔時,可根據 docker image build
指令的 --build-arg
參數,將「test」或「prod」字串傳入。而 Dockerfile 會由「ARG」指令接收,並給予名稱「SERVER_TYPE」,藉此將指定路徑下的配置檔複製到映像檔中。
假設我們把 Spring Boot 的程式專案搬移到 compose 的根目錄裡,則目前檔案的相對位置節錄如下。
my-project
|_ backend
| |_ Dockerfile
| |_ env-config
| |_ test
| |_ prod
|_ docker-compose.yml
|_ service-env-files
|_ mysql.env
|_ rabbitmq.env
那麼在配置檔中可透過如下的寫法,提供 Dockerfile 作為映像檔。
services:
backend:
build:
context: /backend
dockerfile: Dockerfile
args:
- SERVER_TYPE=prod
container_name: DemoBackend
ports:
- 8080:8080
在「build」階層中,能定義 Dockerfile 相關的資訊。
要注意的是,若 Dockerfile 中有透過「COPY」指令複製主機的資料,且主機路徑使用相對路徑,則該路徑依然會以 Dockerfile 的所在位置為基準。
在第一節配置檔的範例中,傳入了名為「SERVER_TYPE」的參數,值為「prod」。用意是根據要部署的目標環境,從對應的路徑複製檔案,而目前是寫死的(hard code)。為了保有彈性,讓我們設計成由外部檔案帶入參數。
請在與配置檔相同的路徑下,建立一個名為「.env」的檔案(不需要有主檔名)。目前檔案的相對位置節錄如下。
my-project
|_ docker-compose.yml
|_ service-env-files
|_ .env
接著在「.env」檔案中寫下參數。
BACKEND_SERVER_TYPE=prod
並且在配置檔中,將 Dockerfile 所需要的「SERVER_TYPE」參數值,以 ${參數名稱}
的寫法挖空,如下。
services:
backend:
build:
# ...
args:
- SERVER_TYPE=${BACKEND_SERVER_TYPE}
如此一來,不論是在執行 docker compose config
指令進行確認,還是 docker compose up
實際建置映像檔並運行,此處的 ${BACKEND_SERVER_TYPE}
將會被 .env 檔案中定義好的參數值填入。
這種從 .env 檔案帶入參數的做法,亦可用於配置檔的其他地方。以下的範例配置,是將服務所用到的映像檔名稱,也設計為帶入參數。
services:
db:
image: ${MYSQL_IMAGE}
# ...
mq:
image: ${RABBITMQ_IMAGE}
# ...
而 .env 檔案的對應參數如此撰寫。
MYSQL_IMAGE=mysql:8.2.0
RABBITMQ_IMAGE=rabbitmq:3.12.4-management
若我們想對容器做掛載,也能進行配置。寫法是在服務的「volumes」階層下,將配置列出來。
若掛載的來源是主機的檔案空間,那麼配置寫法的格式為 {主機相對路徑}:{容器絕對路徑}:{唯讀}
。要注意的是,主機路徑必須在 compose 根目錄底下。
以下的範例是將 compose 根目錄下的「backend-app-logs」資料夾,掛載到容器的「/app/log」路徑。
services:
backend:
# ...
volumes:
- "./backend-app-logs:/app/log"
而以下的範例則是將 compose 根目錄下的「/backend/env-config/prod」資料夾,掛載到容器的「/app/config」路徑。且設為唯讀,代表容器不可異動該資料夾。
services:
backend:
# ...
volumes:
- "./backend/env-config/prod:/app/config:ro"
由於一個 volume 可掛載到多個容器與多個路徑,因此做法上比較特別。會事先在配置檔的第一階層定義好 volume 的資訊,並給它一個 key。接著填寫服務的參數時,再去指向該 key,藉此達到重複利用。
以下的範例,是在配置檔第一階層的「volumes」,定義一個名為「backend_app_logs」的 volume,並給予「key_backend_log」這個 key。
接著在服務參數的「volumes」階層,將該 volume 掛載到容器的「/app/log」路徑。配置寫法的格式為 {volume 的 key}:{容器絕對路徑}
。
services:
backend:
# ...
volumes:
- "key_backend_log:/app/log"
volumes:
key_backend_log:
name: backend_app_logs
external: false
在定義 volume 時,用到了「external」參數。若值為 false,代表要讓 Docker 自動建立 volume,因此可透過「name」參數來命名。若為 true,代表我們要使用事先建立好的 volume,故無法在此做如命名之類的設定。
在這個 compose 中,我們有資料庫(MySQL)、訊息佇列(RabbitMQ)與後端程式(Spring Boot)三個服務。照理來說,後端程式在啟動時,它所需要連接的外部服務都應該處於「準備完成」的狀態。
這裡說的準備完成,指的是容器裡面的軟體。假設啟動容器後,MySQL 需要 2 分鐘才能運行起來,而 Spring Boot 只要 30 秒,這個時間差有可能導致 Spring Boot 啟動失敗,因為連不上 MySQL。為了維護服務之間的相依性,我們需要對啟動順序做配置。
以下範例是設定 backend 服務依賴於 db 和 mq 這兩個服務,等到 db 與 mq 進入健康狀態,backend 的容器才會啟動。至於 db 與 mq 的健康狀態該如何檢測,我們可自行定義。
services:
backend:
# ...
depends_on:
db:
condition: service_healthy
mq:
condition: service_healthy
db:
image: mysql:8.2.0
# ...
healthcheck:
test: ["CMD", "mysqladmin", "-u$MYSQL_USER", "-p$MYSQL_PASSWORD", "ping", "-h", "localhost"]
start_period: 90s
interval: 60s
timeout: 10s
retries: 2
mq:
image: ${RABBITMQ_IMAGE}
# ...
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
start_period: 90s
interval: 60s
timeout: 10s
retries: 2
上面的配置檔中看到了許多新東西,接下來就一一認識。
在 backend 服務使用 depends_on
參數,可設定它會依賴於哪些服務。此處依賴 db 與 mq。
該階層下的 condition
參數,則定義被依賴的服務進入何種狀態,本服務才能啟動。這裡提供的值為「service_healthy」,代表進入健康狀態。另一個可用的值為「service_started」,代表容器啟動。
至於一個服務的健康狀態檢測方式,若來源映像檔的 Dockerfile 並未定義,或者不適合我們,則可於配置檔進行覆寫。範例裡,筆者在 healthcheck
階層中使用了多個參數,分別說明如下。
test
:進行檢測的 command line 指令。start_period
:容器啟動多久,會開始第一次檢測。interval
:兩次檢測的時間間隔。timeout
:檢測的逾時時間。超過則視為不健康。retries
:不健康時,允許重新檢測的次數。若重測達一定次數均不健康,才將容器視為不健康。各個服務可用來當作檢測的指令不盡相同,需要自行上網查詢,或找找是否有類似「ping」效果的指令。
這兩篇文章,我們透過 Docker Compose 啟動了資料庫、訊息佇列與 Spring Boot 後端程式的容器。
在開發 Spring Boot 程式時,會在 application.properties 配置檔,撰寫其他服務的「連線字串」。例如資料庫的 IP 地址、帳號密碼等。
以 Spring Data JPA 為例,連線字串是這樣寫:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/your-db-name
假設資料庫與後端程式位於同一台主機,我們可能會直覺地使用「127.0.0.1」或「localhost」當作資料庫的地址。如今軟體已經在容器內運行,那麼該地址指的將是容器自己,故無法連上另一個容器。
本地開發時,我們會使用 Windows 或 Mac 系統。
為了能在一個容器內,連上另一個 MySQL 資料庫容器,請使用「host.docker.internal」作為地址,如下:
spring.datasource.url=jdbc:mysql://host.docker.internal:3306/your-db-name
根據官方文件的解釋,「host.docker.internal」是一個特殊的 DNS 名稱,會對應到主機的地址。
另外,即便後端程式只是在主機本地運行,沒有打包到容器,此連線字串亦可連上運行中的容器。換句話說,主機端的軟體若想連線容器,同樣可使用這個 DNS 名稱。
交付的軟體,最終會部署到測試或生產環境的 Linux 系統,此時連線字串仍可使用「127.0.0.1」的地址。但在 docker-compose.yml 配置檔中,需對後端程式的服務添加「extra_hosts」的參數,如下:
services:
backend:
# ...
extra_hosts:
- "host.docker.internal:host-gateway"
Ref:
今日文章到此結束!
最後宣傳一下自己的部落格,我是「新手工程師的程式教室」的作者,主要發表後端相關文章,請多指教