Docker從入門到實踐(4-1)

使用 Docker 鏡像

在以前的介紹中,咱們知道鏡像是 Docker 的三大組件之一。php

Docker 運行容器前須要本地存在對應的鏡像,若是本地不存在該鏡像,Docker 會從鏡像倉庫下載該鏡像。html

本章將介紹更多關於鏡像的內容,包括:node

  • 從倉庫獲取鏡像;python

  • 管理本地主機上的鏡像;mysql

  • 介紹鏡像實現的基本原理。nginx

獲取鏡像

以前提到過,Docker Hub 上有大量的高質量的鏡像能夠用,這裏咱們就說一下怎麼獲取這些鏡像。git

從 Docker 鏡像倉庫獲取鏡像的命令是 docker pull。其命令格式爲:github

docker pull [選項] [Docker Registry 地址[:端口號]/]倉庫名[:標籤]

具體的選項能夠經過 docker pull --help 命令看到,這裏咱們說一下鏡像名稱的格式。golang

  • Docker 鏡像倉庫地址:地址的格式通常是 <域名/IP>[:端口號]。默認地址是 Docker Hub。
  • 倉庫名:如以前所說,這裏的倉庫名是兩段式名稱,即 <用戶名>/<軟件名>。對於 Docker Hub,若是不給出用戶名,則默認爲 library,也就是官方鏡像。

好比:web

$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:18.04 

上面的命令中沒有給出 Docker 鏡像倉庫地址,所以將會從 Docker Hub 獲取鏡像。而鏡像名稱是 ubuntu:18.04,所以將會獲取官方鏡像 library/ubuntu 倉庫中標籤爲 18.04 的鏡像。

從下載過程當中能夠看到咱們以前說起的分層存儲的概念,鏡像是由多層存儲所構成。下載也是一層層的去下載,並不是單一文件。下載過程當中給出了每一層的 ID 的前 12 位。而且下載結束後,給出該鏡像完整的 sha256 的摘要,以確保下載一致性。

在使用上面命令的時候,你可能會發現,你所看到的層 ID 以及 sha256 的摘要和這裏的不同。這是由於官方鏡像是一直在維護的,有任何新的 bug,或者版本更新,都會進行修復再以原來的標籤發佈,這樣能夠確保任何使用這個標籤的用戶能夠得到更安全、更穩定的鏡像。

若是從 Docker Hub 下載鏡像很是緩慢,能夠參照 鏡像加速器 一節配置加速器。

運行

有了鏡像後,咱們就可以以這個鏡像爲基礎啓動並運行一個容器。以上面的 ubuntu:18.04 爲例,若是咱們打算啓動裏面的 bash 而且進行交互式操做的話,能夠執行下面的命令。

$ docker run -it --rm \
    ubuntu:18.04 \
    bash

root@e7009c6ce357:/# cat /etc/os-release NAME="Ubuntu" VERSION="18.04.1 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.1 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic 

docker run 就是運行容器的命令,具體格式咱們會在 容器 一節進行詳細講解,咱們這裏簡要的說明一下上面用到的參數。

  • -it:這是兩個參數,一個是 -i:交互式操做,一個是 -t 終端。咱們這裏打算進入 bash 執行一些命令並查看返回結果,所以咱們須要交互式終端。
  • --rm:這個參數是說容器退出後隨之將其刪除。默認狀況下,爲了排障需求,退出的容器並不會當即刪除,除非手動 docker rm。咱們這裏只是隨便執行個命令,看看結果,不須要排障和保留結果,所以使用 --rm 能夠避免浪費空間。
  • ubuntu:18.04:這是指用 ubuntu:18.04 鏡像爲基礎來啓動容器。
  • bash:放在鏡像名後的是 命令,這裏咱們但願有個交互式 Shell,所以用的是 bash

進入容器後,咱們能夠在 Shell 下操做,執行任何所需的命令。這裏,咱們執行了 cat /etc/os-release,這是 Linux 經常使用的查看當前系統版本的命令,從返回的結果能夠看到容器內是 Ubuntu 18.04.1 LTS 系統。

最後咱們經過 exit 退出了這個容器。

列出鏡像

要想列出已經下載下來的鏡像,可使用 docker image ls 命令。

$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
redis                latest              5f515359c7f8        5 days ago          183 MB
nginx                latest              05a60462f8ba        5 days ago          181 MB
mongo                3.2                 fe9198c04d62        5 days ago          342 MB
<none>               <none>              00285df0df87        5 days ago          342 MB
ubuntu               18.04               f753707788c5        4 weeks ago         127 MB
ubuntu               latest              f753707788c5        4 weeks ago         127 MB

列表包含了 倉庫名標籤鏡像 ID建立時間 以及 所佔用的空間

