Docker 指南

Docker 指南

Docker,開源容器化平台,改變軟體開發與部署方式。它提供輕量、可攜、自包含的環境,促進應用程式在不同環境中無縫運行。這份學習筆記將引導你了解 Docker 的核心概念、指令和實作,助你在容器技術的世界中航行自如,提升你的開發和部署效率。

本筆記參考課程:Docker容器技术从入门到精通

講師筆記於此:www.docker.tip

安裝

Rocky Linux 9

參考此篇文章:How To Install and Use Docker on Rocky Linux 9

Raspberry (ARM)

安裝會有點慢,可能是 I/O 問題。

參考官方文件安裝到 Centos 上面:Install Docker Engine on CentOS

容器速成指引

簡易指令介紹

查看版本

docker version
docker info

在 windows 上執行會有以下訊息

Client:
 Cloud integration: v1.0.29
 Version:           20.10.21
 API version:       1.41
 Go version:        go1.18.7
 Git commit:        baeda1f
 Built:             Tue Oct 25 18:08:16 2022
 OS/Arch:           windows/amd64
 Context:           default
 Experimental:      true

Server: Docker Desktop 4.15.0 (93002)
 Engine:
  Version:          20.10.21
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.18.7
  Git commit:       3056208
  Built:            Tue Oct 25 18:00:19 2022
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.6.10
  GitCommit:        770bd0108c32f3fb5c73ae1264f7e503fe7b2661
 runc:
  Version:          1.1.4
  GitCommit:        v1.1.4-0-g5fd4c4d
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

基本指令結構

 docker [OPTIONS] COMMAND
 docker [Management Commands] [Commands]

例如:

# 抓取名為 nginx 的 image
docker image pull nginx
# 停止名為 web 的 container
docker container stop web

Image and Container

Image

  1. image 是一個 read-only 的檔案
  2. 可以視為一個 template
  3. 其有分層的概念存在

Container

  1. 實質是複製 image 並在 image 最上層加上一層 read-write 的層,稱之為容器層 Container layer
  2. 同一個 image 可以建立多的 container
  3. container 大體上可以理解為執行中的 docker image

Basic command

指令有一些常用的是有提供簡單縮短長度的寫法,不是都一定要打完整的 Command,但是以初見者來說打完整的指令會較容易知道指令的用途。

操作 完整指令 簡易指令
創建 Container docker container run <image name> docker run <image name>
列出狀態為 up 的 Container docker container ls docker ps
列出狀態為 up/exit 的 Container docker container ls -a docker ps -a
停止 Container docker container stop <name or ID> docker stop <container name or ID>
刪除 Container docker container rm <name or ID> docker rm <container name or ID>

所有的指令都可以通過 –help 來查看解釋。

大量停止/刪除 Container 的技巧

介紹大量停止與大量刪除的用法:

簡單的作法是下

docker container ps

找出所有的 CONTAINER ID,然後下

docker container stop id1 id2 id3 id4

就可以停止 id1~4 的 Container

但是太麻煩了,可以改用以下複合指令

docker container stop $(docker container ps -aq)

同理刪除時可以使用

docker container rm $(docker container ps -aq)
  • 這裡是用 doller 符號加上小括號,不是大括號。

其他常用刪除 Container 的指令

這是一個可以快速刪除沒有在使用的 Container 的指令

docker system prune
  • 注意如果加了 -a 就會連 Image 也憶起刪除

Attached 與 Detached Mode

Attached Mode:前台執行,不建議使用 Detached Mode:後臺執行

要指定 Detached Mode 需要下一個 -d Option 如下:

docker container run -d -p 80:80 nginx

而要從 Attached Mode 轉為 Detached Mode 可以下以下指令:

docker attach id

Interactive Mode

如果只是要看 Container 裡面程式的 log,可以下:

docker container log id

但是如果要更複雜的操作,可以在 run 的時候啟動 shell ,就可以進行像一般 Console 中的操作

docker container run -it id sh

或者是對 Detached Mode 的 Container 下 exec 指令進入 sh

docker exec -it id sh
  • 請注意要有 -it 這兩個 option

Difference between VM and Container

  1. Container 其實是 Process,這一點可以查看系統正在執行的 process 得知
    • 從 Linux 上才觀察的到
  2. Container 中的 processes 會被限制對 CPU/Ram 等資源的存取。
  3. 如果把 Container Process 停掉,Container 就會關閉。

Container 中的 Process

如果先利用 docker container run -d nginx 來啟動一個 Container 再用 docker container top id 來觀察 container 的 PID 你會發現這個 ID 於 linux 下中使用 ps 指令會找到該 process 的。

但是如果進入到 Container 中去查看其內部 Process 的 PID,會與外部 linux 中看到的 PID 不一致,這是有關於 Container 隔離的議題。

  • Windows 中無法查看。

docker container run 的執行步驟

如果我們下了以下指令:

$ docker container run -d --publish 80:80 --name webhost nginx
  • -d 表示 detached mode
  • –publish 表示 port 綁定
  • –name 表示自訂 Container 的名稱,不要使用預設的隨機字串

則其系統會有以下動作:

  1. 在 Local 找是否有 nginx 這個image
    • 如果沒有,就會去 image registry 找 nginx 這個 image 並且下載,預設是最新版 (nginx:latest)
    • 預設的 registry 是 Docker Hub
  2. 基於 nginx image 來創建一個新的 Container,並且準備運行
  3. Docker engine 分配給這個 Container 一個虛擬 IP 位置
  4. 在宿主機上打開 80 port 並把容器的 80 port 轉發到宿主機上
  5. 啟動 Container,運行指定的命令(這裡是一個 shell script 去啟動nginx)
    • 如果於 run 指令最後沒有加上要執行的 shell script,就會執行其 image 預設的 script。

What is tty?

點擊此文章閱讀:tty 到底是什么

從 Container 中取出檔案

為了測試方便,以下指令可以將檔案自 Container 中取出:cp

docker cp 6619ff360cce:/opt/h2-data/pkslow ./
docker cp 6619ff360cce:/opt/h2-data/pkslow/pksl

Image 的建立管理與發佈

Image 的取得

  1. 使用 pull from 指令:從某個 Registry 抓回來,其有分 private/public regisrty
  2. 使用 build from Dockerfile:從 Dockerfile 的設定中自己建立
  3. 使用 load from :從一個檔案中 import 進來。

常用的 Registry

可以注意一下免費版的使用限制

  1. DockerHub
  2. Quay

docker image 常用操作

Pull 操作

這裡以 DockerHub 為例,先到 DockerHub 輸入搜尋:nginx,選擇官方的 image

可以看到右手邊就會有指令提供輸入了:docker pull nginx

