🌟 前言
Docker 鏡像是 Docker 容器的基石,容器是鏡像的運作執行個體,有了鏡像才能啟動容器。
Docker 鏡像是一個隻讀的模闆,一個獨立的檔案系統,包括運作一個容器所需的資料,可以用來建立容器。
1. base鏡像
base(基礎) 鏡像是指完全從零開始建構的鏡像, 它不會依賴其他鏡像,甚至會成為被依賴的鏡像,其他鏡像以它為基礎進行擴充。
通常 base 鏡像都是 Linux 的系統鏡像, 如 Ubuntu、CentOS、Debian 等。
下面通過 Docker 拉取一個 base 鏡像并檢視, 這裡以 CentOS 為例, 示例代碼如下:
從以上示例中可以看出,一個 CentOS 鏡像大小隻有 202MB,但在安裝系統時,一個 CentOS 大概有幾 GB,這與作業系統有關。
先觀察 Linux 原本的作業系統結構,如圖所示👇
Kernel 是核心空間。bootfs 檔案系統在 Linux 啟動時加載。rootfs 是包含操作指令的檔案系統。
base 鏡像的建立過程中,Kernel、 bootfs 與 rootfs 都會加載,然後 bootfs 檔案系統 (包括 Kernel) 被解除安裝掉,鏡像隻保留 rootfs 檔案系統,供使用者進行操作。bootfs 與 Kernel 将與主控端共享。
另外,為了增加 Docker 的靈活性,base 鏡像提供的都是最小安裝的 Linux 系統。
Linux 系統不同的發行版之間最大的差別就是 rootfs 的不同,例如,Ubuntu 系統的應用程式管理器是 apt,而 CentOS 是 yum。
由此可見,隻要提供不同的 rootfs 檔案系統就可以同時支援多種作業系統,如圖所示👇
從上圖中可以看到,兩個不同的 Linux 發行版提供了各自的 rootfs 檔案系統,而它們共用的是底層主控端的 Kernel。
假設主控端的系統是 Ubuntu 16.04,Kernel 版本是 4.4.0,無論 base 鏡像原本的發行版 Kernel 版本如何,在這台主控端上都是 4.4.0。
下面通過示例來驗證,示例代碼如下:
從上述示例中可以看出,base 鏡像與主控端的 Kernel 版本都是 3.10。
base 鏡像的 Kernel 是與主控端共享的,其版本與主控端一緻,并且不能進行修改。
2. 鏡像的本質
Docker 鏡像是一個隻讀的檔案系統,由一層一層的檔案系統組成,每一層僅鏡像的本質包含前一層的差異部分,這種層級檔案系統被稱為 UnionFS。
大多數 Docker 鏡像都在 base 鏡像的基礎上進行建立,每進行一次新的建立就會在鏡像上建構一個新的 UnionFS。
檢視 ubuntu:15.04 鏡像的層級結構,示例代碼如下:
通常,對 Docker 的操作指令都是以 “docker” 開頭。pull 是下載下傳鏡像的指令,在英文中是 “拉” 的意思,是以下載下傳鏡像又叫作 拉取鏡像。
以上示例中,第 5 行到第 8 行是每一層 UnionFS 的 ID 号,第 9 行是整個鏡像的 ID 号,這個 ID 号可以用來操控鏡像。
然後,檢視鏡像,示例代碼如下:
在以上示例中,不僅可以看到先前下載下傳的 Ubuntu15.04 鏡像,還可以看到其他鏡像,說明
docker images
是檢視本地所有鏡像的指令。而檢視到的資訊中,除了鏡像名稱,還有版本号、鏡像 ID 号、建立時間以及鏡像大小。
接着,通過指令檢視鏡像的建構過程,示例代碼如下:
這裡使用 “history” 與鏡像 ID 号組合的指令檢視鏡像建構過程,所顯示的資訊包括鏡像 ID 号、建立時間、由什麼指令建立以及鏡像大小。
從以上示例中的資訊可以看出,ubuntu:15.04 鏡像由四個隻讀層 (Read Layer) 建構而成,每一層都是由一條指令構成的,最終得到 ID 号為 dlb55fd07600 的鏡像,但以使用者的視角隻能看到最上層。
當使用者将這個鏡像放在容器中運作時,四層之上會建立出一個可讀可寫層(Read-Write Layer),使用者對 Docker 的操作都通過可讀可寫層進行。如果使用者修改了一個已存在的檔案,那該檔案将會從可讀可寫層下的隻讀層複制到可讀可寫層,該檔案的隻讀版本仍然存在,隻是已經被可讀可寫層中該檔案的副本所隐藏。
可讀可寫層又叫作容器層,隻讀層又叫作鏡像層,容器層之下均為鏡像層,層級結構如圖所示👇
鏡像的這種分層機制最大的一個好處就是:共享資源。
例如,有很多個鏡像都基于一個基礎鏡像建構而來,那麼在本地的倉庫中就隻需要儲存一份基礎鏡像,所有需要此基礎鏡像的容器都可以共享它,而且鏡像的每一層都可以被共享,進而節省磁盤空間。
因為有了分層機制,本地儲存的基礎鏡像都是隻讀的檔案系統,不用擔心對容器的操作會對鏡像有什麼影響。
為了将零星的資料整合起來,人們提出了鏡像層 (Image Layer) 這個概念,如圖所示👇
下圖所示為一個鏡像層,我們能夠發現,一個層并不僅僅包含檔案系統的改變,它還能包含其他重要的資訊。
中繼資料 (Metadata) 就是關于這個層的額外資訊,包括 Docker 運作時的資訊與父鏡像層的資訊,并且隻讀層與可讀可寫層都包含中繼資料,如圖所示👇 除此之外,每一層還有一個指向父鏡像層的指針。如果沒有這個指針,說明它處于最底層,是一個基礎鏡像,如圖所示👇
3. 查找本地鏡像
Docker 本地鏡像通常是儲存在伺服器上的,下面驗證本地鏡像的儲存路徑,示例代碼如下: 從以上示例中可以看到,Docker 本地鏡像儲存路徑是
/var/Iib/Docker
。
在本地檢視鏡像時,通常使用
指令,示例代碼如下: 從以上示例中可以看到,結果顯示中有多項鏡像資訊,下面對資訊進行解釋。
docker images
🍇 REPOSITORY
鏡像倉庫,即一些關聯鏡像的集合。
例如,Ubuntu 的每個鏡像對應着不同的版本。與 Docker Registry 不同,鏡像倉庫提供 Docker 鏡像的存儲服務。
即 Docker Registry 中有很多鏡像倉庫,鏡像倉庫中有很多鏡像 (互相獨立)。
🍇 TAG
鏡像的标簽,常用來區分不同的版本,預設标簽為 latest。
🍇 IMAGE ID
鏡像的ID号,鏡像的唯一辨別,常用于操作鏡像 (預設值隻列出前 12 位)。
🍇 CREATED
鏡像建立的時間。
🍇 SIZE
鏡像的大小。
🍇 參數用法
在
docker images
指令後加上不同的參數就形成了不同的查詢方式,導緻不同的查詢結果。
下面介紹各參數的含義以及用法。
- -a
表示顯示所有本地鏡像,預設不顯示中間層鏡像,這是工作中經常使用到的參數,用來從本地鏡像中尋找符合生産條件的鏡像。
示例代碼如下:
- -q
表示隻顯示本地所有鏡像 ID 号。
示例代碼如下:
- -no-trunc
表示使用不截斷的模式顯示,并顯示完整的鏡像 ID 号。
示例代碼如下:
4. 建構鏡像
Docker 的官方鏡像庫 Docker Hub 釋出了成千上萬的公共鏡像供全球使用者使用。使用者可以直接拉取(下載下傳)所需要的鏡像,提高了工作效率。但是在很多工作環境中,一旦對鏡像有特殊需求,就需要我們手動去建構鏡像。
本文章将會介紹基于
指令與 Dockerfile 兩種方式來建構自己的 Docker 鏡像。
docker commit
🍑 使用 docker commit 指令建構鏡像
使用指令将容器的可讀可寫層轉換為一個隻讀層,這樣就把一個容器轉換成了一個不可變的鏡像,如圖所示👇
docker commit
下面我們給一個 Centos 的鏡像安裝一個 Vim 服務,設定開機啟動,并将其建構成一個新的鏡像,以免每次啟動容器都要再次安裝 Vim。
首先啟動一個 Centos 的容器,示例代碼如下:
從以上示例中可以看到,容器啟動之後,主機名發生了改變,說明使用者直接進入了容器,再進行操作就是對容器的操作。
然後,在容器中安裝 Vim,示例代碼如下:
安裝完成之後,退出容器,示例代碼如下:使用 exit 指令退出容器之後, 該容器将預設關閉。
下面使用
指令在 CentOS 鏡像的基礎上建立新的鏡像,示例代碼如下:
docker commit
在指令中需要用鏡像 ID 号來指定基礎鏡像,并不需要将 ID 号都輸入進去,隻要輸入幾個字元使 ID 号與其他鏡像不沖突即可。
此時可以看到剛剛建構的新鏡像,代碼如下:
從以上示例中可以看到, 新鏡像的大小是 326MB,而此前的 CentOS 鏡像隻有 202MB, 這是因為在安裝 Vim 時還安裝了許多依賴包。
然後, 檢視鏡像中是否已經自動安裝了 Vim, 示例代碼如下:
從以上示例中可以看到,新鏡像已經包含了 Vim。
這種建構新鏡像的方式在工作中并不常見,原因如下。
(1)效率低下,如果要給 Ubuntu 鏡像也添加一個Vim,需要将上述全部過程重複一遍。
(2)不透明,使用者使用時不知道鏡像是如何建構的,難以對鏡像做出正确的判斷。
🍑 使用 Dockerfile 建構鏡像
鏡像可以基于 Dockerfile 建構。Dockerfile 是一個描述檔案,包含若幹條指令,每條指令都會為基礎檔案系統建立新的層次結構,這正好彌補了
docker commit
建構鏡像效率低下的缺點。
Dockerfile 定義容器内部環境中發生的事情。網絡接口和磁盤驅動器等資源的通路在此環境内虛拟化,與系統的其餘部分隔離。
Dockerfile 主要使用
指令,根據 Dockerfile 檔案中的指令,執行若幹次
docker build
指令建構鏡像,每次執行
docker commit
指令時都會生成一個新的層,是以許多新的層會被建立,如圖所示👇
docker commit
🍑 Dockerfile常用指令
下面介紹 Dockerfile 中常用的指令,完整說明見官方文檔。
- FROM
指定源鏡像,必須是已經存在的鏡像,必須是Dockerfile中第一條非注釋的指令,因為其後的所有指令都使用該鏡像。
- MAINTAINER
指定作者資訊。
- RUN
在目前容器中運作指定的指令。
- EXPOSE
指定運作容器時要使用的端口。可以使用多個EXPOSE指令。
- CMD
指定容器啟動時運作的指令,Dockerfile 可以出現多個 CMD 指令,但隻有最後一個生效。CMD 可以被啟動容器時添加的指令覆寫。
- ENTRYPOINT
CMD 或容器啟動時添加的指令會被當做參數傳遞給 ENTRYPOINT。
- COPY
檔案或目錄複制到目前容器中。
- ADD
将檔案或者目錄複制到目前容器中,源檔案如果是歸檔(壓縮)檔案,則會被自動解壓到目标位置。
- VOLUME
為容器添加容器卷,可以存在于一個或多個目錄,用來提供共享存儲。該指令會在容器資料卷部分詳細介紹。
- WORKDIR
在容器内設定工作目錄。
- ENV
設定環境變量。
- USER
指定容器以什麼使用者身份運作,預設是 root。
🍑 運作一個Dockerfile
下面示範使用 Docker file 建立,示例代碼如下:
centos/vim
這裡在主控端的 root 目錄下建立了一個 Dockerfile 檔案。
接着,向 Docker file 檔案中添加内容, 示例代碼如下:
添加完成之後,儲存并退出。
有了 Dockerfile 檔案之後即可建立新的鏡像,示例代碼如下:
通過指令執行 Dockerfile 檔案,-t 用來指定新鏡像名為
docker build
,指令行末尾的
centos/vim-Dockerfile
表示 Dockerfile 檔案在目前目錄,Docker 預設從指定的目錄尋找 Dockerfile 檔案,也可以使用 -f 參數指定 Dockerfile 檔案的位置。 建構完成之後,檢視鏡像是否建構成功,示例代碼如下:
.
從以上示例中可以看到,新鏡像已經建構成功。
使用 Dockerfile 建構鏡像基本可以分為以下五步。
(1) 選擇一個基礎鏡像,運作一個臨時容器。
(2) 執行一條指令,對容器做修改。
(3) 執行類似
docker commit
的操作,生成一個新的鏡像。
(4) 删除臨時容器,再基于剛剛建構好的新鏡像運作一個臨時容器。
(5) 重複 (2) (3) (4) 步,直到執行完 Dockerfile 中的所有指令。
由 CentOS 基礎鏡像和
centos/vim-Dockerfile
構成,現在兩個鏡像都包含了 ID 号為 lel148e4cc2c 的隻讀層,如圖所示👇 以上結論可以使用
RUN yum -y install vim
指令驗證,
docker history
指令專門用來檢視鏡像的結構,示例代碼如下:
docker history
這裡可以看到 CentOS 鏡像中确實包含了 ID 号為 1e1148e4cc2c 的隻讀層。
接着再檢視新鏡像
的結構,示例代碼如下:
centos/vim-Dockerfile
從以上示例中可以看到,兩個鏡像都含有一個相同的隻讀層,并且這個隻讀層是共享的。
Docker 建構鏡像時有緩存機制,如果建構鏡像層時該鏡像層已經存在,就直接使用,無須重新建構。
下面為先前的 Dockerfile 檔案添加一點内容,安裝一個 ntp 服務,重新建構一個新的鏡像,示例代碼如下:
這裡多加了一條安裝 ntp 服務的指令。
添加完成後,開始建立鏡像,示例代碼如下:
在示例的第 6 行代碼中可以看到,Docker 沒有重新安裝 Vim,而是直接使用了先前安裝過的緩存。
Dockerfile 檔案是從上至下依次執行的,上層依賴于下層。無論什麼時候,隻要某一層發生變化,其上面所有層的緩存都會失效。
改變先前的 Dockerfile 檔案中兩條 RUN 指令的上下順序,觀察 Docker 還會不會使用緩存機制,示例代碼如下:
将 Dockerfile 中兩條 RUN 指令的順序互換之後,開始建立鏡像,示例代碼如下:由以上驗證可知,将兩條 RUN 指令交換順序導緻鏡像層次發生改變,Docker 會重建鏡像層。由此可見 Docker 的鏡像層級結構特性:隻有下面的層次内容、順序完全一緻才會使用緩存機制。
如果在建構鏡像時不想使用緩存,可以在
指令中添加
docker build
--no-cache
參數,否則預設使用緩存。
除了在使用 Dockerfile 建構鏡像時有緩存機制,在從倉庫拉取鏡像時也會有緩存機制,即已經拉取到本地的鏡像層可以被多個鏡像共同使用,可以說是一次拉取多次使用,前提是下層鏡像完全相同。
通常使用 Dockerfile 建構鏡像時,如果由于某些原因鏡像建構失敗,我們能夠得到前一個指令成功執行建構出的鏡像,繼而可以運作這個鏡像查找指令失敗的原因,這對調試 Dockerfile 有極大的幫助。
從 Docker Hub 拉取的 CentOS 鏡像是最小化的,其中沒有 vim 指令。下面測試錯誤建構 Docker 鏡像的結果,示例代碼如下:
将 Dockerfile 中任意一條 RUN 指令改為錯誤的,再開始建立鏡像,示例代碼如下: 在示例中,由于第三步報錯,鏡像沒有建立成功。但也生成了一個新鏡像,這個鏡像是第二步操作建構的,通常可以通過這個新鏡像排查錯誤,示例代碼如下: Docker 容器技術中,編寫 Dockerfile 檔案是非常重要的部分,下面總結編寫 Dockerfile 檔案的一些小技巧,相信可以幫助大家更好地使用 Docker 與 Dockerfile。
- (1)容器中隻運作單個應用。
從技術角度講,在一個容器中可以實作整個 LNMP (Linux+Nginx+MySQL+PHP) 架構。但這樣做有很大的弊端。首先,鏡像建構的時間會非常長,每次修改都要重新建構;
其次,鏡像檔案會非常大,大大降低容器的靈活性。
- (2)将多個 RUN 指令合并成一個。
衆所周知,Docker 鏡像是分層的,Dockerfile 中的每一條指令都會建立一個新的鏡像層,鏡像層是隻讀的。
Docker鏡像層類似于洋蔥,想要更改内層,需要将外層全部撕掉。
- (3)基礎鏡像的标簽盡量不要使用 latest。
當鏡像的标簽沒有指定時,預設使用 latest 标簽。
當鏡像更新時,latest 标簽會指向不同的鏡像,可能會對服務産生影響。
- (4)執行 RUN 指令後删除多餘檔案。
- (5)合理調整 COPY 與 RUN 的順序。
- (6)選擇合适的基礎鏡像。