6.3 深入 Docker Compose
其實關於 docker-compose.yml 這個檔案的命名, Docker 官方是比較偏好 docker-compose.yaml 的檔名。
但我個人就是懶得多打一個字,所以就長期使用 .yml 做檔名的結尾,要注意如果同時兩個檔案 ( .yaml、.yml ) 都存在,Docker Compose 會以 .yaml 結尾的檔案優先讀取喔!
接著這個範例一樣可以在本書的 GitHub 儲存庫內 ch-06 的 todolist-example 找到。
沒有下載資料夾的人可以看下面是整份的 docker-compose.yml 檔案,會稍微比上一個小節的範例來得複雜,但其實都是從基本觀念出發,就看下去吧!
version: '3.9'
services:
ui:
image: robeeerto/todo-list-ui:latest
container_name: ui
restart: on-failure
networks:
- frontend
ports:
- 3001:3001
environment:
- NEXT_PUBLIC_CABLE_URL=${NEXT_PUBLIC_CABLE_URL}
- NEXT_PUBLIC_API=${NEXT_PUBLIC_API}
depends_on:
- api
api:
image: robeeerto/todo-list-api:latest
restart: on-failure
container_name: api
ports:
- 3000:3000
depends_on:
- database
- redis
networks:
- frontend
- backend
environment:
- DB_HOST=database
- DB_USER=${DB_USER}
- DB_PORT=${DB_PORT}
- DB_PASSWORD=${DB_PASSWORD}
- RAILS_ENV=${RAILS_ENV}
- REDIS_URL=${REDIS_URL}
redis:
image: redis:7-alpine
container_name: redis
restart: on-failure
networks:
- backend
volumes:
- redis-data:/data
database:
image: postgres:14-alpine
container_name: database
restart: on-failure
networks:
- backend
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- database-data:/var/lib/postgresql/data
volumes:
database-data:
external: true
redis-data:
external: true
networks:
backend:
external: true
frontend:
external: true
首先這是一個基本的 Web 應用程式並且採用前後端分離的方式在本機運行 ( 之後會用這個範例部署 ),所以根據下圖,我們可以看到整個應用程式的架構。
services:
ui:
...
api:
...
redis:
...
database:
...
根據 services 的階層,能夠看出這個應用程式總共有 4 個服務。
分別是前端的 ui,後端的 api,資料庫的 database,以及為了實踐 WebSocket 而用的 redis。
這裡有幾個 service 內的參數是上一個小節沒有使用到的,將一個一個解說:
api:
restart: on-failure、always、unless-stopped、no ( default)
restart: 總共有 4 個參數可以使用,分別是 no、always、on-failure、unless-stopped。
預設是 no,意味著不論發生什麼情形,容器都不會重新啟動。
always 則是除了容器被刪除以外,都將會重新啟動,舉例來說,如果這個容器的初始指令是印出一段文字,那在進入停止狀態後,它將會被重新啟動。
on-failure 則是容器是因為非預期錯誤而進入停止狀態時重新啟動。
unless-stopped 若是容器是結束工作而進入退出狀態則不會重新啟動,算是僅次於 always 的一個設定。
api:
container_name: < 你想取什麼都可以 >
container_name: 這純屬我個人的習慣,因為 Docker Compose 預設會以 檔案目錄的名稱-服務名稱-編號 來替容器命名。
這在進入容器或是觀看容器的 Logs 時,要輸入一長串的容器名稱, 對我來說覺得很麻煩,所以我都會替容器命名,讓我在需要操作容器時更輕鬆。
api:
networks:
- frontend
- backend
networks: 就像在運行容器時加入的 --network
參數一樣,相同地,透過 YAML 的 List 寫法,也可以定義複數的虛擬網路。
api:
depends_on:
- database <- service 的名稱
- redis <- service 的名稱
depends_on: 這個功能非常的好用,在一個正式的 Web 應用程式中,很多時候我們需要等待另一個服務的啟動才有效果。
上面的範例,意味著 API 服務要等待 redis 以及 database 容器都啟動後才會進行啟動,這樣可以避免很多非預期的錯誤。
例如,API 服務啟動的太快,資料庫並沒有準備好,導致伺服器端出現 500 的錯誤,但其實不是程式碼出錯,而是啟動順序的問題
volumes:
database-data:
external: true
redis-data:
external: true
networks:
backend:
external: true
frontend:
external: true
這邊要先介紹的是上一個小節沒有介紹到的 networks,同樣作為最上層的參數,使用的方式其實和 volumes 相同,若是有在 services 內使用到 networks 參數,都要提前告知 Docker Compose。
而這邊主要的重點是 external: true 這個參數,意味著該 network 或是 volume 都屬於外部,也就是不隸屬 docker-compose.yml 之中,需要提前建立的意思。
若是加入了 external: true,但卻沒有提前建立,Docker Compose 將不會按照預設行為自動建立被標記 external: true 的物件。
docker-compose.yml 中的環境變數
如果有仔細看過上面完整的 docker-compose.yml 檔案的話,會發現裡面暗藏像是這樣的參數:
DB_USER=${DB_USER}
DB_PORT=${DB_PORT}
DB_PASSWORD=${DB_PASSWORD}
RAILS_ENV=${RAILS_ENV}
REDIS_URL=${REDIS_URL}
這可能就讓人很疑惑了,這些看起來像是環境變數的值是從哪裡來呢?
Docker Compose 還有一個非常厲害的功能,為了避免將機密資訊 ( 資料庫的密碼 ) 寫在 docker-compose.yml 內。
所以在 docker-compose up
時,它能夠自動比對當前資料夾內的 .env 檔案的環境變數,並且映對到 docker-compose.yml 內,這樣就可以讓我們安心地把 docker-compose.yml 檔案上傳到 GitHub 等等的程式碼儲存庫。
下面這個資料夾做個簡單的範例:
# 資料夾結構
├── .env
├── docker-compose.yml
# .env
DB_USER=robert
# docker-compose.yml
version: '3.9'
services:
app:
image: ...
environments:
- DB_USER=${DB_USER} <- DB_USER=robert
需要再科普一下,這真的是隸屬於 Docker Compose 的功能,一般讀取 YAML 檔案並沒有這種神奇的操作,除非是特定框架內的 YAML 檔案在解析時有做過特殊的處理,不然這種寫法一般是行不通的。
我們除了可以透過自己腦補的方式幻想環境變數被安插到我們要的位置之外,也可以透過 docker compose config
的指令,讓 Docker Compose 顯示出添加環境變數後的完整 docker-compose.yml 檔案。
# 資料夾結構
├── .env
├── docker-compose.yml <- TodoList 的範例
$ docker compose config
name: todolist-application
services:
api:
container_name: api
depends_on:
database:
condition: service_started
redis:
condition: service_started
environment:
DB_HOST: database
DB_PASSWORD: robeeerto
DB_PORT: "5432"
DB_USER: postgres
RAILS_ENV: development
REDIS_URL: redis://redis:6379
image: robeeerto/todo-list-api:latest
networks:
backend: null
frontend: null
ports:
- mode: ingress
target: 3000
published: "3000"
protocol: tcp
restart: on-failure
database:
container_name: database
environment:
POSTGRES_PASSWORD: robeeerto
image: postgres:14-alpine
networks:
backend: null
restart: on-failure
volumes:
- type: volume
source: database-data
target: /var/lib/postgresql/data
volume: {}
redis:
container_name: redis
image: redis:7-alpine
networks:
backend: null
restart: on-failure
volumes:
- type: volume
source: redis-data
target: /data
volume: {}
ui:
container_name: ui
depends_on:
api:
condition: service_started
environment:
NEXT_PUBLIC_API: http://localhost:3000
NEXT_PUBLIC_CABLE_URL: ws://localhost:3000/cable
image: robeeerto/todo-list-ui:latest
networks:
frontend: null
ports:
- mode: ingress
target: 3001
published: "3001"
protocol: tcp
restart: on-failure
networks:
backend:
name: backend
external: true
frontend:
name: frontend
external: true
volumes:
database-data:
name: database-data
external: true
redis-data:
name: redis-data
external: true
這個功能也是超級實用,可以幫助您在 docker compose up
之前檢查所有的參數是不是都如同所預期的一樣。
可以看到 docker compose config
出來的結果,比我們所撰寫的還要更加嚴謹,而一般撰寫的 docker-compose.yml 已經是非常精簡的版本了。
對於 Docker Compose 來說,它需要更加詳細的啟動資料,但這都不是我們需要擔心的,Docker 在背景都處理掉了。
Docker Compose 的指令
最常使用到的 Docker Compose 指令就是以下幾個。
$ docker compose up
# 根據 docker-compose.yml 的描述啟動理想的應用程式
$ docker compose up --detach
# 根據 docker-compose.yml 的描述啟動理想的應用程式,並且在背景執行
$ docker compose up --detach --build
# 與上一個的差別就在於,若是某個 service 有標記 build 以及存在 Dockerfile 將會先建置映像檔在執行 Docker Compose,等等會舉例。
$ docker compose stop
# 使 docker-compose.yml 內的所有容器進入停止狀態
$ docker compose start
# 使 docker-compose.yml 內的所有停止狀態的容器啟動
$ docker compose down
# 刪除所有 docker-compose.yml 內的容器、虛擬網路 ( external: true 不會被刪除 )
$ docker compose down --volumes
# 刪除所有 docker-compose.yml 內的容器、虛擬網路、Volume ( external: true 不會被刪除 )
如果您有下載 Todo-list 的資料夾,也可以按照 README.md 上的指示在本機啟動整個應用程式。
同時應該也可以開始感受到 Docker Compose 絕對可以讓新人在上工時減少非常多的環境建置問題,只需要填入相對應的環境變數,並且 docker compose up
就可以進行開發了。
啟動應用程式前,先建置映像檔
剛剛在 Docker Compose 的指令中有提到 --build
這個參數,這邊稍微解釋一下用法,這也是本地端使用 Docker 開發時常常使用到的指令。
本地開發時常常會有需要新增相依套件的情形發生,每次若都需要先 docker image build
再 docker image push
最後才能應用在 Docker Compose 實在是非常沒有效率。
假設我們目前開發的專案是名為 app 的 service,而在資料夾內理應也會有 Dockerfile 才是正確的情形,這時候我們就可以在 docker-compose.yml 檔案內這樣寫。
version: '3.9'
# 短寫法
services:
app:
build: . <- 自動找到當前目錄的 Dockerfile
# 長寫法
services:
app:
build:
context: . <- 路徑
dockerfile: Dockerfile <- 指定 Dockerfile 的檔案
context: 為路徑,. 的意思代表此處,路徑為現在這個資料夾。
dockerfile: 則為建置映像檔所需的 Dockerfile,會需要特別標示是因為有時候我們會根據不同的環境設計不一樣的 Dockerfile。
像是測試環境的 Dockerfile 可能就會叫做 Dockerfile.test,這時候若是我們想要利用不同的 Dockerfile 建置不同的環境就可以特別標示;但若是採用短寫法的話,預設就是找檔名為 Dockerfile 的 Dockerfile。