原文在此
獲取鏡像
之前提到過,Docker Hub 上有大量的高質量的鏡像可以用,這里我們就說一下怎么獲取這些鏡像并運行。
從 Docker Registry 獲取鏡像的命令是 docker pull
。其命令格式為:
docker pull [選項] [Docker Registry地址]<倉庫名>:<標簽>
具體的選項可以通過 docker pull --help
命令看到,這里我們說一下鏡像名稱的格式。
- Docker Registry地址:地址的格式一般是
<域名/IP>[:端口號]
。默認地址是 Docker Hub。 - 倉庫名:如之前所說,這里的倉庫名是兩段式名稱,既
<用戶名>/<軟件名>
。對于 Docker Hub,如果不給出用戶名,則默認為library
,也就是官方鏡像。
比如:
$ docker pull ubuntu:14.04
14.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:14.04
上面的命令中沒有給出 Docker Registry 地址,因此將會從 Docker Hub 獲取鏡像。而鏡像名稱是 ubuntu:14.04
,因此將會獲取官方鏡像 library/ubuntu
倉庫中標簽為 14.04
的鏡像。
從下載過程中可以看到我們之前提及的分層存儲的概念,鏡像是由多層存儲所構成。下載也是一層層的去下載,并非單一文件。下載過程中給出了每一層的 ID 的前 12 位。并且下載結束后,給出該鏡像完整的 sha256
的摘要,以確保下載一致性。
在實驗上面命令的時候,你可能會發現,你所看到的層 ID 以及 sha256
的摘要和這里的不一樣。這是因為官方鏡像是一直在維護的,有任何新的 bug,或者版本更新,都會進行修復再以原來的標簽發布,這樣可以確保任何使用這個標簽的用戶可以獲得更安全、更穩定的鏡像。
如果從 Docker Hub 下載鏡像非常緩慢,可以參照 鏡像加速器 一節配置加速器。
運行
有了鏡像后,我們就可以以這個鏡像為基礎啟動一個容器來運行。以上面的 ubuntu:14.04
為例,如果我們打算啟動里面的 bash
并且進行交互式操作的話,可以執行下面的命令。
$ docker run -it --rm ubuntu:14.04 bash
root@e7009c6ce357:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
root@e7009c6ce357:/# exit
exit
$
docker run
就是運行容器的命令,具體格式我們會在后面的章節講解,我們這里簡要的說明一下上面用到的參數。
-
-it
:這是兩個參數,一個是-i
:交互式操作,一個是-t
終端。我們這里打算進入bash
執行一些命令并查看返回結果,因此我們需要交互式終端。 -
--rm
:這個參數是說容器退出后隨之將其刪除。默認情況下,為了排障需求,退出的容器并不會立即刪除,除非手動docker rm
。我們這里只是隨便執行個命令,看看結果,不需要排障和保留結果,因此使用--rm
可以避免浪費空間。 -
ubuntu:14.04
:這是指用ubuntu:14.04
鏡像為基礎來啟動容器。 -
bash
:放在鏡像名后的是命令,這里我們希望有個交互式 Shell,因此用的是bash
。
進入容器后,我們可以在 Shell 下操作,執行任何所需的命令。這里,我們執行了 cat /etc/os-release
,這是 Linux 常用的查看當前系統版本的命令,從返回的結果可以看到容器內是 Ubuntu 14.04.5 LTS
系統。
最后我們通過 exit
退出了這個容器。
列出鏡像
要想列出已經下載下來的鏡像,可以使用 docker images
命令。
$ docker images
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 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB
列表包含了倉庫名、標簽、鏡像 ID、創建時間以及所占用的空間。
其中倉庫名、標簽在之前的基礎概念章節已經介紹過了。鏡像 ID 則是鏡像的唯一標識,一個鏡像可以對應多個標簽。因此,在上面的例子中,我們可以看到 ubuntu:16.04
和 ubuntu:latest
擁有相同的 ID,因為它們對應的是同一個鏡像。
鏡像體積
如果仔細觀察,會注意到,這里標識的所占用空間和在 Docker Hub 上看到的鏡像大小不同。比如,ubuntu:16.04
鏡像大小,在這里是 127 MB
,但是在 Docker Hub 顯示的卻是 50 MB
。這是因為 Docker Hub 中顯示的體積是壓縮后的體積。在鏡像下載和上傳過程中鏡像是保持著壓縮狀態的,因此 Docker Hub 所顯示的大小是網絡傳輸中更關心的流量大小。而 docker images
顯示的是鏡像下載到本地后,展開的大小,準確說,是展開后的各層所占空間的總和,因為鏡像到本地后,查看空間的時候,更關心的是本地磁盤空間占用的大小。
另外一個需要注意的問題是,docker images
列表中的鏡像體積總和并非是所有鏡像實際硬盤消耗。由于 Docker 鏡像是多層存儲結構,并且可以繼承、復用,因此不同鏡像可能會因為使用相同的基礎鏡像,從而擁有共同的層。由于 Docker 使用 Union FS,相同的層只需要保存一份即可,因此實際鏡像硬盤占用空間很可能要比這個列表鏡像大小的總和要小的多。
虛懸鏡像
上面的鏡像列表中,還可以看到一個特殊的鏡像,這個鏡像既沒有倉庫名,也沒有標簽,均為 <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 images -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
一般來說,虛懸鏡像已經失去了存在的價值,是可以隨意刪除的,可以用下面的命令刪除。
$ docker rmi $(docker images -q -f dangling=true)
中間層鏡像
為了加速鏡像構建、重復利用資源,Docker 會利用 中間層鏡像。所以在使用一段時間后,可能會看到一些依賴的中間層鏡像。默認的 docker images
列表中只會顯示頂層鏡像,如果希望顯示包括中間層鏡像在內的所有鏡像的話,需要加 -a
參數。
$ docker images -a
這樣會看到很多無標簽的鏡像,與之前的虛懸鏡像不同,這些無標簽的鏡像很多都是中間層鏡像,是其它鏡像所依賴的鏡像。這些無標簽鏡像不應該刪除,否則會導致上層鏡像因為依賴丟失而出錯。實際上,這些鏡像也沒必要刪除,因為之前說過,相同的層只會存一遍,而這些鏡像是別的鏡像的依賴,因此并不會因為它們被列出來而多存了一份,無論如何你也會需要它們。只要刪除那些依賴它們的鏡像后,這些依賴的中間層鏡像也會被連帶刪除。
列出部分鏡像
不加任何參數的情況下,docker images
會列出所有頂級鏡像,但是有時候我們只希望列出部分鏡像。docker images
有好幾個參數可以幫助做到這個事情。
根據倉庫名列出鏡像
$ docker images ubuntu
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
ubuntu latest f753707788c5 4 weeks ago 127 MB
ubuntu 14.04 1e0c3dd64ccd 4 weeks ago 188 MB
列出特定的某個鏡像,也就是說指定倉庫名和標簽
$ docker images ubuntu:16.04
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 f753707788c5 4 weeks ago 127 MB
除此以外,docker images
還支持強大的過濾器參數 --filter
,或者簡寫 -f
。之前我們已經看到了使用過濾器來列出虛懸鏡像的用法,它還有更多的用法。比如,我們希望看到在 mongo:3.2
之后建立的鏡像,可以用下面的命令:
$ docker images -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 images -f label=com.example.version=0.1
...
以特定格式顯示
默認情況下,docker images
會輸出一個完整的表格,但是我們并非所有時候都會需要這些內容。比如,剛才刪除虛懸鏡像的時候,我們需要利用 docker images
把所有的虛懸鏡像的 ID 列出來,然后才可以交給 docker rmi
命令作為參數來刪除指定的這些鏡像,這個時候就用到了 -q
參數。
$ docker images -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd
--filter
配合 -q
產生出指定范圍的 ID 列表,然后送給另一個 docker
命令作為參數,從而針對這組實體成批的進行某種操作的做法在 Docker 命令行使用過程中非常常見,不僅僅是鏡像,將來我們會在各個命令中看到這類搭配以完成很強大的功能。因此每次在文檔看到過濾器后,可以多注意一下它們的用法。
另外一些時候,我們可能只是對表格的結構不滿意,希望自己組織列;或者不希望有標題,這樣方便其它程序解析結果等,這就用到了 Go 的模板語法。
比如,下面的命令會直接列出鏡像結果,并且只包含鏡像ID和倉庫名:
$ docker images --format "{{.ID}}: {{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu
或者打算以表格等距顯示,并且有標題行,和默認一樣,不過自己定義列:
$ docker images --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 16.04
f753707788c5 ubuntu latest
1e0c3dd64ccd ubuntu 14.04
利用 commit 理解鏡像構成
鏡像是容器的基礎,每次執行 docker run
的時候都會指定哪個鏡像作為容器運行的基礎。在之前的例子中,我們所使用的都是來自于 Docker Hub 的鏡像。直接使用這些鏡像是可以滿足一定的需求,而當這些鏡像無法直接滿足需求時,我們就需要定制這些鏡像。接下來的幾節就將講解如何定制鏡像。
回顧一下之前我們學到的知識,鏡像是多層存儲,每一層是在前一層的基礎上進行的修改;而容器同樣也是多層存儲,是在以鏡像為基礎層,在其基礎上加一層作為容器運行時的存儲層。
現在讓我們以定制一個 Web 服務器為例子,來講解鏡像是如何構建的。
docker run --name webserver -d -p 80:80 nginx
這條命令會用 nginx
鏡像啟動一個容器,命名為 webserver
,并且映射了 80 端口,這樣我們可以用瀏覽器去訪問這個 nginx
服務器。
如果是在 Linux 本機運行的 Docker,或者如果使用的是 Docker for Mac、Docker for 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 "youdi <liangchangyoujackson@gmail.com>" \
--message "修改了默認網頁" \
webserver \
nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214
其中 --author
是指定修改的作者,而 --message
則是記錄本次修改的內容。這點和 git
版本控制相似,不過這里這些信息可以省略留空。
我們可以在 docker images
中看到這個新定制的鏡像:
$ docker images 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 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
制作鏡像,以及后期修改的話,每一次修改都會讓鏡像更加臃腫一次,所刪除的上一層的東西并不會丟失,會一直如影隨形的跟著這個鏡像,即使根本無法訪問到?。這會讓鏡像更加臃腫。
docker commit
命令除了學習之外,還有一些特殊的應用場合,比如被入侵后保存現場等。但是,不要使用 docker commit
定制鏡像,定制行為應該使用 Dockerfile
來完成。下面的章節我們就來講述一下如何使用 Dockerfile
定制鏡像。