🚀 三句話總結

  1. 了解為什麼我們會需要Docker這類容器化產品
  2. 學會Docker的file、image、container、compose、hub…etc
  3. 了解Docker的網路系統為什麼能映射到本機上

☘️ 為什麼我要了解

因為在上一回中提到Senior為了預防問題產生,所以有個產品是前後端都會需要的,就是Docker。

🎨 改變了什麼

了解Docker的原理和一系列的Docker操作,而我個人的專案就運用Docker實現標準化的CI/CD,而不是像以前使用個人電腦出版後才把出版資料部屬雲端,讓依賴的直接轉移成可以標準化的「容器」,而不是依賴我不乾淨的電腦。

✍️ 總結和心得

為什麼需要Docker?

在軟體業中,我們常把程式分為好幾個branch進行版控,每個branch都有他的定義,而業界常採用的方案為:

  • Git Flow
  • GitHub Flow
  • GitLab Flow

而每種Flow定義都不一樣,但不外乎都有「生產branch」,即產線的程式版本就是這個Branch的程式版本,由此可知這個Branch至關重要。

而程式還需要編譯才能變成「產品」,因為程式的編譯常常被很多外部環境干擾,像是Windows版本、編譯器版本、環境變數 …etc,而這些如果用開發者電腦進行編譯,勢必有很大的風險,因為開發者電腦最「髒」,所以我們常需要額外的 VM、出版機 …etc,用來專門編譯「產品」。

而不論是VM還是出版機,他們的容量都非常的大具有繁多的產品不需要的功能(藍芽驅動、鍵盤驅動、螢幕驅動..etc),並且還是有可能被開發者「玷汙」,無法標準化和最佳化出版環境,只能靠公司團隊治理才能規避,所以有個工具叫做「Docker」,該產品能夠標準化出版和最佳化環境,並且只需要靠軟體人員就能構建,不像是出版機還需要額外硬體部門支援,所以Docker是現代標準化容器系統的工具。

Ex:

小明寫了個javaScript程式,然後他要進行「出版」,如果沒有Docker,他要安裝VM於是他進行了以下操作

  • 選擇OS、配置硬碟容量、安裝各種javaScript不需要的驅動 ..etc
  • 進入OS後 下載瀏覽器、下載並安裝NVM、下載並安裝指定NodeJs版本
  • 安裝依賴套件
  • 執行出版
  • 刪除VM

而上述步驟有一步做錯,出版環境就被玷汙了,但如果有了Docker會變成如何?

  • 利用Docker產生Container
  • 基於Container執行出版
  • 利用Docker刪除Container

Docker是什麼:

想像一下,今天你購買了一個蛋糕

  • 如果是VM,他會就地烘烤蛋糕,而為了蛋糕於是你被迫購買了 烤箱、攪拌機 …etc,最終你會獲得蛋糕與一堆烤箱、攪拌機。
  • 如果是Docker,你會獲得一個貨櫃,貨櫃裡裝的就是一個蛋糕而已。

在上述範例中蛋糕就是程式、烤箱和攪拌機則是驅動軟體,所以綜上所述,我們可以整理出Docker的特性:

  • Docker是一個工具,能夠幫助我們更方便的創建、運行、部屬程式
  • Docker占用資源少,資源利用率高,運行快
  • Docker能提供一次性服務,常被使用在雲服務中和微服務架構

而貨櫃這比喻,在Docker中則稱為 Container

Docker 原理:

Docker之所以能生成一個貨櫃,實現與VM一樣的完全隔離效果主要是利用Linux的功能,如下:

我們利用Linux的功能,將Container完全獨立,就像是VM一樣,所以貨櫃中的程式,就必須要有個Linux系統基底,不然無法實做Docker,但幸運的Linux有各種發行版本,最小的版本可以達到容量不到100M而已。

所以在程式中貨櫃中會裝有兩個東西:

  1. Linux系統
  2. 你的程式

那麼上述中的貨櫃(Container)怎麼來的?

  • Container由Image創建,Container也就是貨櫃
  • Image由File提供,Image也就是蛋糕模具、蛋糕材料 ..etc所組裝的「烘焙組合包」
  • File由廚師所設計,File也就是蛋糕要用什麼巧克力品種、奶油幾克,模具應該長怎樣,一張文字清單

下載Docker

網址:Docker
雖然Docker是基於Linux實做的,但我們仍可以在Mac or Windows上安裝並使用他,是因為

  • Windows 使用 Hyper V 技術安裝Linux
  • Mac 使用 Virtualbox 技術安裝Linux