其中倉庫名、標籤在以前的基礎概念章節已經介紹過了。鏡像 ID 則是鏡像的惟一標識,一個鏡像能夠對應多個 標籤。所以,在上面的例子中,咱們能夠看到 ubuntu:18.04 和 ubuntu:latest 擁有相同的 ID,由於它們對應的是同一個鏡像。

鏡像體積

若是仔細觀察,會注意到,這裏標識的所佔用空間和在 Docker Hub 上看到的鏡像大小不一樣。好比,ubuntu:18.04 鏡像大小,在這裏是 127 MB,可是在 Docker Hub 顯示的倒是 50 MB。這是由於 Docker Hub 中顯示的體積是壓縮後的體積。在鏡像下載和上傳過程當中鏡像是保持着壓縮狀態的,所以 Docker Hub 所顯示的大小是網絡傳輸中更關心的流量大小。而 docker image ls 顯示的是鏡像下載到本地後,展開的大小,準確說,是展開後的各層所佔空間的總和,由於鏡像到本地後,查看空間的時候,更關心的是本地磁盤空間佔用的大小。

另一個須要注意的問題是,docker image ls 列表中的鏡像體積總和並不是是全部鏡像實際硬盤消耗。因爲 Docker 鏡像是多層存儲結構,而且能夠繼承、複用,所以不一樣鏡像可能會由於使用相同的基礎鏡像,從而擁有共同的層。因爲 Docker 使用 Union FS,相同的層只須要保存一份便可,所以實際鏡像硬盤佔用空間極可能要比這個列表鏡像大小的總和要小的多。

你能夠經過如下命令來便捷的查看鏡像、容器、數據卷所佔用的空間。

$ docker system df

TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              24                  0                   1.992GB             1.992GB (100%)
Containers          1                   0                   62.82MB             62.82MB (100%)
Local Volumes       9                   0                   652.2MB             652.2MB (100%)
Build Cache                                                 0B                  0B

虛懸鏡像

上面的鏡像列表中,還能夠看到一個特殊的鏡像,這個鏡像既沒有倉庫名,也沒有標籤,均爲 <none>。:

<none>               <none>              00285df0df87        5 days ago          342 MB

這個鏡像本來是有鏡像名和標籤的,原來爲 mongo:3.2,隨着官方鏡像維護,發佈了新版本後,從新 docker pull mongo:3.2 時,mongo:3.2 這個鏡像名被轉移到了新下載的鏡像身上,而舊的鏡像上的這個名稱則被取消,從而成爲了 <none>。除了 docker pull 可能致使這種狀況,docker build 也一樣能夠致使這種現象。因爲新舊鏡像同名,舊鏡像名稱被取消,從而出現倉庫名、標籤均爲 <none> 的鏡像。這類無標籤鏡像也被稱爲 虛懸鏡像(dangling image) ,能夠用下面的命令專門顯示這類鏡像:

$ docker image ls -f dangling=true REPOSITORY TAG IMAGE ID CREATED SIZE <none> <none> 00285df0df87 5 days ago 342 MB 

通常來講,虛懸鏡像已經失去了存在的價值,是能夠隨意刪除的,能夠用下面的命令刪除。

$ docker image prune

中間層鏡像

爲了加速鏡像構建、重複利用資源,Docker 會利用 中間層鏡像。因此在使用一段時間後,可能會看到一些依賴的中間層鏡像。默認的 docker image ls 列表中只會顯示頂層鏡像,若是但願顯示包括中間層鏡像在內的全部鏡像的話,須要加 -a 參數。

$ docker image ls -a 

這樣會看到不少無標籤的鏡像,與以前的虛懸鏡像不一樣,這些無標籤的鏡像不少都是中間層鏡像,是其它鏡像所依賴的鏡像。這些無標籤鏡像不該該刪除,不然會致使上層鏡像由於依賴丟失而出錯。實際上,這些鏡像也不必刪除,由於以前說過,相同的層只會存一遍,而這些鏡像是別的鏡像的依賴,所以並不會由於它們被列出來而多存了一份,不管如何你也會須要它們。只要刪除那些依賴它們的鏡像後,這些依賴的中間層鏡像也會被連帶刪除。

列出部分鏡像

不加任何參數的狀況下,docker image ls 會列出全部頂層鏡像,可是有時候咱們只但願列出部分鏡像。docker image ls 有好幾個參數能夠幫助作到這個事情。

根據倉庫名列出鏡像

$ docker image ls ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               f753707788c5        4 weeks ago         127 MB
ubuntu              latest              f753707788c5        4 weeks ago         127 MB

列出特定的某個鏡像,也就是說指定倉庫名和標籤

$ docker image ls ubuntu:18.04
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              18.04               f753707788c5        4 weeks ago         127 MB