事實上是推薦 docker image pull nginx 的,因為那是早期 Docker 版本的指令

預設是會用 tag 為 lastest 的版本,如果需要其他版本,則可以去找相應的 tag 後再輸入於冒號後方:

docker image pull nginx:1.20.0

那如果要去 Quay 下載 image 也是同樣道理:

docker pull quay.io/bitnami/nginx

Image 的詳細資訊察看

先利用 ls 找到你要查看的 image 其 ID

docker image ls

然後就可以繼續使用 inspect option 去列出該特定 image 的詳細訊息。

docker image inspect image_id

console 接著會顯示 Json 格式的資料,目前比較值得注意的有以下幾個項目:

  1. “Os”:“linux”
  2. “Architecture”:“amd64”
  3. “layers”:[ “sha256:XXX”,“sha256:YYY”,… ]

Image 刪除

這裡要注意的是,只能刪除『沒有在執行』的 Image ,而且也沒有 Container 有在用這個 Image (包含已經停止中的 Container)

所以你可以先去刪除有用到這個 Image 的 Container,再去刪除 Image 才可以正常執行。

docker image rm image_id
docker system prune -a

其他常用刪除 Image 的指令

這是一個可以快速刪除沒有在使用的 Container 與 Image 的指令

docker system prune -a
  • -a 不是只有刪除 Image 而已

Image 檔案匯出與匯入 (offline)

這種方法少用,是用在需要把 Image 轉給一台無法上網取的 Image 的電腦時可以使用

Save

 docker image save nginx:1.20.0 -o nginx.image
  • -o 表示 Output,後面接你要儲存的檔案位置與名稱

Load

docker image load -i .\nginx.image
  • -i 表示 input,後面接妳要匯入的 Image 檔案名稱

Dockerfile 超簡易介紹

Dockerfile 是用來『建立』Image 用的檔案,裡面的內容會寫如何建立的『步驟』,而這些步驟的寫法會需要遵照其格式語法的規定來寫。

詳細寫法規則可以參照官方文件:Docker Reference

這裡有一個跑 Python 的 hello world 檔案在 ubuntu 上的 dockerfile 範例:

# 基礎 image
FROM ubuntu:20.04
# 下載 python
RUN apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y python3.9 python3-pip python3.9-dev
# 加入檔案到 image 的 root 目錄
ADD hello.py /
# 執行 command
CMD ["python3", "/hello.py"]

詳細介紹請參考下一章節的說明。

Image 的建立與分享

Build

這一節介紹如果有了 Dockerfile 後要如何通過此檔案建立 Image。

將你要包含的檔案內容與 Dockerfile 檔案放在同一個目錄底下,執行:

docker image build -t hello:1.0 .
  • -t 表示你要建立的 Image 其名稱與 tag,注意要是沒有冒號就會使用預設的 latest tag
  • 請注意最後面還有一個點,這是表示你的 dockerfile 位置在哪裡,上面的範例是因為已經在同一層目錄底下所以用 . 表示,這個點很容易忽略請注意。

建立好 Image 就可以用 docker run 去跑了。

Push

Dockerhub 要推的話要先登入:

docker login

Dockerhub 中的 Image 命名是以『帳號ID/名稱』作為通用格式的,在推上 Dockerhub 時請注意遵照此規則。

當一個 ImageID 有多個名稱:Tag,要刪除的話只能用該 Image 名稱來刪除,不能用 ID

docker image push account/hello:1.0

Commit

Image 的建立還可以用 Container 反過來創建新 Image。

docker container commit containerID newName:newTag

如果 Container 內容有被修改過,停掉之後裡面的修改內容是會被保存的,再次開啟動 Container 是可以得到被修改過的內容的 Container。 但是如果利用 Image 再次開新的 Container 就會維持原本乾淨的容器。

所以如果想要永久保存這個 Container 的狀態,可以利用 Commit 指令來儲存目前狀態的 Container 為一個新 Image。

可以先使用一個 ubuntu image 創建 Container 並進入交互模式,把該 Container 內容改造成自己需求的樣子,再使用 Commit 保存為 Image。

空的 Image:Scratch Image

很少會使用這個 Image 當 Base ,一般都會用 Ubuntu、CentOS 等做基礎 Image。

因為什麼都沒有,所以其大小為 0 ,而且因為沒有包含基本需要的作業系統文件,並無法直接執行一些 Java、Python 等檔案,但是 C 語言編譯後是可以被執行的。

Dockerhub Pull 的次數限制

免費的 Personal 方案有以下限制:

  • 200 image pulls per 6 hours
    • 如果是未登入則只有 100 次
    • 未登入是看你 IP 位置來限制的

如果要知道剩餘可拉次數,Dockerhub 有提供 API 去查詢目前剩餘次數。

About buildkit

參考此文章:Buildkit

這可以用來提高建構 Image 的速度,windows 開啟的方法如下:

請把 Json 裡面加入一個 feature 欄位:

"features": {
  "buildkit": true
}

Dockerfile 完全指南

Dockerfile 檔案的寫法有自己的一套規則,需要記憶。

Base Image 的選擇:FROM

Image 選擇原則為以下順序:

  1. 官方 Image
  2. Opensource 的 Image
  3. 容量小的 Image

Alpine Linux 是可以准許超小容量的 Linux Container 版本,可低到 8MB 而已。

Image 中執行指令:RUN

使用 RUN 可以在 Image 中進行你需要的任何操作,包含安裝等等指令。

這裡要注意的是一個 RUN 指令就會多建立一層 Layer,一般不要用太多單獨的 RUN 指令,因為分層會變多,也就是會讓 Image 變得更大。

需要多個指令執行的話,可以採用拼接的方式寫在一個 RUN 就好,這樣可以有效減少 Image 體積大小。

未整合成一個 RUN 的寫法:

FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y wget
RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
RUN tar zxf ipinfo_2.0.1_linux_amd64.tar.gz
RUN mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
RUN rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

寫成一個 RUN 的寫法:

FROM ubuntu:20.04
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \
    tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
    mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

體積大小可以自行建構判斷。

檔案複製與資料夾操作:ADD、COPY、WORKDIR

複製檔案到 Image 中有 ADD 與 COPY 兩種指令:

  • ADD:如果有符合其支援的壓縮格式,這個指令會順便解壓縮進 Image,而 COPY 則沒有,例如 gzip。
  • 兩個指令在 Image 中發現沒有對應的資料夾的話會自動創建。
FROM python:3.9.5-alpine3.13
COPY hello.py /app/hello.py
ADD hello.tar.gz /app/