注意:這邊只是我們為了使用Docker會透過VM方式安裝,但實際打包出來的Container 並不包含任何VM。只有在非Linux系統使用Docker才要透過VM安裝。

而安裝步驟這邊不多贅述,但是要特別注意的是,Docker是一個Client to Server的架構

這邊的Client是非常陽春的Command Line介面 (CLI = Command line interface),而我們如果要使用CLI呼叫Docker的功能我們就必須指定要用哪台Docker機器 (Server)。
正常情況下,Docker Machine預設的機器都是我們上面講到的VM容器,但你也可以指定另一台實體Linux機器,可以是GCP、AWS、樹莓派 …etc 有點像是SSH遠端功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
於終端機輸入 docker version:

Client:
Cloud integration: v1.0.29
Version: 20.10.20
API version: 1.41
Go version: go1.18.7
Git commit: 9fdeb9c
Built: Tue Oct 18 18:28:44 2022
OS/Arch: windows/amd64
Context: default
Experimental: true

Server: Docker Desktop 4.13.0 (89412)
Engine:
Version: 20.10.20
API version: 1.41 (minimum version 1.12)
Go version: go1.18.7
Git commit: 03df974
Built: Tue Oct 18 18:18:35 2022
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.6.8
GitCommit: 9cd3357b7fd7218e4aec3eae239db1f68a5a6ec6
runc:
Version: 1.1.4
GitCommit: v1.1.4-0-g5fd4c4d
docker-init:
Version: 0.19.0
GitCommit: de40ad0

這邊Client是你的電腦,明顯我是使用Windows系統,而我透過Dokcer安裝軟體自動幫我用 Hyper V 創建的 Linxu/amd64 被當作我預設的Docker Machine。
而如果你想要使用別人的Linux機當作你的Docker Server,可以自行搜尋 Docker Machine 指令說明進行切換Server,這邊不多闡述。

Dockerfile

安裝好Docker後就來實作出一個Container,而我們上面說過
Container -> Image -> File

Dockerfile是所有一切源頭,所以就先來聊聊Dockerfile。

Dockerfile其實是一個文字檔(該文字檔檔名必須叫做「Dockerfile」不能是「Dockerfile.txt」否則Docker軟體不會偵測到),其裡面記載了貨櫃

  1. 使用的基底,通常是某個Linux版本 (debian、ubuntu、alpine …etc)
  2. 依賴的東西(使用VM會自動安裝不必要的東西,但是這邊可以指定你要的東西就不會亂裝不必要的東西)
  3. 要做的事(通常依賴的東西下載好後,都要安裝,又或是設定你的程式需要的配置)

Ex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM selenium/standalone-chrome:99.0
## USER 代表使用者帳號,沒意外通常是指定root,類似Windwos的系統管理員帳號
USER root
## 當前預設路徑是什麼,類似打開終端機預設路徑通常是 C:/Users/user
WORKDIR app

## install nodejs
## RUN 代表在File Build成 Image時會下的指令,curl 是linux指令
RUN curl -s https://deb.nodesource.com/setup_16.x | sudo bash
## apt 是 Unbutn 的套件管理器,與之類似的產品是 python中的 pip、nodejs的npm、C#中NuGet、Mac中的homebrew
RUN apt install -y nodejs

## install 依賴
## 把當前目錄resource Copy到容器中的 app/test 資料夾
COPY ./resource test
RUN npm install

## 設定ENV
ENV TZ="Asia/Taipei"

FROM 使用哪個人的帳號名稱/那個人的作品名稱:版本號

這邊使用的是「別人的做好的 File」,當然你也可以自己使用各種發行版的Linux,而發行版的Linux是Docker官方維護的所以無須指定「使用哪個人的帳號名稱」。
其結構就如下:

1
2
3
4
5
FROM ubuntu:latest
RUN 下載爬蟲套件 - Chrome
RUN 安裝nodejs
RUN 安裝依賴
ENV 設定環境變數

這邊FROM的對象如果沒有特別寫URL,則通常是指 DockerHub,這是類似Github的倉庫,只是Docker官方運營的倉庫,專門存放別人做好的Docker Image供任何人使用。
而我們知道類似Github的倉庫有很多Gitlab ..etc,所以Docker倉庫也有很多種:

1
FROM gcp.io/foo/bar:123

其意思是使用gcp.io這個docker倉庫,並使用foo這個人的bar專案,其版本號為123。

而撰寫好Dockerfile後,就必須把他build 成Image。

1
docker build -t lucas-project .