除此之外,docker image ls 還支持強大的過濾器參數 --filter,或者簡寫 -f。以前咱們已經看到了使用過濾器來列出虛懸鏡像的用法,它還有更多的用法。好比,咱們但願看到在 mongo:3.2 以後創建的鏡像,能夠用下面的命令:

$ docker image ls -f since=mongo:3.2 REPOSITORY TAG IMAGE ID CREATED SIZE redis latest 5f515359c7f8 5 days ago 183 MB nginx latest 05a60462f8ba 5 days ago 181 MB 

想查看某個位置以前的鏡像也能夠,只須要把 since 換成 before 便可。

此外,若是鏡像構建時,定義了 LABEL,還能夠經過 LABEL 來過濾。

$ docker image ls -f label=com.example.version=0.1 ... 

以特定格式顯示

默認狀況下,docker image ls 會輸出一個完整的表格,可是咱們並不是全部時候都會須要這些內容。好比,剛纔刪除虛懸鏡像的時候,咱們須要利用 docker image ls 把全部的虛懸鏡像的 ID 列出來,而後才能夠交給 docker image rm 命令做爲參數來刪除指定的這些鏡像,這個時候就用到了 -q 參數。

$ docker image ls -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd

--filter 配合 -q 產生出指定範圍的 ID 列表,而後送給另外一個 docker 命令做爲參數,從而針對這組實體成批的進行某種操做的作法在 Docker 命令行使用過程當中很是常見,不只僅是鏡像,未來咱們會在各個命令中看到這類搭配以完成很強大的功能。所以每次在文檔看到過濾器後,能夠多注意一下它們的用法。

另一些時候,咱們可能只是對錶格的結構不滿意,但願本身組織列;或者不但願有標題,這樣方便其它程序解析結果等,這就用到了 Go 的模板語法

好比,下面的命令會直接列出鏡像結果,而且只包含鏡像ID和倉庫名:

$ docker image ls --format "{{.ID}}: {{.Repository}}" 5f515359c7f8: redis 05a60462f8ba: nginx fe9198c04d62: mongo 00285df0df87: <none> f753707788c5: ubuntu f753707788c5: ubuntu 1e0c3dd64ccd: ubuntu 

或者打算以表格等距顯示,而且有標題行,和默認同樣,不過本身定義列:



$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" IMAGE ID REPOSITORY TAG 5f515359c7f8 redis latest 05a60462f8ba nginx latest fe9198c04d62 mongo 3.2 00285df0df87 <none> <none> f753707788c5 ubuntu 18.04 f753707788c5 ubuntu latest

刪除本地鏡像

若是要刪除本地的鏡像,可使用 docker image rm 命令,其格式爲:

$ docker image rm [選項] <鏡像1> [<鏡像2> ...]

用 ID、鏡像名、摘要刪除鏡像

其中,<鏡像> 能夠是 鏡像短 ID鏡像長 ID鏡像名 或者 鏡像摘要

好比咱們有這麼一些鏡像:

$ docker image ls
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB

咱們能夠用鏡像的完整 ID,也稱爲 長 ID,來刪除鏡像。使用腳本的時候可能會用長 ID,可是人工輸入就太累了,因此更多的時候是用 短 ID 來刪除鏡像。docker image ls 默認列出的就已是短 ID 了,通常取前3個字符以上,只要足夠區分於別的鏡像就能夠了。

好比這裏,若是咱們要刪除 redis:alpine 鏡像,能夠執行:

$ docker image rm 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7 

咱們也能夠用鏡像名,也就是 <倉庫名>:<標籤>,來刪除鏡像。

$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38 

固然,更精確的是使用 鏡像摘要 刪除鏡像。

$ docker image ls --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

Untagged 和 Deleted

若是觀察上面這幾個命令的運行輸出信息的話,你會注意到刪除行爲分爲兩類,一類是 Untagged,另外一類是 Deleted。咱們以前介紹過,鏡像的惟一標識是其 ID 和摘要,而一個鏡像能夠有多個標籤。

所以當咱們使用上面命令刪除鏡像的時候,其實是在要求刪除某個標籤的鏡像。因此首先須要作的是將知足咱們要求的全部鏡像標籤都取消,這就是咱們看到的 Untagged 的信息。由於一個鏡像能夠對應多個標籤,所以當咱們刪除了所指定的標籤後,可能還有別的標籤指向了這個鏡像,若是是這種狀況,那麼 Delete 行爲就不會發生。因此並不是全部的 docker image rm 都會產生刪除鏡像的行爲,有可能僅僅是取消了某個標籤而已。