另外創建資料夾的 WORKDIR 指令,其相當於 CD 指令,但是比較好用的是如果 Image 內沒有該資料夾,則會自動創建並切換到該位置下面。之後的所有檔案操作都會以該目錄進行作業。

複製檔案的條件限制

並不是任意路徑的檔案都可以直接指定複製到 Image 之中,僅有該 dockerifle 檔案位置開始往下的路徑才可以複製。

例如要複製一個在 dockerfile 所在位置的上一層資料夾中的某個檔案,會被 Docker 報錯,所以解決方法可以是在上一層目錄中在執行 docker 的建構。

Image 內的環境變數設置:ARG、ENV

ARG 跟 ENV 在用法上其實一樣,但本質上有很大的不同:

  • ARG 關注在 Image 的建立期間,在 Image 創建完成後並啟動 Container 時,你不會在環境變數中看到這個指定變數
    • 你可以透過一個 –build-arg 來指定建構時的變數,這在 ENV 無法做到
  • ENV 則是關注在執行期間,其可以透過環境變數取得該數值。

在 Dockerfile 中可以使用錢符號來替代該變數。

ARG 寫法:

FROM ubuntu:20.04
ENV VERSION=2.0.1
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
    tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
    mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz

如果 ARG 要改變建構時期的變數,可以使用以下指令寫法:

–build-arg 參數 + 變數=數值

docker image build -f .\Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .

ENV 寫法: 使用此寫法,可以進入該 Container 後下 env 指令就可以看到該變數有存在

FROM ubuntu:20.04
ARG VERSION=2.0.1
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
    tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
    mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz

Container 啟動後的執行指令:CMD

如果於 Dockerfile 中有寫 CMD 的話,則其會在 Container 被啟動的時候被作為預設指令執行。 這裡要注意多個 CMD 只有最後一個會被執行,不會每個都執行。

以執行優先順位來看:

  1. 如果 docker container run 後面有指定指令則最優先
  2. Dockerfile 裡面有指定的最後一個 CMD
  3. 先前的 base image 中有指定 CMD

補充執行完就刪除 Container 的執行方法

一般來說如果多次使用 docker container run imageID,則每次執行都會建造 Container,Container 退出了是不會被刪除的。 如果要每次建造後、執行完退出後要立即刪除,可以使用 –rm option

docker container run --rm -it imageID

Container 啟動後的執行指令:ENTRYPOINT

ENTRYPOINT 跟 CMD 都可以在 Container 啟動時執行指令,以下說明差異:

  • CMD 可以在 docker container run 的時候去覆蓋掉,但是 ENTRYPOINT 是一定會被執行的,無法被覆蓋
  • CMD 和 ENTRYPOINT 可以聯合使用,ENTRYPOINT 作為指令,CMD 則做為其參數傳入

Shell 與 EXEC 格式

撰寫 CMD 跟 ENTRYPOINT 在寫法上有兩種:

  1. Shell:直接後面接指令
    • CMD echo “hello docker”
    • ENTRYPOINT echo “hello docker”
  2. EXEC:使用中括號包裹不同片段
    • ENTRYPOINT [“echo”, “hello docker”]
    • CMD [“echo”, “hello docker”]

CMD 在 Shell 寫法上是可以使用變數的,例如:

FROM alpine:3.17.0
ENV NAME=docker
CMD echo "hello $NAME"

但是改成 Exec 的寫法就要注意,直接打上變數是沒有用的,要特意在 CMD 中指明使用 shell 才行:

FROM alpine:3.17.0
ENV NAME=docker
CMD ["sh", "-c", "echo hello $NAME"]

簡易 Kotlin Hello world 實作

這裡記錄一下用 kotlin jar 的練習操作,還不涉及網路部分。

  1. 用 IDEA 開一個 kotlin gradle project,什麼都不用動
  2. 把 build.gradle 加入 shadowJar,這樣打包 jar 才會包含 kotlin library 進去
plugins {
    /** 用 shadowJar plugin 打包 */
    id 'com.github.johnrengelman.shadow' version '7.1.2'
    id 'org.jetbrains.kotlin.jvm' version '1.7.21'
    id 'application'
}
/** 加入 manifest 否則就要自己指定 */
shadowJar {
    manifest {
        attributes([
                'Main-Class': 'MainKt'
        ])
    }
}
  1. 在打包好的 jar 旁邊建立一個 Dockerfile:
FROM eclipse-temurin:17.0.4.1_1-jre-alpine
ENV NAME = Enix
COPY ./DockerExpert-1.0-all.jar .
ENTRYPOINT ["java","-jar","DockerExpert-1.0-all.jar"]
CMD []
  1. 建構 Image
docker image build -t kotlin-hello .
  1. 執行 Container
docker container run --rm -it kotlin-hello
  1. 可以看到 Console 顯示
Hello World!
Program arguments:

Dockerfile 使用技巧說明

合理使用暫存

docker 在建構的時候,會去使用暫存,如果前面有經過建構過的話。

Dockerfile 的每一行都會是一個新的 layer,所以只要 dockerfile 的某一行被修改了,這一行之後的所有建構指令都不會用暫存了。

所以我們可以把常常會改變的建構指令放在比較後的地方,這樣使用暫存的部分就會比較多,建構的速度也會變快。

.dockerignore 的使用

Docker 是一個 Server-Client 的架構,Server 跟 Client 是可以分開的。

在建構 Image 的時候,Client 會把指定的 BuildContext 中的內容送給 Server 做處理。

docker image build -t name . 後面的點是表示當下位置,也就是 BuildContext 了。

如果沒有去過濾該位置下的文件,你很有可能會把沒有必要納入建構流程的檔案也送給 Server ,這樣就會花費大量時間在傳輸。

這時候使用 .dockerignore 就可以過濾不需要的檔案,其用法跟 gitignore 差不多。

或者是有些較為機密的檔案,也可以設定在此 ignore 中。

範例:

.vscode/
env/

多階段建構

如果需要將 Code 編譯的部分也交給 Docker 執行,我們可以使用多階段建構的方法做,如此就算 Local 沒有設定 Java 的環境也可以做編譯。

將 Dockerfile 中的建構部分分成兩塊:編譯與執行階段,過程是將編譯完成的檔案自編譯階段取出後放入第二階段的 建構流程後再進行構建。

兩階段建構範例:

# 建構 Jar 階段
FROM gradle:7.4.2-jdk17-alpine as builder

WORKDIR /DockerExpert
# 複製資料夾也要寫對目的地資料夾名稱
COPY ./src /DockerExpert/src
COPY ./build.gradle /DockerExpert
# 因為前面有執行 WORKDIR 了,所以這一行的位置已經是在 DockerExpert 資料夾下面了
RUN gradle build