-t 代表Image到時候要叫什麼名稱,而如果要丟到 DockerHub命名方式必須是「帳號名稱/專案名稱」
. 代表Dockerfile的位置,這邊只有一個「.」代表Dockerfile在當前目錄下,當然也可以使用絕對路徑

1
docker build -t lucas-project C:\Users\jlthu\OneDrive\桌面\Lucas\shopee-coins-bot

Docker Image

上述可以透過指令把 Dockerfile build成 Image,而此時Docker Image指是一個檔案而已,毫無任何作用,你無法直接交互他,你如果要使用他,就必須把它 run 成一個Container

1
2
docker run -d -p 123:80 lucas-project 

而一個Image可以生成多個Container,用完即刪,以此達到標準化功能。
所以實務上,我們可以寫一個出版專用的Image,裡面包含了所有出版會需要的依賴。以後只要我的Code修改了,我只用把這Image生成一個Container就可以進行編譯,用完後就刪掉,下次Source Code又改就可以再次使用Image生成,再次利用。

PS: -d 是代表這個Container跑起來後就進入背景模式,與之相反的是 -it,這個Container跑起來後我要進入這個Container
PS: -p 123:80 是說本機中123 Port 對接這個Container的 80 Port

Docker Compose

我們知道怎麼從Dockerfile To Container後,其實Docker的作用並不只是出版專用這麼簡單而已,在業界中系統架構有很多種,而其中「微服務」是現今主流的架構,而為了實現微服務功能我們會需要使用Docker部屬各個微小的服務,以此進行獨立並解偶。

假設我們有個訂單系統,我們會有三個基本單元 前端、後端、DBM,這就是三個服務也就是三個Container,但是他們要如何溝通呢?因為根據Docker的特性他們是虛擬化隔離網路隔離甚至與父層級的本機也是隔離的,那們要讓
前端下單 -> 後端寫單 -> DB儲存
這樣的溝通要怎麼實現?

在Docker中有一個概念網域,我們可以建立一個網域,然後把 前端、後端、DB 的Container塞入,讓他們可以彼此溝通得到,類似以下指令

1
2
3
4
let network = [];
netowrk.push(前端)
netowrk.push(後端)
netowrk.push(DB)

PS: 詳細如何建立網路的指令和把容器加入自定義網域,請自行搜尋因為不常用,Docker有另外一個指令可以取代他。

但Docker有個指令可以更簡單實作該功能,就是Docker Compose。
Compose會自動建立一個網域,然後所有被Compose Run起來的容器,都會被自動塞進這個網域中。Compose可以用來解決容器與容器之間的合作關係,並且功能非常多,甚至你可以透過Comopse就取代上面的 Docker run、build指令,所以Compose算是蠻方便的工具。

而要使用Docker Compose功能就必須建立一個檔案「docker-compose.yml」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.9"

services:
filebeat_0:
build:
context: ./filebeat/.
args:
- CONFIG_NAME=filebeat_kafka.yml
- KAFKA_PORT=9092
ports:
- 9000:9000
volumes:
- ./filebeat/input:/var/log
- ./filebeat/output:/tmp/filebeat

kafka_1:
image: bitnami/kafka:3.8
ports:
- 9201:9201
volumes:
- ./kafka/kafka_1/persistence:/bitnami
environment:
- KAFKA_CFG_LISTENERS=CLIENT://:9092,EXTERNAL://:9201

該檔案是yaml格式是種常見的config格式,類似python的空白規則,從上述yaml檔案看得出來,有兩個Container名叫filebeat_0kafka_1

1
2
3
4
5
6
7
8
9
10
  filebeat_0:
build:
context: ./filebeat/.
args:
- CONFIG_NAME=filebeat_kafka.yml
- KAFKA_PORT=9092
...
kafka_1:
image: bitnami/kafka:3.8
...
  • filebeat_0的Image是由filebeat資料夾內的Dockerfile所build而得,並且那個Dockerfile需要提供兩個自定義的參數(CONFIG_NAME、KAFKA_PORT)
  • kafka_1的Image是由DockerHub中bitnami這人所提供
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C:\Users\jlthu\OneDrive\桌面\Lucas\docker-filebeat-kafka-clickhouse
.
│ .gitIgnore
│ docker-compose.yml
│ README.md

├─filebeat
│ │ Dockerfile
│ │ filebeat_http.yml
│ │ filebeat_kafka.yml
│ │ filebeat_local.yml
│ │
│ ├─input
│ └─output
├─kafka
│ │ .gitkeep
│ │
│ ├─kafka_1
│ │ └─persistence
│ └─kafka_2
│ └─persistence
│ ...etc