當該鏡像全部的標籤都被取消了,該鏡像極可能會失去了存在的意義,所以會觸發刪除行爲。鏡像是多層存儲結構,所以在刪除的時候也是從上層向基礎層方向依次進行判斷刪除。鏡像的多層結構讓鏡像複用變得很是容易,所以頗有可能某個其它鏡像正依賴於當前鏡像的某一層。這種狀況,依舊不會觸發刪除該層的行爲。直到沒有任何層依賴當前層時,纔會真實的刪除當前層。這就是爲何,有時候會奇怪,爲何明明沒有別的標籤指向這個鏡像,可是它仍是存在的緣由,也是爲何有時候會發現所刪除的層數和本身 docker pull 看到的層數不同的緣由。

除了鏡像依賴之外,還須要注意的是容器對鏡像的依賴。若是有用這個鏡像啓動的容器存在(即便容器沒有運行),那麼一樣不能夠刪除這個鏡像。以前講過,容器是以鏡像爲基礎,再加一層容器存儲層,組成這樣的多層存儲結構去運行的。所以該鏡像若是被這個容器所依賴的,那麼刪除必然會致使故障。若是這些容器是不須要的,應該先將它們刪除,而後再來刪除鏡像。

用 docker image ls 命令來配合

像其它能夠承接多個實體的命令同樣,可使用 docker image ls -q 來配合使用 docker image rm,這樣能夠成批的刪除但願刪除的鏡像。咱們在「鏡像列表」章節介紹過不少過濾鏡像列表的方式均可以拿過來使用。

好比,咱們須要刪除全部倉庫名爲 redis 的鏡像:

$ docker image rm $(docker image ls -q redis)

或者刪除全部在 mongo:3.2 以前的鏡像:

$ docker image rm $(docker image ls -q -f before=mongo:3.2) 

充分利用你的想象力和 Linux 命令行的強大,你能夠完成不少很是讚的功能。

CentOS/RHEL 的用戶須要注意的事項

如下內容僅適用於 Docker CE 18.09 如下版本,在 Docker CE 18.09 版本中默認使用的是 overlay2驅動。

在 Ubuntu/Debian 上有 UnionFS 可使用,如 aufs 或者 overlay2,而 CentOS 和 RHEL 的內核中沒有相關驅動。所以對於這類系統,通常使用 devicemapper 驅動利用 LVM 的一些機制來模擬分層存儲。這樣的作法除了性能比較差外,穩定性通常也很差,並且配置相對複雜。Docker 安裝在 CentOS/RHEL 上後,會默認選擇 devicemapper,可是爲了簡化配置,其 devicemapper 是跑在一個稀疏文件模擬的塊設備上,也被稱爲 loop-lvm。這樣的選擇是由於不須要額外配置就能夠運行 Docker,這是自動配置惟一能作到的事情。可是 loop-lvm 的作法很是很差,其穩定性、性能更差,不管是日誌仍是 docker info 中都會看到警告信息。官方文檔有明確的文章講解了如何配置塊設備給 devicemapper 驅動作存儲層的作法,這類作法也被稱爲配置 direct-lvm

除了前面說到的問題外,devicemapper + loop-lvm 還有一個缺陷,由於它是稀疏文件,因此它會不斷增加。用戶在使用過程當中會注意到 /var/lib/docker/devicemapper/devicemapper/data 不斷增加,並且沒法控制。不少人會但願刪除鏡像或者能夠解決這個問題,結果發現效果並不明顯。緣由就是這個稀疏文件的空間釋放後基本不進行垃圾回收的問題。所以每每會出現即便刪除了文件內容,空間卻沒法回收,隨着使用這個稀疏文件一直在不斷增加。

因此對於 CentOS/RHEL 的用戶來講,在沒有辦法使用 UnionFS 的狀況下,必定要配置 direct-lvm 給 devicemapper,不管是爲了性能、穩定性仍是空間利用率。

或許有人注意到了 CentOS 7 中存在被 backports 回來的 overlay 驅動,不過 CentOS 裏的這個驅動達不到生產環境使用的穩定程度,因此不推薦使用。

利用 commit 理解鏡像構成

注意:若是您是初學者,您能夠暫時跳事後面的內容,直接學習 容器 一節。

注意: docker commit 命令除了學習以外,還有一些特殊的應用場合,好比被入侵後保存現場等。可是,不要使用 docker commit 定製鏡像,定製鏡像應該使用 Dockerfile 來完成。若是你想要定製鏡像請查看下一小節。

鏡像是容器的基礎,每次執行 docker run 的時候都會指定哪一個鏡像做爲容器運行的基礎。在以前的例子中,咱們所使用的都是來自於 Docker Hub 的鏡像。直接使用這些鏡像是能夠知足必定的需求,而當這些鏡像沒法直接知足需求時,咱們就須要定製這些鏡像。接下來的幾節就將講解如何定製鏡像。

回顧一下以前咱們學到的知識,鏡像是多層存儲,每一層是在前一層的基礎上進行的修改;而容器一樣也是多層存儲,是在以鏡像爲基礎層,在其基礎上加一層做爲容器運行時的存儲層。

