跳至主要内容

6.4 Docker Compose 的擴充欄位

說到 docker-compose.yml,還可以透過擴充欄位的方式來降低許多重複的動作,以及寫錯字的隱藏錯誤。

至於要如何撰寫呢?Docker 有提供一個很好用的參數叫做 x-labels,結合 YAML 本身的 <<: 語法一起使用,就能夠大幅度地減少重複的欄位。

接著來做一個示範吧!

下面是一個簡單的 docker-compose.yml,有注意到 networks 的欄位都是重複撰寫的,這樣在新增服務的時候,不小心打錯字,就有可能發生意料之外的錯誤,相信我,找錯字絕對是當工程師最討厭遇到的事情。

version: '3.9'

services:
app:
image: app
networks:
- production
db:
image: db
networks:
- production
redis:
image: redis
networks:
- production

networks:
production:

利用 x-labels,我們可以統一在最上方管理,並且用 <<: 將其安插進去。

version: '3.9'

x-labels: &networks <- &networks 是這個擴充欄位的命名
networks:
- production

services:
app:
image: app
<<: *networks <- 這裡是 YAML 的語法,像是安插變數一樣
db:
image: db
<<: *networks
redis:
image: redis
<<: *networks

networks:
production:

接著就能透過前面使用過的 docker compose config 來觀看,會發現 networks 如同原先的格式一樣被寫進去了,是不是非常方便?而且只要改一個就能一次全部更改,避免掉很多的隱形失誤。

$ docker compose config
name: example
services:
app:
image: app
networks:
production: null
db:
image: db
networks:
production: null
redis:
image: redis
networks:
production: null
networks:
production:
name: example_production

Docker Compose 的覆寫檔案

接著是關於覆寫檔案的功能,這會應用在非常多的場景,大部分的應用程式都會分成不同階段部署,就以最基本的情形來說,都應該要有開發環境、測試環境以及正式環境。

以上述的情形來說,就有可能衍生出三種不同的 Docker Compose 檔案,可以叫做 docker-compose.yml ( 核心 ),docker-compose-dev.yml ( 開發 ),docker-compose-production.yml ( 正式 )

那除了透過剛剛學過的擴充欄位來減少不必要的複製貼上以及手動改寫的情境,還可以透過覆寫檔案來更大幅度減少所需要寫的內容。

以下將分為三個階段進行,首先最重要的是核心的檔案,通常核心檔案有的 services 到了正式環境也八九不離十。

docker-compose.yml (核心)

這作為最核心的 docker-compose.yml,把應用程式會使用到的服務都放了進去,而基本的設定也都先設定好。

version: '3.9'

services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: app
restart: on-failure
depends_on:
- db
- redis

db:
image: postgres:14-alpine
container_name: db
restart: on-failure
volumes:
- database:/var/lib/postgresql/data

redis:
image: redis:7-alpine
container_name: redis
restart: on-failure
volumes:
- redis:/data

volumes:
database:
redis:

一樣,透過 docker compose config 來確認一次開發環境的整體設定。

name: example
services:
app:
build:
context: /Users/RobertChang/docker/example
dockerfile: Dockerfile
container_name: app
depends_on:
db:
condition: service_started
redis:
condition: service_started
networks:
default: null
restart: on-failure
db:
container_name: db
image: postgres:14-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: database
target: /var/lib/postgresql/data
volume: {}
redis:
container_name: redis
image: redis:7-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: redis
target: /data
volume: {}
networks:
default:
name: example_default
volumes:
database:
name: example_database
redis:
name: example_redis

確認核心檔案沒有問題後,就可以來撰寫 docker-compose-dev.yml 開發環境的檔案。

docker-compose-dev.yml (開發)

services:
app:
ports:
-3000:3000

這就是開發環境會需要撰寫的內容,非常的少,可以透過 --file 的指令來覆寫 docker-compose.yml 的檔案,接著使用 docker compose config 看看得出來的結果。

$ docker compose --file ./docker-compose.yml --file ./docker-compose-dev.yml config # 不換行
name: example
services:
app:
build:
context: /Users/RobertChang/docker/example
dockerfile: Dockerfile
container_name: app
depends_on:
db:
condition: service_started
redis:
condition: service_started
networks:
default: null
ports:
- mode: ingress
target: 3000 <- 多了我們在 dev 覆寫的內容
published: "3000" <- 多了我們在 dev 覆寫的內容
protocol: tcp
restart: on-failure
db:
container_name: db
image: postgres:14-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: database
target: /var/lib/postgresql/data
volume: {}
redis:
container_name: redis
image: redis:7-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: redis
target: /data
volume: {}
networks:
default:
name: example_default
volumes:
database:
name: example_database
redis:
name: example_redis