上面就是Docker Compose取代Docker build的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    filebeat_0:
...
ports:
- 9000:9000
volumes:
- ./filebeat/input:/var/log
- ./filebeat/output:/tmp/filebeat

kafka_1:
...
ports:
- 9201:9201
volumes:
- ./kafka/kafka_1/persistence:/bitnami
environment:
- KAFKA_CFG_LISTENERS=CLIENT://:9092,EXTERNAL://:9201

上面就是Docker Compose取代Docker run的功能

  • filebeat_0 的Container Run起來後,監聽本機9000對應到Container內的9000,並且把Container中/var/log路徑的東西同步到本機的./filebeat/input
  • kafka_1 的Container Run起來後,監聽本機9100對應到Container內的2181,並且把Container中/bitnami路徑的東西同步到本機的./kafka/kafka_1/persistence並設定Container的環境變數(ENV)

當這些設置文件都配置好後,我們就可以使用docker-compose up,就會根據docker-compose.yml記載所運行,而要關掉則可以docker-compose down --rmi local,就會把compose的容器都關掉,並且rmove local image也就是filebeat這種本地build的Image檔案。

1
2
3
4
5
6
C:\Users\jlthu>docker network ls
NETWORK ID NAME DRIVER SCOPE
70adcd35d649 bridge bridge local
92dde023f305 docker-filebeat-kafka-clickhouse_default bridge local
84d037c58f0a host host local
45f8ac4c0ca3 none null local

上面就是Docker Compose取代Docker Network的功能,使用docker-compose up指令會自動建立一個network,他的名字是docker-filebeat-kafka-clickhouse_default 規則是 $資料夾名稱_default,而我們可以使用docker inspect進行檢查docker-filebeat-kafka-clickhouse_default內的Containers有誰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
C:\Users\jlthu\OneDrive\桌面\Lucas\docker-filebeat-kafka-clickhouse> docker inspect docker-filebeat-kafka-clickhouse_default
[
{
"Name": "docker-filebeat-kafka-clickhouse_default",
"Id": "92dde023f305ca43a07c2f22d8ac24b9f956d65961d6ed97229784424f0346ba",
"Created": "2022-12-04T12:32:01.361081354Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"23e3789efd434b695a9bca8d285b96121ed69e1e2e5f5ea31d2f5fec973d887e": {
"Name": "docker-filebeat-kafka-clickhouse-filebeat_slb_0-1",
"EndpointID": "d6bdaf92ccb529ad727abea80b276458e51c20260cefb6cb9fc3b8fd7d8534ec",
"MacAddress": "02:42:ac:18:00:03",
"IPv4Address": "172.24.0.3/16",
"IPv6Address": ""
},
"7e6c99015b0e741eb0df623b236350f04c7d315e72e27d1da0371d93acb69f37": {
"Name": "docker-filebeat-kafka-clickhouse-kafka_1-1",
"EndpointID": "1da02e5ca91b46acff552b7ebae959ba12c9d56fe39395d9f5cdbf89fe036224",
"MacAddress": "02:42:ac:18:00:04",
"IPv4Address": "172.24.0.4/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "docker-filebeat-kafka-clickhouse",
"com.docker.compose.version": "2.12.0"
}
}
]

顯然Containers欄位就有我們在Compose記載的Container

Docker Network

在上述我們大致了解Network,只有在同一個網域內的Containers 才能互相溝通到,但是在Docker中Network算是一門學問,所以這邊稍微解釋下。

1
2
3
4
5
6
C:\Users\jlthu>docker network ls
NETWORK ID NAME DRIVER SCOPE
70adcd35d649 bridge bridge local
84d037c58f0a host host local
45f8ac4c0ca3 none null local
1df61848e6bb lucas bridge local

在安裝Docker後,Docker會自動建立三個Network,這三個是無法被刪除的,也是最常用的Docker Network Drivers

PS:Docker有七種Network Drivers,這邊僅簡述其中三種,畢竟還有NAT、GateWay、veth ..etc 概念,詳細規則可能日後會單獨寫一篇文章。

Null

這類型如其名,就是null,無法進行與外部和其他容器任何連結

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Users\jlthu>docker run -it --network=none ubuntu:14.04 /bin/bash
root@66308c6686be:/## ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)


C:\Users\jlthu>docker inspect 66308c6686be | grep -i ipaddr
"SecondaryIPAddresses": null,
"IPAddress": "",
"IPAddress": "",

你可以從上面的各種指令看得出來,這個Container的網卡並沒有能力向外發出連線能力,甚至curlapt這類的網路指令都無法運作,完全屏蔽了網路隔離但是並沒有完全屏蔽虛擬化隔離,也就是說雖然無法收發任何網路封包,但是我們可以透過 volumes 同步Container內的資源