如今讓咱們以定製一個 Web 服務器爲例子,來說解鏡像是如何構建的。

$ docker run --name webserver -d -p 80:80 nginx 

這條命令會用 nginx 鏡像啓動一個容器,命名爲 webserver,而且映射了 80 端口,這樣咱們能夠用瀏覽器去訪問這個 nginx 服務器。

若是是在 Linux 本機運行的 Docker,或者若是使用的是 Docker Desktop for Mac/Windows,那麼能夠直接訪問:http://localhost;若是使用的是 Docker Toolbox,或者是在虛擬機、雲服務器上安裝的 Docker,則須要將 localhost 換爲虛擬機地址或者實際雲服務器地址。

直接用瀏覽器訪問的話,咱們會看到默認的 Nginx 歡迎頁面。

如今,假設咱們很是不喜歡這個歡迎頁面,咱們但願改爲歡迎 Docker 的文字,咱們可使用 docker exec命令進入容器,修改其內容。

$ docker exec -it webserver bash root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html root@3729b97e8226:/# exit exit 

咱們以交互式終端方式進入 webserver 容器,並執行了 bash 命令,也就是得到一個可操做的 Shell。

而後,咱們用 <h1>Hello, Docker!</h1> 覆蓋了 /usr/share/nginx/html/index.html 的內容。

如今咱們再刷新瀏覽器的話,會發現內容被改變了。

咱們修改了容器的文件,也就是改動了容器的存儲層。咱們能夠經過 docker diff 命令看到具體的改動。

$ docker diff webserver
C /root
A /root/.bash_history C /run C /usr C /usr/share C /usr/share/nginx C /usr/share/nginx/html C /usr/share/nginx/html/index.html C /var C /var/cache C /var/cache/nginx A /var/cache/nginx/client_temp A /var/cache/nginx/fastcgi_temp A /var/cache/nginx/proxy_temp A /var/cache/nginx/scgi_temp A /var/cache/nginx/uwsgi_temp 

如今咱們定製好了變化,咱們但願能將其保存下來造成鏡像。

要知道,當咱們運行一個容器的時候(若是不使用卷的話),咱們作的任何文件修改都會被記錄於容器存儲層裏。而 Docker 提供了一個 docker commit 命令,能夠將容器的存儲層保存下來成爲鏡像。換句話說,就是在原有鏡像的基礎上,再疊加上容器的存儲層,並構成新的鏡像。之後咱們運行這個新鏡像的時候,就會擁有原有容器最後的文件變化。

docker commit 的語法格式爲:

docker commit [選項] <容器ID或容器名> [<倉庫名>[:<標籤>]]

咱們能夠用下面的命令將容器保存爲鏡像:

$ docker commit \
    --author "Tao Wang <twang2218@gmail.com>" \ --message "修改了默認網頁" \ webserver \ nginx:v2 sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214 

其中 --author 是指定修改的做者,而 --message 則是記錄本次修改的內容。這點和 git 版本控制類似,不過這裏這些信息能夠省略留空。

咱們能夠在 docker image ls 中看到這個新定製的鏡像:

$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               v2                  07e334659748        9 seconds ago       181.5 MB
nginx               1.11                05a60462f8ba        12 days ago         181.5 MB
nginx               latest              e43d811ce2f4        4 weeks ago         181.5 MB

咱們還能夠用 docker history 具體查看鏡像內的歷史記錄,若是比較 nginx:latest 的歷史記錄,咱們會發現新增了咱們剛剛提交的這一層。

$ docker history nginx:v2 IMAGE CREATED CREATED BY SIZE COMMENT 07e334659748 54 seconds ago nginx -g daemon off; 95 B 修改了默認網頁 e43d811ce2f4 4 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon 0 B <missing> 4 weeks ago /bin/sh -c #(nop) EXPOSE 443/tcp 80/tcp 0 B <missing> 4 weeks ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx/ 22 B <missing> 4 weeks ago /bin/sh -c apt-key adv --keyserver hkp://pgp. 58.46 MB <missing> 4 weeks ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.11.5-1 0 B <missing> 4 weeks ago /bin/sh -c #(nop) MAINTAINER NGINX Docker Ma 0 B <missing> 4 weeks ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:23aa4f893e3288698c 123 MB 

新的鏡像定製好後,咱們能夠來運行這個鏡像。

docker run --name web2 -d -p 81:80 nginx:v2 

這裏咱們命名爲新的服務爲 web2,而且映射到 81 端口。若是是 Docker Desktop for Mac/Windows 或 Linux 桌面的話,咱們就能夠直接訪問 http://localhost:81 看到結果,其內容應該和以前修改後的 webserver 同樣。

至此,咱們第一次完成了定製鏡像,使用的是 docker commit 命令,手動操做給舊的鏡像添加了新的一層,造成新的鏡像,對鏡像多層存儲應該有了更直觀的感受。