# 打包階段
FROM eclipse-temurin:17.0.4.1_1-jre-alpine
# 從上一階段的內容中取出編譯好的 jar
COPY --from=builder /DockerExpert/build/libs/DockerExpert-1.0-all.jar .
ENTRYPOINT ["java","-jar","DockerExpert-1.0-all.jar"]
CMD []

盡量不要使用 root 身分操作

Linux 使用者需要注意!!!!!!

原本在系統中沒有 Root 權限的帳號,但是該帳號有執行 Docker 的權限的話,就可以利用 Container 中的 Root 權限查看任何 Host 中的任何資料夾,甚至是賦予自己 SUDO ,非常危險。

故可以在建立 Image 的時候,在 Dockerfile 中寫明執行時的帳號為非 root 帳號,可以解決此問題。

請先於 Host 中創建一個帳號並賦予一樣的 Group

# 建立群組
groupadd zero
# 新建使用者
# 其實直接 useradd zero 也可以
useradd -g zero zero

再來在 Dockerfile 中使用 USER 指令就可以了。

USER zero

Summary

請仔細閱讀官方文件:Docker Reference

如果要參考學習對象,可以到官方 Image 的 Github 中參考其文件說明學習及模仿,例如 Java 的:docker-library/openjdk

找到不同版本的 Dockerfile 查看來學習寫法。

Container 的資料儲存

預設情況下,Container 所儲存的資料,雖然在停止的狀態下仍可以被儲存於其 Container 的 R/W Layer,但是這些資料不能被其他 Container 使用,而且只要 Container 被刪除,這些資料也會消失。

Docker 針對資料儲存的方法有三種:

  1. Data Volume:由 Docker 管理這些資料,常用
    • Linux 會存在 /var/lib/docker/volumes/ Linux
  2. Bind Mount:自行決定資料會存在那個位置
  3. tmpfs mount:資料存於記憶體中,少用所以不介紹

Data Volumn

於 Dockerfile 中使用 VOLUME 指令,可以指定特定位置要使用 volumn。

但是這裡要注意,如果是寫在 Dockerfile 中,每次都會創建一個新的 volume 去儲存,這並不是我們的目的。

所以需要在 Run 新 Container 的時候去指定 Volumn 的名稱,這樣每次開新的 Container 才會使用一樣的 Volumn。

  • 在 Linux 才可以輕易的看到 Volume 儲存的資料
  • 在 Windows 你需要進入到 WSL 裡面才能看到你的 Volume 實際儲存內容。
    • 或者簡單一點可以用 Docker Desktop 也可以直接查看檔案內容。

以下說明使用差異:-v 參數的使用

使用 -v 參數創建 Container

使用 -v 的話就不用於 Dockerfile 中使用 VOLUME 指令,只要於創建 Container 時指定自訂名稱與路徑就可以了。

-v 參數後面接『Volume名稱:路徑』

docker container run --rm -it -v vol01:/volumn docker-expert01

不使用 -v 參數創建 Container

這就是在 Dockerfile 寫 VOLUME 的方式。

操作 Volume

可以使用 volume inspect 指令 + Volume 名稱去查看該 Volume 資訊

docker volume inspect vol01

即可顯示詳細資訊:

[
    {
        "CreatedAt": "2022-12-29T08:45:48Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/vol01/_data",
        "Name": "vol01",
        "Options": null,
        "Scope": "local"
    }
]

如果要刪除 Volume 可以用 rm 或者是 system prune -f 也行

練習:Mysql 搭配 Volume

使用 Mysql 8 來操作。

docker pull mysql:8.0.31

再來關於此 Image 的詳細官方說明:How to use this image

官方說明沒有用 Volume,我們自己加:

docker container run --name mysql-expert -e MYSQL_ROOT_PASSWORD=mypw -d -v mysql-data:/var/lib/mysql mysql:8.0.31
  • -d:背景執行
  • -e:環境變數
docker container exec -it ??? sh

進入 mysql 後建立 database

 mysql -u root -p
 create database demo;
 show databases;

將這個 Container stop 後再創一個 Container 起來,一樣進入 mysql ,你就可以看到剛剛在上一個 Container 中建立好的 database 是有出現的。

Bind Mount

相較於 VOLUME,Bind 的方式是直接將 Container 中的某位置接到 Host 上的某一個位置,不像 VOLUME 是在一個虛擬機上面的位置上建立空間。

使用方法一樣是用 -v 參數,並不是只能用 –mount,–mount 是早期版本使用的指令,不要搞混。

docker container run --rm -it -v ${pwd}:/volumn docker-expert01
  • 可以使用參數的方式,在 Windows 的 powershell 中可以用錢符號+大括號

直接指定位置可以有以下範例寫法:

docker run -d --name my-http -v D:\DockerMountTest\my-http:/usr/local/apache2/htdocs -p 8080:80 httpd

練習:開發環境 – Bind Mount

就是把含有 JDK 環境的 Container 給掛一個開發用的含有 code 的資料夾,然後於 Container 中編譯、執行等等。

VS Code 有外掛可以這樣做,Intellij 未知。

跨裝置/網路共享 Volume 的做法

Docker 有提供一系列的 Driver 讓使用者可以將 Container 不只能存取 Local 建立的 Volume,可以存取像是 Amazon 的 S3 等等。

參考官方文件:Click

以下介紹其中一個簡單的 SSHFS Driver:

  1. 準備兩台裝置,通通都安裝 Driver
docker plugin install --grant-all-permissions vieux/sshfs
  1. 其中一台連到另一台建立 Volume
docker volume create --driver vieux/sshfs \
-o sshcmd=vagrant@192.168.200.12:/home/vagrant \
-o password=vagrant \
sshvolume
  1. 查看 Volume 並建立 Container 並掛上該 Volume
docker volume ls
docker run -it -v sshvolume:/app busybox sh
  1. 對其進行任意操作,例如建檔,可以看到另一台裝置上的目錄會有被操作的紀錄。

其他的 Volume driver 可以 Google 再去了解。

Container 的網路

網路的基礎知識

可以簡單蓋覽以下文章了解,無須太過深入。

網路相關常用指令

請注意 Linux 相關的指令。

作用 windows linux
查看 IP ipconfig ifconfig、ip addr
IP 連通性 ping ping
Port 連通性 telnet telnet
路徑追蹤 TRACERT.EXE tracepath
網路服務 curl curl

Container 網路相關基礎問題

接下來的小節將對下列問題做說明:

  1. Container 為何能得到 IP ?
  2. 為何 Host 可以 Ping Container 的 IP ?
  3. 為何 Container 之間 IP 是互通的 ?
  4. 為何 Container 可以 Ping 到外網 ?
  5. Container 的 port 轉發是如何做到的 ?