如果你今天需要執行一個不需要網路需求的Container,可以考慮用null類型。

Bridge

這是Docker默認的網路模式,我們可以透過 port 參數進行橋接,Host只能透過 Conatiner Export 的Port 進行網路溝通(Container內的Port對應到Host的Port),而其他網路功能完全與Host隔絕。
而在相同的網路中,彼此的Container可以互相溝通到,但是但是

  • 如果Container的網路不是自定義的Bridge,換句話說也就是使用系統自動建立的Bridge網路的那些Container,不會享有Conatiner Name map to DNS 功能,要溝通必須透過ping ip,不能ping Name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
C:\Users\jlthu>docker inspect bridge
[
{
"Name": "bridge",
"Id": "1f0a0dd9a2e9e0c97510bc8015f2f89c5ce2d08f9cccdbd798f910a7e611b3b4",
"Created": "2022-12-05T03:38:48.105702522Z",
"Scope": "local",
"Driver": "bridge",
...etc
"Containers": {
"4ebcb5c0698e278f1aed99ed437d316ae14e9229ffff9b43fdaf9e80c9b734a5": {
"Name": "abc",
"EndpointID": "9439061af972f6673d507925c1db47118b63acf1c246e3ac4cbc5923e9ead4f2",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"8737ece3f19393bca1490b0c1b70dd18c345576c31eb0b73f8ec4bd308da0b85": {
"Name": "def",
"EndpointID": "e19b43b34205f0c8642df6684ea17cf26b29b5638933079242b727f4e62ce1fb",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
}
...etc
}
]

C:\Users\jlthu>docker exec -it abc sh
## ping def
ping: unknown host def
## ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.051 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.044 ms
64 bytes from 172.17.0.3: icmp_seq=4 ttl=64 time=0.090 ms
  • 如果Container的網路是使用者自定義的Bidge(包含Docker-compose自動建立的網域),將會享有同個網路內的 Conatiner Name map to DNS 功能,可以使用 ping Name。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Users\jlthu>docker network create -d bridge lucas
74aa6bee776408eb7cb13d89f239d1326bccec4de151aaf30e739988be69da0f

C:\Users\jlthu>docker run -itd --rm --network lucas --name qqq ubuntu:14.04
0b186ea56bebc480493ea81ab353a5bab9fbf2554ce07f006c0fe5eeb5c314df

C:\Users\jlthu>docker run -itd --rm --network lucas --name ccc ubuntu:14.04
2f1fdd829d195ff4ba83d457a6c2be206ad9157f061676e902cb026ce6ae9f4e

C:\Users\jlthu>docker exec -it qqq sh
## ping ccc
PING ccc (172.18.0.3) 56(84) bytes of data.
64 bytes from ccc.lucas (172.18.0.3): icmp_seq=1 ttl=64 time=0.052 ms
64 bytes from ccc.lucas (172.18.0.3): icmp_seq=2 ttl=64 time=0.059 ms
64 bytes from ccc.lucas (172.18.0.3): icmp_seq=3 ttl=64 time=0.067 ms

Host

這是一個可以把Container直接運作在Host的網域,完全捨棄網域隔離的功能,也就是可以不經過 -p 80:80這種指定export Port方式,因為Conatiner的網域就是Host網域

1
docker run -itd --rm --network host --name nginx-test nginx

然後你在網頁上輸入127.0.0.1:80 就可以看到畫面了,因為nginx預設是80 port,並且host的網域。

注意!!!!
host網域有限制!!!
如果你是透過 docker desktop 安裝Docker,請注意這邊的host網域並非是你主機網域。

因為我們說過 Docker是基於Linux系統,而你之所以能在Mac or Windwos上運行,全因為是 Hyper-V or Virtualbox,所以你host的對象是他們而不是你的主機,換而言之,Host網域無法運作在Windwos Mac

Prerequisites

  • This procedure requires port 80 to be available on the Docker host. To make Nginx listen on a different port, see the documentation for the nginx image
  • The host networking driver only works on Linux hosts, and is not supported on Docker Desktop for Mac, Docker Desktop for Windows, or Docker EE for Windows Server.

PS: 詳細請參考 Docker官方說明stackoverflow

docker swarm

這個可以不用學,因為業界都在使用 K8S ,K8S可以完全取代他,而K8S還可以運行非Docker的容器。所以去學K8S吧。

PS: 誰說容器化只有Docker的,市面上還有很多類似Docker的產品。