慎用 docker commit

使用 docker commit 命令雖然能夠比較直觀的幫助理解鏡像分層存儲的概念,可是實際環境中並不會這樣使用。

首先,若是仔細觀察以前的 docker diff webserver 的結果,你會發現除了真正想要修改的 /usr/share/nginx/html/index.html 文件外,因爲命令的執行,還有不少文件被改動或添加了。這還僅僅是最簡單的操做,若是是安裝軟件包、編譯構建,那會有大量的無關內容被添加進來,若是不當心清理,將會致使鏡像極爲臃腫。

此外,使用 docker commit 意味着全部對鏡像的操做都是黑箱操做,生成的鏡像也被稱爲 黑箱鏡像,換句話說,就是除了製做鏡像的人知道執行過什麼命令、怎麼生成的鏡像,別人根本無從得知。並且,即便是這個製做鏡像的人,過一段時間後也沒法記清具體在操做的。雖然 docker diff 或許能夠告訴獲得一些線索,可是遠遠不到能夠確保生成一致鏡像的地步。這種黑箱鏡像的維護工做是很是痛苦的。

並且,回顧以前說起的鏡像所使用的分層存儲的概念,除當前層外,以前的每一層都是不會發生改變的,換句話說,任何修改的結果僅僅是在當前層進行標記、添加、修改,而不會改動上一層。若是使用 docker commit 製做鏡像,以及後期修改的話,每一次修改都會讓鏡像更加臃腫一次,所刪除的上一層的東西並不會丟失,會一直如影隨形的跟着這個鏡像,即便根本沒法訪問到。這會讓鏡像更加臃腫。

使用 Dockerfile 定製鏡像

從剛纔的 docker commit 的學習中,咱們能夠了解到,鏡像的定製實際上就是定製每一層所添加的配置、文件。若是咱們能夠把每一層修改、安裝、構建、操做的命令都寫入一個腳本,用這個腳原本構建、定製鏡像,那麼以前說起的沒法重複的問題、鏡像構建透明性的問題、體積的問題就都會解決。這個腳本就是 Dockerfile。

Dockerfile 是一個文本文件,其內包含了一條條的 指令(Instruction),每一條指令構建一層,所以每一條指令的內容,就是描述該層應當如何構建。

還以以前定製 nginx 鏡像爲例,此次咱們使用 Dockerfile 來定製。

在一個空白目錄中,創建一個文本文件,並命名爲 Dockerfile

$ mkdir mynginx
$ cd mynginx $ touch Dockerfile 

其內容爲:

FROM nginx RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html 

這個 Dockerfile 很簡單,一共就兩行。涉及到了兩條指令,FROM 和 RUN

FROM 指定基礎鏡像

所謂定製鏡像,那必定是以一個鏡像爲基礎,在其上進行定製。就像咱們以前運行了一個 nginx 鏡像的容器,再進行修改同樣,基礎鏡像是必須指定的。而 FROM 就是指定 基礎鏡像,所以一個 Dockerfile 中 FROM 是必備的指令,而且必須是第一條指令。

在 Docker Hub 上有很是多的高質量的官方鏡像,有能夠直接拿來使用的服務類的鏡像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便開發、構建、運行各類語言應用的鏡像,如 nodeopenjdkpythonrubygolang 等。能夠在其中尋找一個最符合咱們最終目標的鏡像爲基礎鏡像進行定製。

若是沒有找到對應服務的鏡像,官方鏡像中還提供了一些更爲基礎的操做系統鏡像,如 ubuntudebiancentosfedoraalpine 等,這些操做系統的軟件庫爲咱們提供了更廣闊的擴展空間。

除了選擇現有鏡像爲基礎鏡像外,Docker 還存在一個特殊的鏡像,名爲 scratch。這個鏡像是虛擬的概念,並不實際存在,它表示一個空白的鏡像。

FROM scratch ... 

若是你以 scratch 爲基礎鏡像的話,意味着你不以任何鏡像爲基礎,接下來所寫的指令將做爲鏡像第一層開始存在。

不以任何系統爲基礎,直接將可執行文件複製進鏡像的作法並不罕見,好比 swarmetcd。對於 Linux 下靜態編譯的程序來講,並不須要有操做系統提供運行時支持,所需的一切庫都已經在可執行文件裏了,所以直接 FROM scratch 會讓鏡像體積更加小巧。使用 Go 語言 開發的應用不少會使用這種方式來製做鏡像,這也是爲何有人認爲 Go 是特別適合容器微服務架構的語言的緣由之一。

RUN 執行命令

RUN 指令是用來執行命令行命令的。因爲命令行的強大能力,RUN 指令在定製鏡像時是最經常使用的指令之一。其格式有兩種:

  • shell 格式:RUN <命令>,就像直接在命令行中輸入的命令同樣。剛纔寫的 Dockerfile 中的 RUN 指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html 
  • exec 格式:RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。