Bridge 網路:Container 之間的網路互通

  • 為何 Container 之間 IP 是可以互通的 ??

Linux Bridge 是一個純粹用軟體實作的一個虛擬交換器。

Docker 有建了一個預設的 Bridge,可以使用 docker network ls 查看

所有 Container 預設都有連到這個 Bridge 所以可以互通。

在 host 中 (Linux),可以使用 ip addr 找到這個名為 docker0 的 linux bridge。

[enixlin@enix-service ~]$ ip addr
...
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ed:95:b1:e7 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:edff:fe95:b1e7/64 scope link
       valid_lft forever preferred_lft forever
...
PS C:\Users\EnixLin> docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
fb9c33ef3372   bridge    bridge    local
457de2d35734   host      host      local
46697b065a17   none      null      local
PS C:\Users\EnixLin> docker network inspect fb9
[
    {
        "Name": "bridge",
        "Id": "fb9c33ef3372c304d910c28a0f481632cf59b7a3df19e602c71d68105209fd4e",
        "Created": "2023-01-05T06:42:37.1678171Z",
        ...
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        ...
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

Bridge 網路:Container 對外的網路互通

veth = Virtual Ethernet 虛擬網卡介面

Container 的對外會通過 docker0 再出去,這中間會經過 IP 轉發過程,詳細路徑轉換規則可以使用 ip route 查看

[enixlin@enix-service ~]$ ip route
default via 10.X.0.1 dev eth0 proto dhcp src 10.X.0.2 metric 100
10.X.0.1 dev eth0 proto dhcp scope link src 10.X.0.2 metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1

另外 iptable 的詳細轉換規則可由 sudo iptables –list -t nat -nvxL 查看,包含 port 的部分。

NAT (Network Address Translation):IP 轉換

Private IP 到 Public IP 之間的轉換過程,反過來也是。 另外 Private IP 到 Private IP 轉換也算是。

自建 Docker bridge

網路相關的指令是 docker network。

預設 Container 是會連接到名為 bridge 的 bridge。

# 建立 Bridge
docker network create -d bridge custom-bridge
# 查看現有網路
docker network ls

啟動的時候,使用 –network 指定要連接的網路。

# 指定網路連接並啟動
docker container run --rm -it --network custom-bridge  alpine:3.17.0 sh
# 查看網路狀態
docker network inspect custom-bridge

已經啟動狀態之下也可以去連接新的或斷開舊的網路。 而且一個 Container 是可以連接多個網路的。

docker network connect custom-bridge container-name
docker network disconnect custom-bridge container-name

自訂的好處

預設的 bridge 是不提供 DNS 的功能的,也就是說同一個 Bridge 下的 Container 要互相 Ping 只能打 IP。

如果是連接到自己的一個自建 Bridge 就可以用 Container 的名稱來取代 IP,因為 Bridge 會自動解析這個名子到正確的 IP 上。

Subnet、Gateway 設定

在建立 Bridge 時,可以直接指定 Subnet、Gateway 的數值。

可以使用參數 subnet、gateway 設定

docker network create -d bridge --gateway 172.200.0.1 --subnet 172.200.0.0/16 my-bridge

Port Forwarding

內網到外網可以利用 NAT 達到效果,那反過來外網向內網就需要使用『連接阜轉發』。

Container 在創建的時候,就是使用 -p 參數去設定轉發的,Docker 會自動在 host 上加上設定。

這可以從下列指令查看:

sudo iptable -t nat -nvxL

Dockerfile 的 EXPORT 用途

The EXPOSE instruction does not actually publish the port. It functions as a type of documentation between the person who builds the image and the person who runs the container, about which ports are intended to be published. To actually publish the port when running the container, use the -p flag on docker run to publish and map one or more ports, or the -P flag to publish all exposed ports and map them to high-order ports.

根據官方文件,EXPOSE 是用來讓使用者知道這個 Image 是有開這個 Port 的功能,如果要使用相關功能可於啟動 Container 時另外加 -p 參數來設置。

所以需要注意,只有寫 EXPOSE 而沒有於啟動的時候指定 port 還是等於沒有打開 port 的。

Host 網路、Null 網路

在建立 Network 的時候,除了可選擇 Bridge,還有另一種類型為 Host。

這是 Container 與 Host 共享同樣的網路。 讓 Container 能不透過 NAT 等轉換的消耗,可以直接使用 Host 上的 port,性能較好,但會有 port 占用問題的情況發生。

  • Windows 不支援使用 host 網路

The host networking driver only works on Linux hosts, and is not supported on Docker for Mac, Docker for Windows, or Docker EE for Windows Server.

最後還有一種 Null 網路,平常不用,有些不需要網路的條件下才去考慮使用。

Linux Network Namespace

詳細示範直接看講師講義。

練習:建立一組含有 Redis 的服務

  1. 先建立一個 Bride 名為 custom-bridge,這樣就可以用 Container name 代替 IP
docker network create -d bridge custom-bridge
  1. 再來建立一個 Redis 於 local:
    • 63790 是為了 local 測試看有沒有正常啟動用的,-p 非必要
docker pull redis:7.0.7-alpine3.17
docker run --name my-redis --network custom-bridge -p 63790:6379  -d redis:7.0.7-alpine3.17
  1. 建立一個 Jar 去讀取 Redis 的儲存的數字,每次讀取並累加其數字 10 後儲存
    • 這裡用 Redis 去存取。
fun main(args: Array<String>) {
    println("Hello World!")
    println("Program arguments: ${args.joinToString()}")

    val url = System.getenv("url") ?: "localhost"
    val port = System.getenv("port") ?: "63790"
    println("url:port is $url:$port")

    val jedis = Jedis(url, port.toInt())
    if (!jedis.exists("count")) jedis.set("count", "0")
    (0..10).forEach {
        val count = jedis.get("count").toInt()
        println("count is $count")
        jedis.set("count", (count + 1).toString())
        Thread.sleep(500)
    }
}
plugins {
    /** 用 shadowJar plugin 打包 */
    id 'com.github.johnrengelman.shadow' version '7.1.2'
    id 'org.jetbrains.kotlin.jvm' version '1.7.21'
    id 'application'
}
dependencies {
    implementation 'redis.clients:jedis:4.3.1'
}
/** 加入 manifest 否則就要自己指定 */
shadowJar {
    manifest {
        attributes([
                'Main-Class': 'com.enix.MainKt'
        ])
    }
}
  1. 打包的 Dockerfile 如下:
FROM gradle:7.4.2-jdk17-alpine as builder

WORKDIR /DockerExpert
COPY ./src /DockerExpert/src
COPY ./build.gradle /DockerExpert
RUN gradle build

FROM eclipse-temurin:17.0.4.1_1-jre-alpine
COPY --from=builder /DockerExpert/build/libs/DockerExpert-1.0-all.jar .
ENTRYPOINT ["java","-jar","DockerExpert-1.0-all.jar"]
CMD []
docker image build -t docker-expert02 .
  1. 啟動的時候指定 Redis 的 port 與 url
    • 這裡的 –env 可以由 System.getenv(“key”) 取得
    • 可以發現數字在每次執行都會增加,表示正確存取 Redis
docker container run --rm -it --env port=6379 --env url=my-redis docker-expert02

Docker Compose

創建、啟動 Container 是由一連串固定套路的指令來完成的,Docker Componse 就是為了簡化這些繁瑣步驟而生,特別是一次要建立出多個 Container 與設定的時候。

所有的指令都寫成一個 yaml 的檔案中。

安裝

DockerComponse 是一個由 python 寫的工具。

如果是 Windows,在安裝 Docker Desktop 的時候就已經安裝了。

docker-compose --version

Linux 的話可以去 Github 上下載來安裝:DockerCompose Github release page

安裝範例指令:

$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.29.2, build 5becea4c

或者用 python 的 pip 來安裝:

pip install docker-compose

Compose 檔案的格式與用法

詳細可以參考官方文件:Compose specification

這是一個 yaml 格式的檔案。

基本結構如下:

# 此檔案的語法版本
version: "3.8"

# Container
services:
    servicename: # Container 名稱,可以在 bridge網路中使用的 DNS name
        image: # Image 的名字
        command: # 可選,如果有設定則會覆蓋預設 Image 裡面的 CMD 指令
        environment: # 可選,相當於 docker run 裡的 --env
        volumes: # 可選,相當於docker run 裡的 -v
        networks: # 可選,相當於 docker run 裡的 --network
        ports: # 可選,相當於 docker run 裡的 -p
    servicename2:
        ...
volumes: # 可選,相當於 docker volume create
networks: # 可選,相當於 docker network create
  • docker-compose 檔案有版本之分,以初學者來說版本之間不需要關心差異,因為基礎與法基本一致。

Docker-compose 的基本使用指令

要使用 Docker-compose 就需要將目前位置移動到 docker-compose.yaml 的資料夾位置上,檔案名稱也不能亂改。 當然這也可以設定的,與 dockerfile 一樣的操作。 (可以使用 -f 參數指定位置)

基礎的指令:

指令 用途 說明
up Create and start containers
down Stop and remove containers, networks
logs View output from containers
ls List running compose projects
ps List containers
rm Removes stopped service containers

另外 Option 的部分有兩個值得記憶:

  1. -f:指定 docker-compose.yml 位置
  2. -p:指定專案名稱,這關乎到預設的 Container name、newwork name 等等的預設名稱的修改。

Image 的建立與拉取

在 docker-compose.yml 的設定中,如果 image 這一項在 local 找不到對應的 Image 的話,則會去 Dockerhub 中下載。

build 選項可以指定一個 dockerfile 去提供建立 image,而不是直接從 Dockerhub 上下載。

version: 3.4

services:
    servicename:
        build: # 可以指定建立位置
            context: ./project
            dockerfile: mydockerfile # 可以指定不同名稱,例如不同環境
        image: projectname:tags

除此之外也可以先使用 pull 指令在真正建構服務前先將缺少的 Image 下載下來。

docker-compose build
docker-compose pull

Compose 如何處理更新行為?

“ 一般來說,若一個專案已經 up 了,而其中的 Code 有部分修改,如果直接 stop 再 up 是不會有任何改變的,因為沒有重建 Image。

以下介紹三種常用指令:

  1. 啟動時包含重建 Image 的行為
docker-compose up -d --build
  1. 如果有 Service 被移除了,DockerCompose 會提醒你要用這個選項去移除無用的服務
docker-compose up -d --remove-orphans
  1. 通常如果 Container 有掛載 Volume,其中可能包含了一些設定檔案,如果需要重啟重讀檔案,可以使用 restart。
docker-compose restart

網路議題

如果於 docker-compose.yml 中沒有設定 network,則其預設會創建一個 Bridge$^1$ 並且設定好 DNS ,於 Container 中就可以互 Ping 同一個 Project 中的 Container name。

$^1$單機下,預設的 network 的 driver 會是 Bridge,如果是 Swarm 等等情況就不會還是 Bridge 了。

當然每個 Container 都可以分開建立不同的 Network。

# 詳細去看文件,通通都有
network:
    net1:
        ipam:
            driver: default

平行擴充

docker-compose 提供了一個水平擴充的功能:–scale

 docker-compose up -d --scale servicename=3

根據前面的說明內容,當 Container 被創建後,因為有 DNS 功能,所以可以使用 Container Name 去 Ping,但是當水平擴充了的話,再去用同樣的名稱去 Ping 會如何??

答案是 docker-compose 會建立一個負載均衡的機制,所以每次 ping 都會是多台輪流 ping。

範例檔案:Download

講師範例中的示範專案,其結構如下: Nginx <–> Flask <–> Redis

Nginx 反向代理後端的 url 位置,而 url 的會由於附載均衡的關係,由所有的 Flask 服務去輪流執行,而最後共同存取最後端的 Redis 資料。

下面可以看到網路的設定結構:

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      - flask
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

# 兩段網路
networks:
  backend:
  frontend:

Nginx 的設定只有反向代理,負載均衡會由 DockerCompose 完成:

server {
  listen  80 default_server;
  location / {
    proxy_pass http://flask:5000;
  }
}

環境變數

如果於 docker-compose.yml 中需要輸入一些像是密碼的屬性的話,在非 Local 的環境是可以使用 Secret 的功能,而 Local 的話,就需另擇他法。

在 docker-compose.yml 的相同位置下創建 .env 這個檔案,裡面的參數是可以被 yml 讀取到的,也就是說可以藉由替換 .env 這個檔案達到隱蔽數值、切分環境等等效果。

範例如下: 可以使用錢符號 ${varible}

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}

.env 檔案

REDIS_PASSWORD=ABC123

服務相依性與健康度檢查

dockerfile 與 docker-compose 都可以加入健康度檢查的指令。

dockerfile 健康度的用法

使用 HEALTHCHECK 這個指令去操作。 參考官方文件:HEALTHCHECK

例如:

HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:5000/ || exit 1
  • 每 30 秒檢查一次,失敗就會退出,返回 1
    • shell 中 exit 0 是正常 retuen,1 等其他的數字就是非正常。

docker-compose 健康度的用法

改用 Docker-compose 的健康度寫法,原本寫在 dockerfile 的寫法就必須要移除。

參考官方文件:healthcheck

以下為範例:

version: "3.8"

services:
  flask:
    build:
      context: ./flask
      dockerfile: Dockerfile
    image: flask-demo:latest
    environment:
      - REDIS_HOST=redis-server
      - REDIS_PASS=${REDIS_PASSWORD}
    # 跟 dockerfile 差不多的結構轉移
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s
    depends_on:
      redis-server:
        condition: service_healthy
    networks:
      - backend
      - frontend

  redis-server:
    image: redis:latest
    command: redis-server --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 1s
      timeout: 3s
      retries: 30
    networks:
      - backend

  nginx:
    image: nginx:stable-alpine
    ports:
      - 8000:80
    depends_on:
      flask:
        # 增加健康度檢查的條件
        condition: service_healthy
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./var/log/nginx:/var/log/nginx
    networks:
      - frontend

networks:
  backend:
  frontend:

這裡額外要注意 depends_on 的用法:

  1. depends_on 只會因為其相依的 Container 並不是 up 狀態時才會停止創建。
    • 也就是說就算健康度檢查失敗一樣會建立 Container。
  2. depends_on 可以增加額外的條件去判斷是否要正常啟動

一個還不錯的參考範例:Click

練習:講師提供的良好範例

dockersamples/example-voting-app

練習:自我練習

使用 Springboot + MySql + Nginx 示範:

Docker Swarm

DockerCompose 是以單一節點的服務,只要一掛,全 Container 就都會全死掉,是只適合開發環境下使用,Production 完全不建議。

kubernetes 在市佔率上是輾壓的 80%,所以這一章主要是要透過學習 Swarm 的概念去理解分散式架構的觀念,如此在之後的 k8s 學習會較為輕鬆。

單一節點:快速啟動與關閉

DockerSwarm 預設就是關閉的。 從 docker info 可以查看到其中一項:Swarm 的啟動狀態

docker info
Swarm: inactive

使用 Swarm 有兩種方式:

  1. 自己建立一個群集
  2. 加入一個群集

單一節點要啟動與關閉的指令分別如下:

# 啟動
docker swarm init
# 離開群集,最後一個離開需要再加上 --force
docker swarm leave

init 指令做了什麼?

主要是PKI和安全相關的自動化

  • 創建 swarm 群集的根證書
  • manager 節點的證書
  • 其它節點加入集群需要的 tokens
  • 創建 Raft 資料庫用於存儲證書,設定,密碼等資料

以下有 Raft 演算法相關的介紹可以閱讀:

  • http://thesecretlivesofdata.com/raft/
  • https://raft.github.io/
  • https://docs.docker.com/engine/swarm/raft/

Swarm 單節點快速用例

  1. 建立單節點
docker swarm init
  1. 建立服務:以 Nginx 示範
 docker service create nginx:1.23.3-alpine
  1. 查看該服務的狀態以及含有的 Container 資訊:
    • 注意 ID 是不一樣意義的。
# 查看 Service ID 等等
docker service ls
ID             NAME               MODE         REPLICAS   IMAGE                 PORTS
geyh2u98c7mb   reverent_mclaren   replicated   1/1        nginx:1.23.3-alpine

# 查看 Service 詳細資訊
docker service ps gey
ID             NAME                 IMAGE                 NODE             DESIRED STATE   CURRENT STATE            ERROR     PORTS
ut0xg09vs8oy   reverent_mclaren.1   nginx:1.23.3-alpine   docker-desktop   Running         Running 50 seconds ago
  1. 增加服務的 Container 數量
# 增加數量
docker service update gey --replicas 3

# 查看 Service 詳細資訊
docker service ps gey
ID             NAME                 IMAGE                 NODE             DESIRED STATE   CURRENT STATE            ERROR     PORTS
ut0xg09vs8oy   reverent_mclaren.1   nginx:1.23.3-alpine   docker-desktop   Running         Running 7 minutes ago
hx61xva89qd4   reverent_mclaren.2   nginx:1.23.3-alpine   docker-desktop   Running         Running 40 seconds ago
i2xhew5fe4ay   reverent_mclaren.3   nginx:1.23.3-alpine   docker-desktop   Running         Running 40 seconds ago
  1. 如果其中一個 Container 炸了,例如我們手動刪除,Swarm 會自動維持其設定的數量,保持於 3。

三節點的練習

練習方法有三種:

  1. play with docker:這個網站可以快速練習,但是只有 4 小時的時間就會重置
  2. Local 跑三個虛擬機:Vagrant + Virtualbox
  3. 使用 GCP、Amazon 等等服務來建立
    • Port 的部分需要注意要開,以及一些安全性設定
      • TCP port 2376
      • TCP port 2377
      • TCP and UDP port 7946
      • UDP port 4789

教學採用 Vagrant + Virtualbox 的方式建立三個虛擬機。 講師在 Youtube 的 Vagrant 課程 可以參考

Swarm 的網路

Swarm 網路可以分成兩類來理解:

  1. 整個群集對內與對外的流量:進入的流量由 Ingress 網路處理,Container 對外的流量由 Linux Bridge + NAT 處理
  2. 群集內 Container 互相溝通的流量:可以由 overlay 網路處理。

Overlay 網路

群集內的溝通所採用的,查看 network 可以看到其 Scope 是 Swarm。

Ingress 網路

外部要連通內部時使用。

多服務建構

Swarm 是一個正是環境使用的工具,不像 DockerCompose 可以在要使用的時候才去建立 Image,必須事先建立好

secret 與 volume 的使用

Skip。

Docker vs Podman

Podman 是 Red Hat 在 2018 年推出的,原始程式碼開放。

Podman 與 Docker 有以下的不同:最重要的是前兩項

  1. 最主要的區別是 Podman 是 Daemonless 的,而 Docker 在執行任務的時候,必須依存於後台的 docker daemon
  2. Podman 不需要使用 root user 或者 root 權限,所以更安全。
  3. Podman 可以創建 pod,pod 的概念和 Kubernetes 定義的 pod 類似
  4. Podman 運行把鏡像和容器存儲在不同的地方,但是 docker 必須存儲在docker engineer 所在的本地
  5. Podman 是傳統的 fork-exec 模式,而 Docker 是 client-server 架構

安裝

Windows 版本只能裝在 WSL2 上,講師表示建議是開虛擬機裝,但我還是要裝在 WSL2 不然不方便。

Podman 的獨立環境

Podman 對於每個使用者帳號,Image、Container 是獨立分開的,互相 ls 是看不到的。

而 Docker 雖然在非 Docker 群組或者 root 使用者之外的帳號是無法執行指令的,但是每個帳號都還是可以藉由 ps 指令觀察的到已經起起來的 Container。

Pod 的概念

Podman 的 Pod 幾乎和 Kubernate 的 Pod 一樣。

# 創建 Pod
[user@DESKTOP-L47M39G ~]$ podman pod create --name my-pod
cf0be2b2c45795fd942bf9697e10933d62090ba19192fdc491d0702c4ad85109

# 列出 Pod
[user@DESKTOP-L47M39G ~]$ podman pod ls
POD ID        NAME        STATUS      CREATED        INFRA ID      # OF CONTAINERS
cf0be2b2c457  my-pod      Created     6 seconds ago  752688ac274c  1

# 列出 Container 與其所屬 Pod
[user@DESKTOP-L47M39G ~]$ podman container ls -ap
CONTAINER ID  IMAGE                                    COMMAND     CREATED             STATUS      PORTS       NAMES               POD ID        PODNAME
752688ac274c  localhost/podman-pause:4.4.1-1676629882              About a minute ago  Created                 cf0be2b2c457-infra  cf0be2b2c457  my-pod
  • Pod 創建後,裡面會包含一個預設的 Container,用來處理其內部運作。

要於 Pod 內建立 Container 的話,只要在建立 Container 時指定 Pod 就可以了。

podman pull docker.io/library/nginx:1.23.3-alpine

Podman 要從 dockerhub 抓 Image 時,前面要加上 docker.io/library 才能正確抓取。

podman container run -d --name my-nginx --pod my-pod docker.io/library/nginx:1.23.3-alpine

在同一個 Pod 的 Container 有什麼特性??

同一個 Pod 的 Container 可以觀察到其共用了一個 IP,也就是說在同一個 Pod 下,所有的 Container 要互相連通只要打 127.0.0.1 與 port 就可以打的到。

相當於 Container 是運作在一個機器下。

Pod 的創建腳本

與 dockercompile 差不多,其也有一個 yaml 的檔案可以做 pod 的建立。

  • 相關的指令為:podman play –help

詳細請參考 K8s 的內容。

Docker 的非 root 模式

根據官方文件,非 root 模式對 Docker 版本是有限定的。

Rootless mode was introduced in Docker Engine v19.03 as an experimental feature. Rootless mode graduated from experimental in Docker Engine v20.10.

非 root 模式之下,Docker 會變得有一些限制,例如 port 不能指定 1024 以下對外等等,詳細可以參考文件。 而因為 Podman 非 root 也能用,所以在使用上一樣會有一些限制。

關於 Kubernetes 停止對 Docker 支援這件事

可參考 Youtube 影片:Kubernetes停止支持Docker了?| 对我们有什么影响?| Docker还值得学习么?

  • Dockershim 要被移除了。
  • Docker 是 Container 體系的開山推廣者,還是學一下比較好
  • 支援 CRI (Container Runtime Interface) 的 CR 僅有:
    • Containerd
    • Cri-O

多種架構的 Image 建立:ARM 與 X86

一般從 Dockerhub 看到比較大的 Image 都有支援多種架構。

本節說明如何建立支援多架構的 Image,而不需要把每種環境都要準備好才能建立。

ARM 的 Docker

樹梅派就是用 ARM,可以用樹梅派來玩玩。

  • 就算該 Image 並沒有支援 local 的架構,還是可以 pull 下來,只是你不能跑起來而已。
  • 在哪種架構下建立的 Image 就會是那種架構的 Image。
  • 在某架構的系統下去 push image 到 Dockerhub 會直接蓋原本的 Image ,並不會並存。

使用 buildx 來建立多架構 Imgae

建立方法其實有很多,但此小節說明的 buildx 是其中一種比較簡單的做法。

Window/Mac 的 Docker 本身就已經有這個工具了,但是 Linux 需要額外安裝。

# 登入
docker login

# 列出建構環境
docker buildx ls
docker buildx create --name mybuilder --use mybuilder

# 建立後直接推上去
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 -t xiaopeng163/flask-redis:latest .
  • 如果建構 Image 時有錯誤,可以新建一個新的 buildx 環境嘗試看看。

包含已經對應架構的 Image 如何處理

可以在 dockerfile 中去定義,在建構不同架構的 Image 時要下載不同的 Image。

FROM alpine:3.16

ARG TARGETARCH=amd64 TERRAFORM_VERSION="1.2.9"

RUN apk update && apk add --no-cache curl

RUN curl \
    --location \
    --output /tmp/terraform.zip \
    https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip \
    && unzip /tmp/terraform.zip -d /usr/local/bin \
    && rm -rf /tmp/terraform.zip

CMD []
  • 使用 TARGETARCH 與 TERRAFORM_VERSION 來設定。

補充

  1. 如果要分享自己的 Image 的話,最好要能做多架構。
  2. manifest 指令:可以不用下載 Image 就可以 inspect 其訊息,但是需要有網路才能正常運作。

GitHub Actions 與 CI/CD

Image 自動化構建與推送。

自動化部屬。

Dockerhub 的 Image 自動化構建服務

以前是免費的,現在要升級付費帳號才能用,,看看就好。

可以與 Github 連動,很簡單,但是不支援多架構的建構。

Github Action 使用

可以閱讀官方文件

關於 Action 的部分,如果是比較常用的指令,應該都有人寫好了現成的 Action 可以套進來使用。 可以到 Marketplace 搜尋看看

Ansible – CD 工具

Ansible 是一款組態管理平台,可將儲存設備、伺服器與連網設備自動化。使用 Ansible 配置這些元件時,困難的手動工作就變得可重複執行,因此比較不容易出錯。 使用 Ansible 可大幅減少組態時間和部署準備工作。

教學影片可以參考:麦兜搞IT – Ansible入门

Container Security

以安全問題分層級來看,可以有以下層次:

此小節僅說明 Container 與 Image 的部分,但並不代表其他層次並不重要。

根據每個層面,能夠討論的部分有以下:

  • Application:自己撰寫的程式
    • Code 方面的漏洞
  • Image:
    • 請選擇官方的 Image
    • Image 需要進行漏洞的弱點掃描
  • Container:
    • Container 需要進行漏洞的弱點掃描
    • Container 需要有監控的機制
  • Host:跑 Docker 的主機
    • Linux Kernel
    • Kernel namespaces
    • Control groups
    • 可以使用非 root 模式

Docker 執行環境的檢查

此工具可以幫助你檢查 Docker 的執行環境是否有風險存在。

Image 漏洞檢查

Snyk 可以連結 Github 進行掃描。

Container 監控

可以了解相關內容,像是 ELK 的運用等等。

Docker Interview Questions

參考此文件:docker-interview-questions.pdf

可以藉由 PDF 中的問題回顧自身所學。

發佈留言