至於覆寫的順序則是由後方的覆蓋掉前方,以上方這個開發環境的例子來說,最前方的 --file ./docker-compose.yml 會是核心的檔案,而後方的 --file ./docker-compose-dev.yml 則是開發環境要覆寫的檔案。

而在開發環境下,只需要確認需要啟動的應用程式開啟 port 去對應到本機的 port 即可。

不得不說 Docker Compose 真的非常聰明,它能夠去對應到已存在的 service 並且在覆寫上正確的資訊。

docker-compose-production.yml (正式)

到了正式環境,就不會把應用程式的 port (3000) 打開到機器上,而是透過反向代理伺服器來做到發佈外網的功能,以一個 Web 應用程式來說,只需要開啟 80 以及 443 port 讓使用者能夠透過 HTTP 協定進入到網站即可。

而在正式環境時,應用程式也不再透過 build 的方式來建置映像檔,都會透過已經建置好的映像檔來運作,這時候的映像檔會放在像是 DockerHub 之類的映像檔儲存庫上。

而下方則是 docker-compose-production.yml 的檔案內容,這邊的主要目的是試著練習檔案覆寫的功能,所以重點並不在 traefik 的設定,之後的 正式部署篇 會稍微講解這個反向代理的伺服器。

services:
proxy:
image: traefik:v2.8
container_name: proxy
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock

app:
image: app:production

一樣透過 docker compose config 的方式來覆寫檔案看看正式環境下的設定是不是正確的。

name: example
services:
app: <- 沒有再開啟 port 了
build:
context: /Users/RobertChang/docker/example
dockerfile: Dockerfile
container_name: app
depends_on:
db:
condition: service_started
redis:
condition: service_started
image: app:production
networks:
default: null
restart: on-failure
db:
container_name: db
image: postgres:14-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: database
target: /var/lib/postgresql/data
volume: {}
proxy: <- 新增的服務被覆寫上去了
container_name: proxy
image: traefik:v2.8
networks:
default: null
ports:
- mode: ingress
target: 80
published: "80"
protocol: tcp
- mode: ingress
target: 443
published: "443"
protocol: tcp
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
bind:
create_host_path: true
redis:
container_name: redis
image: redis:7-alpine
networks:
default: null
restart: on-failure
volumes:
- type: volume
source: redis
target: /data
volume: {}
networks:
default:
name: example_default
volumes:
database:
name: example_database
redis:
name: example_redis

可以看到確實如同我們想要的結果,在正式環境的服務上加入了 traefik 這個反向代理伺服器。

更多有關 docker-compose.yml 的參數

礙於 docker-compose.yml 內可以加入的參數實在是多到不行,書中只能以最基礎會使用到的參數做範例。

最好的方式還是透過閱讀官方文件找到自己所需要的參數,畢竟每次的專案所需要的設定都不太一樣。

可以透過訪問 https://docs.docker.com/compose/compose-file/ 這個頁面,找到許多有關 docker-compose.yml 內各階層的參數設定。

不論是從 CPU 的使用量到記憶體的使用量限制等等,都可以針對每一個 service 做獨特的設定。

我自己也不是每一個都有使用過,通常都是遇到問題了,才會從文件中找到可以幫助自己克服問題的設定。

為什麼不在核心檔案加入 port 就好?

這是因為 port 並沒有辦法被覆蓋掉,如果我們在核心檔案開啟了 3000 port,則透過覆寫的方式,到了正式環境還是會開啟 3000 port。

而這就牽扯到安全的問題,一個 Web 應用程式應該把 port 收斂到 80 以及 443 是比較好的作法。

為什麼在正式環境還是有 build?

這是無法避免的問題,通常在開發環境,我們會透過 build 的方式來因應添加套件或是檔案更動的情形,所以去重新建置映像檔是一件常常發生的事。

但在正式環境中,因為我們有指定映像檔,Docker Compose 會以映像檔為主,而不是去找 Dockerfile 來建置;所以這部分也不算是一個問題。