[Note] Docker and Kubernetes: The Complete Guide @ Udemy 學習筆記
此篇為筆記之整理,非原創內容,資料來源主要來自:Docker and Kubernetes: The Complete Guide @ Udemy
重要概念
- Image(映像檔):有所有執行程式時所需要相依的內容、設定、指令和執行檔。當在執行 container 時,它會使用一個獨立的檔案系統,而這個檔案系統即是透過 image 來提供。
- Container(容器):image 的 instance,真正執行的程式,它是獨立於本機所有 process 以外 的一個 process。Container 包含了 image 中的 FS snapshot 和 Startup Command。
- Docker Client(Docker CLI):輸入 docker 指令的地方,可以透過安裝 Docker Desktop 取得
- Docker Server(daemon):建立映像檔、執行容器的地方
Kernel 和 Container
Kernel 介於硬體和軟體的中間,當應用程式在執行時,會透過 system call 告知 kernel 要執行的項目(例如,寫檔),kernel 收到指令後會再對硬體進行操作:
- name spacing:可以將單一 process(或 group of processes)使用到的資源加以隔離
- control groups(cgroups):可以限制每個 process 可以使用到的資源量多寡(例如,memory 的大小、CPU 的效能)
Container 的概念就如同下圖中紅色框線的位置:
Image 和 Container
- image 會在 container 中執行
- 當我們在講 image 的時候,實際上包含了一個「檔案系統的快照(FS Snapshot)」和「啟動他的指令(Startup Command)」
- 當一個 image 要在 container 中執行時,會把 image 中對應的 FS Snapshot 和 Startup Command 放一份到 container 中加以執行
Docker Life Cycle(Status of Container)
- created
- up(表示在執行中)
- exited
建立 Image
Dockerfile 建立的流程如下:
- 選定一個現有的 image 當作 base image
- 執行一些指令來安裝其他的程式
- 定義一些指令來啟動該 container
建立 Dockerfile
The Build Process in Detail @ Docker and Kubernetes: The Complete Guide @ Udemy
Dockerfile 的內容就像是告訴一台沒有內建 OS 的電腦,要如何重零開始把應用程式安裝進去。
# Dockerfile
# Use an existing docker image as a base
FROM alpine
# Download and install a dependency
RUN apk add --update redis
# Tell the image what to do when it starts as a container
CMD ["redis-server"]
- 我們之所以要安裝 OS,是因為它通常內建了我們所需要的程式,這裡使用
alpine
是因為它有內建apk
的指令,方便我們安裝所需的套件
docker build:建立 image
在建立好 Dockerfile 後,可以執行 docker build
指令:
# 建立好 Docker file 後
$ docker build . # 執行建立好的 Dockerfile 後,會得到 image 的 id
$ docker run b9124cbfdc30 # 透過 docker run 可以執行
docker build
會做以下行為:
- 每一個 Step 都會啟動一個 container 執行對應的行為,完成後會把它存成一個暫時的 image。
- 接著,在下一個 Step 開始時,會去使用上一個 Step 留下的 Image,並以此再次啟動一個 Container,執行該 Step 對應的指令,該指令執行完後,同樣的會再把最終完成的結果存在一個 Image,以這樣的流程重複完成每個 Step。
- 待所有 Step 都執行完後,最終會有一個 Image 可以讓使用者執行
docker run <image-id>
一般我們在使用別人的 image 時,是根據 image 來建立對應的 container,但當我們要透過 Dockerfile 來建立 image 時,則是根據 container 來產生出 image。也就是說,逐步執行這個 Dockerfile 的過程會像是下面這樣(通常不會這麼做,單純示範用):
# 從 container 建立一個 image
$ docker run -it alpine sh
$ / apk add --update redis
# 開啟另一個 Terminal
$ docker commit -c 'CMD ["redis-server"]' <container-id> # 會回傳 image-id
$ docker run <image-id>
tagging a image:幫 image 加上 tag
每次都要輸入 image-id 非常麻煩,透過 image tag 可以為 image 建立標籤。慣例來說,標籤的命名會是 dockerId/projectName:version
,例如 pjchender/redis:latest
:
# tagging docker image
# docker build -t <tag-name> .
$ docker build -t pjchender/redis-server:latest .
$ docker run pjchender/redis-server # 預設會使用 latest version
這整個為 image 建立標籤的流程稱作 tagging a image,但就技術上來說,只有標籤名稱最後的版本號是真正的 tag,前面單純只是 dockerId 和 projectName。
Rebuild with cache
- 每一次 Dockerfile 被執行後,Docker 都會將該次執行的內容快取下來,只要 Dockerfile 沒有改變(添加或變更順序),下次重複執行
docker build
指令時,會直接取用 cache 中的內容,在終端機中會看到 Using Cache 的說明文字。
Dockerize an Node.js App
將 App Dockerize 的過程包含以下步驟:
- 建立 Node JS Web App
- 建立 Dockerfile
- 根據 Dockerfile 來 build image
- 將 image 以 container 方式執 行
- 從瀏覽器連到該 Web App
1. 建立 App
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hi there');
});
app.listen(8080, () => {
console.log('Listening on port 8080');
});
2. 建立 Dockerfile
COPY <local-machine> <container-drive>
- local-machine 的路徑是相對於 build context,也就是
docker build .
,後面的這個.
就是 build context
- local-machine 的路徑是相對於 build context,也就是
- 其中
COPY ./ ./
的目的是要把專案資料夾中的所有檔案都複製一份到 docker container 的根目錄(root directory) - 由於只使用
COPY ././
會把本機專案的所有檔案都複製到 container FS 的根目錄中,這麼做並不是很好,因此可以使用WORKDIR <path>
這個指令來指定工作目錄,如此,所有後續的指令都會在這個工作資料夾中被執行
# Specify a base image
FROM node:alpine
# 定義 container 中的工作資料夾
WORKDIR /usr/app
# 將本機資料夾的檔案複製到工作資料夾中
COPY ./ ./ # COPY ./local ./container
# Install some dependencies
RUN npm install
# Default command
CMD ["npm", "start"]
這樣的寫法有一個問題,因為 Dockerfile 會由上往下執行,如果沒有改變就直接拿 cache,但若其中一行對應的內容有改變,則該行後的所有指令都會被迫重新執行,因此可以盡可能把容易改變的那行抽成獨立的指令,放到 Dockerfile 的越後面越好。
舉例來說,在原本的 Dockerfile 中,每次只要專案的中任何檔案(例如,index.js
)有變更時,就算套件(即package.json
的內容沒有改變)沒有變更,都會重新執行一次 npm install
。因為使用的是 COPY ./ ./
,Docker 會發現 index.js
有改變而重新執行一次該行及其後續的指令。
因此,可以改成只有在 package.json
的檔案有變更時,再執行 npm install
這個指令,就可以減少很多不必要的重 build 而直接使用 cache。把 Dockerfile 改成:
# Specify a base image
FROM node:alpine
# 定義 container 中的工作資料夾
WORKDIR /usr/app
# 先將 package.json 複製到 container 的工作資料夾中
# 只有當 package.json 有改變時,才會執行 npm install 的指令
COPY ./package.json ./
RUN npm install
# 將本機資料夾的檔案複製到工作資料夾中
COPY ./ ./
# Default command
CMD ["npm", "start"]
3. 根據 Dockerfile 來 build image
# -t 表示為該 image 建立 tag
$ docker build -t pjchender/docker-node-sandbox .
4. 將 image 以 container 方式執行
$ docker run pjchender/docker-node-sandbox
5. 從瀏覽器連進該 Web App
- 透過 port mapping 的方式將 localhost 的 port 和 container 的 port 相對應
# docker run with port mapping
# -p [localhost-port]:[container-port]
$ docker run -p 8080:8080 <image-id>
# 在本機瀏覽器輸入 localhost:5000 將會連到 container 的 8080 port
$ docker run -p 5000:8080 pjchender/docker-node-sandbox
當 container 已經在執行時,希望能夠對同一個 container 執行某一些指令時,可以使用 docker exec
的指令:
$ docker exec -t <container-id> sh
Docker Compose CLI
sample code @ pjchender gist
為什麼需要使用 docker compose
- 雖然可以把 Database Server 和 Web App Server 放在同一個 Docker Container 中,但這並不是個好的作法,因為在未來流量變大,需要啟動多個 Web Server Container 來處理時,因為 Database 是包在和 web server 同一個 container 內,因此每個 Container 間的 Database 都會是獨立的,等同於有多個獨立的 database。
- 這種情境比較適合把 database 獨立在另一個 container 中,不要和 web app 的 container 放在一起,並透過 docker compose 來管理多個 container 間的網路溝通。
Docker Compose CLI 是什麼
- Docker Compose CLI 和 Docker CLI 是獨立不同的工具,但在安裝 docker client 時會一併安裝。
- 它可以用來簡化每次都需要在 terminal 中透過 Docker CLI 輸入許多指令的困擾(類似 docker CLI 的 script)
- 它可以用來一次啟動多個 Docker Container
- 它可以設定各個 container 間的網路溝通
建立 docker compose file
- docker compose file 內定義的 services 不需要額外設定,預設就會在同一個 network 內,彼此就能溝通
# 定義 docker-compose 使用的版本
version: '3'
# 定義兩個 services,一個是 redis-server,另一個是 node-app
# 這兩個 services 不需要額外設定,預設就會在同一個 network 內,彼此就能溝通
services:
redis-server:
image: 'redis'
node-app:
build: . # 根據當前根目錄的 Dockerfile 執行 build
ports:
- '8081:8081' # 將 local 的 8081 對應到 container 內的 8081
要讓 node-app 知道如何連線到 redis server,需要在 node-app 與 redis 建立連線的地方使用 docker-compose 中定義的 server 名稱:
// index.js
// ...
const client = redis.createClient({
host: 'redis-server', // 原本可能會填的是 localhost:6379,現在要用 service 的名稱
port: 6379,
});
使用 docker compose CLI
docker-compose up
後面可以不用加路徑,會直接去吃docker-compose.yml
檔案- 如果需要根據 Dockerfile rebuild 新的 image 時,可以在最後加上
--build
# docker-compose 的指令需要在有 docker-compose.yml 的資料夾中執行
$ docker-compose up # 對應到類似 docker run <image-id>
$ docker-compose up --build # 對應到 docker build . 和 docker run <image-id>
$ docker-compose up -d # 在背景執行
$ docker-compose down
$ docker-compose ps # 列出該 docker-compose 中有哪些 services 在執行
自動重新啟動 container
重新啟動的策略包含:
"no"
:不要嘗試重新啟動,使用no
要加上引號,讓它知道是字串always
:總是嘗試重新啟動,適合用在 Web Serveron-failure
:只有在 container 停止且有 error code 時才重新啟動(以 Node.js 來說,就是 exit 內的 code 是 0 以外的代碼),也就是程式正確終 止時,不用重新啟動,適合用在 workerunless-stopped
:除非是開發者強制停止它,否則總是重新啟動
...
services:
redis-server:
image: 'redis'
node-app:
+ restart: always
build: . # 根據當前根目錄的 Dockerfile 執行 build
ports:
- '8081:8081' # 將 local 的 8081 對應到 container 內的 8081