既然 RUN 就像 Shell 腳本同樣能夠執行命令,那麼咱們是否就能夠像 Shell 腳本同樣把每一個命令對應一個 RUN 呢?好比這樣:

FROM debian:stretch RUN apt-get update RUN apt-get install -y gcc libc6-dev make wget RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install 

以前說過,Dockerfile 中每個指令都會創建一層,RUN 也不例外。每個 RUN 的行爲,就和剛纔咱們手工創建鏡像的過程同樣:新創建一層,在其上執行這些命令,執行結束後,commit 這一層的修改,構成新的鏡像。

而上面的這種寫法,建立了 7 層鏡像。這是徹底沒有意義的,並且不少運行時不須要的東西,都被裝進了鏡像裏,好比編譯環境、更新的軟件包等等。結果就是產生很是臃腫、很是多層的鏡像,不只僅增長了構建部署的時間,也很容易出錯。 這是不少初學 Docker 的人常犯的一個錯誤。

Union FS 是有最大層數限制的,好比 AUFS,曾經是最大不得超過 42 層,如今是不得超過 127 層。

上面的 Dockerfile 正確的寫法應該是這樣:

FROM debian:stretch RUN buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps 

首先,以前全部的命令只有一個目的,就是編譯、安裝 redis 可執行文件。所以沒有必要創建不少層,這只是一層的事情。所以,這裏沒有使用不少個 RUN 對一一對應不一樣的命令,而是僅僅使用一個 RUN 指令,並使用 && 將各個所需命令串聯起來。將以前的 7 層,簡化爲了 1 層。在撰寫 Dockerfile 的時候,要常常提醒本身,這並非在寫 Shell 腳本,而是在定義每一層該如何構建。

而且,這裏爲了格式化還進行了換行。Dockerfile 支持 Shell 類的行尾添加 \ 的命令換行方式,以及行首 # 進行註釋的格式。良好的格式,好比換行、縮進、註釋等,會讓維護、排障更爲容易,這是一個比較好的習慣。

此外,還能夠看到這一組命令的最後添加了清理工做的命令,刪除了爲了編譯構建所須要的軟件,清理了全部下載、展開的文件,而且還清理了 apt 緩存文件。這是很重要的一步,咱們以前說過,鏡像是多層存儲,每一層的東西並不會在下一層被刪除,會一直跟隨着鏡像。所以鏡像構建時,必定要確保每一層只添加真正須要添加的東西,任何無關的東西都應該清理掉。

不少人初學 Docker 製做出了很臃腫的鏡像的緣由之一,就是忘記了每一層構建的最後必定要清理掉無關文件。

構建鏡像

好了,讓咱們再回到以前定製的 nginx 鏡像的 Dockerfile 來。如今咱們明白了這個 Dockerfile 的內容,那麼讓咱們來構建這個鏡像吧。

在 Dockerfile 文件所在目錄執行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html ---> Running in 9cdc27646c7b ---> 44aa4490ce2c Removing intermediate container 9cdc27646c7b Successfully built 44aa4490ce2c 

從命令的輸出結果中,咱們能夠清晰的看到鏡像的構建過程。在 Step 2 中,如同咱們以前所說的那樣,RUN 指令啓動了一個容器 9cdc27646c7b,執行了所要求的命令,並最後提交了這一層 44aa4490ce2c,隨後刪除了所用到的這個容器 9cdc27646c7b

這裏咱們使用了 docker build 命令進行鏡像構建。其格式爲:

docker build [選項] <上下文路徑/URL/->

在這裏咱們指定了最終鏡像的名稱 -t nginx:v3,構建成功後,咱們能夠像以前運行 nginx:v2 那樣來運行這個鏡像,其結果會和 nginx:v2 同樣。

鏡像構建上下文(Context)

若是注意,會看到 docker build 命令最後有一個 .. 表示當前目錄,而 Dockerfile 就在當前目錄,所以很多初學者覺得這個路徑是在指定 Dockerfile 所在路徑,這麼理解實際上是不許確的。若是對應上面的命令格式,你可能會發現,這是在指定 上下文路徑。那麼什麼是上下文呢?

首先咱們要理解 docker build 的工做原理。Docker 在運行時分爲 Docker 引擎(也就是服務端守護進程)和客戶端工具。Docker 的引擎提供了一組 REST API,被稱爲 Docker Remote API,而如 docker 命令這樣的客戶端工具,則是經過這組 API 與 Docker 引擎交互,從而完成各類功能。所以,雖然表面上咱們好像是在本機執行各類 docker 功能,但實際上,一切都是使用的遠程調用形式在服務端(Docker 引擎)完成。也由於這種 C/S 設計,讓咱們操做遠程服務器的 Docker 引擎變得垂手可得。

