在容器技術的世界中,理解映像(Image)的層疊結構是掌握Docker精髓的關鍵。當玄貓初次接觸Docker時,我立刻被其獨特的層疊檔案系統設計所吸引。這種設計不僅提供了極佳的資源分享機制,還讓容器變得輕量與高效。
Docker映像是由多個層(Layer)組成的,每一層代表著Dockerfile中的一條指令所產生的變更。當我們執行RUN
、COPY
或ADD
等指令時,Docker會建立一個新層來儲存這些變更。這些層疊在一起,透過AUFS(Advanced Multi-Layered Unification File System)等檔案系統技術形成最終的容器檔案系統。
映像層的限制與最佳化策略
實務上,AUFS檔案系統對層數有127層的硬性限制。這也是為什麼許多Dockerfile會嘗試將多個Unix命令合併在單一RUN
指令中執行:
# 不建議的做法:每個命令產生一層
RUN apt-get update
RUN apt-get install -y cowsay
RUN apt-get install -y fortune
# 建議的做法:多個命令合併為一層
RUN apt-get update && apt-get install -y cowsay fortune
這種合併不只是為了避免達到層數限制,更重要的是能夠大幅減少映像的體積。在玄貓的容器最佳化實務中,我經常建議開發團隊審視他們的Dockerfile,找出可合併的指令來減少不必要的層數。
容器狀態與生命週期管理
容器可處於多種狀態,理解這些狀態對於有效管理容器至關重要:
- created:已透過
docker create
初始化但尚未啟動的容器 - running:正在執行中的容器
- restarting:正在重新啟動的容器(實務上較少見)
- paused:暫停執行的容器
- exited:已停止的容器,主要程式已結束
值得注意的是,“exited”(已結束)狀態通常被稱為"stopped"(已停止)。這表示容器中沒有執行中的程式,但與"created"狀態不同的是,“exited"容器至少已經啟動過一次。
停止容器與映像的區別
很多Docker初學者常混淆已停止的容器與映像。已停止的容器會保留其設定、元資料和檔案系統的變更,包括IP位址等執行時設定,而這些都不會儲存在映像中。我們可以使用docker start
命令重新啟動已停止的容器,並保留這些變更。
使用ENTRYPOINT提升容器使用體驗
在開發容器化應用時,玄貓發現正確使用ENTRYPOINT
指令能大幅提升使用者經驗。這個指令允許我們指定一個可執行檔案,用來處理傳遞給docker run
的任何引數。
讓我們透過一個實際案例來理解這個概念。假設我們正在建立一個根據cowsay
程式的容器:
FROM debian
RUN apt-get update && apt-get install -y cowsay fortune
ENTRYPOINT ["/usr/games/cowsay"]
有了這個ENTRYPOINT
設定,使用者可以直接執行:
$ docker run test/cowsay-dockerfile "Moo"
而不需要指定cowsay
命令本身。這大簡化了容器的使用方式。
建立靈活的ENTRYPOINT指令碼
然而,上述方法有個限制:我們失去了使用fortune
命令作為cowsay
輸入的能力。解決方案是提供自訂的ENTRYPOINT指令碼,這是建立Dockerfile時的常見模式。
以下是一個名為entrypoint.sh
的指令碼範例:
#!/bin/bash
if [ $# -eq 0 ]; then
/usr/games/fortune | /usr/games/cowsay
else
/usr/games/cowsay "$@"
fi
此指令碼的邏輯很簡單:如果沒有提供引數,就將fortune
的輸出導向cowsay
;否則,直接使用提供的引數呼叫cowsay
。
接著,我們需要修改Dockerfile以將此指令碼加入映像並透過ENTRYPOINT
指令呼叫它:
FROM debian
RUN apt-get update && apt-get install -y cowsay fortune
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
這裡的COPY
指令將主機上的檔案複製到映像的檔案系統中,第一個引數是主機上的檔案,第二個是目標路徑,非常類別似於cp
命令。
現在我們可以建立新映像並嘗試使用或不使用引數執行容器:
$ docker build -t test/cowsay-dockerfile .
$ docker run test/cowsay-dockerfile
____________________________________
/ The last thing one knows in \
| constructing a work is what to put |
| first. |
| |
\ -- Blaise Pascal /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Docker註冊與映像管理系統
Docker的映像管理採用層級式結構,包含以下關鍵概念:
註冊服務、儲存函式庫像和標籤
註冊服務(Registry):負責託管和分發映像的服務。預設的註冊服務是Docker Hub。
儲存函式庫epository):相關映像的集合,通常提供同一應用程式或服務的不同版本。
標籤(Tag):附加到儲存函式庫像的字母數字識別碼(例如
14.04
或stable
)。
例如,命令docker pull amouat/revealjs:latest
會從Docker Hub註冊服務下載amouat/revealjs
儲存函式庫記為latest
的映像。
上載自訂映像至Docker Hub
要上載我們的cowsay
映像,首先需要在Docker Hub註冊帳戶(可以線上或使用docker login
命令)。接著,我們需要使用適當的儲存函式庫標記映像,然後使用docker push
命令上載。
在此之前,讓我們先在Dockerfile中加入MAINTAINER
指令,它用來設定映像的作者聯絡資訊:
FROM debian
MAINTAINER John Smith <john@smith.com>
RUN apt-get update && apt-get install -y cowsay fortune
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
現在重新建立映像並上載至Docker Hub:
$ docker build -t amouat/cowsay .
$ docker push amouat/cowsay
由於我沒有在儲存函式庫後指定標籤,它會自動被分配latest
標籤。若要指定標籤,只需在儲存函式庫後加上冒號和標籤名稱(例如docker build -t amouat/cowsay:stable .
)。
上載完成後,全世界都可以透過docker pull
命令下載你的映像(例如docker pull amouat/cowsay
)。
私有儲存函式庫
如果不希望全世界都能存取你的映像,有幾種選擇:
- 付費使用託管的私有儲存函式庫Docker Hub或類別似的服務如quay.io上)
- 執行自己的註冊服務
映像名稱空間
推播的Docker映像可以屬於三種名稱空間,可以從映像名稱識別:
使用者名稱空間:以字串和
/
為字首的名稱,如amouat/revealjs
,屬於Docker Hub上特定使用者上載的映像。根名稱空間:沒有字首或
/
的名稱,如debian
和ubuntu
,由Docker Inc.控制,保留給Docker Hub上的官方映像。第三方註冊服務:以主機名或IP為字首的名稱,指向第三方註冊服務(非Docker Hub)上託管的映像。例如,
localhost:5000/wordpress
指向本地註冊服務上的WordPress映像。
這種名稱空間確保使用者不會混淆映像的來源;如果使用debian
映像,就知道它是來自Docker Hub的官方映像,而不是其他註冊服務的debian版本。
使用Redis官方映像:實際案例
讓我們看如何使用Docker官方儲存函式庫像——以Redis(一種流行的鍵值儲存)為例。
官方儲存函式庫要性
在Docker Hub上搜尋熱門應用或服務時,你會發現數百個結果。Docker官方儲存函式庫提供已知品質和來源的精選映像,應該是你的首選。它們通常會出現在搜尋結果的頂部,並標記為官方。
當從官方儲存函式庫時,名稱將沒有使用者部分,或設定為library
(例如,MongoDB儲存函式庫mongo
和library/mongo
取得)。你還會收到"正在提取的映像已被驗證"的訊息,表示Docker守護程式已驗證映像的校驗和。
首先取得Redis映像:
$ docker pull redis
接著啟動Redis容器,這次使用-d
引數在背景執行:
$ docker run --name myredis -d redis
585b3d36e7cec8d06f768f6eb199a29feb8b2e5622884452633772169695b94a
-d
告訴Docker在背景執行容器。Docker正常啟動容器,但不顯示容器的輸出,而是回傳容器ID並結束。容器仍在背景執行,可以使用docker logs
命令檢視容器的輸出。
容器連結例項
如何使用這個Redis容器呢?我們需要以某種方式連線到資料函式庫於沒有應用程式,我們將使用redis-cli
工具。我們可以在主機上安裝redis-cli
,但更簡單與更有教育意義的方法是啟動一個新容器來執行redis-cli
,並將兩者連結:
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@ca38735c5747:/data# redis-cli -h redis -p 6379
redis:6379> ping
PONG
redis:6379> set "abc" 123
OK
redis:6379> get "abc"
"123"
redis:6379> exit
root@ca38735c5747:/data# exit
這個簡潔的例子展示了容器技術的強大之處——我們只需幾秒鐘就完成了兩個容器的連結並向Redis新增了一些資料。
在這個過程中,--link
標誌建立了從新容器到已命名為myredis
的Redis容器的連線,並在新容器中將其別名設定為redis
。這使得新容器可以透過名稱redis
存取Redis容器,而無需知道其IP位址。
Docker容器技術的魅力就在於此:它提供了一種簡單、可重複與一致的方式來佈署和連線應用程式,無論是在開發環境還是生產環境中。這種方法大簡化了應用程式的佈署和擴充套件過程,同時保持了環境的一致性。
Docker容器網路通訊的演進與實作
在我深入研究容器技術的這幾年中,Docker的網路架構一直在持續演進。雖然本文將使用--link
指令來連線容器,但值得注意的是,Docker網路模型正朝著更現代化的方向發展。未來,使用「發布服務」的方式會比直接連結容器更符合Docker的設計理念。不過,為了保持向後相容性,連結功能在可預見的未來仍會得到支援。
容器連結的魔法
在實際操作中,連結容器的關鍵在於docker run
命令中的--link myredis:redis
引數。這個引數告訴Docker系統:我們希望將新建立的容器與已存在的「myredis」容器連線起來,並在新容器中以「redis」作為其識別名稱。
Docker如何實作這一點?它在新容器的/etc/hosts
檔案中新增了一個「redis」專案,指向「myredis」容器的IP位址。這讓我們能夠在redis-cli
中直接使用「redis」作為主機名稱,而無需手動查詢或傳入Redis容器的IP位址。
這種設計讓我能夠輕鬆地:
- 使用Redis的ping命令確認連線狀態
- 透過set和get命令來存取資料
資料持久化的挑戰
容器間通訊解決了,但還有一個關鍵問題:如何實作資料的持久化與備份?
在容器世界中,標準的容器檔案系統並不適合長期儲存資料,因為容器本身可能會被刪除或重建。這時,我們需要一種能夠在容器與主機之間,或者容器與容器之間輕鬆分享的儲存機制。Docker透過「卷」(volumes)概念解決了這個問題。
卷是直接掛載在主機上的檔案或目錄,它們不屬於Docker的聯合檔案系統(Union File System)。這意味著卷可以:
- 在多個容器間分享
- 所有變更直接寫入主機檔案系統
- 即使容器被移除,資料也能保留
Docker卷的宣告與使用方式
在實務中,我發現有兩種方式可以將目錄宣告為卷:
- 在Dockerfile中使用VOLUME指令:
VOLUME /data
- 在執行容器時使用-v引數:
docker run -v /data test/webserver
這兩種方式都會在容器內建立一個/data
路徑作為卷。預設情況下,這個目錄或檔案會掛載到主機上的Docker安裝目錄中(通常是/var/lib/docker/
)。
值得注意的是,我們也可以在docker run
命令中指定主機上的掛載目錄:
docker run -d -v /host/dir:/container/dir test/webserver
但根據可攜性和安全性考量,在Dockerfile中是不能指定主機目錄的。這是有道理的,因為:
- 指定的目錄在其他系統上可能不存在
- 容器不應該在沒有明確授權的情況下掛載敏感檔案(如
/etc/passwd
)
實戰範例:Redis資料的備份
假設我們已經有一個名為「myredis」的Redis容器在執行,以下是如何使用Docker卷進行資料備份的步驟:
$ docker run --rm -it --link myredis:redis redis /bin/bash
root@09a1c4abf81f:/data# redis-cli -h redis -p 6379
redis:6379> set "persistence" "test"
OK
redis:6379> save
OK
redis:6379> exit
root@09a1c4abf81f:/data# exit
exit
$ docker run --rm --volumes-from myredis -v $(pwd)/backup:/backup \
debian cp /data/dump.rdb /backup/
$ ls backup
dump.rdb
在這個範例中,我做了以下操作:
- 首先啟動一個臨時的Redis客戶端容器,連線到現有的myredis容器
- 在Redis中設定一個鍵值對並執行save命令,將資料儲存到RDB檔案
- 啟動另一個臨時容器,使用
--volumes-from
引數連線到myredis容器的卷 - 同時使用
-v
引數將當前目錄下的backup資料夾掛載到容器內的/backup路徑 - 將Redis的dump.rdb檔案複製到備份目錄
這個技術讓我能夠在不中斷Redis服務的情況下完成資料備份。
容器的清理
當不再需要myredis容器時,可以停止並刪除它:
$ docker stop myredis
myredis
$ docker rm -v myredis
myredis
注意使用-v
引數移除容器時會同時移除與該容器關聯的卷(如果沒有其他容器使用這些卷)。
若要移除所有閒置的容器,可以使用:
$ docker rm $(docker ps -aq)
45e404caa093
e4b31d0550cd
7a24491027fc
...
Docker的基礎架構
理解Docker的基本架構有助於我們更好地使用這個平台並解釋某些看似不尋常的行為。Docker安裝的主要元件包括:
Docker守護程式 (Docker daemon) - 位於系統核心,負責:
- 建立、執行和監控容器
- 構建和儲存映像檔
Docker客戶端 (Docker client) - 透過HTTP與Docker守護程式通訊。預設情況下,這種通訊透過Unix domain socket進行,但也可以使用TCP socket以支援遠端客戶端。
Docker登入檔 (Docker registries) - 儲存和分發映像檔。預設的登入檔是Docker Hub,它託管了數千個公共映像檔以及官方認證的映像檔。
Docker的底層技術
Docker守護程式使用「執行驅動」來建立容器。預設情況下,這是Docker自己的runc驅動,它與以下Linux核心功能緊密結合:
- cgroups - 負責管理容器使用的資源(如CPU和記憶體)。cgroups也負責凍結和解凍容器,這在
docker pause
功能中被使用。 - namespaces - 負責隔離容器,確保容器的檔案系統、主機名、使用者、網路和程式與系統其他部分離。
Libcontainer還支援SELinux和AppArmor,這些可以啟用以提供更嚴格的安全性。
另一個重要的底層技術是聯合檔案系統(UFS),用於儲存容器的層。UFS可以由AUFS、devicemapper、BTRFS或Overlay等多種儲存驅動提供。
Docker生態系統技術
在我的容器化專案實踐中,我發現Docker引擎和Docker Hub本身並不構成使用容器的完整解決方案。大多數使用者需要支援服務和軟體,例如叢集管理、服務發現工具和進階網路功能。
Docker Inc.計劃建立一個包含這些功能的完整解決方案,同時允許使用者輕鬆地將預設元件替換為第三方元件。這種「可替換電池」策略主要指API層面,但也可以看作是允許作為獨立二進位檔案封裝的支援Docker技術輕鬆替換為第三方等效物。
Docker提供的支援技術包括:
Swarm - Docker的叢集解決方案,可以將多個Docker主機組合在一起,使用者能夠將它們視為統一資源。
Compose - 用於構建和執行由多個Docker容器組成的應用程式,主要用於開發和測試而非生產環境。
Machine - 在本地或遠端資源上安裝和設定Docker主機,同時設定Docker客戶端,使在不同環境之間切換變得容易。
Kitematic - Mac OS和Windows的Docker容器執行和管理GUI。
Docker Trusted Registry - Docker的企業級解決方案,用於儲存和管理Docker映像檔。本質上是Docker Hub的本地版本,可以與現有安全基礎設施整合。
第三方生態系統整合
在我參與多個容器化專案的過程中,發現已經有大量來自第三方的服務和應用程式構建在Docker上或與Docker一起工作。在以下領域已經出現了多種解決方案:
網路 - 建立跨主機的容器網路是一個非平凡的問題,可以透過多種方式解決。這一領域已經出現了幾種解決方案,包括Weave和Project Calico。此外,Docker不久將推出名為Overlay的整合網路解決方案。
服務發現 - 當Docker容器啟動時,它需要某種方式找到它需要與之通訊的其他服務,這些服務通常也在容器中執行。由於容器被動態分配IP位址,這在大型系統中並不是一個簡單的問題。這方面的解決方案包括Consul、Registrator、SkyDNS和etcd。
協調和叢集管理 - 在大型容器佈署中,工具對於監控和管理系統至關重要。每個新容器都需要放置在主機上、被監控和更新。系統需要透過適當地移動、啟動或停止容器來回應故障或負載變化。這方面已經有幾個競爭解決方案,包括Google的Kubernetes、Marathon(Mesos的框架)、CoreOS的Fleet和Docker自己的Swarm工具。
值得一提的是,除了Docker Trusted Registry外,還有其他替代方案,包括CoreOS Enterprise Registry和JFrog的Artifactory。
Docker的生態系統正在快速發展,這些技術和工具使容器化解決方案更加完整和強大。在我的實踐中,選擇合適的工具組合對於成功實施容器化專案至關重要。
Docker容器技術的廣泛影響
容器技術的興起產生了一個有趣的副作用:專為託管容器而設計的新型作業系統。雖然Docker在Ubuntu和Red Hat等大多數當前的Linux發行版上執行良好,但有幾個專案正在進行中,旨在建立專門為容器設計的輕量級作業系統。
在我實際佈署容器化應用的經驗中,這些專門的容器主機系統在生產環境中提供了許多優勢,包括更小的攻擊面、更少的維護需求和更高的效能。隨著容器化技術的不斷成熟,我們可以期待看到更多針對容器執行最佳化的基礎設施解決方案。
在實踐Docker技術的過程中,理解這些基本概念至關重要。無論是構建映像檔、網路連線容器還是處理資料卷,這些知識都能幫助我們更有效地利用Docker平台,並為更高階的使用案例做好準備。
Docker的設計理念是將複雜的容器技術變得簡單易用,但要充分發揮其潛力,我們需要深入理解其基礎架構和運作原理。隨著Docker技術的不斷演進,保持對最新發展的關注將有助於我們在容器化道路上走得更遠。
Docker 基礎架構與容器管理實戰
Docker 生態系統與佈署選項
輕量級分散式系統基礎
在容器技術的基礎上,已經發展出許多專為容器執行而設計的極簡、易維護發行版。這些系統專注於執行容器(或容器與虛擬機器),特別適合於資料中心或叢集環境。代表性的系統包括 Project Atomic、CoreOS 和 RancherOS,這些都是為容器最佳化的精簡化系統。
Docker 雲端託管服務概覽
雖然第九章將會詳細介紹 Docker 託管服務,這裡先簡要說明一些重要選項:
- 傳統雲端供應商:Amazon、Google、Digital Ocean 等都提供了不同程度的 Docker 支援服務
- Google Container Engine:可能是最值得關注的服務,因為它直接建立在 Kubernetes 之上
- 一般性支援:即使雲端供應商沒有專門的 Docker 服務,通常仍可以佈建執行 Docker 容器的虛擬機器
Joyent 也以自己的容器產品 Triton 加入市場,該產品建立在 SmartOS 之上。透過實作 Docker API 並結合自身的容器和 Linux 模擬技術,Joyent 建立了一個能與標準 Docker 客戶端介面的公有雲。值得注意的是,Joyent 認為其容器實作安全性足以直接在裸機上執行,而不必放在虛擬機器中,這意味著可以大幅提高效率,特別是在 I/O 方面。
此外,還有一些專案在 Docker 上構建 PaaS 平台,包括 Deis、Flynn 和 Paz,為開發者提供更高層級的抽象。
Docker 映像檔構建機制深入解析
在前面我們瞭解到,建立新映像檔的主要方式是透過 Dockerfile 和 docker build
命令。這一節將探討這一過程的內部機制,並提供 Dockerfile 中各種指令的使用。瞭解 build
命令的內部工作原理相當重要,因為它的行為有時會令人意外。
構建連貫的背景與環境的重要性
docker build
命令需要一個 Dockerfile 和一個構建連貫的背景與環境(可以是空的)。構建連貫的背景與環境是一組可以在 Dockerfile 的 ADD 或 COPY 指令中參照的本地檔案和目錄,通常指定為一個目錄路徑。例如,當我們使用命令 docker build -t test/cowsay-dockerfile .
時,將當前工作目錄「.」設為連貫的背景與環境。這個目錄下的所有檔案和子目錄都構成了構建連貫的背景與環境,會作為構建過程的一部分傳送給 Docker 守護程式。
當沒有指定連貫的背景與環境時(如僅提供 Dockerfile 的 URL 或從 STDIN 管道輸入 Dockerfile 內容),構建連貫的背景與環境被視為空。
不要使用根目錄作為構建連貫的背景與環境
由於構建連貫的背景與環境會被封裝成 tarball 並傳送給 Docker 守護程式,因此不應使用包含大量檔案的目錄。例如,使用 /home/user
、Downloads
或根目錄 /
作為連貫的背景與環境,會導致 Docker 客戶端花費大量時間封裝並傳輸所有內容。
如果提供以 http 或 https 開頭的 URL,它被假定為 Dockerfile 的直接連結。這種方式用處不大,因為 Dockerfile 沒有關聯的連貫的背景與環境(與不接受指向壓縮檔的連結)。
Git 倉函式庫以作為構建連貫的背景與環境。在這種情況下,Docker 客戶端會將倉函式庫所有子模組克隆到臨時目錄,然後將其傳送給 Docker 守護程式作為構建連貫的背景與環境。當路徑以 github.com/
、git@
或 git://
開頭時,Docker 會將連貫的背景與環境解釋為 git 倉函式庫過,玄貓建議避免使用這種方法,而是手動簽出倉函式庫為這樣更靈活,也減少了出錯的可能性。
Docker 客戶端也可以接受 STDIN 的輸入,只需在構建連貫的背景與環境的位置給出「-」引數。輸入可以是沒有連貫的背景與環境的 Dockerfile(例如 docker build - < Dockerfile
),也可以是包含連貫的背景與環境和 Dockerfile 的壓縮檔(例如 docker build - < context.tar.gz
)。壓縮檔可以是 tar.gz、xz 或 bzip2 格式。
連貫的背景與環境中 Dockerfile 的位置可以用 -f
引數指定(例如 docker build -f dockerfiles/Dockerfile.debug .
)。如果未指定,Docker 會在連貫的背景與環境根目錄中尋找名為 Dockerfile 的檔案。
使用 .dockerignore 檔案管理連貫的背景與環境
為了從構建連貫的背景與環境中排除不必要的檔案,可以使用 .dockerignore
檔案。該檔案應包含要排除的檔案名,每行一個。允許使用萬用字元 *
和 ?
。例如,以下 .dockerignore
檔案:
.git
*/.git
*/*/.git
*.sw?
這些規則的作用分別是:
- 忽略構建連貫的背景與環境根目錄中的
.git
檔案或目錄,但允許任何子目錄中的.git
- 忽略恰好位於根目錄下一層的
.git
檔案或目錄,但根目錄和二層以下的不受影響 - 忽略恰好位於根目錄下兩層的
.git
檔案或目錄,但其他層級不受影響 - 忽略
test.swp
、test.swo
和bla.swp
等檔案,但不忽略子目錄中的類別似檔案
目前不支援完整的正規表示式,如 [A-Z]*
。此外,目前還沒有辦法比對所有子目錄中的檔案(例如,無法在一個表示式中同時忽略 /test.tmp
和 /dir1/test.tmp
)。
映像檔層的概念與重要性
初次接觸 Docker 的使用者常對映像檔的構建方式感到困惑。Dockerfile 中的每個指令都會產生一個新的映像檔層,這個層也可以用來啟動容器。新層的建立過程是:啟動一個使用前一層映像檔的容器,執行 Dockerfile 指令,然後儲存為新映像檔。當 Dockerfile 指令成功完成時,中間容器會被刪除,除非指定了 --rm=false
引數。
由於每個指令都會產生一個靜態映像檔(本質上只是一個檔案系統和一些元資料),所有在指令中執行的程式都會被停止。這意味著,雖然可以在 RUN 指令中啟動長期執行的程式,如資料函式庫SSH 守護程式,但它們在處理下一個指令或啟動容器時不會執行。如果希望服務或程式隨容器啟動,必須從 ENTRYPOINT 或 CMD 指令啟動它。
可以透過 docker history
命令檢視構成映像檔的完整層集合。例如:
$ docker history mongo:latest
IMAGE CREATED CREATED BY ...
278372cb22b2 4 days ago /bin/sh -c #(nop) CMD ["mongod"]
341d04fd3d27 4 days ago /bin/sh -c #(nop) EXPOSE 27017/tcp
ebd34b5e9c37 4 days ago /bin/sh -c #(nop) ENTRYPOINT &{["/entrypoint.
f3b2b8cf226c 4 days ago /bin/sh -c #(nop) COPY file:ef2883b33ed7ba0cc
ba53e9f50f18 4 days ago /bin/sh -c #(nop) VOLUME [/data/db]
c537910de5cc 4 days ago /bin/sh -c mkdir -p /data/db && chown -R mong
f48ad436057a 4 days ago /bin/sh -c set -x
df59596772ab 4 days ago /bin/sh -c echo "deb http://repo.mongodb.org/
96de83c82d4b 4 days ago /bin/sh -c #(nop) ENV MONGO_VERSION=3.0.6
0dab801053d9 4 days ago /bin/sh -c #(nop) ENV MONGO_MAJOR=3.0
5e7b428dddf7 4 days ago /bin/sh -c apt-key adv --keyserver ha.pool.sk
e81ad85ddfce 4 days ago /bin/sh -c curl -o /usr/local/bin/gosu -SL "h
7328803ca452 4 days ago /bin/sh -c gpg --keyserver ha.pool.sks-keyser
ec5be38a3c65 4 days ago /bin/sh -c apt-get update
430e6598f55b 4 days ago /bin/sh -c groupadd -r mongodb && useradd -r
19de96c112fc 6 days ago /bin/sh -c #(nop) CMD ["/bin/bash"]
ba249489d0b6 6 days ago /bin/sh -c #(nop) ADD file:b908886c97e2b96665
當構建失敗時,啟動失敗前的層可能非常有用。例如,假設我們有以下 Dockerfile:
FROM busybox:latest
RUN echo "This should work"
RUN /bin/bash -c echo "This won't"
嘗試構建時:
$ docker build -t echotest .
Sending build context to Docker daemon 2.048 kB
Step 0 : FROM busybox:latest
---> 4986bf8c1536
Step 1 : RUN echo "This should work"
---> Running in f63045cc086b
This should work
---> 85b49a851fcc
Removing intermediate container f63045cc086b
Step 2 : RUN /bin/bash -c echo "This won't"
---> Running in e4b31d0550cd
/bin/sh: /bin/bash: not found
The command '/bin/sh -c /bin/bash -c echo "This won't"' returned a non-zero
code: 127
雖然在這種情況下,從錯誤訊息很容易看出問題,但我們可以執行從最後一個成功層建立的映像檔來除錯指令。注意,我們使用的是最後一個映像檔 ID(85b49a851fcc),而不是最後一個容器 ID(e4b31d0550cd):
$ docker run -it 7831e2ca1809
/ # /bin/bash -c "echo hmm"
/bin/sh: /bin/bash: not found
/ # /bin/sh -c "echo ahh!"
ahh!
/ #
問題變得更加明顯:busybox 映像檔不包含 bash shell。
快取機制與效率最佳化
Docker 會快取每一層,以加速映像檔的構建過程。這種快取對於高效工作流程非常重要,但其機制相對簡單。使用快取的條件是:
- 前一個指令在快取中找到,與
- 快取中有一個層具有完全相同的指令和父層(即使是多餘的空格也會使快取失效)
此外,對於 COPY 和 ADD 指令,如果任何檔案的校驗和或元資料發生變化,快取將失效。
這意味著不能保證在多次呼叫中產生相同結果的 RUN 指令仍將被快取。特別要注意這一點,如果你下載檔案、執行 apt-get update
或克隆原始碼函式庫
如果需要使快取失效,可以使用 --no-cache
引數執行 docker build
。也可以在希望使快取失效的點之前新增或更改指令。因此,有時會看到 Dockerfile 中有類別似這樣的行:
ENV UPDATED_ON "14:12 17 February 2015"
RUN git clone....
玄貓不建議使用這種技術,因為它往往會混淆映像檔的後續使用者,特別是當映像檔的實際構建日期與行中建議的日期不同時。
實戰經驗與最佳實踐
透過多年容器實戰經驗,玄貓發現合理的構建連貫的背景與環境管理和層快取理解是高效 Docker 工作流程的關鍵。在大型專案中,我常建立專門的 Docker 構建目錄,只包含必要檔案,並使用全面的 .dockerignore
設定,避免將版本控制資料、測試檔案和暫存檔案包含在構建連貫的背景與環境中。
對於需要頻繁重建的映像檔,我建議按照變更頻率組織 Dockerfile 指令,將不常變更的指令(如安裝系統套件)放在前面,而經常變更的程式碼和設定放在後面,以最大化快取利用率。
當遇到映像檔構建問題時,逐層除錯的方法是解決複雜問題的關鍵。透過啟動中間層映像檔,可以精確定位問題發生的環境,大縮短故障排除時間。
容器技術的核心價值在於其一致性和可重現性,理解
選擇合適的基礎映像:平衡輕量與實用
在容器化應用程式的旅程中,選擇適當的基礎映像是決定最終成品質的關鍵第一步。經過多年的容器化專案實踐,玄貓發現基礎映像的選擇往往會對佈署效率、安全性和維護性產生深遠影響。
優先考慮現有官方映像
在建立自己的映像前,首先應該評估是否真的需要從頭建立:
- 直接使用官方映像:對於常見的應用程式軟體(如資料函式庫頁伺服器),通常有完善的官方映像可用。這些映像已經過專業最佳化,融合了社群的集體智慧。
- 掛載設定檔:許多情況下,僅需將自定義的設定檔或資料掛載到現有映像中,無需重新封裝映像。
在我協助一家電商平台最佳化其微服務架構時,發現團隊為每個服務都客製了根據Ubuntu的映像。透過改用PostgreSQL、Redis和NGINX的官方映像,並採用設定掛載的方式,我們不僅減少了維護負擔,還顯著提升了佈署速度。
語言與框架特定映像
若需要為自己的應用程式建立映像,應考慮以下策略:
- 選擇語言專用基礎映像:如Go、Node.js或Ruby on Rails等都有官方映像。
- 分離建置與執行環境:採用多階段建置(multi-stage builds)分離建置和執行環境,例如:
# 建置階段 FROM java:jdk AS builder COPY . /app WORKDIR /app RUN ./mvnw package # 執行階段 FROM java:jre COPY --from=builder /app/target/myapp.jar /app/ CMD ["java", "-jar", "/app/myapp.jar"]
- 使用精簡變體:許多官方映像提供「slim」或「alpine」變體,移除了開發工具和標頭檔。
輕量級Linux發行版選擇
當需要一個完整但精簡的Linux環境時,我推薦以下選擇:
- Alpine:極致輕量,僅約5MB,但擁有完整的套件管理系統(apk)。適合追求最小映像體積的場景。
- Debian:比Ubuntu更輕量,但能使用相同的套件。在需要更多工具但仍想保持合理大小時是不錯的選擇。
在構建一個需要處理PDF的服務時,我起初選擇了Alpine,但發現一些依賴函式庫lpine上有相容性問題。切換到Debian後,雖然基礎映像增加了約40MB,但減少了大量的疑難排解時間,這是一個值得的折衷。
極簡映像的利與弊
追求極致輕量的映像確實有其吸引力:
- 根據scratch構建:可以從完全空白的檔案系統開始,僅複製必要的二進位檔案。
FROM scratch COPY ./myapp / CMD ["/myapp"]
這種方法需要注意:
- 應用程式需要靜態編譯,包含所有依賴函式庫 無法呼叫外部命令
- 編譯時需考慮容器內的目標架構
- 除錯能力極度受限,沒有shell或任何工具
在我的實踐中,極簡映像適合於那些經過充分測試、穩定與不需要執行時診斷的應用程式。例如,我為一個高效能的API閘道器採用了這種方法,但對於複雜的應用服務,則傾向於保留基本的除錯工具。
Phusion基礎映像的爭議
在Docker社群中,phusion/baseimage-docker曾引起廣泛討論。這個基礎映像與官方Ubuntu映像的主要區別在於:
- init服務:Phusion主張缺乏init服務可能導致殭屍程式。然而,這通常只在應用程式有缺陷時才會發生。
- cron守護程式:Phusion預設啟動cron,而官方映像則認為只有在應用需要時才應啟動。
- SSH守護程式:Phusion包含SSH服務,而Docker的理念是透過
docker exec
進入容器。
我的立場是,除非有特殊需求(如需要在容器內執行多個程式、定時任務和SSH),否則應堅持使用官方映像。這不僅符合容器「一容器一應用」的設計理念,也能避免不必要的資源消耗。
映像重建的重要性
一個容易被忽視的細節是映像的更新機制。執行docker build
時,Docker只會在本地不存在FROM指定的映像時才會提取它。這意味著:
- 即使基礎映像有更新(如安全補丁),本地已有的映像也不會自動更新
- 需要定期執行
docker pull
或刪除本地映像來強制更新
這一點在處理如debian、ubuntu等常見基礎映像時尤為重要,因為這些映像經常會有安全更新。在我維護的生產系統中,我們建立了自動化流程,定期重建所有映像以保持基礎層的最新狀態。
Dockerfile指令完整解析
Dockerfile是Docker映像構建的藍圖,掌握其指令集對於建立高效映像至關重要。以下是對Dockerfile指令的深入解析,融合了我多年來的最佳實踐經驗。
執行形式與Shell形式
在開始探討具體指令前,需要理解一個重要概念:執行形式(exec form)與Shell形式(shell form)。
RUN
、CMD
和ENTRYPOINT
指令支援兩種形式:
- 執行形式:使用JSON陣列,如
["executable", "param1", "param2"]
- Shell形式:字元串形式,如
command param1 param2
執行形式直接呼叫指定的可執行檔案,而Shell形式則透過/bin/sh -c
執行。在以下情況下應優先使用執行形式:
- 避免Shell對字元串的處理(如變數替換、萬用字元展開)
- 映像中不包含
/bin/sh
- 希望接收Unix訊號(Shell形式下,shell程式會成為PID 1,而非你的應用)
核心指令詳解
FROM - 指定基礎映像
FROM debian:bullseye-slim
FROM
必須是Dockerfile的第一條指令,它設定了所有後續指令的基礎。我強烈建議始終指定具體的標籤版本(如debian:bullseye-slim
),而非使用latest
標籤,以確保構建的可重複性。
MAINTAINER - 設定維護者資訊
MAINTAINER 玄貓 <blackcat@example.com>
此指令設定映像的「作者」元資料。雖然它仍被支援,但在較新版本中推薦使用LABEL
指令:
LABEL maintainer="玄貓 <blackcat@example.com>"
RUN - 執行命令
RUN apt-get update && apt-get install -y --no-install-recommends \
nginx \
&& rm -rf /var/lib/apt/lists/*
RUN
指令在映像構建階段執行命令並提交結果。最佳實踐是將相關命令合併為單一RUN
指令,以減少層數。注意上例中清理apt快取的做法,這是減少映像大小的常用技巧。
CMD - 設定預設命令
CMD ["nginx", "-g", "daemon off;"]
CMD
指令提供容器啟動時的預設命令。如果使用者在docker run
時提供了命令,則會覆寫CMD
。每個Dockerfile中只有最後一個CMD
指令生效。
ENTRYPOINT - 設定主要執行點
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT
設定容器啟動時執行的應用。當同時使用ENTRYPOINT
和CMD
時,CMD
中的內容會作為引數傳遞給ENTRYPOINT
。這種組合非常適合構建可接受引數的映像。
COPY與ADD - 複製檔案
COPY ./app /app
ADD https://example.com/file.tar.gz /opt/
這兩個指令都用於將檔案新增到映像中,但有重要區別:
COPY
僅複製本地檔案ADD
可以從URL下載檔案,並自動解壓縮檔案
一般來說,除非需要自動解壓功能,否則應優先使用更簡單的COPY
指令。對於下載遠端資源,我更傾向於使用RUN
配合curl
或wget
,這樣可以在同一層中處理和清理下載。
ENV - 設定環境變數
ENV NODE_ENV production
ENV PATH /app/node_modules/.bin:$PATH
ENV
指令設定持久的環境變數,這些變數在構建過程中和容器執行時都可用。使用環境變數可以增強Dockerfile的可維護性和靈活性。
EXPOSE - 宣告連線埠
EXPOSE 80 443
EXPOSE
指令宣告容器執行時監聽的連線埠。這純粹是資訊性的,除非在docker run
時使用-P
選項,否則不會實際發布連線埠。
VOLUME - 定義掛載點
VOLUME /data
VOLUME
指令建立掛載點,用於持久化資料或分享資料。出於安全和可移植性考慮,Dockerfile中不能指定主機上的掛載目錄。
WORKDIR - 設定工作目錄
WORKDIR /app
WORKDIR
設定後續指令的工作目錄。使用絕對路徑是最佳實踐,這樣可以避免路徑混淆。
USER - 指定使用者
USER nginx
USER
指令指定後續RUN
、CMD
和ENTRYPOINT
指令的執行使用者。出於安全考慮,避免使用root使用者執行應用是一種良好實踐。
ONBUILD - 設定觸發指令
ONBUILD COPY ./app /app
ONBUILD RUN npm install
ONBUILD
指令設定在當前映像被用作另一個映像的基礎時才執行的指令。這對於建立應用框架映像特別有用,例如,一個Node.js應用框架可以在子映像構建時自動安裝依賴。
映像構建的最佳實踐
根據我在各種環境中構建和最佳化Docker映像的經驗,以下是一些值得分享的最佳實踐:
減少映像層數
每條RUN
、COPY
和ADD
指令都會建立一個新層。過多的層會增加映像大小和複雜性。
不推薦:
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*
推薦:
RUN apt-get update && apt-get install -y nginx \
&& rm -rf /var/lib/apt/lists/*
利用構建快取
Docker在構建時使用快取來加速過程。瞭解快取機制可以大幅提升構建效率:
- 將較少變化的指令(如安裝依賴)放在前面
- 將經常變化的內容(如應用程式碼)放在後面
COPY package.json /app/
RUN npm install
COPY . /app/
這樣,只要package.json
沒變,npm install
就會使用快取,大節省時間。
使用多階段構建
多階段構建是減小最終映像大小的強大技術:
# 建置階段
FROM node:14 AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# 執行階段
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
這種方法讓最終映像只包含執行所需的檔案,不含任何建置工具或中間產物。
安全考量
映像安全是容器化應用不可忽視的一環:
- 使用非root使用者執行應用
- 定期更新基礎映像以取得安全補丁
- 使用映像掃描工具檢測漏洞
- 移除不必要的工具和套件
在我為一家金融科技公司設計容器化策略時,我們建立
Docker容器的對外連線策略
在容器化應用程式的世界裡,讓容器能夠與外界通訊是一項基礎但關鍵的需求。當我們在容器中執行服務(例如網頁伺服器),如何讓外部世界存取這些服務?Docker提供了多種方法讓容器能夠對外連線,而選擇合適的連線策略對於建立穩健的容器化系統至關重要。
連線埠發布:向世界開放容器服務
在Docker中,連線埠發布是最直接的容器對外通訊方式。透過連線埠發布,我們可以將容器內部的服務對映到主機上的連線埠,使外部能夠存取容器內的服務。Docker提供了兩種主要的連線埠發布方法:
手動指定連線埠對映
使用-p
引數可以明確指定主機連線埠到容器連線埠的對映關係:
docker run -d -p 8000:80 nginx
這個命令會啟動一個Nginx容器,並將主機的8000連線埠對映到容器的80連線埠。我們可以用以下方式測試連線:
curl localhost:8000
這將回傳Nginx的歡迎頁面,證明我們已經成功連線到容器內的網頁伺服器。
自動連線埠分配
如果我們不想手動管理連線埠分配,可以使用-P
引數讓Docker自動選擇一個可用的主機連線埠:
ID=$(docker run -d -P nginx)
docker port $ID 80
執行上述命令後,Docker會自動分配一個可用連線埠(例如32771)並對映到容器的80連線埠。這種方式的主要優勢是免除了手動跟蹤已分配連線埠的負擔,特別是在執行多個容器時尤為有用。我們可以使用docker port
命令來查詢Docker分配的連線埠。
在實際專案中,玄貓發現自動連線埠分配特別適合開發環境,因為它避免了開發者之間的連線埠衝突;而在生產環境中,手動指定連線埠則更為常見,因為它提供了更好的可預測性和檔案一致性。
容器間的連線機制
當多個容器需要相互通訊時,Docker提供了容器連結(Container Linking)機制,這是同一主機上容器互相通訊的最簡單方式。
Docker連結的基本應用
在預設的Docker網路模型中,連結的容器間通訊是在Docker內部網路上進行的,不會暴露到主機網路中:
docker run -d --name myredis redis
docker run --link myredis:redis debian env
在這個例子中,我們首先啟動了一個名為myredis
的Redis容器,然後啟動了一個Debian容器並連結到Redis容器。--link myredis:redis
引數中,myredis
是被連結容器的名稱,而redis
是在主容器中用於參考被連結容器的別名。
Docker連結會在主容器中設定一系列環境變數,這些變數包含了連結容器的連線資訊:
REDIS_PORT
:連結容器的連線埠資訊REDIS_PORT_6379_TCP_ADDR
:連結容器的IP位址REDIS_PORT_6379_TCP_PORT
:連結容器的連線埠
這些環境變數使得主容器可以輕鬆地連線到被連結的容器。此外,Docker還會將連結容器的別名和ID新增到主容器的/etc/hosts
檔案中,使主容器可以透過名稱存取連結容器。
連結容器的安全性考量
預設情況下,所有容器都可以相互通訊,無論它們是否被顯式連結。如果我們希望限制容器間的通訊,可以在啟動Docker守護程式時使用--icc=false
和--iptables
引數。這樣,只有被顯式連結的容器才能相互通訊,Docker會設定Iptables規則來允許連結容器間的通訊。
Docker連結的侷限性
雖然Docker連結很有用,但它也有一些侷限性:
- 靜態性:連結是靜態的,雖然它們可以在容器重啟後存活,但如果被連結的容器被替換,連結不會自動更新。
- 啟動順序限制:被連結的容器必須在主容器之前啟動,這意味著無法建立雙向連結。
在我多年的容器化實踐中,發現這些限制在微服務架構中尤為明顯。為瞭解決這些問題,現代Docker實踐更傾向於使用Docker網路或服務發現機制,而不是傳統的Docker連結。
透過卷管理容器資料
在Docker架構中,資料持久化是一個關鍵問題。Docker卷(Volumes)提供了一種機制,使資料可以獨立於容器的生命週期存在。卷本質上是主機上的目錄,透過繫結掛載(bind mount)到容器中。
建立和使用Docker卷的三種方式
1. 執行時使用-v
引數宣告卷
docker run -it --name container-test -h CONTAINER -v /data debian /bin/bash
這個命令會將容器內的/data
目錄設定為卷。如果映像中的/data
目錄已經包含檔案,這些檔案會被複製到卷中。我們可以使用docker inspect
命令檢視卷在主機上的位置:
docker inspect -f {{.Mounts}} container-test
2. 在Dockerfile中使用VOLUME
指令
FROM debian:wheezy
VOLUME /data
這種方法與執行時使用-v /data
引數效果相同,都會建立一個掛載點為/data
的卷。
3. 使用主機指定目錄作為卷
docker run -v /home/adrian/data:/data debian ls /data
這個命令會將主機上的/home/adrian/data
目錄掛載為容器內的/data
目錄。與前兩種方法不同,這種方法不會從映像中複製檔案到卷中,與Docker不會自動刪除這個卷(即docker rm -v
不會刪除使用者指定的目錄)。
在Dockerfile中設定卷許可權的技巧
在Dockerfile中使用卷時,需要注意許可權和所有權的設定順序。任何在VOLUME
指令之後的指令都無法修改該卷的內容:
# 錯誤示範 - 不會按預期工作
FROM debian:wheezy
RUN useradd foo
VOLUME /data
RUN touch /data/x # 這行不會生效
RUN chown -R foo:foo /data # 這行不會生效
正確的做法是在宣告卷之前設定許可權和所有權:
# 正確示範
FROM debian:wheezy
RUN useradd foo
RUN mkdir /data && touch /data/x
RUN chown -R foo:foo /data
VOLUME /data
在構建映像時,Docker會在臨時容器中執行這些命令,當容器啟動時,Docker會將映像中卷目錄的檔案複製到容器的卷中。
分享資料的策略
Docker提供了多種方式來分享資料:
在容器間分享卷
使用--volumes-from
引數可以讓容器存取其他容器的卷:
docker run -it -h NEWCONTAINER --volumes-from container-test debian /bin/bash
這個命令建立了一個新容器,該容器可以存取container-test
容器的卷。值得注意的是,無論container-test
容器是否在執行,這種分享都有效。只要至少有一個容器連結到卷,該卷就不會被刪除。
資料容器模式
資料容器是一種常見的實踐,它們的唯一目的是在其他容器之間分享資料:
docker run --name dbdata postgres echo "Data-only container for postgres"
docker run -d --volumes-from dbdata --name db1 postgres
在這個例子中,我們首先建立了一個名為dbdata
的資料容器,然後啟動了一個使用該資料容器卷的PostgreSQL容器。資料容器不需要保持執行狀態,一旦建立完成它就可以停止,這樣可以節省資源。
在選擇資料容器的映像時,一個實用的做法是使用與消費資料的容器相同的映像,而非使用所謂的「最小映像」。這不會佔用額外空間,並且可以確保映像能夠正確初始化卷中的任何必要資料。
容器資料管理的實用考量
在實際應用Docker卷管理資料時,有幾個關鍵點需要考慮:
資料永續性策略
卷的主要目的是提供永續性儲存,但不同類別的資料可能需要不同的永續性策略:
- 應用程式資料:通常需要長期儲存,適合使用命名卷或主機繫結掛載
- 暫時性資料:如日誌或快取,可以使用匿名卷,在容器銷毀時一併清理
- 設定資料:通常以唯讀方式掛載到多個容器中,適合使用主機繫結掛載
資料備份與遷移
卷中的資料備份是容器化系統中的重要考量:
- 對於使用Docker管理的卷,可以建立一個臨時容器掛載該卷,然後將資料複製或備份到主機
- 對於繫結掛載,可以直接在主機上執行備份操作
- 使用資料容器模式時,可以透過備份資料容器來一併備份所有相關資料
效能與安全性平衡
在使用卷時,需要平衡效能和安全性:
- 繫結掛載通常提供更好的效能,但可能存在更多的安全風險
- Docker管理的卷提供了更好的隔離性,但在某些檔案系統上可能有輕微的效能開銷
- 對於敏感資料,應考慮使用Docker的金鑰管理功能,而非直接儲存在卷中
在玄貓的實務經驗中,合理的卷管理策略能夠大幅提升容器化應用的可靠性和可維護性。特別是在設計微服務架構時,明確的資料分離和分享策略是成功的關鍵因素之一。
容器網路與資料管理的整合實踐
將容器網路連線和資料管理結合起來,可以構建更強大的容器化應用。以下是一些整合實踐的案例:
微服務架構中的資料分享與通訊
在微服務架構中,服務之間既需要通訊又可能需要分享某些資料:
- 使用Docker網路實作服務間的安全通訊
- 對於需要分享的設定或靜態資料,使用只讀卷掛載
- 對於各服務獨有的資料,使用專用的資料容器
有狀態服務的高用性設定
對於有狀態的服務(如資料函式庫可以結合網路和卷實作高用性:
- 使用資料容器儲存持久化資料
- 設定主從複製,透過Docker網路實作內部通訊
- 使用健康檢查和自動容錯移轉確保服務可用性
在我設計金融科技系統時,曾經使用這種方式成功實作了一個具有自動容錯移轉能力的PostgreSQL叢集,既保證了資料安全,又確保了服務的高用性。
Docker的網路連線和資料管理功能為構建彈性、可擴充套件的容器化系統提供了堅實的基礎。透過深入理解這些概念並靈活運用,我們可以設計出既滿足技術需求又符合業務目標的容器化解決方案。隨著容器技術的不斷發展,這些基礎能力也在不斷增強,為更複雜的應用場景提供支援。
在容器化的旅程中,掌握這些基本概念是每位開發者和維運人員的必修課。無論是簡單的單容器應用,還是複雜的多容器系統,良好的網路連線和資料管理策略都是成功的關鍵。
Docker 基礎操作完全:掌握容器生命週期
Docker 容器卷的刪除機制
當使用 Docker 容器時,理解容器卷(Volume)的刪除機制非常重要。容器卷只會在特定條件下被刪除:
- 當容器使用
docker rm -v
指令被刪除時 - 當容器啟動時使用了
--rm
標誌
同時必須滿足以下條件:
- 沒有其他容器連結到該卷
- 未指定主機目錄作為卷的掛載點(未使用
-v HOST_DIR:CONTAINER_DIR
這種語法)
目前這意味著,除非玄貓非常小心地總是使用這些引數執行容器,否則很可能會在 Docker 安裝目錄中留下「孤兒」檔案和目錄,而與沒有簡單的方法知道它們代表什麼。Docker 正在開發一個頂層的「volume」指令,允許獨立於容器列出、建立、檢查和刪除卷。這個功能預計會在 Docker 1.9 版本中推出。
常用 Docker 指令概覽
這部分提供了常用 Docker 指令的簡要概述,專注於日常使用的指令。由於 Docker 發展迅速,玄貓建議參考 Docker 官方網站的檔案取得更完整和最新的指令詳情。以下並未詳細說明各指令的引數和語法(docker run
例外),這些資訊可透過在任何指令後新增 --help
引數或使用 docker help
指令取得。
Docker 布林標誌的特殊行為
在大多數 Unix 命令列工具中,有一些不需要值的標誌,例如 ls -l
中的 -l
。因為這些標誌要麼設定要麼不設定,Docker 將其視為布林標誌,與與其他工具不同的是,Docker 支援明確提供布林值(例如同時接受 -f=true
和 -f
)。
此外,Docker 有預設為真和預設為假的標誌。與預設為假不同,如果未指定,預設為真的標誌被視為已設定。不指定引數與將其設為 true 效果相同。預設為真的標誌不能透過具有值的引數取消設定;取消設定預設為真的標誌的唯一方法是明確將其設定為 false(例如 -f=false
)。
要了解標誌是預設為真還是預設為假,請參考該指令的 docker help
。例如:
$ docker logs --help
...
-f, --follow=false Follow log output
--help=false Print usage
-t, --timestamps=false Show timestamps
...
顯示 -f
、--help
和 -t
引數都是預設為假的。
以 docker run
的預設為真引數 --sig-proxy
為例:唯一關閉此引數的方法是明確將其設定為 false:
$ docker run --sig-proxy=false ...
以下都是等效的:
$ docker run --sig-proxy=true ...
$ docker run --sig-proxy ...
$ docker run ...
對於預設為假的引數(如 --read-only
),以下設定會將其設為真:
$ docker run --read-only=true
$ docker run --read-only
不指定或明確設定為 false 是等效的。
這也會導致一些奇特的行為,例如 docker ps --help=false
將正常工作而不顯示幫助訊息。
run 指令深入解析
docker run
是啟動新容器的主要指令,也是最複雜的指令,支援大量引數。這些引數允許使用者設定映像如何執行、覆寫 Dockerfile 設定網路以及設定容器的許可權和資源。
容器生命週期控制選項
以下選項控制容器的生命週期和基本執行模式:
-a, –attach 將指定的流(STDOUT 等)附加到終端。如果未指定,則同時附加 stdout 和 stderr。如果未指定與容器以互動模式(-i)啟動,則也會附加 stdin。 不相容 -d。
-d, –detach 以「分離」模式執行容器。該指令將在後台執行容器並回傳容器 ID。
-i, –interactive 保持 stdin 開啟(即使未附加)。通常與 -t 一起使用以啟動互動式容器工作階段。例如:
$ docker run -it debian /bin/bash
root@bd0f26f928bb:/# ls
...省略...
–restart
設定 Docker 嘗試重新啟動已結束容器的情況。引數 no
永不嘗試重新啟動容器,always
總是嘗試重新啟動,不論結束狀態如何。引數 on-failure
將嘗試重新啟動以非零狀態結束的容器,可以帶一個可選引數指定放棄前嘗試重新啟動的次數(如果未指定,它將無限重試)。例如,docker run --restart on-failure:10 postgres
將啟動 postgres 容器,如果它以非零程式碼結束,則嘗試重新啟動它 10 次。
–rm 容器結束時自動移除它。不能與 -d 一起使用。
-t, –tty 分配一個虛擬終端。通常與 -i 一起使用以啟動互動式容器。
容器命名與環境變數設定選項
以下選項允許設定容器名稱和變數:
-e, –env 在容器內設定環境變數。例如:
$ docker run -e var1=val -e var2="val 2" debian env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=b15f833d65d8
var1=val
var2=val 2
HOME=/root
也可以使用 --env-file
選項透過檔案傳遞變數。
-h, –hostname 將容器的 Unix 主機名設定為 NAME。例如:
$ docker run -h "myhost" debian hostname
myhost
–name NAME 為容器分配名稱 NAME。該名稱可用於在其他 Docker 指令中參照該容器。
卷管理選項
以下選項允許使用者設定卷:
-v, –volume 有兩種形式的引數來設定卷(容器內的檔案或目錄,它是原生主機檔案系統的一部分,不是容器的聯合檔案系統)。第一種形式只指定容器內的目錄,並會繫結到 Docker 選擇的主機目錄。第二種形式指定要繫結的主機目錄。
–volumes-from 掛載指定容器中的卷。通常與資料容器一起使用。
網路相關選項
有幾個影響網路的選項。以下是常用的基本指令:
–expose 等同於 Dockerfile 中的 EXPOSE 指令。識別容器中使用的連線埠或連線埠範圍,但不開放連線埠。只有與 -P 和容器連結一起使用時才有意義。
–link 設定到指定容器的私有網路介面。
-p, –publish
「發布」容器上的連線埠,使其可從主機存取。如果未定義主機連線埠,將選擇一個隨機的高編號連線埠,可以使用 docker port
指令檢視。也可以指定要暴露連線埠的主機介面。
-P, –publish-all
將容器上的所有暴露連線埠發布到主機。每個暴露的連線埠將分配一個隨機的高編號連線埠。可以使用 docker port
指令檢視對映。
還有一些更進階的網路選項,如果需要進行更複雜的網路設定可能會用到。
Dockerfile 設定覆寫選項
以下選項直接覆寫 Dockerfile 設定:
–entrypoint 將容器的入口點設定為給定的引數,覆寫 Dockerfile 中的 ENTRYPOINT 指令。
-u, –user 設定指令執行的使用者。可以指定為使用者名或 UID。覆寫 Dockerfile 中的 USER 指令。
-w, –workdir 將容器中的工作目錄設定為提供的路徑。覆寫 Dockerfile 中的任何值。
容器管理指令
除了 docker run
外,以下 Docker 指令用於管理容器的生命週期:
docker attach [OPTIONS] CONTAINER attach 指令允許使用者檢視或與容器內的主程式互動。例如:
$ ID=$(docker run -d debian sh -c "while true; do echo 'tick'; sleep 1; done;")
$ docker attach $ID
tick
tick
tick
tick
注意,使用 CTRL-C 結束將終止程式並導致容器結束。
docker create
從映像建立容器但不啟動它。接受與 docker run
大部分相同的引數。使用 docker start
啟動容器。
docker cp 在容器和主機之間複製檔案和目錄。
docker exec 在容器內執行指令。可用於執行維護任務或作為 ssh 的替代方式登入容器。 例如:
$ ID=$(docker run -d debian sh -c "while true; do sleep 1; done;")
$ docker exec $ID echo "Hello"
Hello
$ docker exec -it $ID /bin/bash
root@5c6c32041d68:/# ls
bin dev home lib64 mnt proc run selinux sys usr
boot etc lib media opt root sbin srv tmp var
root@5c6c32041d68:/# exit
exit
docker kill 向容器內的主程式(PID 1)傳送訊號。預設傳送 SIGKILL,將立即導致容器結束。也可以使用 -s 引數指定訊號。回傳容器 ID。 例如:
$ ID=$(docker run -d debian bash -c \
"trap 'echo caught' SIGTRAP; while true; do sleep 1; done;")
$ docker kill -s SIGTRAP $ID
e33da73c275b56e734a4bbbefc0b41f6ba84967d09ba08314edd860ebd2da86c
$ docker logs $ID
caught
$ docker kill $ID
e33da73c275b56e734a4bbbefc0b41f6ba84967d09ba08314edd860ebd2da86c
docker pause
暫停指定容器內的所有程式不會收到任何訊號表明它們被暫停,因此無法關閉或清理。可以使用 docker unpause
重啟程式。docker pause
內部使用 Linux cgroups freezer 功能。這與 docker stop
不同,後者會停止程式並傳送程式可觀察到的訊號。
在玄貓多年的容器化系統開發經驗中,理解這些指令的細節對於有效管理容器生命週期至關重要。特別是當需要在生產環境中自動化容器操作時,熟悉這些指令的精確行為可以幫助開發者建立更穩定的佈署流程。
善用 Docker 的卷管理和網路功能可以構建更複雜的應用架構,而掌握容器生命週期管理指令則能確保系統的可靠執行和有效維護。對於初學者,建議先從基本的 run
、stop
和 rm
指令入手,逐步探索更進階的功能。
Docker 指令全面解析:從入門到精通
在容器化技術的浪潮中,Docker 已成為現代開發與佈署的根本。作為一名開發者和 DevOps 工程師,我發現掌握 Docker 的指令集是提高工作效率的關鍵。本文將探討 Docker 的核心指令,從容器管理、映像處理到系統資訊查詢,為你提供全方位的 Docker 指令。
容器生命週期管理
容器的啟動與停止
Docker 容器的生命週期管理是日常工作中最頻繁的操作。以下是管理容器生命週期的核心指令:
docker restart
重新啟動一個或多個容器。這個指令相當於依序執行 docker stop
和 docker start
。你可以使用 -t
引數指定容器關閉前的等待時間,超過這個時間容器將被強制終止。
docker rm
移除一個或多個容器。成功刪除後會回傳容器名稱或 ID。預設情況下,docker rm
不會移除容器相關的資料卷。可以使用 -f
引數強制移除執行中的容器,而 -v
引數則會移除容器建立的資料卷(除非這些資料卷被其他容器使用或是繫結掛載)。
舉例來說,若要刪除所有已停止的容器:
$ docker rm $(docker ps -aq)
b7a4e94253b3
e33da73c275b
f47074b60757
這個指令組合非常實用,在我清理開發環境時經常使用。首先 docker ps -aq
會列出所有容器的 ID,然後 docker rm
將這些容器全部刪除。
docker start
啟動已停止的容器。可用於重啟已結束的容器,或啟動用 docker create
建立但尚未啟動的容器。
docker stop
停止(但不移除)一個或多個容器。在執行 docker stop
後,容器會轉為「已結束」狀態。可使用 -t
引數指定等待容器關閉的時間,超過這個時間容器將被強制終止。
docker unpause
還原先前用 docker pause
暫停的容器。
從容器中分離的技巧
當你透過互動模式連線到 Docker 容器時(使用 -i
和 -t
引數啟動或使用 docker attach
連線),如果嘗試使用 CTRL-C
斷開連線,會導致容器停止。而更好的做法是使用 CTRL-P
接著 CTRL-Q
的組合鍵,這樣可以在不停止容器的情況下分離。
這個小技巧在我的日常工作中非常有用,尤其是需要臨時檢查容器狀態又不想影響其執行時。不過要注意,這種方式只在使用 -i
和 -t
引數(互動模式與分配 TTY)的情況下有效。
Docker 系統資訊查詢
以下指令可用於取得 Docker 系統的相關資訊:
docker info 輸出 Docker 系統和主機的各種資訊,包括容器數量、映像檔數量、儲存驅動等。
docker help
顯示指定子指令的使用說明和幫助資訊。效果等同於對指令使用 --help
引數。
docker version 顯示 Docker 客戶端和伺服器的版本資訊,以及編譯時使用的 Go 版本。
容器資訊查詢
以下指令提供有關執行中和已停止容器的詳細資訊:
檢視容器變更與狀態
docker diff 顯示容器檔案系統相對於啟動它的映像檔的變更。例如:
$ ID=$(docker run -d debian touch /NEW-FILE)
$ docker diff $ID
A /NEW-FILE
這裡的 A
表示新增了一個檔案。這個指令在排查容器問題時非常有用,可以快速檢視容器執行過程中對檔案系統做了哪些修改。
docker events
即時輸出 Docker 守護程式的事件。使用 CTRL-C
結束。這對於監控容器行為和自動化指令碼編寫非常有幫助。
docker inspect
提供指定容器或映像檔的詳細資訊。包括大部分設定資訊、網路設定和資料卷對映。可以使用 -f
引數提供 Go 範本來格式化和過濾輸出。
在我的自動化指令碼中,經常使用類別似這樣的指令取得容器 IP:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name
docker logs 輸出容器的「日誌」。這實際上是容器內部寫入 STDERR 或 STDOUT 的所有內容。在排查容器問題時,這是最先使用的指令之一。
docker port
列出指定容器的連線埠對映。可以選擇性地指定內部容器連線埠和協定進行查詢。通常在使用 docker run -P <image>
後,用來發現被分配的連線埠。
例如:
$ ID=$(docker run -P -d redis)
$ docker port $ID
6379/tcp -> 0.0.0.0:32768
$ docker port $ID 6379
0.0.0.0:32768
$ docker port $ID 6379/tcp
0.0.0.0:32768
docker ps
提供當前容器的高階資訊,如名稱、ID 和狀態。接受多種引數,其中 -a
用於取得所有容器,而不僅是執行中的。特別注意 -q
引數,它只回傳容器 ID,在作為其他命令的輸入時非常有用。
docker top
提供指定容器內執行中程式的資訊。實際上,這個指令在主機上執行 UNIX ps 實用程式,並過濾出指定容器中的程式。可以給予與 ps 實用程式相同的引數,預設為 -ef
(但要確保 PID 欄位仍在輸出中)。
例如:
$ ID=$(docker run -d redis)
$ docker top $ID
UID PID PPID C STIME TTY TIME CMD
999 9243 1836 0 15:44 ? 00:00:00 redis-server *:6379
$ ps -f -u 999
UID PID PPID C STIME TTY TIME CMD
999 9243 1836 0 15:44 ? 00:00:00 redis-server *:6379
$ docker top $ID -axZ
LABEL PID TTY STAT TIME COMMAND
docker-default 9243 ? Ssl 0:00 redis-server *:6379
映像檔操作技巧
以下指令提供了建立和處理映像檔的工具:
映像檔的建立與管理
docker build 從 Dockerfile 建立映像檔。這是建立自定義映像檔最推薦的方式,因為它易於重複與可維護。
docker commit
從指定容器建立映像檔。雖然 docker commit
可能有用,但通常更推薦使用 docker build
建立映像檔,因為後者更容易重複操作。預設情況下,容器在提交前會暫停,但可以使用 --pause=false
引數關閉此行為。接受 -a
和 -m
引數用於設定元資料。
例如:
$ ID=$(docker run -d redis touch /new-file)
$ docker commit -a "Joe Bloggs" -m "Comment" $ID commit:test
ac479108b0fa9a02a7fb290a22dacd5e20c867ec512d6813ed42e3517711a0cf
$ docker images commit
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
commit test ac479108b0fa About a minute ago 111 MB
$ docker run commit:test ls /new-file
/new-file
在開發過程中,我偶爾會使用這種方式快速儲存容器的狀態,但在正式環境中,我堅持使用 Dockerfile 以保證環境的一致性和可重現性。
docker export
將容器檔案系統的內容以 tar 壓縮檔的形式輸出到 STDOUT。產生的壓縮檔可以用 docker import
載入。注意,只有檔案系統被匯出;任何元資料,如匯出的連線埠、CMD 和 ENTRYPOINT 設定都會丟失。此外,任何資料卷都不會包含在匯出中。與 docker save
形成對比。
docker history 輸出映像檔中每一層的資訊,有助於瞭解映像檔的構建過程和層級結構。
docker images
提供本地映像檔列表,包括儲存函式庫、標籤名稱和大小等資訊。預設情況下,不顯示中間映像檔(用於建立頂級映像檔的映像檔)。VIRTUAL SIZE 是映像檔包括所有底層級在內的總大小。由於這些層級可能與其他映像檔分享,因此簡單地將所有映像檔的大小相加不能提供準確的磁碟使用估計。另外,如果映像檔有多個標籤,它們將出現多次;可以透過比較 ID 來識別不同的映像檔。接受多個引數,特別注意 -q
,它只回傳映像檔 ID,在作為其他指令的輸入時非常有用。
例如:
$ docker images | head -4
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
identidock_identidock latest 9fc66b46a2e6 26 hours ago 839.8 MB
redis latest 868be653dea3 6 days ago 110.8 MB
containersol/pres-base latest 13919d434c95 2 weeks ago 401.8 MB
要移除所有懸掛(dangling)映像檔:
$ docker rmi $(docker images -q -f dangling=true)
Deleted: a9979d5ace9af55a562b8436ba66a1538357bc2e0e43765b406f2cf0388fe062
這個指令組合在清理系統時特別有用,可以移除那些不再被任何標籤參照的映像檔層級。
映像檔的匯入與匯出
docker import
從包含檔案系統的壓縮檔(如由 docker export
建立的)建立映像檔。壓縮檔可以透過檔案路徑或 URL 識別,或透過 STDIN 流式傳輸(使用 -
標誌)。回傳新建立映像檔的 ID。透過提供儲存函式庫籤名稱,可以給映像檔新增標籤。請注意,從 import 建立的映像檔只包含一個層級,並且會丟失 Docker 設定,如匯出連線埠和 CMD 值。與 docker load
形成對比。
透過匯出和匯入「壓平」映像檔的例子:
$ docker export 35d171091d78 | docker import - flatten:test
5a9bc529af25e2cf6411c6d87442e0805c066b96e561fbd1935122f988086009
$ docker history flatten:test
IMAGE CREATED CREATED BY SIZE COMMENT
981804b0c2b2 59 seconds ago 317.7 MB Imported from -
這種技術在某些情況下很有用,比如需要減少映像檔層數或想要清除映像檔歷史時。我在最佳化某些特定用途的映像檔時會使用這種方法。
docker load
從透過 STDIN 傳遞的 tar 壓縮檔載入儲存函式庫儲函式庫包含多個映像檔和標籤。與 docker import
不同,映像檔將包含歷史和元資料。適用的壓縮檔由 docker save
建立,使 save 和 load 成為分發映像檔和製作備份的可行替代方案。
docker rmi
刪除給定的映像檔。映像檔由 ID 或儲存函式庫籤名稱指定。如果提供了儲存函式庫但沒有標籤名稱,則假定標籤為 latest。要刪除存在於多個儲存函式庫映像檔,請透過 ID 指定該映像檔並使用 -f
引數。每個儲存函式庫執行一次。
docker save
將命名的映像檔或儲存函式庫到一個 tar 壓縮檔中,該壓縮檔被流式傳輸到 STDOUT(使用 -o
寫入到檔案)。映像檔可以透過
Docker 常用指令與軟體生命週期管理
在現代軟體開發流程中,Docker 已成為不可或缺的工具,它徹底改變了應用程式的建置、測試與佈署方式。本文將探討 Docker 的核心指令與如何透過容器技術最佳化整個軟體生命週期。
Docker 的核心指令集
登入與登出 Docker Registry
當我們需要存取私有的映像檔函式庫送映像檔到 Docker Hub 時,登入功能是必要的:
docker logout
這個指令會登出 Docker 登入檔。若沒有指定伺服器,預設會登出 Docker Hub。在團隊開發環境中,這對於安全性管理特別重要,尤其是在共用工作站上工作時。
映像檔管理指令
映像檔是 Docker 生態系統的基礎,以下指令可協助我們有效管理映像檔:
docker pull
從登入檔下載指定的映像檔。如果沒有指定伺服器,預設是 Docker Hub。若未指定標籤名稱,則會下載標記為 latest
的映像檔(如果存在)。使用 -a
引數可從儲存函式庫所有映像檔。
在實際開發中,這個指令讓團隊成員能夠確保使用相同版本的基礎映像檔,大幅減少「在我的機器上可以執行」的問題。
docker push
將映像檔或儲存函式庫到登入檔。如果沒有指定標籤,這將推播儲存函式庫所有映像檔,而不僅是標記為 latest
的映像檔。
當我建立自定義映像檔時,這個指令讓我能夠與團隊分享或佈署到生產環境。記得在推播前使用適當的版本標籤,這是維護良好映像檔管理策略的關鍵。
docker search
在 Docker Hub 上搜尋符合搜尋條件的公共儲存函式庫果限制為 25 個儲存函式庫可以依星數和自動建置進行過濾。通常直接使用網站介面搜尋更方便。
在開發環境中使用 Docker
Docker 的真正價值在於它如何簡化開發流程。以下我將展示如何從零開始建立一個簡單的網頁應用程式環境。
建立基本的 Web 應用程式
讓我們從一個簡單的 “Hello World” 網頁伺服器開始。首先,建立一個名為 identidock
的專案目錄,並在其中建立一個 app
子目錄來存放 Python 程式碼:
identidock/
└── app
└── identidock.py
在 identidock.py
中,我們建立一個基本的 Flask 應用程式:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!\n'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
這段程式碼的重點在於:
- 初始化 Flask 並設定應用程式物件
- 建立與 URL 相關的路由,當請求此 URL 時,會呼叫
hello_world
函式 - 初始化 Python 網頁伺服器,使用
0.0.0.0
作為主機引數以繫結所有網路介面,這樣才能從主機或其他容器存取
建立 Docker 環境
接下來,在 identidock
目錄中建立 Dockerfile
:
FROM python:3.4
RUN pip install Flask==0.10.1
WORKDIR /app
COPY app /app
CMD ["python", "identidock.py"]
這個 Dockerfile 使用官方 Python 映像檔作為基礎,安裝 Flask 並複製我們的程式碼。最後,CMD 指令簡單地執行我們的 identidock 程式碼。
建置與執行容器
現在可以建置並執行我們的簡單應用程式:
$ cd identidock
$ docker build -t identidock .
$ docker run -d -p 5000:5000 identidock
這裡使用 -d
引數讓容器在背景執行,而 -p 5000:5000
引數將容器的 5000 連線埠轉發到主機的 5000 連線埠。
測試應用程式:
$ curl localhost:5000
Hello World!
最佳化開發工作流程
雖然基本功能已經實作,但目前的工作流程存在一個重大問題:每次對程式碼進行小修改,都需要重新建置映像檔並重新啟動容器。我們可以透過繫結掛載來解決這個問題:
$ docker stop $(docker ps -lq)
$ docker rm $(docker ps -lq)
$ docker run -d -p 5000:5000 -v "$(pwd)"/app:/app identidock
-v $(pwd)/app:/app
引數將主機上的 app 目錄掛載到容器內的 /app 位置,這樣會覆寫容器內的 /app 內容,並且在容器內是可寫入的。
這樣修改後,我們可以直接編輯主機上的檔案,立即看到變更效果:
$ sed -i '' s/World/Docker/ app/identidock.py
$ curl localhost:5000
Hello Docker!
Docker 與軟體生命週期
Docker 最大的優勢在於它能夠顯著減少開發環境和生產環境之間的差異。在軟體開發生命週期中,Docker 提供了以下關鍵優勢:
- 快速反饋迴圈 - 容器化開發環境可以快速迭代,加速開發、測試和驗證過程
- 一致性環境 - 從開發到生產使用相同的容器映像檔,大幅減少「在我機器上能執行」的問題
- 微服務架構支援 - 容器天然適合微服務開發模式,每個服務可以獨立佈署和擴充套件
- 持續佈署支援 - 容器化應用程式更容易實作持續佈署,一天可以安全地多次推播到生產環境
在我多年的開發經驗中,容器化的開發流程為團隊帶來了顯著的生產力提升。不過需要注意的是,目前的範例使用的是 Flask 的預設網頁伺服器,這只適合開發環境,對於生產環境來說效率太低與不夠安全。在實際佈署時,我們需要考慮使用更適合生產環境的網頁伺服器設定。
官方映像檔變體選擇
在選擇基礎映像檔時,我們常會遇到不同的變體選項。以 Python、Go 和 Ruby 等流行程式語言的官方儲存函式庫,除了不同版本號的映像檔外,通常還有以下變體:
slim - 這些映像檔是標準映像檔的精簡版本,許多常見的套件和函式庫失。當需要減少映像檔大小以便分發時,這些映像檔非常有用,但通常需要額外的工作來安裝和維護標準映像檔中已經可用的套件。
onbuild - 這些映像檔使用 Dockerfile 的 ONBUILD 指令來延遲執行某些命令,直到建立繼承 onbuild 映像檔的新「子」映像檔。這些命令在子映像檔的 FROM 指令過程中處理,通常會複製程式碼並執行編譯步驟。這些映像檔可以讓你更快速、更容易地開始使用某種語言,但從長遠來看,它們往往會受到限制與令人困惑。我通常只建議在首次探索儲存函式庫用 onbuild 映像檔。
在我們的範例應用程式中,我們使用的是 Python 3 的標準基礎映像檔,而不是這些變體。
結語
Docker 徹底改變了軟體開發的方式,從開發階段到佈署生產環境,它提供了一致、可靠與高效的工作流程。透過本文介紹的基本指令和開發流程,你已經具備了使用 Docker 進行開發的基礎知識。
隨著你繼續深入學習,你會發現 Docker 不僅是一個工具,更是一種思維模式的轉變,它鼓勵開發者從一開始就考慮佈署和執行環境,使整個軟體生命週期更加順暢和可預測。在接下來的文章中,我們將探討如何使用 Docker 構建更複雜的應用程式,並將其佈署到生產環境中。
容器環境中的 Python 開發:與傳統 virtualenv 的差異
在踏入 Docker 容器化開發之前,virtualenv 幾乎是每位 Python 開發者的標準配備。我記得剛開始接觸 Python 時,第一堂課就學會了使用 virtualenv 來隔離不同專案的環境。然而,當我們進入容器化開發的世界,這個工具的重要性開始有了微妙的變化。
為何容器中不一定需要 virtualenv
如果你是經驗豐富的 Python 開發者,可能會對我們不在容器中使用 virtualenv 感到訝異。畢竟,virtualenv 是隔離 Python 環境的絕佳工具,讓開發者能為每個應用程式使用獨立的 Python 版本和相關函式庫傳統開發中,它幾乎是必不可少與無所不在的。
然而,在使用容器時,virtualenv 的價值有所降低,因為容器本身就提供了隔離的環境。當然,如果你習慣了 virtualenv 的工作流程,在容器內使用它也完全可行,但除非你遇到與容器內其他應用程式或函式庫突,否則可能不會看到太多實質效益。
使用 uWSGI 開發生產級容器
uWSGI 是一個生產級的應用程式伺服器,能夠在 nginx 等網頁伺服器後方運作。相較於 Flask 的預設伺服器,使用 uWSGI 將為我們提供更靈活與適用於多種環境的容器。我們只需修改 Dockerfile 中的兩行程式碼,即可讓容器使用 uWSGI:
FROM python:3.4
RUN pip install Flask==0.10.1 uWSGI==2.0.8
WORKDIR /app
COPY app /app
CMD ["uwsgi", "--http", "0.0.0.0:9090", "--wsgi-file", "/app/identidock.py", \
"--callable", "app", "--stats", "0.0.0.0:9191"]
在這個 Dockerfile 中,我們做了兩個關鍵變更:
- 將 uWSGI 加入要安裝的 Python 套件清單中
- 建立新的命令來執行 uWSGI,指示它在 9090 埠啟動 HTTP 伺服器,執行來自 /app/identidock.py 的 app 應用程式,並在 9191 埠啟動統計伺服器
讓我們建置並執行這個容器,看差異:
$ docker build -t identidock .
...
Successfully built 3133f91af597
$ docker run -d -p 9090:9090 -p 9191:9191 identidock
00d6fa65092cbd91a97b512334d8d4be624bf730fcb482d6e8aecc83b272f130
$ curl localhost:9090
Hello Docker!
如果你現在執行 docker logs
並指定容器 ID,你會看到 uWSGI 的日誌資訊,確認我們確實在使用 uWSGI 伺服器。此外,我們要求 uWSGI 公開一些統計資料,你可以在 http://localhost:9191 檢視。由於我們沒有直接從命令列執行 Python 程式碼,所以通常啟動預設網頁伺服器的程式碼不會被執行。
增強容器安全性:避免以 root 身份執行
伺服器現在運作正常,但我們還有一些基本安全工作要做。如果你檢查 uWSGI 日誌,你會發現伺服器正在抱怨以 root 身份執行,這是一個容易避免的安全漏洞。我們可以在 Dockerfile 中指定要執行的使用者來輕鬆修復這個問題。同時,我們將明確宣告容器監聽的埠口:
FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8
WORKDIR /app
COPY app /app
EXPOSE 9090 9191
USER uwsgi
CMD ["uwsgi", "--http", "0.0.0.0:9090", "--wsgi-file", "/app/identidock.py", \
"--callable", "app", "--stats", "0.0.0.0:9191"]
新增的指令說明:
- 以標準 Unix 方式建立 uwsgi 使用者和群組
- 使用 EXPOSE 指令宣告主機和其他容器可存取的埠口
- 將所有後續指令(包括 CMD 和 ENTRYPOINT)的使用者設定為 uwsgi
讓我們建置這個映像並測試新的使用者設定:
$ docker build -t identidock .
...
$ docker run identidock whoami
uwsgi
注意,我們用 whoami 命令覆寫了呼叫網頁伺服器的預設 CMD 指令,它會回傳容器內執行中使用者的名稱。
容器內的使用者和群組理解
Linux 核心使用 UID 和 GID 來識別使用者並決定其存取許可權。將 UID 和 GID 對映到識別符是由作業系統在使用者空間中處理的。因此,容器內的 UID 與主機上的 UID 相同,但在容器內建立的使用者和群組不會傳播到主機。
這產生了一個副作用:存取許可權可能變得令人困惑;檔案在容器內外可能顯示為不同使用者所有。例如,注意以下檔案的擁有者變化:
$ ls -l test-file
-rw-r--r-- 1 docker staff 0 Dec 28 18:26 test-file
$ docker run -it -v $(pwd)/test-file:/test-file debian bash
root@e877f924ea27:/# ls -l test-file
-rw-r--r-- 1 1000 staff 0 Dec 28 18:26 test-file
root@e877f924ea27:/# useradd -r test-user
root@e877f924ea27:/# chown test-user test-file
root@e877f924ea27:/# ls -l /test-file
-rw-r--r-- 1 test-user staff 0 Dec 28 18:26 /test-file
root@e877f924ea27:/# exit
exit
docker@boot2docker:~$ ls -l test-file
-rw-r--r-- 1 999 staff 0 Dec 28 18:26 test-file
為何總是應該設定 USER
在所有 Dockerfile 中設定 USER 陳述式(或在 ENTRYPOINT 或 CMD 指令碼中更改使用者)非常重要。如果不這樣做,你的程式將以 root 身份在容器內執行。由於 UID 在容器內和主機上是相同的,如果攻擊者設法破壞容器,他將擁有對主機器的 root 存取權。
我在處理多個企業級容器化專案時,總是將這作為安全稽核的第一項檢查。雖然現在有些容器執行時可以自動將容器內的 root 使用者對映到主機上的高編號使用者,但在撰寫時的 Docker 版本(1.8)中,這一功能尚未到位。
自動化連線埠對映
我們已經確保容器內的命令不再以 root 身份執行。現在讓我們以稍微不同的引數再次啟動容器:
$ docker run -d -P --name port-test identidock
這次我們沒有指定要繫結的特定主機埠。相反,我們使用了 -P
引數,它使 Docker 自動將主機上的隨機高編號埠對映到容器上的每個「公開」埠。我們需要詢問 Docker 這些埠是什麼,然後才能存取服務:
$ docker port port-test
9090/tcp -> 0.0.0.0:32769
9191/tcp -> 0.0.0.0:32768
這裡我們可以看到 Docker 已將容器的 9090 埠繫結到主機的 32769 埠,將 9191 埠繫結到 32768 埠,所以現在我們可以存取服務(注意埠號在你的環境中可能不同):
$ curl localhost:32769
Hello Docker!
起初這似乎是個多餘的步驟,但當你在單一主機上執行多個容器時,讓 Docker 自動對映可用埠要比自己追蹤未使用的埠要容易得多。
在開發與生產環境之間平衡
現在我們有一個執行中的網頁服務,其架構已相當接近生產環境的樣子。在生產環境中仍有許多需要調整的地方(例如 uWSGI 的程式和執行緒選項),但我們已經大幅縮小了與預設 Python 偵錯網頁伺服器之間的差距。
然而,現在我們遇到了一個新問題:我們失去了預設 Python 網頁伺服器提供的開發工具,如偵錯輸出和即時程式碼多載功能。雖然我們可以極大地減少開發和生產環境之間的差異,但它們仍有根本不同的需求,這將始終需要一些變更。
理想情況下,我們希望在開發和生產環境中使用相同的映像,但根據執行環境啟用稍微不同的功能集。我們可以透過使用環境變數和簡單的指令碼來實作這一點,根據連貫的背景與環境切換功能。
在與 Dockerfile 相同的目錄中建立一個名為 cmd.sh 的檔案,內容如下:
#!/bin/bash
set -e
if [ "$ENV" = 'DEV' ]; then
echo "Running Development Server"
exec python "identidock.py"
else
echo "Running Production Server"
exec uwsgi --http 0.0.0.0:9090 --wsgi-file /app/identidock.py \
--callable app --stats 0.0.0.0:9191
fi
這個指令碼的意圖相當明確。如果環境變數 ENV 設定為 DEV,它將執行偵錯網頁伺服器;否則,它將使用生產伺服器。exec 命令用於避免建立新的程式,確保任何訊號(如 SIGTERM)都由我們的 uwsgi 程式接收,而不是被父程式吞掉。
需要更新 Dockerfile 以使用這個指令碼:
FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8
WORKDIR /app
COPY app /app
COPY cmd.sh /
EXPOSE 9090 9191
USER uwsgi
CMD ["/cmd.sh"]
主要變更:
- 將指令碼新增到容器中
- 從 CMD 指令呼叫它
在我們測試新版本之前,是時候停止任何正在執行的舊容器了。以下命令將停止並移除主機上的所有容器:
$ docker stop $(docker ps -q)
c4b3d240f187
9be42abaf902
78af7d12d3bb
$ docker rm $(docker ps -aq)
1198f8486390
c4b3d240f187
9be42abaf902
78af7d12d3bb
現在我們可以使用指令碼重建映像並進行測試:
$ chmod +x cmd.sh
$ docker build -t identidock .
...
$ docker run -e "ENV=DEV" -p 5000:5000 identidock
Running Development Server
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
* Restarting with stat
很好,現在當我們使用 -e "ENV=DEV"
執行時,我們得到開發伺服器;否則,我們得到生產伺服器。
開發伺服器的選擇
你可能會發現,預設的 Python 伺服器在開發過程中無法滿足你的需求,尤其是當連線多個容器時。在這種情況下,你也可以在開發中執行 uWSGI。你仍然需要切換環境的能力,以便啟用不應在生產環境中執行的 uWSGI 功能,如即時程式碼多載。
使用 Docker Compose 自動化開發流程
我們可以增加最後一點自動化來使事情更簡單。Docker Compose 旨在快速啟動和執行 Docker 開發環境。本質上,它使用 YAML 檔案來儲存一組容器的設定,使開發者免於重複和容易出
Docker Compose 實務:開發完美開發與佈署環境
架構與設定深度解析
Docker Compose 作為容器協調工具,讓我們能夠透過簡單的 YAML 檔案定義和管理多容器應用程式。在多年的容器化專案經驗中,玄貓發現 Compose 是橋接開發與生產環境的關鍵工具。讓我們探討 Docker Compose 的核心設定與最佳實踐。
容器定義解析
ports:
- "5000:5000"
environment:
ENV: DEV
volumes:
- ./app:/app
這段設定包含了三個基本的容器定義元素:
連線埠對映(Ports)
連線埠對映直接對應 docker run
的 -p
引數,將容器內的 5000 連線埠對映到主機的 5000 連線埠。在 YAML 中,連線埠對映可以不使用引號,但建議始終加上引號以避免 YAML 解析器將類別似 56:56
的值誤解為 base-60 數字。
環境變數(Environment)
環境變數設定等同於 docker run
的 -e
引數。這裡我們設定 ENV
為 DEV
,這將觸發 Flask 的開發伺服器模式。在實際專案中,我常用環境變數來區分開發、測試和生產設定,無需修改程式碼即可適應不同環境。
卷掛載(Volumes)
卷掛載對應 docker run
的 -v
引數。這裡我們將主機的 ./app
目錄掛載到容器內的 /app
路徑,實作即時程式碼修改而無需重建容器。這是開發階段的關鍵設定,能夠大幅提高開發效率。
Compose 工作流程
掌握 Compose 工作流程對於提高開發效率至關重要。以下是常用命令及其實際應用場景:
啟動服務:
docker-compose up -d
- 在背景模式啟動所有容器
- 自動聚合日誌輸出便於監控
重建映像:
docker-compose build
- 當 Dockerfile 變更時更新映像
- 注意:
up
命令不會自動重建已存在的映像
檢視狀態:
docker-compose ps
- 監控 Compose 管理的容器狀態
執行一次性命令:
docker-compose run
- 適用於執行資料函式庫、測試等臨時任務
- 使用
--no-deps
引數可避免啟動相關聯容器
檢視日誌:
docker-compose logs
- 集中檢視彩色編碼的容器日誌
停止服務:
docker-compose stop
- 停止容器但不移除,保留狀態
移除容器:
docker-compose rm
- 清理停止的容器
- 使用
-v
引數同時移除 Docker 管理的卷
實際開發工作流程例項
在實際開發中,我通常採用以下工作流程:
- 使用
docker-compose up -d
啟動開發環境 - 透過
docker-compose logs
和ps
監控應用狀態 - 修改程式碼後執行
docker-compose build
重建映像 - 再次執行
docker-compose up -d
更新執行中的容器
這個流程的優勢在於 Compose 會保留原容器的卷,確保資料函式庫取在容器重建後仍然存在。不過這也可能導致一些混淆,特別是當你期望全新環境時。如果需要強制重建所有容器,可以使用 --force-recreate
引數。
完成開發後,使用 docker-compose stop
暫停應用,或 docker-compose rm
徹底清理環境。
構建簡單 Web 應用程式
在開發過程中,容器化環境提供了極大的靈活性。讓我們將前面的 “Hello World” 程式轉變為一個能夠產生獨特使用者影像的簡單 Web 應用程式。
Identicons 簡介
Identicons 是根據特定值(通常是 IP 地址或使用者名稱的雜湊)自動生成的影像,為使用者提供視覺識別。這種技術最早由 Don Park 在 2007 年開發,用於識別其部落格的評論者。
目前,GitHub 和 Stack Overflow 等大型網站廣泛使用 Identicons 作為未設定頭像的使用者的預設影像。Stack Overflow 使用 Gravatar 服務生成的 Identicons,而 GitHub 則自行生成。
基礎網頁實作
首先,讓我們建立一個基本網頁。為了簡單起見,我們直接以字串形式回傳 HTML:
from flask import Flask
app = Flask(__name__)
default_name = 'Joe Bloggs'
@app.route('/')
def get_identicon():
name = default_name
header = '<html><head><title>Identidock</title></head><body>'
body = '''<form method="POST">
Hello <input type="text" name="name" value="{}">
<input type="submit" value="submit">
</form>
<p>You look like a:
<img src="/monster/monster.png"/>
'''.format(name)
footer = '</body></html>'
return header + body + footer
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
這段程式碼建立了一個簡單的 HTML 頁面,包含一個供使用者輸入名稱的表單。format
函式將 {}
替換為 name
變數的值,目前硬編碼為 “Joe Bloggs”。
使用 docker-compose up -d
執行應用程式,並在瀏覽器中存取 http://localhost:5000,可以看到一個基本頁面,但圖片連結尚未生效,提交按鈕也未實作功能。
利用現有映像增強功能
接下來,我們需要一個能夠根據字串生成獨特影像的服務。這裡我們將使用 dnmonster
,一個現成的 Docker 映像,它提供了一個 RESTful API 可供呼叫。
修改程式碼,加入影像生成功能:
from flask import Flask, Response
import requests
app = Flask(__name__)
default_name = 'Joe Bloggs'
@app.route('/')
def mainpage():
name = default_name
header = '<html><head><title>Identidock</title></head><body>'
body = '''<form method="POST">
Hello <input type="text" name="name" value="{}">
<input type="submit" value="submit">
</form>
<p>You look like a:
<img src="/monster/monster.png"/>
'''.format(name)
footer = '</body></html>'
return header + body + footer
@app.route('/monster/<name>')
def get_identicon(name):
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
return Response(image, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
這段程式碼新增了 get_identicon
函式,透過 requests
模組向 dnmonster
服務傳送 HTTP 請求,取得對應名稱的怪物影像,並使用 Flask 的 Response
類別回傳影像內容。
在實際開發中,這種微服務架構使得我們能夠輕鬆替換或升級特定功能元件,而不影響整體應用。透過 Docker Compose,我們可以將這些服務協調在一起,形成完整的應用程式生態系統。
Docker 在開發環境中提供了熟悉與可控的工具集,同時確保我們能夠在近似生產的環境中快速測試。隨著應用程式的發展,我們還需要考慮測試和持續整合/佈署等方面,這些將在後續章節中進一步探討。
開發靈活的 Web 服務:使用 Docker 容器化你的應用程式
在現代軟體開發中,容器化已成為佈署應用程式的主流方式。透過容器化,我們可以將應用程式與其依賴項封裝在一起,確保在不同環境中的一致執行。在這篇文章中,玄貓將帶你一步建立一個簡單但功能完整的 Web 應用,並探討如何利用 Docker 生態系統中的現有資源來增強應用功能。
運用請求處理與外部服務整合
首先,我們需要在應用程式中整合外部服務。在這個例子中,將使用一個名為 dnmonster
的服務來生成個人化的頭像(identicon)。讓我們看如何實作這個功能:
# 匯入 requests 函式庫於與 dnmonster 服務通訊
from flask import Flask, Response, request
import requests
app = Flask(__name__)
@app.route('/')
def mainpage():
# 基本頁面內容...
@app.route('/monster/<name>')
def get_identicon(name):
# 向 dnmonster 服務傳送 HTTP GET 請求
# 我們請求一個大小為 80 畫素的頭像,使用 name 變數的值
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
# 使用 Response 函式告訴 Flask 我們回傳的是 PNG 圖片,而非 HTML 或文字
return Response(image, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
更新 Dockerfile 確保依賴完整
接著,我們需要修改 Dockerfile 以確保我們的新程式碼擁有正確的函式庫:
FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8 requests==2.5.1
WORKDIR /app
COPY app /app
COPY cmd.sh /
EXPOSE 9090 9191
USER uwsgi
CMD ["/cmd.sh"]
在這個 Dockerfile 中,我們新增了 requests 函式庫是在 Python 程式碼中使用的 HTTP 客戶端。
使用容器連結(Container Links)
現在我們已準備好啟動 dnmonster 容器並將其連結到我們的應用程式容器。為了清楚展示背後發生的事情,我先使用基本的 Docker 命令來實作,之後再轉向使用 Compose:
# 建構我們的應用容器
$ docker build -t identidock .
# 啟動 dnmonster 容器
$ docker run -d --name dnmonster amouat/dnmonster:1.0
# 啟動應用容器並連結到 dnmonster
$ docker run -d -p 5000:5000 -e "ENV=DEV" --link dnmonster:dnmonster identidock
關鍵在於 --link dnmonster:dnmonster
引數,這是讓 Python 程式碼中的 URL http://dnmonster:8080
可以被解析的魔法所在。
升級到 Compose
手動輸入 Docker 命令雖然有助於理解,但在實際開發中使用 Docker Compose 會更方便。更新 docker-compose.yml
檔案:
identidock:
build: .
ports:
- "5000:5000"
environment:
ENV: DEV
volumes:
- ./app:/app
links:
- dnmonster
dnmonster:
image: amouat/dnmonster:1.0
這個設定檔案宣告了從 identidock 容器到 dnmonster 容器的連結。Compose 會負責以正確的順序啟動容器。
增強功能:處理表單提交與使用者輸入
為了讓應用更實用,我們需要處理 POST 請求並使用表單變數(包含使用者名稱)來生成影像。同時,我們會對使用者輸入進行雜湊處理,這樣可以匿名化敏感輸入,並確保輸入適合用於 URL。
from flask import Flask, Response, request
import requests
import hashlib
app = Flask(__name__)
salt = "UNIQUE_SALT"
default_name = 'Joe Bloggs'
@app.route('/', methods=['GET', 'POST'])
def mainpage():
name = default_name
if request.method == 'POST':
name = request.form['name']
# 對名稱加鹽並雜湊
salted_name = salt + name
name_hash = hashlib.sha256(salted_name.encode()).hexdigest()
header = '<html><head><title>Identidock</title></head><body>'
body = '''<form method="POST">
Hello <input type="text" name="name" value="{0}">
<input type="submit" value="submit">
</form>
<p>You look like a:
<img src="/monster/{1}"/>
'''.format(name, name_hash)
footer = '</body></html>'
return header + body + footer
@app.route('/monster/<name>')
def get_identicon(name):
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
return Response(image, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
這裡的關鍵變化包括:
- 匯入
hashlib
函式庫雜湊使用者輸入 - 定義一個鹽值,使不同網站可以為相同輸入產生不同的頭像
- 擴充套件 route 裝飾器以處理 POST 和 GET 請求
- 檢查請求方法,如果是 POST,則使用表單提交的值更新名稱
- 計算輸入的雜湊值,並在影像 URL 中使用這個雜湊值
關於 dnmonster 服務
dnmonster 是一個封裝在 Docker 容器中的 Node.js 應用程式。它是 Kevin Guadin 的 monsterid.js 從瀏覽器內 JavaScript 到 Node.js 的移植版本。Monsterid.js 本身根據 Andreas Gohr 的 MonsterID,能夠建立 8 位元風格的怪獸影像。
與 monsterid.js 不同,dnmonster 不會對輸入進行任何雜湊處理,而是將這個任務交給呼叫者處理。
增加快取功能
目前我們的應用存在一個效能問題:每次請求頭像時,都會向 dnmonster 服務發出計算成本高昂的呼叫。這是不必要的,因為 identicon 的特點就是對於給定輸入,影像始終保持不變,所以我們應該進行快取。
我會使用 Redis 來實作這一功能。Redis 是一個記憶體內的鍵值資料儲存,非常適合這種資訊量不大與不需要高永續性的任務(如果一個條目丟失或刪除,我們可以重新生成影像)。
雖然可以將 Redis 伺服器加入到我們的 identidock 容器中,但更簡單與更符合容器哲學的做法是建立一個新容器。這樣我們可以利用 Docker Hub 上已有的官方 Redis 映像,避免在容器中執行多個程式的額外麻煩。
更新程式碼以使用快取
from flask import Flask, Response, request
import requests
import hashlib
import redis
app = Flask(__name__)
# 設定 Redis 快取,透過 Docker links 使 redis 主機名可解析
cache = redis.StrictRedis(host='redis', port=6379, db=0)
salt = "UNIQUE_SALT"
default_name = 'Joe Bloggs'
@app.route('/', methods=['GET', 'POST'])
def mainpage():
# 首頁面處理邏輯...
@app.route('/monster/<name>')
def get_identicon(name):
# 檢查名稱是否已在快取中
image = cache.get(name)
if image is None:
print("Cache miss", flush=True)
# 快取未命中,向 dnmonster 服務請求影像
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
# 將影像新增到快取並且給定名稱關聯
cache.set(name, image)
return Response(image, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
更新 Dockerfile 和 docker-compose.yml
我們需要更新 Dockerfile 以安裝 Redis 客戶端函式庫
FROM python:3.4
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask==0.10.1 uWSGI==2.0.8 requests==2.5.1 redis==2.10.3
WORKDIR /app
COPY app /app
COPY cmd.sh /
EXPOSE 9090 9191
USER uwsgi
CMD ["/cmd.sh"]
同時更新 docker-compose.yml 新增 Redis 容器:
identidock:
build: .
ports:
- "5000:5000"
environment:
ENV: DEV
volumes:
- ./app:/app
links:
- dnmonster
- redis
dnmonster:
image: amouat/dnmonster:1.0
redis:
image: redis:3.0
關於容器中執行多個程式
大多數容器只執行單一程式。當需要多個程式時,最好的做法是執行多個容器並將它們連線在一起,就像我們在這個例子中所做的那樣。
然而,有時你確實需要在單一容器中執行多個程式。在這些情況下,最好使用程式管理工具如 supervisord 或 runit 來處理程式的啟動和監控。雖然可以編寫簡單的指令碼來啟動程式,但請注意,你將負責清理程式並轉發任何訊號。
透過這種方式,我們建立了一個簡單但功能完整的 Web 應用,它利用外部服務生成個人化頭像,並使用 Redis 進行快取最佳化。這個範例展示瞭如何使用 Docker 容器化應用程式,並利用容器連結將多個服務組合在一起形成一個完整的應用。
在實際開發中,這種模式非常見:將應用拆分為多個專注於特定功能的容器,然後透過容器網路將它們連線起來。這種方法不僅符合微服務架構的理念,還能讓每個元件獨立擴充套件和更新,大提高了系統的彈性和可維護性。
微服務架構與容器化應用:從單體到分散式系統的轉變
在現代軟體開發領域,微服務架構已成為構建可擴充套件系統的主流方法。從我多年的容器化開發經驗來看,微服務不只是一種技術選擇,更是一種思維轉變。本文將透過一個實際案例 - identidock 應用,探討微服務的本質特徵,並深入分析容器映像檔的發布策略。
微服務架構的本質
identidock 應用雖然是個簡單的範例,卻完美體現了微服務架構的核心特性。在這個應用中,我們將系統拆分為多個獨立的服務:
- 一個以 Python 編寫的 Web 應用(identidock)
- 一個以 JavaScript 開發的服務(dnmonster)
- 一個 C 語言實作的鍵值儲存(Redis)
這些服務透過三個不同的容器執行,彼此間透過網路通訊協作。這種設計與單體架構(Monolithic Architecture)形成鮮明對比。
單體架構的特徵
在單體架構中,上述三個元件會被整合到一個大型應用中,通常使用單一程式語言開發,並在單一容器內執行。即使設計良好的單體應用也會將這些功能拆分為獨立的程式函式庫盡可能利用現有程式函式庫
微服務的優勢
在實際專案中,我發現微服務架構帶來了幾個關鍵優勢:
擴充套件性提升:微服務架構更容易橫向擴充套件到多台機器上。在處理高流量網站時,這點尤為重要。
服務獨立更新:各個服務可以單獨替換或回復,而不影響整體系統。我曾在一個金融系統中,只需替換認證服務而無需停機整個平台。
技術多樣性:不同服務可使用最適合其任務的程式語言。例如,我們可以為計算密集型任務選擇 Go 或 Rust,為 Web 介面選擇 JavaScript。
微服務的挑戰
然而,微服務架構也帶來了一些挑戰:
分散式系統開銷:服務間通訊從程式函式庫變成了網路呼叫,增加了延遲和複雜性。
協調與佈署複雜性:需要使用如 Docker Compose 等工具確保所有元件正確啟動和連線。
服務發現問題:隨著系統規模擴大,服務發現和協調成為必須解決的關鍵問題。
Netflix、Amazon 和 SoundCloud 等企業的成功案例證明,現代網路應用能從微服務架構提供的擴充套件性和動態特性中獲得巨大好處。然而,微服務並非萬能藥,開發團隊需要根據專案特性和團隊能力做出合適的架構選擇。
容器映像檔的發布策略
開發容器化應用後,下一步是讓這些映像檔可供同事、持續整合伺服器或最終使用者使用。映像檔發布有多種方式:從 Dockerfile 重建、從映像檔函式庫,或使用 docker load
從存檔案安裝。
映像檔命名與標籤策略
映像檔的命名和標籤對於建立有效的開發工作流程至關重要。Docker 對名稱的限制很少,因此開發團隊需制定並遵循一套可行的命名方案。
映像檔名稱和標籤可在建構時設定,或使用 docker tag
指令:
# 建立映像檔並設定標籤
docker build -t "identidock:0.1" .
# 為映像檔設定 Docker Hub 使用者名稱
docker tag "identidock:0.1" "amouat/identidock:0.1"
第一個命令將映像檔函式庫設為 “identidock”,標籤為 “0.1”。第二個命令為映像檔關聯了一個 Docker Hub 使用者空間的名稱。
關於 latest 標籤的警告
我在輔導許多團隊時發現,latest
標籤常導致誤解。需要注意的是:
- Docker 在未指定標籤時會使用
latest
作為預設值,但它沒有特殊含義 - 許多映像檔函式庫用作最新穩定版本的別名,但這只是慣例,沒有強制執行
- 標記為
latest
的映像檔不會自動更新 - 仍需執行docker pull
來檢索更新的版本 - 當
docker run
或docker pull
參照沒有標籤的映像檔名稱時,Docker 會使用標記為latest
的映像檔
由於 latest
標籤引起的混淆,我建議完全避免使用它,尤其是導向公眾的映像檔函式庫
使用 Docker Hub 分享映像檔
最直接的映像檔分享方式是使用 Docker Hub。它提供免費的公共映像檔函式庫付費的私有映像檔函式庫了 Docker Hub,還有其他選擇如 quay.io,它提供了一些額外功能,價格也具競爭力。
將 identidock 映像檔上載到 Docker Hub 非常簡單:
docker tag identidock:latest amouat/identidock:0.1
docker push amouat/identidock:0.1
首先,我們為映像檔建立一個別名,格式為 <username>/<repository name>
,其中 <username>
是您在 Docker Hub 的使用者名稱,<repository name>
是您希望映像檔函式庫Hub 上的名稱。我們同時將標籤設定為 “0.1”。
接著,使用 docker push
上載映像檔。如果映像檔函式庫在,會自動建立,然後上載映像檔。
上載後,任何人都可以透過 docker pull
取得您的映像檔。在 Docker Hub 網站上,您可以管理映像檔函式庫設定描述、標記協作者和設定 webhook。
自動化建置流程
當程式碼更新時自動更新映像檔是一個常見需求。Docker Hub 提供了自動建置功能來滿足這一需求。
設定自動建置需要一個 GitHub 或 Bitbucket 倉函式庫驟如下:
- 登入 Docker Hub 網站
- 從右上角下拉選單選擇 “Create Automated Build”
- 選擇包含 identidock 程式碼的倉函式庫. 設定映像檔函式庫(如 identidock_auto)和描述
- 設定標籤欄位,追蹤 master 分支
- 指定 Dockerfile 位置(如 /identidock/Dockerfile)
- 設定映像檔標籤(可保留為 latest 或設為更有意義的名稱如 auto)
- 點選 “Create”
完成設定後,每當您推播更改到原始碼倉函式庫ocker Hub 就會自動建立新的映像檔。您可以隨時手動觸發建置,並檢視建置日誌以排除故障。
讓我們測試自動建置功能:
- 建立一個 README.md 檔案:
identidock
==========
Simple identicon server based on monsterid from Kevin Gaudin.
From "Using Docker" by Adrian Mouat published by O'Reilly Media.
- 提交並推播變更:
git add README.md
git commit -m "Added README"
git push
推播後,前往 Docker Hub 上的建置頁面,您應該能看到系統正在建立映像檔的新版本。
容器化微服務的實踐心得
在實際專案中實施容器化微服務架構時,我總結了幾點關鍵經驗:
服務邊界定義:明確定義每個微服務的責任邊界是成功的關鍵。服務應有單一明確的職責,避免職責蔓延。
容器間通訊設計:精心設計服務間的 API 和通訊模式。在我的實踐中,REST API 適契約步請求,而事件驅動模式更適合非同步流程。
統一監控策略:隨著微服務數量增加,統一的監控和日誌策略變得至關重要。我通常會實施集中式日誌收集和分析系統。
自動化佈署管道:建立從程式碼提交到生產環境的自動化佈署管道,確保每個微服務都經過適當的測試和驗證。
版本管理策略:為映像檔制定明確的版本管理策略,確保開發、測試和生產環境的一致性。
容器化微服務架構雖然增加了某些複雜性,但在可擴充套件性、靈活性和團隊自主性方面帶來的好處往往超過了這些挑戰。關鍵在於根據團隊規模和專案需求,採用適當的工具和實踐來管理這種複雜性。
透過 identidock 這個簡單應用案例,我們已經看到容器如何自然地引導我們走向由小型、定義明確的服務組成的系統。這種微服務方法為構建可擴充套件的現代應用提供了強大基礎。無論是重用現有映像檔作為基礎(如 Python 基礎映像檔),還是作為提供服務的黑盒子(如 dnmonster 映像檔),容器都為我們提供了極大的靈活性和效率。
容器化微服務架構不僅是技術選擇,也是一種思維模式的轉變。它鼓勵開發團隊思考系統的分解方式,以及服務間的邊界和互動。這種思維模式在構建現代、可擴充套件的雲端原生應用時尤為重要。
隨著組織和應用的成長,容器化微服務架構的優勢將變得愈發明顯。透過合理的映像檔發布策略和自動化建置流程,團隊可以建立高效的開發工作流程,從而更快地交付價值給使用者。
超越 Docker Hub:私有映像檔分發策略
在容器技術日趨成熟的今日,許多團隊開始思考如何更有效地管理和分發他們的 Docker 映像檔。雖然 Docker Hub 提供了便利的公開映像檔託管服務,但它並非適用於所有專案和企業場景。
Docker Hub 的侷限性
使用 Docker Hub 建立和分發映像檔存在幾項明顯的限制:
- 公開性問題:除非付費使用私有儲存函式庫則你的映像檔都是公開的
- 依賴風險:完全依賴 Docker Hub 服務,一旦服務中斷,你將無法更新或下載映像檔
- 效率考量:當需要快速建立並在管道中移動映像檔時,從 Hub 傳輸檔案和等待排隊建立會帶來不必要的延遲
對於開放原始碼專案和小型個人專案,Docker Hub 無疑是理想選擇。但對於規模較大或更重要的專案,你可能需要考慮其他解決方案來替代或補充它。
私有映像檔分發選項
脫離 Docker Hub 後,我們有幾種選擇:
- 手動操作:透過匯出和匯入映像檔,或在每個 Docker 主機上從 Dockerfile 重建映像檔
- 自建 Registry:執行自己的 Docker Registry 服務
- 第三方託管服務:使用其他商業 Registry 服務
手動方式明顯不是最佳選擇:每次從 Dockerfile 重建映像檔既緩慢又可能導致不同主機上的映像檔產生差異;而匯出和匯入映像檔則較為繁瑣與容易出錯。
讓我們先看免費的解決方案——執行自己的 Registry,然後再簡單探討一些商業選擇。
自建 Docker Registry
首先需要釐清一點:Docker Registry 和 Docker Hub 不同。二者都實作了 Registry API,允許使用者推播、提取和搜尋映像檔,但 Docker Hub 是一個封閉原始碼的遠端服務,而 Registry 則是可以在本地執行的開放原始碼應用程式。Docker Hub 還包含使用者帳戶、統計資料和網頁介面等 Docker Registry 沒有的功能。
Registry v2 說明
在本文中,我們將專注於 Registry 的第二版,它僅與 Docker 1.6 及更高版本相容。如果你需要支援更舊版本的 Docker,則需要執行舊版 Registry(也可以同時執行兩個版本以便過渡)。Registry v2 在安全性、可靠性和效率方面相較於 v1 有了重大進步,因此玄貓強烈建議盡可能使用 v2 版本。
快速啟動本地 Registry
使用官方映像檔執行本地 Registry 是最簡單的方法:
$ docker run -d -p 5000:5000 registry:2
...
75fafd23711482bbee7be50b304b795a40b7b0858064473b88e3ddcae3847c37
Registry 啟動後,我們可以為映像檔加上適當的標籤並推播它。如果使用 docker-machine,仍可使用 localhost 地址,因為它將由與 Registry 執行在同一主機上的 Docker 引擎解釋(而非使用者端):
$ docker tag amouat/identidock:0.1 localhost:5000/identidock:0.1
$ docker push localhost:5000/identidock:0.1
The push refers to a repository [localhost:5000/identidock] (len: 1)
...
0.1: digest: sha256:d20affe522a3c6ef1f8293de69fea5a8621d695619955262f3fc2885...
如果我們現在移除本地版本,可以再次提取它:
$ docker rmi localhost:5000/identidock:0.1
Untagged: localhost:5000/identidock:0.1
$ docker pull localhost:5000/identidock:0.1
0.1: Pulling from identidock
...
76899e56d187: Already exists
Digest: sha256:d20affe522a3c6ef1f8293de69fea5a8621d695619955262f3fc28852e173108
Status: Downloaded newer image for localhost:5000/identidock:0.1
Docker 發現我們已經有一個具有相同內容的映像檔,所以實際上只是將標籤加回來。你可能已經注意到 Registry 為映像檔生成了一個摘要(digest)。這是根據映像檔內容及其元資料的唯一雜湊值。你可以使用摘要來提取映像檔:
$ docker pull localhost:5000/identidock@sha256:\
d20affe522a3c6ef1f8293de69fea5a8621d695619955262f3fc28852e173108
sha256:d20affe522a3c6ef1f8293de69fea5a8621d695619955262f3fc28852e173108: Pul...
...
76899e56d187: Already exists
Digest: sha256:d20affe522a3c6ef1f8293de69fea5a8621d695619955262f3fc28852e173108
Status: Downloaded newer image for localhost:5000/identidock@sha256:d20affe5...
使用摘要的優勢
使用摘要的主要優點是它保證你提取的正是你認為的那個映像檔。當按標籤提取時,底層映像檔可能隨時變更而你不會知道。此外,使用摘要確保了映像檔的完整性;你可以確信它在傳輸或儲存過程中沒有被篡改。
遠端存取的挑戰與解決方案
Registry 的主要用途是作為團隊或組織的中央儲存函式庫意味著你需要能夠從遠端 Docker 守護程式提取映像檔。但如果我們嘗試使用剛啟動的 Registry,會遇到以下錯誤:
$ docker pull 192.168.1.100:5000/identidock:0.1
Error response from daemon: unable to ping registry endpoint
https://192.168.99.100:5000/v0/
v2 ping attempt failed with error: Get https://192.168.99.100:5000/v2/:
tls: oversized record received with length 20527
v1 ping attempt failed with error: Get https://192.168.99.100:5000/v1/_ping:
tls: oversized record received with length 20527
這裡我將伺服器的 IP 地址替換為"localhost”。無論你是從另一台機器上的守護程式還是從與 Registry 相同的機器上提取,都會遇到這個錯誤。
發生了什麼?Docker 守護程式拒絕連線到遠端主機,因為它沒有效的傳輸層安全性(TLS)憑證。之前之所以能正常工作,是因為 Docker 對從"localhost"伺服器提取有特殊例外。
TLS 安全連線的三種方案
我們可以透過以下三種方式解決這個問題:
- 使用
--insecure-registry 192.168.1.100:5000
引數重新啟動每個需要存取 Registry 的 Docker 守護程式(替換為你伺服器的適當地址和連線埠) - 在主機上安裝來自受信任憑證頒發機構的簽名憑證,就像為透過 HTTPS 存取的網站做的那樣
- 在主機上安裝自簽名憑證,並在每個需要存取 Registry 的 Docker 守護程式上安裝副本
第一個選項最簡單,但因安全考慮不建議使用。第二個選項最佳,但需要從受信任的憑證頒發機構取得憑證,通常需要付費。第三個選項安全但需要手動將憑證複製到每個守護程式。
自簽名憑證設定
如果要建立自己的自簽名憑證,可以使用 OpenSSL 工具。這些步驟應該在你想長期執行作為 Registry 伺服器的機器上執行:
root@reginald:~# mkdir registry_certs
root@reginald:~# openssl req -newkey rsa:4096 -nodes -sha256 \
> -keyout registry_certs/domain.key -x509 -days 365 \
-out registry_certs/domain.crt
Generating a 4096 bit RSA private key
....................................................++
....................................................++
writing new private key to 'registry_certs/domain.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:reginald
Email Address []:
root@reginald:~# ls registry_certs/
domain.crt domain.key
這個指令建立了一個 x509 自簽名憑證和一個 4096 位元的 RSA 私鑰。憑證使用 SHA256 摘要簽名,有效期為 365 天。OpenSSL 會請求資訊,你可以輸入或保留預設值。
通用名稱(Common Name)很重要;它必須與你希望存取伺服器的名稱比對,並且不應該是 IP 地址(“reginald"是我的伺服器名稱)。
完成此過程後,我們有一個名為 domain.crt 的憑證檔案(將與使用者端分享)和一個必須保持安全與不分享的私鑰 domain.key。
使用 IP 位址存取 Registry
如果你想使用 IP 位址存取 Registry,情況會更複雜一些。你不能簡單地使用 IP 位址作為通用名稱,而需要為想使用的 IP 位址設定主體替代名稱(Subject Alternative Names,或 SANs)。
一般而言,玄貓不建議這種方法。最好為伺服器選擇一個名稱,並使其在內部可透過該名稱定址(在最壞的情況下,你可以手動將伺服器名稱新增到 /etc/hosts)。這通常更容易設定,而與如果你想更改 IP 位址,不需要重新標記所有映像檔。
設定 Docker 守護程式信任憑證
接下來,我們需要將憑證複製到每個將存取 Registry 的 Docker 守護程式。憑證應複製到檔案 /etc/docker/certs.d/<registry_address>/ca.crt
,其中 <registry_address>
是你的 Registry 伺服器的地址和連線埠。你還需要重新啟動 Docker 守護程式:
root@reginald:~# sudo mkdir -p /etc/docker/certs.d/reginald:5000
root@reginald:~# sudo cp registry_certs/domain.crt \
/etc/docker/certs.d/reginald:5000/ca.crt
root@reginald:~# sudo service docker restart
docker stop/waiting
docker start/running, process 3906
如果要在遠端主機上執行,你需要使用 scp 或類別似工具將 CA 憑證傳輸到 Docker 主機。如果你使用了公共受信任的 CA,可以跳過此步驟。
啟動安全的 Registry
現在我們可以啟動 Registry:
root@reginald:~# docker run -d -p 5000:5000 \
-v $(pwd)/registry_certs:/certs \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
--restart=always --name registry registry:2
...
b79cb734d8778c0e36934514c0a1ed13d42c342c7b8d7d4d75f84497cc6f45f4
這裡我們將憑證作為卷掛載到容器中,並使用環境變數設定 Registry 使用我們的憑證。
驗證 Registry 功能
最後,我們可以提取一個映像檔,重新標記並推播它,以證明一切正常運作:
root@reginald:~# docker pull debian:wheezy
wheezy: Pulling from library/debian
ba249489d0b6: Pull complete
19de96c112fc: Pull complete
library/debian:wheezy: The image you are pulling has been verified.
Important: image verification is a tech preview feature and should not be
relied on to provide security.
Digest: sha256:90de9d4ecb9c954bdacd9fbcc58b431864e8023e42
## Docker Registry進階設定與最佳實務
在容器化佈署的世界中,擁有一個可靠的私有映像檔倉函式庫egistry)對於企業環境至關重要。在上一節我們已經完成了基本的Registry設定,現在讓我們探討如何最佳化和擴充套件這個重要的基礎設施元件。
### 主機名稱與憑證比對
實務上,我遇過許多開發者嘗試用IP位址替代主機名稱來簡化設定,但這會導致憑證驗證失敗:
```bash
# 錯誤做法 - 不要這樣做
docker pull 192.168.1.100:5000/my-image:latest # 會導致憑證錯誤
正確的做法是:
- 編輯
/etc/hosts
檔案新增對映 - 或設定DNS解析讓主機名稱能正確解析
客戶端機器也需要:
- 複製憑證檔案到
/etc/docker/certs.d/<registry_address>/ca.crt
- 確保Docker引擎能解析Registry的位址
Registry設定深入解析
Registry的設定是透過YAML檔案進行的,位於容器內的 /go/src/github.com/docker/distribution/cmd/registry/config.yml
。我們可以透過環境變數或掛載自訂設定檔案來調整設定。
儲存設定
預設情況下,Registry使用檔案系統驅動儲存映像檔,這對開發環境來說很合適。以下是基本的儲存設定:
storage:
filesystem:
rootdirectory: /var/lib/registry
這個設定會將所有映像檔資料儲存在 /var/lib/registry
目錄下,請確保將其宣告為volume以確保資料持久化。
在我參與的大型專案中,我們通常選擇雲端儲存方案,Registry支援以下儲存後端:
- Amazon S3
- Microsoft Azure Storage
- Ceph分散式物件儲存
- Redis (作為快取層加速)
身份驗證設定
安全的Registry必須實施身份驗證。我曾在金融科技專案中實施過兩種方式:
1. 代理伺服器身份驗證
使用nginx作為前端代理,負責驗證使用者:
location /v2/ {
auth_basic "Registry realm";
auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;
proxy_pass http://registry:5000;
}
設定好後,使用者可以使用 docker login
命令進行身份驗證。
2. 根據令牌的身份驗證
這是更進階的方式,使用JSON Web Tokens (JWT):
- Registry會拒絕未提供有效令牌的客戶端
- 客戶端被重定向到身份驗證伺服器
- 取得令牌後才能存取Registry
這種方式更適合大型或分散式組織,但設定相對複雜。目前開放原始碼選項有限,Cesanta Software提供了一個解決方案,或者企業可以考慮商業解決方案。
HTTP設定
HTTP設定對Registry正常運作至關重要,特別是TLS憑證的設定:
http:
addr: registry.example.com:5000
secret: DD100CC4-1356-11E5-A926-33C19330F945
tls:
certificate: /certs/domain.crt
key: /certs/domain.key
這裡的關鍵設定項包括:
addr
: Registry的位址和portsecret
: 用於簽署狀態資訊的隨機字串,應隨機生成tls
: 憑證和金鑰的位置,必須確保容器能夠存取這些檔案
商業Registry解決方案
若需要更完整的解決方案,可以考慮以下商業選項:
- Docker Trusted Registry
- CoreOS Enterprise Registry
這些解決方案提供了更豐富的功能:
- 網頁管理介面
- 精細的許可權控制
- 安裝和管理工具
- 團隊協作功能
在我輔導的企業客戶中,大型組織通常會選擇商業Registry來滿足其合規和安全需求。
最佳化Docker映像檔大小
Docker映像檔的大小問題一直是容器化過程中的痛點。多數映像檔動輒數百MB,耗費大量傳輸時間。雖然Docker的分層結構能夠重用分享層,但仍有最佳化空間。
理解映像檔分層機制
首先,我們需要理解一個關鍵概念:刪除檔案不會減少映像檔大小。這是因為映像檔由多個層組成,每個層對應Dockerfile中的一條指令。讓我們看一個範例:
FROM debian:wheezy
RUN dd if=/dev/zero of=/bigfile count=1 bs=50MB
RUN rm /bigfile
構建並檢查這個映像檔:
$ docker build -t filetest .
$ docker images filetest
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
filetest latest e2a98279a101 8 seconds ago 135 MB
使用docker history
檢視各層大小:
$ docker history filetest
IMAGE CREATED BY SIZE
e2a98279a101 /bin/sh -c rm /bigfile 0 B
5d0f04380012 /bin/sh -c dd if=/dev/zero 50 MB
30d39e59ffe2 /bin/sh -c #(nop) ADD file 85.01 MB
可以看到,雖然我們刪除了檔案,但映像檔大小仍然增加了50MB。這是因為刪除動作只是在頂層新增了一個新的層,原始檔案在下層仍然存在。
有效的大小最佳化策略
根據分層機制,以下是有效的最佳化策略:
1. 在同一層中建立和刪除臨時檔案
FROM debian:wheezy
RUN dd if=/dev/zero of=/bigfile count=1 bs=50MB && rm /bigfile
構建後檢查:
$ docker images filetest
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
filetest latest 40a9350a4fa2 34 seconds ago 85.01 MB
映像檔大小沒有增加,因為檔案在同一層中被建立和刪除。
2. 合併命令減少層數
MongoDB官方映像檔中的範例:
RUN curl -SL "https://$MONGO_VERSION.tgz" -o mongo.tgz \
&& curl -SL "https://$MONGO_VERSION.tgz.sig" -o mongo.tgz.sig \
&& gpg --verify mongo.tgz.sig \
&& tar -xvf mongo.tgz -C /usr/local --strip-components=1 \
&& rm mongo.tgz*
3. 合併套件管理操作
RUN apt-get update \
&& apt-get install -y curl numactl \
&& rm -rf /var/lib/apt/lists/*
4. 選擇適當的基礎映像檔
Alpine Linux基礎映像檔僅約5MB,比Ubuntu或Debian小得多。對於許多應用來說,使用Alpine可以大幅減少映像檔大小。
5. 多階段構建
在Docker 17.05+版本中,可以使用多階段構建:
FROM golang:1.16 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine:3.14
COPY --from=builder /app/myapp /usr/local/bin/
ENTRYPOINT ["myapp"]
映像檔扁平化技術
在極端情況下,還可以使用映像檔扁平化技術:
$ docker create identidock:latest
fe165be64117612c94160c6a194a0d8791f4c6cb30702a61d4b3ac1d9271e3bf
$ docker export $(docker ps -lq) | docker import -
146880a742cbd0e92cd9a79f75a281f0fed46f6b5ece0219f5e1594ff8c18302
$ docker tag 146880a identidock:import
檢查結果:
$ docker images identidock
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
identidock import 146880a742cb 5 minutes ago 730.9 MB
identidock latest 1432cc6c20e5 4 days ago 839 MB
這種方法確實可以減少映像檔大小,但有以下缺點:
- 所有Dockerfile指令如EXPOSE、CMD、PORTS需要重新設定
- 映像檔相關的所有元資料都會丟失
- 無法與分享相同父層的其他映像檔分享空間
在我的實踐中,這種方法僅適用於特殊情況,不建議在正常工作流程中使用。
映像檔來源可信度保障
在分散式環境中,確保映像檔的來源可信至關重要。當你下載一個映像檔時,需要確信:
- 它確實由聲稱的建立者製作
- 它沒有被篡改
- 它與建立者測試的是完全相同的映像檔
Docker的解決方案是Docker Content Trust,這是一個用於驗證映像檔完整性和來源的機制。在撰文時,此功能正在測試階段,預設並未啟用。
在我負責的金融系統佈署中,我們實施了嚴格的映像檔簽名政策,確保所有用於生產的映像檔都經過數字簽名並可驗證其來源。這對於滿足合規要求和安全稽核至關重要。
Docker Registry是容器化基礎設施的核心元件,正確設定和最佳化Registry可以顯著提高開發和佈署效率。無論是選擇自建還是商業解決方案,合理的儲存策略、嚴格的身份驗證以及映像檔最佳化都是不可忽視的關鍵要素。
在容器化旅程中,建立一個安全、高效的私有映像檔倉函式庫功的基礎。透過本文介紹的設定技巧和最佳實務,你可以為團隊開發一個符合企業級要求的Docker Registry,並在確保安全的同時提升整體工作流程效率。
容器持續整合與測試實務
持續整合(CI)與測試是現代軟體開發流程中不可或缺的環節。透過Docker容器化技術,我們能夠建立更一致、可重複與高效的測試環境。本文將探討如何結合Docker與Jenkins建立持續整合工作流程,並介紹容器化環境下的測試策略。
容器化測試的挑戰與優勢
在微服務架構中,測試面臨獨特的挑戰。一方面,微服務結構使單元測試變得簡單;另一方面,系統整合測試因服務數量增加和網路連結複雜化而變得困難。在這種環境下,網路服務模擬比傳統單體應用中的類別模擬更為重要。
將測試程式碼納入映像檔雖然增加了其大小,但保持了容器的可攜性和一致性優勢。這是一個需要權衡的設計決策,我們將在例項中探討這種做法的優缺點。
為應用程式新增單元測試
我們以一個名為「identidock」的應用為例,首先為它新增基本的單元測試。這些測試將檢驗應用的核心功能,而不依賴外部服務。
首先,建立一個測試檔案 identidock/app/tests.py
:
import unittest
import identidock
class TestCase(unittest.TestCase):
def setUp(self):
identidock.app.config["TESTING"] = True
self.app = identidock.app.test_client()
def test_get_mainpage(self):
page = self.app.post("/", data=dict(name="Moby Dock"))
assert page.status_code == 200
assert 'Hello' in str(page.data)
assert 'Moby Dock' in str(page.data)
def test_html_escaping(self):
page = self.app.post("/", data=dict(name='"><b>TEST</b><!--'))
assert '<b>' not in str(page.data)
if __name__ == '__main__':
unittest.main()
這個簡單的測試檔案包含三個方法:
setUp
:初始化Flask應用的測試版本test_get_mainpage
:測試首頁面是否能正確處理輸入test_html_escaping
:測試應用是否正確轉義HTML實體
執行測試:
$ docker build -t identidock .
$ docker run identidock python tests.py
發現第二個測試失敗了,這表明我們的應用存在安全漏洞——沒有正確轉義使用者輸入。這類別問題在大型應用中可能導致資料洩露和跨網站指令碼攻擊(XSS)。
修復安全漏洞
我們需要修改應用程式來淨化使用者輸入,將HTML實體和引號替換為轉義碼。更新identidock.py
:
from flask import Flask, Response, request
import requests
import hashlib
import redis
import html
app = Flask(__name__)
cache = redis.StrictRedis(host='redis', port=6379, db=0)
salt = "UNIQUE_SALT"
default_name = 'Joe Bloggs'
@app.route('/', methods=['GET', 'POST'])
def mainpage():
name = default_name
if request.method == 'POST':
name = html.escape(request.form['name'], quote=True)
salted_name = salt + name
name_hash = hashlib.sha256(salted_name.encode()).hexdigest()
header = '<html><head><title>Identidock</title></head><body>'
body = '''<form method="POST">
Hello <input type="text" name="name" value="{0}">
<input type="submit" value="submit">
</form>
<p>You look like a:
<img src="/monster/{1}"/>
'''.format(name, name_hash)
footer = '</body></html>'
return header + body + footer
@app.route('/monster/<name>')
def get_identicon(name):
name = html.escape(name, quote=True)
image = cache.get(name)
if image is None:
print ("Cache miss", flush=True)
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
cache.set(name, image)
return Response(image, mimetype='image/png')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
關鍵修改是使用html.escape
方法來淨化使用者輸入。重新構建並測試應用:
$ docker build -t identidock .
$ docker run identidock python tests.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.009s
OK
這次測試透過了。值得注意的是,如果我們使用真正的範本引擎而不是簡單的字串連線,轉義處理會自動完成,從而避免這類別問題。這再次提醒我們,即使看似簡單的程式碼也需要測試,並盡可能使用經過驗證的現有程式碼和工具。
擴充套件命令指令碼支援測試
接下來,我們應該擴充套件cmd.sh
檔案以支援自動執行測試:
#!/bin/bash
set -e
if [ "$ENV" = 'DEV' ]; then
echo "Running Development Server"
exec python "identidock.py"
elif [ "$ENV" = 'UNIT' ]; then
echo "Running Unit Tests"
exec python "tests.py"
else
echo "Running Production Server"
exec uwsgi --http 0.0.0.0:9090 --wsgi-file /app/identidock.py \
--callable app --stats 0.0.0.0:9191
fi
現在我們可以透過更改環境變數來執行測試:
$ docker build -t identidock .
$ docker run -e ENV=UNIT identidock
Running Unit Tests
..
----------------------------------------------------------------------
Ran 2 tests in 0.010s
OK
在映像檔中包含測試
在本例項中,我們將identidock的測試捆綁到identidock映像中,這符合Docker使用單一映像貫穿開發、測試和生產的理念。這種方法的優點是我們可以輕鬆地在不同環境中驗證測試,有助於排除偵錯過程中的問題。
缺點是會產生更大的映像檔——必須包含測試程式碼及其依賴項。這也意味著攻擊面增加,理論上攻擊者可能利用測試工具或程式碼破壞生產系統。
不過,在大多數情況下,使用單一映像的簡單性和可靠性優勢將大於稍微增加的大小和理論上的安全風險。
使用容器實作快速測試
所有測試,尤其是單元測試,需要快速執行,以鼓勵開發人員頻繁執行它們。容器提供了一種啟動乾淨隔離環境的快速方式,這對處理改變其環境的測試特別有用。
例如,假設你有一套使用預先填充測試資料的服務的測試。每個使用該服務的測試可能會以某種方式改變資料——新增、刪除或修改。一種方法是讓每個測試在執行後嘗試清理資料,但這有問題:如果測試(或清理)失敗,會汙染所有後續測試的資料,使故障源難以診斷。
另一種方法是在每次測試後銷毀服務,為每個測試啟動新的服務。使用虛擬機器做這件事太慢了,但用容器則完全可行。
容器在測試不同環境/設定下的服務方面也表現出色。如果你的軟體必須在一系列Linux發行版上執行,容器可以讓你快速輕鬆地在這些環境中測試程式碼。
持續整合與Docker
持續整合是一種軟體開發實踐,團隊成員頻繁地將程式碼整合到分享儲存函式庫通常每天多次。每次整合都會經過自動化構建和測試驗證,以快速發現整合錯誤。
使用Docker進行持續整合有以下優勢:
- 環境一致性:保證開發、測試和生產環境的一致性
- 隔離性:測試在隔離環境中執行,減少幹擾
- 可重複性:任何人都可以使用相同的環境重現問題
- 速度:容器啟動快速,縮短測試週期
在實際工作中,玄貓經常將Docker與Jenkins結合使用,建立一個強大的CI/CD管道。Jenkins可以監控程式碼倉函式庫更,自動提取最新程式碼,在Docker容器中構建和測試,然後根據結果決定是否佈署到生產環境。
測試微服務架構
微服務架構的測試比傳統單體應用更複雜,主要因為:
- 服務之間的依賴關係增加
- 網路通訊失敗的可能性增加
- 版本管理變得更複雜
- 分散式系統的測試難度本質上更高
針對這些挑戰,我推薦以下策略:
- 強調單元測試:確保每個微服務的核心功能正確
- 使用契約測試:確保服務之間的介面一致
- 建立端對端測試:模擬真實使用者行為
- 引入混沌測試:隨機故障注入以測試系統彈性
在我參與的一個金融科技專案中,我們使用Docker Compose建立了一個完整的測試環境,包含所有微服務。這使我們能夠在本地複製生產環境的行為,大提高了測試效率。
實用技巧
使用多階段構建:將測試和構建階段分開,只將必要的檔案包含在最終映像中
FROM python:3.9 AS builder WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . RUN pytest FROM python:3.9-slim WORKDIR /app COPY --from=builder /app/src /app/src CMD ["python", "src/main.py"]
利用快取:合理組織Dockerfile中的層次,最大限度利用快取加速構建
COPY requirements.txt . RUN pip install -r requirements.txt # 僅在需求更改時才重新安裝依賴 COPY . .
平行測試:使用Docker平行執行測試,提高測試效率
docker-compose up -d service1 service2 service3 docker-compose exec service1 pytest & docker-compose exec service2 pytest & docker-compose exec service3 pytest & wait
專注於測試資料管理:使用專門的容器來管理測試資料
docker run --name test-db -d postgres:13 docker run --link test-db:db my-test-app docker rm -f test-db
容器化測試的未來趨勢
隨著容器技術的發展,玄貓觀察到幾個值得關注的測試趨勢:
- 測試即程式碼:測試環境設定作為程式碼管理,提高可重複性
- 自動化測試選擇:根據程式碼變更人工智慧選擇需要執行的測試
- 整合安全測試:將安全測試整合到CI/CD流程中
- AI輔助測試:使用人工智慧輔助生成測試使用案例和分析測試結果
容器化測試不僅是一種技術選擇,它代表了一種更加敏捷、可靠的軟體開發方法。透過採用這些實踐,開發團隊可以更快地交付高品質的軟體,並在競爭激烈的市場中保持領先地位。
在實際專案中,Docker不僅加速了測試過程,還改善了開發團隊的協作方式。開發人員可以輕鬆地分享完整的應用環境,消除了「在我的機器上能執行」的問題。這種一致性是高效軟體交付的根本,也是容器技術在測試領域廣受歡迎的關鍵原因。
Docker 與 Jenkins 的完美結合:開發高效 CI/CD 流程
在現代軟體開發的世界裡,持續整合(CI)和持續佈署(CD)已經成為提高軟體品質和加速交付的關鍵實踐。而 Docker 容器技術則為這些實踐帶來了革命性的變化。作為一位長期在大型專案中實施 DevOps 的技術工作者,玄貓發現 Docker 與 CI/CD 工具的結合能夠顯著提升開發團隊的效率和產出品質。
Docker Socket:容器通訊的核心橋樑
Docker 的架構中,Docker socket 是一個至關重要的元件,它作為 Docker 客戶端與守護程式(daemon)之間通訊的端點。在預設情況下,這是一個 IPC socket,透過檔案 /var/run/docker.sock
進行存取。不過,Docker 也支援透過網路位址暴露的 TCP socket 以及 systemd 風格的 socket。
當我們需要在容器內部控制宿主機的 Docker 時,可以簡單地將這個 socket 作為一個卷(volume)掛載到容器中。這種方式讓容器能夠建立「兄弟容器」(sibling containers),而不是「子容器」(child containers)。
CI/CD 環境中的容器策略
在建立 CI/CD 環境時,容器化的測試環境提供了顯著優勢。玄貓曾在一個金融科技專案中採用這種方法,透過為不同測試設定建立專用映像檔,將測試執行時間從原本的數小時縮短到了幾分鐘。這種方法特別適合需要測試不同資料函式庫的場景—只需建立對應的映像檔,就能快速完成測試。
不過,這種方法有一個限制:它無法考慮不同發行版之間的核心(kernel)差異。在需要考慮這些差異的情境中,可能需要採取其他策略。
建立 Jenkins 容器:實作自動化測試與佈署
Jenkins 是一個廣受歡迎的開放原始碼 CI 伺服器,我們將使用它來為我們的 Web 應用程式建立自動化流程。目標是設定 Jenkins,使其在我們推播變更到專案時,自動簽出變更、建立新映像檔、執行單元測試和系統測試,並生成測試結果報告。
容器內執行 Docker 的兩種方法
為了讓 Jenkins 容器能夠建立 Docker 映像檔,我們有兩種主要方法:
- Socket 掛載方式:將宿主機的 Docker socket 掛載到容器中
- Docker-in-Docker (DinD):在 Docker 容器中執行另一個 Docker 例項
這兩種方法的架構差異如下:

Docker-in-Docker 的深入理解
Docker-in-Docker 是在 Docker 容器內執行 Docker 本身。這需要特殊設定,主要是以特權模式執行容器並處理一些檔案系統問題。最簡單的方式是使用 Jérôme Petazzoni 的 DinD 專案,可在 GitHub 上找到。
要快速上手,可以使用 Docker Hub 上的 DinD 映像檔:
$ docker run --rm --privileged -t -i -e LOG=file jpetazzo/dind
ln: failed to create symbolic link '/sys/fs/cgroup/systemd/name=systemd':
Operation not permitted
root@02306db64f6a:/# docker run busybox echo "Hello New World!"
Unable to find image 'busybox:latest' locally
Pulling repository busybox
d7057cb02084: Download complete
cfa753dfea5e: Download complete
Status: Downloaded newer image for busybox:latest
Hello New World!
DinD 與 socket 掛載方法的主要區別在於,DinD 建立的容器與宿主機容器隔離。在 DinD 容器中執行 docker ps
只會顯示由 DinD Docker 守護程式建立的容器。相比之下,在使用 socket 掛載方法的情況下,無論在哪裡執行 docker ps
,都會顯示所有容器。
選擇 Socket 掛載的理由
玄貓在大多數專案中更傾向於使用 socket 掛載方法,主要是因為它的簡單性。但在某些情況下,你可能會需要 DinD 提供的額外隔離性。如果選擇 DinD,需要注意以下幾點:
獨立的快取:你將有自己的快取,所以一開始建構會較慢,並且需要重新提取所有映像檔。這可以透過使用本地登入檔或映象來緩解。
特權模式:容器必須以特權模式執行,所以在安全性方面並不比 socket 掛載技術更好。未來 Docker 增加更精細的許可權支援後,這種情況應該會改善。
磁碟空間消耗:DinD 使用卷來儲存
/var/lib/docker
目錄,如果在移除容器時忘記刪除卷,會迅速消耗磁碟空間。
實作 Jenkins 容器:完整步驟
為了掛載宿主機的 socket,我們需要確保容器內的 Jenkins 使用者具有足夠的存取許可權。在一個名為 identijenk 的新目錄中,建立一個包含以下內容的 Dockerfile:
FROM jenkins:1.609.3
USER root
RUN echo "deb http://apt.dockerproject.org/repo debian-jessie main" \
> /etc/apt/sources.list.d/docker.list \
&& apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 \
--recv-keys 58118E89F3A912897C070ADBF76221572C52609D \
&& apt-get update \
&& apt-get install -y apt-transport-https \
&& apt-get install -y sudo \
&& apt-get install -y docker-engine \
&& rm -rf /var/lib/apt/lists/*
RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers
USER jenkins
這個 Dockerfile 以 Jenkins 基礎映像檔為起點,安裝 Docker 二進位檔案,並為 jenkins 使用者新增無密碼的 sudo 許可權。我們故意沒有將 jenkins 加入 Docker 群組,所以所有 Docker 命令都需要加上 sudo 字首。
為何不使用 Docker 群組
我們可以將 jenkins 使用者加入宿主機的 docker 群組,而不是使用 sudo。但這樣做的問題是需要找到並使用 CI 宿主機上 docker 群組的 GID,並將其硬編碼到 Dockerfile 中。這使我們的 Dockerfile 不可移植,因為不同的宿主機對 docker 群組有不同的 GID。為了避免這種混淆和痛苦,使用 sudo 是更好的選擇。
建構與測試 Jenkins 映像檔
現在我們可以建構映像檔並進行測試:
$ docker build -t identijenk .
...
Successfully built d0c716682562
$ docker run -v /var/run/docker.sock:/var/run/docker.sock \
identijenk sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS ...
a36b75062e06 identijenk "/bin/tini -- /usr/lo" 1 seconds ago Up Less tha...
在 docker run
命令中,我們掛載了 Docker socket 以連線到宿主機的 Docker 守護程式。在較舊版本的 Docker 中,通常也會掛載 Docker 二進位檔,而不是在容器內安裝 Docker。這樣做的優點是保持宿主機和容器中的 Docker 版本同步。然而,從版本 1.7.1 開始,Docker 開始使用動態函式庫意味著任何相依性也需要掛載到容器中。與其處理找到和更新正確函式庫題,直接在映像檔中安裝 Docker 更為簡單。
增強 Jenkins 容器功能
現在我們已經在容器內部啟用了 Docker,可以安裝一些其他需要的工具來讓 Jenkins 建構工作正常執行。更新 Dockerfile 如下:
FROM jenkins:1.609.3
USER root
RUN echo "deb http://apt.dockerproject.org/repo debian-jessie main" \
> /etc/apt/sources.list.d/docker.list \
&& apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 \
--recv-keys 58118E89F3A912897C070ADBF76221572C52609D \
&& apt-get update \
&& apt-get install -y apt-transport-https \
&& apt-get install -y sudo \
&& apt-get install -y docker-engine \
&& rm -rf /var/lib/apt/lists/*
RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers
RUN curl -L https://github.com/docker/compose/releases/download/1.4.1/\
docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; \
chmod +x /usr/local/bin/docker-compose
USER jenkins
COPY plugins.txt /usr/share/jenkins/plugins.txt
RUN /usr/local/bin/plugins.sh /usr/share/jenkins/plugins.txt
這個更新的 Dockerfile 安裝了 Docker Compose(我們將用它來構建和執行我們的映像檔),並複製和處理一個 plugins.txt 檔案,該檔案定義了要在 Jenkins 中安裝的外掛列表。
在與 Dockerfile 相同的目錄中建立一個內容如下的 plugins.txt 檔案:
scm-api:0.2
git-client:1.16.1
git:2.3.5
greenballs:1.14
前三個外掛設定了一個介面,我們可以用它來設定對 Git 中專案的存取。“greenballs” 外掛將預設的 Jenkins 藍色球(表示成功的構建)替換為綠色球。
持久化 Jenkins 設定:資料容器的運用
在我們啟動 Jenkins 容器並開始設定我們的構建之前,應該建立一個資料容器來儲存我們的設定:
$ docker build -t identijenk .
...
$ docker run --name jenkins-data identijenk echo "Jenkins Data Container"
Jenkins Data Container
我們使用 Jenkins 映像檔作為資料容器,以確保許可權設定正確。容器在 echo 命令完成後結束,但只要它不被刪除,就可以在 –volumes-from 引數中使用。
啟動 Jenkins 容器
現在我們準備啟動 Jenkins 容器:
$ docker run -d --name jenkins -p 8080:8080 \
--volumes-from jenkins-data \
-v /var/run/docker.sock:/var/run/docker.sock \
identijenk
75c4b300ade6a62394a328153b918c1dd58c5f6b9ac0288d46e02d5c593929dc
如果在瀏覽器中開啟 http://localhost:8080,應該能看到 Jenkins 正在初始化。
設定專案的 CI 環境
在為我們的 identidock 專案設定構建和測試之前,我們需要對專案本身進行一個小的修改。目前,專案的 docker-compose.yml 檔案初始化了一個開發版本的 identidock,但我們即將開發一些系統測試,這些測試需要在更接近生產的環境中執行。
為此,我們需要建立一個新檔案 jenkins.yml,用於在 Jenkins 中啟動 identidock 的生產版本:
identidock:
build: .
expose:
- "9090"
environment:
ENV: PROD
links:
- dnmonster
- redis
dnmonster:
image: amouat/dnmonster:1.0
redis:
image: redis:3.0
由於 Jenkins 位於兄弟容器中,我們不需要在宿主機上發布連線埠就能連線到它。我包含了 expose 命令主要是作為檔案;假設你沒有修改預設的網路設定,你仍然可以從 Jenkins 存取 identidock 容器。
這個檔案需要新增到 Jenkins 將檢索源程式碼的 identidock 儲存函式庫
設定 Jenkins 建置流程
現在我們已經準備好開始設定 Jenkins 建置。
Docker 自動化建置與測試:從指令碼到容器佈署
在現代軟體開發流程中,自動化測試與佈署已成為標準實踐。本文將探討如何透過 Docker 與 Jenkins 實作持續整合與佈署管道,特別著重於如何處理容器的建置、測試與標記等關鍵環節。
Docker Compose 建置系統
在整合環境中使用 Docker Compose 是管理多容器應用程式的有效方式。以下是一個典型的建置與測試流程:
# 建置系統
sudo docker-compose $COMPOSE_ARGS build --no-cache
sudo docker-compose $COMPOSE_ARGS up -d
# 執行單元測試
sudo docker-compose $COMPOSE_ARGS run --no-deps --rm -e ENV=UNIT identidock
ERR=$?
# 如果單元測試透過,執行系統測試
if [ $ERR -eq 0 ]; then
IP=$(sudo docker inspect -f {{.NetworkSettings.IPAddress}} jenkins_identidock_1)
CODE=$(curl -sL -w "%{http_code}" $IP:9090/monster/bla -o /dev/null) || true
if [ $CODE -ne 200 ]; then
echo "Site returned " $CODE
ERR=1
fi
fi
# 關閉系統
sudo docker-compose $COMPOSE_ARGS stop
sudo docker-compose $COMPOSE_ARGS rm --force -v
return $ERR
這個指令碼執行了幾個關鍵步驟:
- 使用
--no-cache
選項建置全新的容器映像 - 啟動服務
- 執行單元測試
- 如果單元測試透過,執行系統測試
- 最後無論測試結果如何,都會停止並移除容器
在這個例子中,使用 sudo
是因為 Jenkins 使用者不在 docker 群組中。系統測試使用 curl
呼叫服務並檢查是否回傳 HTTP 200 狀態碼,這是一種基本的「冒煙測試」,驗證服務能否正常運作。
自動觸發建置
手動觸發建置並非最佳實踐。更好的方法是設定自動觸發機制:
輪詢 SCM:在 Jenkins 中設定 “Poll SCM” 並輸入 “H/5 * * * *",這會讓 Jenkins 每五分鐘檢查一次儲存函式庫。
網路鉤子 (Webhooks):更有效的方案是設定 GitHub 或 BitBucket 的 Webhooks 在程式碼變更時通知 Jenkins,但這需要 Jenkins 伺服器能被公開網際網路存取。
映像標記與推播策略
在測試透過後,下一步是標記映像並推播到儲存函式庫便後續佈署使用。以下是一個經過增強的建置指令碼:
# 預設 compose 引數
COMPOSE_ARGS=" -f jenkins.yml -p jenkins "
# 確保舊容器已移除
sudo docker-compose $COMPOSE_ARGS stop
sudo docker-compose $COMPOSE_ARGS rm --force -v
# 建置系統
sudo docker-compose $COMPOSE_ARGS build --no-cache
sudo docker-compose $COMPOSE_ARGS up -d
# 執行單元測試
sudo docker-compose $COMPOSE_ARGS run --no-deps --rm -e ENV=UNIT identidock
ERR=$?
# 如果單元測試透過,執行系統測試
if [ $ERR -eq 0 ]; then
IP=$(sudo docker inspect -f {{.NetworkSettings.IPAddress}} jenkins_identidock_1)
CODE=$(curl -sL -w "%{http_code}" $IP:9090/monster/bla -o /dev/null) || true
if [ $CODE -eq 200 ]; then
echo "測試透過 - 標記中"
HASH=$(git rev-parse --short HEAD)
sudo docker tag -f jenkins_identidock amouat/identidock:$HASH
sudo docker tag -f jenkins_identidock amouat/identidock:newest
echo "推播中"
sudo docker login -e joe@bloggs.com -u jbloggs -p jbloggs123
sudo docker push amouat/identidock:$HASH
sudo docker push amouat/identidock:newest
else
echo "網站回傳 " $CODE
ERR=1
fi
fi
# 關閉系統
sudo docker-compose $COMPOSE_ARGS stop
sudo docker-compose $COMPOSE_ARGS rm --force -v
return $ERR
這個改進版指令碼增加了幾個重要功能:
- 取得 Git 提交的短雜湊值
- 使用 Git 雜湊和
newest
標記映像 - 登入 Docker 儲存函式庫. 推播標記後的映像
映像標記的最佳實踐
正確標記映像對維護容器管道的控制和追溯至關重要。玄貓在多年開發經驗中發現,每個映像都應該能夠追溯到確切的 Dockerfile 和建置環境。
在上面的例子中,我們使用兩個標記:
- Git 雜湊值:確保能夠追溯到具體的程式碼版本
- newest 標記:指向最新透過測試的建置
值得注意的是,這裡刻意避免使用 latest
標記,因為它容易造成混淆。在實際專案中,可能需要更複雜的標記策略,例如使用 git describe
命令生成根據標籤的更有意義的名稱。
查詢映像的所有標記
由於每個標記都是分別儲存的,要找出一個映像的所有標記,需要根據映像 ID 過濾完整的映像清單:
$ docker images --no-trunc | grep \
$(docker inspect -f {{.Id}} amouat/identidock:newest)
這會顯示映像 ID 對應的所有標記。但請記住,只能看到本地存在的標記。例如,如果我提取了 debian:latest
,不會同時得到 debian:7
標記,即使它們可能指向相同的映像。
處理映像擴散問題
在生產系統中,映像擴散是一個必須解決的問題:
- Jenkins 伺服器應定期清除舊映像
- 儲存函式庫映像數量也需要控制
解決方案包括:
- 移除超過特定日期的映像(必要時備份)
- 使用更先進的工具如 CoreOS Enterprise Registry 或 Docker Trusted Registry
使用 Docker 設定 Jenkins 從節點
隨著建置需求增長,可能需要更多資源來執行測試。Jenkins 使用「建置從節點」概念形成一個任務叢集。若想使用 Docker 動態設定這些從節點,可以使用 Jenkins 的 Docker 外掛。
Jenkins 備份策略
由於我們使用資料容器來儲存 Jenkins 資料,備份過程相對簡單:
$ docker run --volumes-from jenkins-data -v $(pwd):/backup \
debian tar -zcvf /backup/jenkins-data.tar.gz /var/jenkins_home
這會在當前目錄下產生 jenkins-data.tar.gz
檔案。執行備份前最好停止或暫停 Jenkins 容器。還原時可以執行:
$ docker run --name jenkins-data2 identijenk echo "New Jenkins Data Container"
$ docker run --volumes-from jenkins-data2 -v $(pwd):/backup \
debian tar -xzvf /backup/backup.tar
這種方法需要知道容器的掛載點,但可以透過檢查容器來自動化這個過程。
測試與實踐核心原則
在建立 CI/CD 管道時,有一個關鍵原則:確保測試的容器與生產環境中執行的容器完全相同。不應該在測試和生產環境分別從 Dockerfile 建置映像,而是要測試和執行完全相同的容器。這就是為什麼需要某種形式的映像儲存函式庫以在測試、預演和生產環境之間分享映像。
透過遵循這些最佳實踐,可以建立一個可靠、可追溯與自動化的容器佈署管道,大幅提升開發團隊的效率和軟體品質。
在實際實施這些技術時,需要根據專案規模和特性進行調整,但基本原則保持不變:自動化測試、標記映像以確保可追溯性,以及始終確保測試與佈署的是同一個容器。
容器的持續整合與測試
容器服務的數量與資源規劃
通常每個服務會設定一個容器,但若需要更多資源時,也可以為單一服務設定多個容器。現今有如docker-backup等工具可協助進行容器備份,未來的Docker版本應該會提供更完善的工作流程支援。
雲端CI解決方案
市場上有許多雲端CI服務,從提供雲端Jenkins管理的公司,到更專業的解決方案如Travis、Wercker、CircleCI和drone.io。目前大多數解決方案主要針對預定義語言堆積積疊的單元測試,而非測試容器系統。不過,這個領域正在發展中,玄貓預期不久後會看到專為測試Docker容器設計的服務出現。
微服務架構的測試策略
若你正在使用Docker,很可能也採用了微服務架構。測試微服務架構時,會有更多層級的測試需要考慮,你需要決定如何以及測試什麼。一個基本的測試框架可能包含:
單元測試
每個服務都應該有全面的單元測試應該只測試小型、隔離的功能片段。測試與其他服務的依賴關係時,可以使用測試替身(test doubles)。由於單元測試數量龐大,確保它們能盡可能快速執行非常重要,這樣能鼓勵頻繁測試並避免開發人員等待結果。單元測試應該佔系統中最大比例的測試。
元件測試
這類別測試可以針對單個服務的外部介面,也可以是對服務群組的子系統測試。在這兩種情況下,你可能會發現依賴其他服務,這時可能需要使用前面提到的測試替身。在測試時,透過服務API公開指標和日誌也很有用,但確保這些與功能性API分開(例如使用不同的URL字首)。
端對端測試
確保整個系統正常運作的測試。由於這些測試執行成本高昂(無論是資源還是時間),數量應該有限—你絕對不希望測試執行需要數小時,嚴重延誤佈署和修復(可考慮使用我們稍後描述的排程執行)。系統中某些部分可能無法測試或成本過高,仍需要替換為測試替身(在測試中發射核彈可能不是個好主意)。我們的identidock測試屬於端對端測試;測試從頭到尾執行完整系統,不使用測試替身。
此外,你可能還想考慮:
消費者契約測試
這些測試(也稱為消費者驅動契約)由服務的消費者編寫,主要定義預期的輸入和輸出資料。它們也可以涵蓋副作用(狀態變化)和效能預期。每個服務消費者都應該有單獨的契約。這類別測試的主要好處是,服務開發人員能知道何時可能破壞與消費者的相容性;如果契約測試失敗,他們知道需要修改服務,或與消費者開發人員合作更改契約。
整合測試
這些測試用於檢查每個元件之間的通訊管道是否正常運作。在微服務架構中,元件之間的連線和協調比單體架構多出一個數量級,因此這類別測試變得重要。不過,你可能會發現大多數通訊管道已經被元件和端對端測試覆寫。
排程執行
由於保持CI構建速度快是重要的,通常沒有足夠時間執行大量測試,例如測試異常設定或不同平台。這些測試可以安排在夜間執行,利用閒置資源。
許多這些測試可以根據它們發生在將映像新增到登入檔之前還是之後,分類別為登入檔前(pre-registry)和登入檔後(post-registry)測試。例如,單元測試是登入檔前測試:如果映像未透過單元測試,就不應該推播到登入檔。一些消費者契約測試和元件測試也是如此。相反,映像必須先推播到登入檔,才能進行端對端測試。如果登入檔後測試失敗,就需要考慮接下來怎麼做。雖然任何新映像都不應推播到生產環境(或者如果已經佈署則應回復),但故障實際上可能是由其他較舊的映像或新映像之間的互動引起的。這類別故障可能需要更深入的調查和思考才能正確處理。
生產環境中的測試
最後,你可能會考慮在生產環境中進行測試。別擔心,這並不像聽起來那麼瘋狂。特別是當面對大量使用者,與他們的環境和設定差異很大,難以測試時,這種方法非常有意義。
一種常見的方法被稱為藍/綠佈署。假設我們想要將現有的生產服務(稱為"藍色"版本)更新到新版本(稱為"綠色"版本)。與其直接用綠色版本替換藍色版本,我們可以在一段時間內同時執行它們。一旦綠色版本啟動並執行,我們翻轉開關開始將流量路由到它。然後監控系統是否有任何意外的行為變化,如錯誤率增加或延遲。如果我們對新版本不滿意,只需翻回開關,將藍色版本還原到生產環境。一旦確認一切正常運作,我們可以關閉藍色版本。
其他方法遵循類別似原則—舊版本和新版本應同時執行。在A/B測試或多變數測試中,兩個(或更多)版本的服務在測試期間同時執行,使用者隨機分配到不同版本。監控某些統計資料,並根據測試結束時的結果,保留其中一個版本。在漸進式佈署中,新版本服務最初只向一小部分使用者開放。如果這些使用者沒有發現問題,新版本將逐步向更多使用者開放。在影子佈署中,所有請求都會同時執行服務的兩個版本,但只使用舊的穩定版本的結果。透過比較舊版本和新版本的結果,可以確保新版本的行為與舊版本相同(或以預期與積極的方式不同)。影子佈署在測試沒有功能變化的新版本(如效能改進)時特別有用。
需要記住的關鍵概念是容器自然適合持續整合/交付工作流程。有幾點需要注意—主要是必須將相同的映像推播到整個管道中,而不是在不同階段重建—但你應該能夠將現有的CI工具適配到容器中而不會遇到太多問題,未來很可能會有更多專門針對這一領域的工具。
如果你正在採用大型微服務架構,值得花更多時間思考如何進行測試,並研究本章概述的一些技術。