Dockerfile & Docker Compose 筆記
13 min read
繼上一篇 Docker Container 架構的說明,這一篇要來討論 Docker 實作,並實際實作幾個自己會用到的 Container,同時也會從指令開始了解這些檔案是怎麼設定的。
Docker 第一步
一般我們使用 Docker 都是從 Docker Image 開始:
# 這邊可以參考 docker hub 上的各種 image,或是直接透過 docker search 進行搜尋
$ docker pull <image name>
# 根據該 Image 產生出 Container
$ docker run <image name> -it (直接進互動模式,簡言之,進入 container 使用 CLI 操作) --name <name>
# 確認有哪些 Container 正在執行
$ docker ps
Bang ! 三個步驟就可以完成一個環境的建置,如果今天是要清理掉一個 Container 呢?
# 今天如果只是要停止一個 container 運作,stop 就可以了
$ docker stop <container name or container id>
# 今天如果是要砍掉一個 container 服務,類似 archive 的概念,可以透過 kill
$ docker kill <container name or container id>
# 今天如果是要清理掉所有關於 container 的資料連 archive 都不,則是透過 rm
$ docker rm <container name or container id>
# 今天如果連 Image 都不留那就 ...
$ docker image rm <image name>
從上面指令我們可以了解到 Docker Container 的一個生命週期:
- create:container 被建立出來,隔離空間被劃分出來。
- start:container 執行指令,內部的 process 開始執行(run 即是 create 與 start 的結合)。
- pause:container 內的 process 被暫停,暫時不消耗更多資源
- stop & die:kill container,內部 process 被終止,記憶體釋出。
- rm: container 的隔離空間資訊也刪除。
Docker 常見指令
# 了解 image 的詳細內容
$ docker inspect <image name>
# 暫停 container 的運作,如果今天只是要 process 暫停而不是停止並釋出記憶體可以使用
$ docker pause <container id or name>
$ docker unpause <container id or name>
# 在對應的 container 進入執行 command line 模式
$ docker exec <container id or name>
# 看看 container 的 output
$ docker logs <contvainer id or name>
# 在官方的 docker hub 上搜尋 image
$ docker search <name>
以一個 node server 與 mysql 為例
知道了上面的步驟,就讓我們透過 docker CLI 來迅速開啟一個 node.js + mysql DB 的 server 為例,想像我們現在有一包 node server 的專案檔如下列的檔案結構:
.
├── package.json
├── server
├──── index.js
└── yarn.lock
首先我們要起一個裝好 node 環境的 container 並把我們開發好的檔案放進去:
# alpine 在 docker hub 裡面常指的是基於輕量版 alpine linux 的意思
$ docker pull node:12-alpine
# -d 讓 container 在背景執行
# -p 將 docker port 對應到 host port
# -v 使用 volumes mount 特定資料夾到 docker 內的資料夾
# -w 設定 work directory 在 src
$ docker run -d --name docker-node-server -p 3000:3000 -v /Users/minw/Desktop/learning-casino-api:/src -w /src node:12-alpine
# 啟動 server
$ docker exec <container id> yarn install & yarn start
這時候輸入 docker ps
可以看到剛剛建立的 docker-node-server 正在執行。如果我們要將前面的指令與 mount 傳給別人,我們可以直接 commit 進 image 檔,並將 image 傳給別人,除了例如要保存一些錯誤現場的狀況,基於 Image 來保存狀態是很不切實際的:
# 了解在 container 儲存層裡面有什麼變動
$ docker diff <container id>
# 將 container 中的變動 commit 進 image 裡面,形成新的 image 檔案
$ docker commit <container id> node:<my-test-version>
因為 commit 的原理是將現在的 container 記錄下來,類似 snapshot 的觀念。例如:yarn install
的node_modules
、server 過程中產生的 Log 檔都會被放進 image 裡面,而且前一層是不能被改動的,這樣這些修改記錄都會持續保留在 image 裡面。所以更好的做法是利用 dockerfile 來附註 image。
Dockerfile
dockerfile 是用來設定 image 如何建立 container 的檔案,一般來說,我們會盡量維持環境基礎,在這之上透過 dockerfile 來執行,讓環境 image 檔案保持單純,而不是全部包進 image 之中。
所以一樣以上方的 node server 為例,我們可以在相同的專案資料夾內設定一個 Dockerfile,並放入以下內容:
FROM node:12-alpine
WORKDIR /src
COPY . /src
RUN yarn install
CMD [ "yarn", "start" ]
EXPOSE 3000
這邊每一行對於 Image 都是一個 Layer 的概念,在撰寫 Dockerfile 的時候要思考的是,這一層 Image 要做什麼事情,類似於 Git 在 Commit 的概念,讓每一層都是有分隔而且不會重複的意義,而 Docker 每一次基於 Dockerfile 去執行的時候,會比對內容是否有改變來 rebuilt,若我們希望加速這個過程,可以將比較常變動的內容放在後方,來避免前面的指令需要被重複執行。
所以我們針對 dockerfie 進行,只要在有 dockerfile 的環境裡行 build
:
$ docker build -t node-simple .
這邊要瞭解到,當我們補上 .
亦即提供了 Context,對於一些需要路徑的操作 e.g. COPY, WORKDIR 等來說,會依據這個傳入的 Context 來處理。
常見指令 QA
這邊在看 Dockerfile 指令時大部分都相當直接,但有幾個時常讓我混淆-
COPY
vsADD
: 最主要的差別在ADD
可以從一個 URL 裡面抓資料並進行解壓縮,並不會做 Cache 處理,所以一般來說大部分時候還是用 COPY 居多。RUN
vsCMD
:RUN
會被 commit 進 image 裡面,CMD
則是單純的執行指令,也是如果今天我們docker inspect <image name>
的時候會在裡面看到最後執行的指令。ENTRYPOINT
vsCMD
:ENTRYPOINT
與CMD
的概念相同,但他比較像是接受CMD
作為參數的指令,為什麼會有這個指令?因為當我們如果試圖為CMD
帶入參數時,會發現在 CLI 輸入的指令會覆蓋掉CMD
的指令,所以才會需要ENTRYPOINT
放腳本檔,讓我們可以在CMD
帶入參數。ENV
vsARG
: 兩者都是設定環境變數,但ARG
的環境變數不會在 container 之中,通常用於 docker build 階段,而ENV
就是一般我們使用的環境變數。
改善 Dockerfile
- alpine version: 選擇使用 alpine tag 的環境可以得到比較簡潔的 image。
- Multi-stage build: 透過雙層的
FROM
將一些只是需要環境來建置的資源保留,環境移除。 - dockerignore / 移除過程中的檔案:避免一些 node_modules, log 檔案一併被複製進 image。
Docker Compose
如果今天是一個多 Container 的服務,舉例來說:除了上述的 node server 之外,我們還需要一個 mysql container,過去我們可以怎麼做?
先開一個共享網路,接著把我們的服務都加進這個網路裡:
$ docker network create -d bridge <network name>
$ docker run -d -p 3306:3306 -v /my/own/datadir:/var/lib/mysql --network <network name> mysql:5.4
$ docker run -it --network <network name> . /bin/bash
只有一個兩個還好,今天如果有很多 container 這邊操作的複雜度就會提升,於是有了 docker compose 協助處理多 docker container 的配置。
version: '2.1'
services:
mysql:
container_name: db-server
image: mysql:5.7
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- 3306:3306
environment:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
- MYSQL_DATABASE=test
- MYSQL_USER=root
- MYSQL_PASSWORD=root
healthcheck:
test: "exit 0"
interval: 30s
timeout: 10s
retries: 5
api:
container_name: api-server
build:
context: .
dockerfile: local.Dockerfile
image: api-server
depends_on:
mysql:
condition: service_healthy
ports:
- 3000:3000
environment:
- MYSQL_HOST=host.docker.internal
- MYSQL_PORT=6017
- MYSQL_DATABASE=test
- MYSQL_USERNAME=root
- MYSQL_PASSWORD=root
volumes:
- ./server:/app/server
這邊一個 docker-compose.yml 的範例讓我們可以看到,大致 docker-compose 其實就是把我們設定的過程變成一個 yml 檔,要注意的是,這邊 yml 是有執行順序的差別,以上段為例:若 mysql 還未啟動,那 api 可能無法順利運行。若要在 yml 上定義相依關係,可以透過 link
參數。
Docker 部署實作
而實際上我們在部署 Dockerize 的 App 服務時,我們可以使用高度集成的服務像是:AWS ECS 來管理 Container 的生命週期也不需要再安裝 docker 的環境,但其實也可以在 EC2 上執行,順帶一提 Docker Compose 並不適合 Production 的環境,顯而易見的 Docker Compose 並沒有辦法做到多機器多容器的部署,如果需要可以考慮 k8s 或 docker swarm 等服務。
這邊就以兩個過去學習中常需要的案例來示範 AWS 跟 GCP 版本:
LAMP
首先我們先在 Local 端嘗試建置起一個 LAMP 的服務,我們需要一個基於 apache 的 php server,與一個 mysql DB。
所以我們今天可以直接上 Dockerhub 找一個 LAMP image、又或者基於 AWS 部署上的安排,將 apache server 跟 mysql DB 分離在不同主機上。
假設選擇後者,這邊決定起兩個 container 來執行,mysql 資料使用 volume 保留在外埠資料夾中,並且將 container port 對應到 local 的 9016 port,而 php server 程式碼為了保持開發時更新,將程式碼同樣保留外部的資料夾。
version: '3'
services:
mysql:
container_name: lamp-mysql-db
image: mysql:5.7
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: "always"
ports:
- "9016:3306"
volumes:
- ./data:/var/lib/mysql
environment:
- MYSQL_RANDOM_ROOT_PASSWORD=yes
- MYSQL_DATABASE=test
- MYSQL_USER=admin
- MYSQL_PASSWORD=admin
server:
container_name: lamp-server
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:80"
environment:
- MYSQL_HOST=host.docker.internal
- MYSQL_PORT=9016
- MYSQL_DATABASE=test
- MYSQL_USER=admin
- MYSQL_PASSWORD=admin
depends_on:
- mysql
volumes:
- ./src:/var/www/html
另外 php 這邊需要再疊上一層 layer 放 mysqli driver,所以針對 php-apache 我們加上 dockerfile,值得注意的是,當更新 dockerfile 的時候,若已經存在原本的 image 記得在 docker compose up
加上 --build
flag 產生包新 layer 的 image:
FROM php:7-apache
RUN docker-php-ext-install mysqli
綜合以上我們將資料夾結構保持下列形式:
.
├── data
├── src
├── docker-compose.yml
└── dockerfile
接著我們將這份檔案推上安裝好 Docker 的 EC2 即可直接部署我們 server,記得將 RDS 的參數保存在 EC2 環境變數中,而不是使用原本的 mysql container。
小結
Docer Compose 加上 Dockerfile 真的讓平時自己開發省力許多,在 debug 上逐漸可以掌握一些流程:
- 確認 Container 是否成功啟動
- docker log 確認最後 Container 回傳的訊息
- 確認 docker image 的參數內容確認最後執行的 CMD 是否合理
以上可以初步排除很多入門時會踩到的坑。