當咱們進行鏡像構建的時候,並不是全部定製都會經過 RUN 指令完成,常常會須要將一些本地文件複製進鏡像,好比經過 COPY 指令、ADD 指令等。而 docker build 命令構建鏡像,其實並不是在本地構建,而是在服務端,也就是 Docker 引擎中構建的。那麼在這種客戶端/服務端的架構中,如何才能讓服務端得到本地文件呢?

這就引入了上下文的概念。當構建的時候,用戶會指定構建鏡像上下文的路徑,docker build 命令得知這個路徑後,會將路徑下的全部內容打包,而後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會得到構建鏡像所需的一切文件。

若是在 Dockerfile 中這麼寫:

COPY ./package.json /app/ 

這並非要複製執行 docker build 命令所在的目錄下的 package.json,也不是複製 Dockerfile 所在目錄下的 package.json,而是複製 上下文(context) 目錄下的 package.json

所以,COPY 這類指令中的源文件的路徑都是相對路徑。這也是初學者常常會問的爲何 COPY ../package.json /app 或者 COPY /opt/xxxx /app 沒法工做的緣由,由於這些路徑已經超出了上下文的範圍,Docker 引擎沒法得到這些位置的文件。若是真的須要那些文件,應該將它們複製到上下文目錄中去。

如今就能夠理解剛纔的命令 docker build -t nginx:v3 . 中的這個 .,其實是在指定上下文的目錄,docker build 命令會將該目錄下的內容打包交給 Docker 引擎以幫助構建鏡像。

若是觀察 docker build 輸出,咱們其實已經看到了這個發送上下文的過程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解構建上下文對於鏡像構建是很重要的,避免犯一些不該該的錯誤。好比有些初學者在發現 COPY /opt/xxxx /app 不工做後,因而乾脆將 Dockerfile 放到了硬盤根目錄去構建,結果發現 docker build執行後,在發送一個幾十 GB 的東西,極爲緩慢並且很容易構建失敗。那是由於這種作法是在讓 docker build 打包整個硬盤,這顯然是使用錯誤。

通常來講,應該會將 Dockerfile 置於一個空目錄下,或者項目根目錄下。若是該目錄下沒有所需文件,那麼應該把所需文件複製一份過來。若是目錄下有些東西確實不但願構建時傳給 Docker 引擎,那麼能夠用 .gitignore 同樣的語法寫一個 .dockerignore,該文件是用於剔除不須要做爲上下文傳遞給 Docker 引擎的。

那麼爲何會有人誤覺得 . 是指定 Dockerfile 所在目錄呢?這是由於在默認狀況下,若是不額外指定 Dockerfile 的話,會將上下文目錄下的名爲 Dockerfile 的文件做爲 Dockerfile。

這只是默認行爲,實際上 Dockerfile 的文件名並不要求必須爲 Dockerfile,並且並不要求必須位於上下文目錄中,好比能夠用 -f ../Dockerfile.php 參數指定某個文件做爲 Dockerfile

固然,通常你們習慣性的會使用默認的文件名 Dockerfile,以及會將其置於鏡像構建上下文目錄中。

其它 docker build 的用法

直接用 Git repo 進行構建

或許你已經注意到了,docker build 還支持從 URL 構建,好比能夠直接從 Git repo 中構建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1 Sending build context to Docker daemon 2.048 kB Step 1 : FROM gitlab/gitlab-ce:11.1.0-ce.0 11.1.0-ce.0: Pulling from gitlab/gitlab-ce aed15891ba52: Already exists 773ae8583d14: Already exists ... 

這行命令指定了構建所需的 Git repo,而且指定默認的 master 分支,構建目錄爲 /11.1/,而後 Docker 就會本身去 git clone 這個項目、切換到指定分支、並進入到指定目錄後開始構建。

用給定的 tar 壓縮包構建

$ docker build http://server/context.tar.gz

若是所給出的 URL 不是個 Git repo,而是個 tar 壓縮包,那麼 Docker 引擎會下載這個包,並自動解壓縮,以其做爲上下文,開始構建。

從標準輸入中讀取 Dockerfile 進行構建

docker build - < Dockerfile

cat Dockerfile | docker build -

若是標準輸入傳入的是文本文件,則將其視爲 Dockerfile,並開始構建。這種形式因爲直接從標準輸入中讀取 Dockerfile 的內容,它沒有上下文,所以不能夠像其餘方法那樣能夠將本地文件 COPY 進鏡像之類的事情。

從標準輸入中讀取上下文壓縮包進行構建

$ docker build - < context.tar.gz

若是發現標準輸入的文件格式是 gzipbzip2 以及 xz 的話,將會使其爲上下文壓縮包,直接將其展開,將裏面視爲上下文,並開始構建。

相關文章
相關標籤/搜索