一:獲取鏡像
之前提到過,Docker Hub 上有大量的高質量的鏡像可以用,這里我們就說一下怎么獲取這些鏡像。
從 Docker 鏡像倉庫獲取鏡像的命令是 docker pull
。其命令格式為:
[root@docker ~]# docker pull [選項] [Docker Registry 地址[:端口號]/]倉庫名[:標簽]
具體的選項可以通過 docker pull --help
命令看到,這里我們說一下鏡像名稱的格式。
- Docker 鏡像倉庫地址:地址的格式一般是
<域名/IP>[:端口號]
。默認地址是 Docker Hub。 - 倉庫名:如之前所說,這里的倉庫名是兩段式名稱,即
<用戶名>/<軟件名>
。對於 Docker Hub,如果不給出用戶名,則默認為library
,也就是官方鏡像。
比如:
[root@docker ~]# docker pull nginx:1.12
1.12: Pulling from library/nginx
Digest: sha256:72daaf46f11cc753c4eab981cbf869919bd1fee3d2170a2adeac12400f494728
Status: Downloaded newer image for nginx:1.12
docker.io/library/nginx:1.12
上面的命令中沒有給出 Docker 鏡像倉庫地址,因此將會從 Docker Hub 獲取鏡像。而鏡像名稱是 ubuntu:18.04
,因此將會獲取官方鏡像 library/ubuntu
倉庫中標簽為 18.04
的鏡像。
從下載過程中可以看到我們之前提及的分層存儲的概念,鏡像是由多層存儲所構成。下載也是一層層的去下載,並非單一文件。下載過程中給出了每一層的 ID 的前 12 位。並且下載結束后,給出該鏡像完整的 sha256
的摘要,以確保下載一致性。
在使用上面命令的時候,你可能會發現,你所看到的層 ID 以及 sha256
的摘要和這里的不一樣。這是因為官方鏡像是一直在維護的,有任何新的 bug,或者版本更新,都會進行修復再以原來的標簽發布,這樣可以確保任何使用這個標簽的用戶可以獲得更安全、更穩定的鏡像。
如果從 Docker Hub 下載鏡像非常緩慢,可以參照 網易雲加速器 一節配置加速器。
-
運行
有了鏡像后,我們就能夠以這個鏡像為基礎啟動並運行一個容器。以上面的 nginx:1.12
為例,如果我們打算啟動里面的 bash
並且進行交互式操作的話,可以執行下面的命令。
[root@docker ~]# docker run -it --rm nginx:1.12 bash
root@767671792abd:/#
root@767671792abd:/# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
root@767671792abd:/#
docker run
就是運行容器的命令,具體格式我們會在 容器
一節進行詳細講解,我們這里簡要的說明一下上面用到的參數。
-it
:這是兩個參數,一個是-i
:交互式操作,一個是-t
終端。我們這里打算進入bash
執行一些命令並查看返回結果,因此我們需要交互式終端。--rm
:這個參數是說容器退出后隨之將其刪除。默認情況下,為了排障需求,退出的容器並不會立即刪除,除非手動docker rm
。我們這里只是隨便執行個命令,看看結果,不需要排障和保留結果,因此使用--rm
可以避免浪費空間。nginx:1.12
:這是指用nginx:1.12
鏡像為基礎來啟動容器。bash
:放在鏡像名后的是 命令,這里我們希望有個交互式 Shell,因此用的是bash
。
進入容器后,我們可以在 Shell 下操作,執行任何所需的命令。這里,我們執行了 cat /etc/os-release
,這是 Linux 常用的查看當前系統版本的命令,從返回的結果可以看到容器內是 Debian GNU/Linux 9 (stretch)
系統。
最后我們通過 exit
退出了這個容器。
二:查看鏡像
要想列出已經下載下來的鏡像,可以使用 docker image ls
or docker images
命令。
[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 84c5f6e03bf0 8 days ago 104MB
alpine latest a24bb4013296 3 months ago 5.57MB
<none> <none> fdab8031e252 2 years ago 232MB
nginx 1.12 4037a5562b03 2 years ago 108MB
nginx latest 4037a5562b03 2 years ago 108MB
mongo 3.1 199e537da3a8 4 years ago 303MB
列表包含了 倉庫名
、標簽
、鏡像 ID
、創建時間
以及 所占用的空間
。
其中倉庫名、標簽在之前的基礎概念章節已經介紹過了。鏡像 ID 則是鏡像的唯一標識,一個鏡像可以對應多個 標簽。因此,在上面的例子中,我們可以看到 nginx:1.12
和 nginx:latest
擁有相同的 ID,因為它們對應的是同一個鏡像。
-
鏡像體積
如果仔細觀察,會注意到,這里標識的所占用空間和在 Docker Hub 上看到的鏡像大小不同。比如,nginx:1.12
鏡像大小,在這里是 108 MB
,但是在 Docker Hub 顯示的卻是 41 MB
。這是因為 Docker Hub 中顯示的體積是壓縮后的體積。在鏡像下載和上傳過程中鏡像是保持着壓縮狀態的,因此 Docker Hub 所顯示的大小是網絡傳輸中更關心的流量大小。而 docker image ls
顯示的是鏡像下載到本地后,展開的大小,准確說,是展開后的各層所占空間的總和,因為鏡像到本地后,查看空間的時候,更關心的是本地磁盤空間占用的大小。
另外一個需要注意的問題是,docker image ls
列表中的鏡像體積總和並非是所有鏡像實際硬盤消耗。由於 Docker 鏡像是多層存儲結構,並且可以繼承、復用,因此不同鏡像可能會因為使用相同的基礎鏡像,從而擁有共同的層。由於 Docker 使用 Union FS,相同的層只需要保存一份即可,因此實際鏡像硬盤占用空間很可能要比這個列表鏡像大小的總和要小的多。
你可以通過以下命令來便捷的查看鏡像、容器、數據卷所占用的空間。
[root@docker ~]# docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 3 0 218.1MB 218.1MB (100%)
Containers 0 0 0B 0B
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B
-
虛懸鏡像
上面的鏡像列表中,還可以看到一個特殊的鏡像,這個鏡像既沒有倉庫名,也沒有標簽,均為 <none>
。:
<none> <none> fdab8031e252 2 years ago 232MB
這個鏡像原本是有鏡像名和標簽的,原來為 mongo:3.1
,隨着官方鏡像維護,發布了新版本后,重新 docker pull mongo:3.1
時,mongo:3.1
這個鏡像名被轉移到了新下載的鏡像身上,而舊的鏡像上的這個名稱則被取消,從而成為了 <none>
。除了 docker pull
可能導致這種情況,docker build
也同樣可以導致這種現象。由於新舊鏡像同名,舊鏡像名稱被取消,從而出現倉庫名、標簽均為 <none>
的鏡像。這類無標簽鏡像也被稱為 虛懸鏡像(dangling image) ,可以用下面的命令專門顯示這類鏡像:
[root@docker ~]# docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> fdab8031e252 2 years ago 232MB
一般來說,虛懸鏡像已經失去了存在的價值,是可以隨意刪除的,可以用下面的命令刪除。
docker image prune
-
中間層鏡像
為了加速鏡像構建、重復利用資源,Docker 會利用 中間層鏡像。所以在使用一段時間后,可能會看到一些依賴的中間層鏡像。默認的 docker image ls
列表中只會顯示頂層鏡像,如果希望顯示包括中間層鏡像在內的所有鏡像的話,需要加 -a
參數。
[root@docker ~]# docker image ls -a
這樣會看到很多無標簽的鏡像,與之前的虛懸鏡像不同,這些無標簽的鏡像很多都是中間層鏡像,是其它鏡像所依賴的鏡像。這些無標簽鏡像不應該刪除,否則會導致上層鏡像因為依賴丟失而出錯。實際上,這些鏡像也沒必要刪除,因為之前說過,相同的層只會存一遍,而這些鏡像是別的鏡像的依賴,因此並不會因為它們被列出來而多存了一份,無論如何你也會需要它們。只要刪除那些依賴它們的鏡像后,這些依賴的中間層鏡像也會被連帶刪除。
-
列出部分鏡像
不加任何參數的情況下,docker image ls
會列出所有頂層鏡像,但是有時候我們只希望列出部分鏡像。docker image ls
有好幾個參數可以幫助做到這個事情。
根據倉庫名列出鏡像
[root@docker ~]# docker image ls nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx 1.12 4037a5562b03 2 years ago 108MB
nginx latest 4037a5562b03 2 years ago 108MB
列出特定的某個鏡像,也就是說指定倉庫名和標簽
[root@docker ~]# docker image ls nginx:1.12
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx 1.12 4037a5562b03 2 years ago 108MB
除此以外,docker image ls
還支持強大的過濾器參數 --filter
,或者簡寫 -f
。之前我們已經看到了使用過濾器來列出虛懸鏡像的用法,它還有更多的用法。比如,我們希望看到在 nginx:3.2
之后建立的鏡像,可以用下面的命令:
[root@docker ~]# docker image ls -f since=mongo:3.1
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 84c5f6e03bf0 8 days ago 104MB
alpine latest a24bb4013296 3 months ago 5.57MB
<none> <none> fdab8031e252 2 years ago 232MB
nginx 1.12 4037a5562b03 2 years ago 108MB
nginx latest 4037a5562b03 2 years ago 108MB
想查看某個位置之前的鏡像也可以,只需要把 since
換成 before
即可。
此外,如果鏡像構建時,定義了 LABEL
,還可以通過 LABEL
來過濾。
[root@docker]# docker image ls -f label=com.example.version=0.1
-
以特定格式顯示
默認情況下,docker image ls
會輸出一個完整的表格,但是我們並非所有時候都會需要這些內容。比如,剛才刪除虛懸鏡像的時候,我們需要利用 docker image ls
把所有的虛懸鏡像的 ID 列出來,然后才可以交給 docker image rm
命令作為參數來刪除指定的這些鏡像,這個時候就用到了 -q
參數。
[root@docker ~]# docker image ls -q
84c5f6e03bf0
a24bb4013296
fdab8031e252
4037a5562b03
4037a5562b03
199e537da3a8
--filter
配合 -q
產生出指定范圍的 ID 列表,然后送給另一個 docker
命令作為參數,從而針對這組實體成批的進行某種操作的做法在 Docker 命令行使用過程中非常常見,不僅僅是鏡像,將來我們會在各個命令中看到這類搭配以完成很強大的功能。因此每次在文檔看到過濾器后,可以多注意一下它們的用法。
另外一些時候,我們可能只是對表格的結構不滿意,希望自己組織列;或者不希望有標題,這樣方便其它程序解析結果等,這就用到了 Go 的模板語法。
比如,下面的命令會直接列出鏡像結果,並且只包含鏡像ID和倉庫名:
[root@docker ~]# docker image ls --format "{{.ID}}: {{.Repository}}"
84c5f6e03bf0: redis
a24bb4013296: alpine
fdab8031e252: <none>
4037a5562b03: nginx
4037a5562b03: nginx
199e537da3a8: mongo
或者打算以表格等距顯示,並且有標題行,和默認一樣,不過自己定義列:
[root@docker ~]# docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
84c5f6e03bf0 redis latest
a24bb4013296 alpine latest
fdab8031e252 <none> <none>
4037a5562b03 nginx 1.12
4037a5562b03 nginx latest
199e537da3a8 mongo 3.1
三:刪除鏡像
如果要刪除本地的鏡像,可以使用 docker image rm
命令,其格式為:
[root@docker ~]# docker image rm [選項] <鏡像1> [<鏡像2> ...]
-
用 ID、鏡像名、摘要刪除鏡像
其中,<鏡像>
可以是 鏡像短 ID
、鏡像長 ID
、鏡像名
或者 鏡像摘要
。
比如我們有這么一些鏡像:
[root@docker ~]# docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 84c5f6e03bf0 8 days ago 104MB
alpine latest a24bb4013296 3 months ago 5.57MB
<none> <none> fdab8031e252 2 years ago 232MB
nginx 1.12 4037a5562b03 2 years ago 108MB
nginx latest 4037a5562b03 2 years ago 108MB
mongo 3.1 199e537da3a8 4 years ago 303MB
我們可以用鏡像的完整 ID,也稱為 長 ID
,來刪除鏡像。使用腳本的時候可能會用長 ID,但是人工輸入就太累了,所以更多的時候是用 短 ID
來刪除鏡像。docker image ls
默認列出的就已經是短 ID 了,一般取前3個字符以上,只要足夠區分於別的鏡像就可以了。
比如這里,如果我們要刪除 redis:alpine
鏡像,可以執行:
[root@docker ~]# docker image rm 199
Untagged: mongo:3.1
Untagged: mongo@sha256:bb3388e777b5d42c3d51370d2dcf2b1bd045b7169cf5e9373d6ef3bd56d9e78a
Deleted: sha256:199e537da3a86126cd6eb114bd0b13ab178dc291bbb6ea4a4a3aa257b2366b71
Deleted: sha256:bbae07fc2476a3a0f76c7f0a2aa3a4b2a29e14fda7e7f4c1d121072fa3ed88f1
Deleted: sha256:2cae1ff595c3da5da2ae0a220e0431a220717937f9487e169e0f5e6b46c5d6cb
Deleted: sha256:1c0d68ff97500919d291360cff621926ae490471febc32d3558c09a83dc13bbf
Deleted: sha256:1d39201fecaf1391bb27213ca15df2fb726416ed6943aab4d92131dc0e1748db
Deleted: sha256:52297729c9653a0691d2d9238a22b82b3afcb4200224409814129fd6b89acffc
Deleted: sha256:ee73c98df27e4547a0edcdf23cebe45911a271cda0c8359f58587ce027fc31f0
Deleted: sha256:9c1f53ffa8fd3ec4227f24fe9a61e41e143e82642a6f27702604507a81759fe3
Deleted: sha256:6575eac175690ce72d78d30333c50a7e8d71f7a2753c7ebd7a95eb54e5b82e6d
Deleted: sha256:fd3cef1c9da2724b2bbce14e8b5c1b0da4fecb50e25f57e050a5a94917bcc3ed
Deleted: sha256:c04f128a4d0b82f2fd64b1c4245a5d58e3a18d24bf1b141d7ae9453305383847
Deleted: sha256:350abc293848b9e8850c42e99a4d12dd25188bbf5585f344679521299a9a8f64
Deleted: sha256:fc308fe9910acff640c2af01a75faca08f7d0882c86e7bcb38d527e9d83e0d41
Deleted: sha256:3e789ea717437bb8ca4213efdd3445cbf4223d07d3f4fb4c99ed07ec6653f36f
Deleted: sha256:6307204f5f7a2af7cf5ebabc59253a35ce500a2c10fe9564e8d4fc515e82fb64
Deleted: sha256:729218c63d2c20a8fa9c86ee95ec0bbc49db6f9d77ab90b6220ac3c3bf4ed934
Deleted: sha256:264c36aff8c42e9b24fa3d69fcff4122adab9f73e61a6479170b6963564a75ac
Deleted: sha256:5cf01afeecd49d89fcaf1deecc595e86948fe534678d3566831e28350368800e
我們也可以用鏡像名
,也就是 <倉庫名>:<標簽>
,來刪除鏡像。
[root@docker ~]# docker image rm nginx:1.12
Untagged: nginx:1.12
-
Untagged 和 Deleted
如果觀察上面這幾個命令的運行輸出信息的話,你會注意到刪除行為分為兩類,一類是 Untagged
,另一類是 Deleted
。我們之前介紹過,鏡像的唯一標識是其 ID 和摘要,而一個鏡像可以有多個標簽。
因此當我們使用上面命令刪除鏡像的時候,實際上是在要求刪除某個標簽的鏡像。所以首先需要做的是將滿足我們要求的所有鏡像標簽都取消,這就是我們看到的 Untagged
的信息。因為一個鏡像可以對應多個標簽,因此當我們刪除了所指定的標簽后,可能還有別的標簽指向了這個鏡像,如果是這種情況,那么 Delete
行為就不會發生。所以並非所有的 docker image rm
都會產生刪除鏡像的行為,有可能僅僅是取消了某個標簽而已。
當該鏡像所有的標簽都被取消了,該鏡像很可能會失去了存在的意義,因此會觸發刪除行為。鏡像是多層存儲結構,因此在刪除的時候也是從上層向基礎層方向依次進行判斷刪除。鏡像的多層結構讓鏡像復用變得非常容易,因此很有可能某個其它鏡像正依賴於當前鏡像的某一層。這種情況,依舊不會觸發刪除該層的行為。直到沒有任何層依賴當前層時,才會真實的刪除當前層。這就是為什么,有時候會奇怪,為什么明明沒有別的標簽指向這個鏡像,但是它還是存在的原因,也是為什么有時候會發現所刪除的層數和自己 docker pull
看到的層數不一樣的原因。
除了鏡像依賴以外,還需要注意的是容器對鏡像的依賴。如果有用這個鏡像啟動的容器存在(即使容器沒有運行),那么同樣不可以刪除這個鏡像。之前講過,容器是以鏡像為基礎,再加一層容器存儲層,組成這樣的多層存儲結構去運行的。因此該鏡像如果被這個容器所依賴的,那么刪除必然會導致故障。如果這些容器是不需要的,應該先將它們刪除,然后再來刪除鏡像。
-
用 docker image ls 命令來配合
像其它可以承接多個實體的命令一樣,可以使用 docker image ls -q
來配合使用 docker image rm
,這樣可以成批的刪除希望刪除的鏡像。我們在“鏡像列表”章節介紹過很多過濾鏡像列表的方式都可以拿過來使用。
比如,我們需要刪除所有倉庫名為 redis
的鏡像:
[root@docker ~]# docker image rm $(docker image ls -q redis)
或者刪除所有在 mongo:3.1
之前的鏡像:
[root@docker ~]# docker image rm $(docker image ls -q -f before=mongo:3.1)
Linux 命令行的強大,你可以完成很多非常贊的功能。
四:使用docker commit提交鏡像
注意初學者直接學習,直接學習容器 。
注意: docker commit
命令除了學習之外,還有一些特殊的應用場合,比如被入侵后保存現場等。但是,不要使用 docker commit
定制鏡像,定制鏡像應該使用 Dockerfile
來完成。如果你想要定制鏡像請查看下一小節。
鏡像是容器的基礎,每次執行 docker run
的時候都會指定哪個鏡像作為容器運行的基礎。在之前的例子中,我們所使用的都是來自於 Docker Hub 的鏡像。直接使用這些鏡像是可以滿足一定的需求,而當這些鏡像無法直接滿足需求時,我們就需要定制這些鏡像。接下來的幾節就將講解如何定制鏡像。
回顧一下之前我們學到的知識,鏡像是多層存儲,每一層是在前一層的基礎上進行的修改;而容器同樣也是多層存儲,是在以鏡像為基礎層,在其基礎上加一層作為容器運行時的存儲層。
現在讓我們以定制一個 Web 服務器為例子,來講解鏡像是如何構建的。
[root@docker ~]# docker run --name nginx -d -p 80:80 nginx:1.12
這條命令會用 nginx
鏡像啟動一個容器,命名為 nginx
,並且映射了 80 端口,這樣我們可以用瀏覽器去訪問這個 nginx
服務器。
如果是在 Linux 本機運行的 Docker,或者如果使用的是 Docker Desktop for Mac/Windows,那么可以直接訪問:http://localhost;如果使用的是 Docker Toolbox,或者是在虛擬機、雲服務器上安裝的 Docker,則需要將 localhost
換為虛擬機地址或者實際雲服務器地址。
直接用瀏覽器訪問的話,我們會看到默認的 Nginx 歡迎頁面。nginx
現在,假設我們非常不喜歡這個歡迎頁面,我們希望改成歡迎 Docker 的文字,我們可以使用 docker exec
命令進入容器,修改其內容。
[root@docker ~]# docker exec -it nginx bash
root@9d48c1fa08b4:/# echo '<h1 style='color:red'>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@9d48c1fa08b4:/# exit
exit
我們以交互式終端方式進入 webserver
容器,並執行了 bash
命令,也就是獲得一個可操作的 Shell。
然后,我們用 <h1>Hello, Docker!</h1>
覆蓋了 /usr/share/nginx/html/index.html
的內容。
現在我們再刷新瀏覽器的話,會發現內容被改變了。
我們修改了容器的文件,也就是改動了容器的存儲層。我們可以通過 docker diff
命令看到具體的改動。
[root@docker ~]# docker diff nginx
C /root
A /root/.bash_history
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
C /run
A /run/nginx.pid
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
A /var/cache/nginx/uwsgi_temp
現在我們定制好了變化,我們希望能將其保存下來形成鏡像。
要知道,當我們運行一個容器的時候(如果不使用卷的話),我們做的任何文件修改都會被記錄於容器存儲層里。而 Docker 提供了一個 docker commit
命令,可以將容器的存儲層保存下來成為鏡像。換句話說,就是在原有鏡像的基礎上,再疊加上容器的存儲層,並構成新的鏡像。以后我們運行這個新鏡像的時候,就會擁有原有容器最后的文件變化。
docker commit
的語法格式為:
[root@docker ~]# docker commit [選項] <容器ID或容器名> [<倉庫名>[:<標簽>]]
我們可以用下面的命令將容器保存為鏡像:
[root@docker ~]# docker commit --author "jie li <lijie@gmail.com>" --message "修改了默認網頁歡迎頁面" nginx nginx:v1
sha256:f214eb1e8f12b2655a92d9f9863412f238167be962ddd6a02cc731aeaef2b844
其中 --author
是指定修改的作者,而 --message
則是記錄本次修改的內容。這點和 git
版本控制相似,不過這里這些信息可以省略留空。
我們可以在 docker image ls
中看到這個新定制的鏡像:
[root@docker ~]# docker image ls nginx
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx v1 f214eb1e8f12 About a minute ago 108MB
nginx 1.12 4037a5562b03 2 years ago 108MB
nginx latest 4037a5562b03 2 years ago 108MB
我們還可以用 `docker history` 具體查看鏡像內的歷史記錄,如果比較 `nginx:latest` 的歷史記錄,我們會發現新增了我們剛剛提交的這一層。
[root@docker ~]# docker history nginx:v1
IMAGE CREATED CREATED BY SIZE COMMENT
f214eb1e8f12 2 minutes ago nginx -g daemon off; 131B 修改了默認網頁
4037a5562b03 2 years ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemon… 0B
<missing> 2 years ago /bin/sh -c #(nop) STOPSIGNAL [SIGTERM] 0B
<missing> 2 years ago /bin/sh -c #(nop) EXPOSE 80/tcp 0B
<missing> 2 years ago /bin/sh -c ln -sf /dev/stdout /var/log/nginx… 22B
<missing> 2 years ago /bin/sh -c set -x && apt-get update && apt… 53.1MB
<missing> 2 years ago /bin/sh -c #(nop) ENV NJS_VERSION=1.12.2.0.… 0B
<missing> 2 years ago /bin/sh -c #(nop) ENV NGINX_VERSION=1.12.2-… 0B
<missing> 2 years ago /bin/sh -c #(nop) LABEL maintainer=NGINX Do… 0B
<missing> 2 years ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 years ago /bin/sh -c #(nop) ADD file:ec5be7eec56a74975… 55.3MB
新的鏡像定制好后,我們可以來運行這個鏡像。
[root@docker ~]# docker run --name nginx2 -d -p 81:80 nginx:v1
這里我們命名為新的服務為 nginx2
,並且映射到 81
端口。如果是 Docker Desktop for Mac/Windows 或 Linux 桌面的話,我們就可以直接訪問 http://localhost:81 看到結果,其內容應該和之前修改后的 nginx
一樣。
至此,我們第一次完成了定制鏡像,使用的是 docker commit
命令,手動操作給舊的鏡像添加了新的一層,形成新的鏡像,對鏡像多層存儲應該有了更直觀的感覺。
-
生產環境不要用
docker commit
使用 docker commit
命令雖然可以比較直觀的幫助理解鏡像分層存儲的概念,但是實際環境中並不會這樣使用。
首先,如果仔細觀察之前的 docker diff nginx
的結果,你會發現除了真正想要修改的 /usr/share/nginx/html/index.html
文件外,由於命令的執行,還有很多文件被改動或添加了。這還僅僅是最簡單的操作,如果是安裝軟件包、編譯構建,那會有大量的無關內容被添加進來,如果不小心清理,將會導致鏡像極為臃腫。
此外,使用 docker commit
意味着所有對鏡像的操作都是黑箱操作,生成的鏡像也被稱為 黑箱鏡像,換句話說,就是除了制作鏡像的人知道執行過什么命令、怎么生成的鏡像,別人根本無從得知。而且,即使是這個制作鏡像的人,過一段時間后也無法記清具體的操作。這種黑箱鏡像的維護工作是非常痛苦的。
而且,回顧之前提及的鏡像所使用的分層存儲的概念,除當前層外,之前的每一層都是不會發生改變的,換句話說,任何修改的結果僅僅是在當前層進行標記、添加、修改,而不會改動上一層。如果使用 docker commit
制作鏡像,以及后期修改的話,每一次修改都會讓鏡像更加臃腫一次,所刪除的上一層的東西並不會丟失,會一直如影隨形的跟着這個鏡像,即使根本無法訪問到。這會讓鏡像更加臃腫。
五:使用Dockerfile生成鏡像 建議新手跳過下,直接學習容器
這個只是一小部分后面有dockerfile詳情講解
從剛才的 docker commit
的學習中,我們可以了解到,鏡像的定制實際上就是定制每一層所添加的配置、文件。如果我們可以把每一層修改、安裝、構建、操作的命令都寫入一個腳本,用這個腳本來構建、定制鏡像,那么之前提及的無法重復的問題、鏡像構建透明性的問題、體積的問題就都會解決。這個腳本就是 Dockerfile。
Dockerfile 是一個文本文件,其內包含了一條條的 指令(Instruction),每一條指令構建一層,因此每一條指令的內容,就是描述該層應當如何構建。
還以之前定制 nginx
鏡像為例,這次我們使用 Dockerfile 來定制。
在一個空白目錄中,建立一個文本文件,並命名為 Dockerfile
:
root@docker ~]# mkdir docker_nginx
[root@docker ~]# cd docker_nginx/
[root@docker docker_nginx]# vim Dockerfile
其內容為:
FROM nginx:1.12
RUN echo '<h1 style='color:red'>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
這個 Dockerfile 很簡單,一共就兩行。涉及到了兩條指令,FROM
和 RUN
。
-
FROM 指定基礎鏡像
所謂定制鏡像,那一定是以一個鏡像為基礎,在其上進行定制。就像我們之前運行了一個 nginx
鏡像的容器,再進行修改一樣,基礎鏡像是必須指定的。而 FROM
就是指定 基礎鏡像,因此一個 Dockerfile
中 FROM
是必備的指令,並且必須是第一條指令。
在 Docker Hub 上有非常多的高質量的官方鏡像,有可以直接拿來使用的服務類的鏡像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便開發、構建、運行各種語言應用的鏡像,如 node
、openjdk
、python
、ruby
、golang
等。可以在其中尋找一個最符合我們最終目標的鏡像為基礎鏡像進行定制。
如果沒有找到對應服務的鏡像,官方鏡像中還提供了一些更為基礎的操作系統鏡像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,這些操作系統的軟件庫為我們提供了更廣闊的擴展空間。
除了選擇現有鏡像為基礎鏡像外,Docker 還存在一個特殊的鏡像,名為 scratch
。這個鏡像是虛擬的概念,並不實際存在,它表示一個空白的鏡像。
FROM scratch
...
如果你以 scratch
為基礎鏡像的話,意味着你不以任何鏡像為基礎,接下來所寫的指令將作為鏡像第一層開始存在。
不以任何系統為基礎,直接將可執行文件復制進鏡像的做法並不罕見,比如 swarm
、etcd
。對於 Linux 下靜態編譯的程序來說,並不需要有操作系統提供運行時支持,所需的一切庫都已經在可執行文件里了,因此直接 FROM scratch
會讓鏡像體積更加小巧。使用 Go 語言 開發的應用很多會使用這種方式來制作鏡像,這也是為什么有人認為 Go 是特別適合容器微服務架構的語言的原因之一。
-
RUN 執行命令
RUN
指令是用來執行命令行命令的。由於命令行的強大能力,RUN
指令在定制鏡像時是最常用的指令之一。其格式有兩種:
- shell 格式:
RUN <命令>
,就像直接在命令行中輸入的命令一樣。剛才寫的 Dockerfile 中的RUN
指令就是這種格式。
RUN echo '<h1 style='color:red'>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
文件所在目錄執行:
[root@docker docker_nginx]# docker build -t nginx:v2 .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM nginx:1.12
---> 4037a5562b03
Step 2/2 : RUN echo '<h1 style='color:red'>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 2ef0887e0afd
Removing intermediate container 2ef0887e0afd
---> a1526ee51636
Successfully built a1526ee51636
Successfully tagged nginx:v2
從命令的輸出結果中,我們可以清晰的看到鏡像的構建過程。在 Step 2
中,如同我們之前所說的那樣,RUN
指令啟動了一個容器 2ef0887e0afd
,執行了所要求的命令,並最后提交了這一層 a1526ee51636
,隨后刪除了所用到的這個容器 2ef0887e0afd
。
這里我們使用了 docker build
命令進行鏡像構建。其格式為:
[root@docker ~]# docker build [選項] <上下文路徑/URL/->
在這里我們指定了最終鏡像的名稱 -t nginx:v2
,構建成功后,我們可以像之前運行 nginx:v1
那樣來運行這個鏡像,其結果會和 nginx:v1
一樣。
-
鏡像構建上下文(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
輸出,我們其實已經看到了這個發送上下文的過程:
[root@docker ~]#docker build -t nginx:v2 .
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
的用法 -
用給定的 tar 壓縮包構建
[root@docker ~]# docker build http://server/context.tar.gz
如果所給出的 URL 不是個 Git repo,而是個 tar
壓縮包,那么 Docker 引擎會下載這個包,並自動解壓縮,以其作為上下文,開始構建。
-
從標准輸入中讀取 Dockerfile 進行構建
[root@docker ~]#docker build - < Dockerfile
或
[root@docker ~]#cat Dockerfile | docker build -
如果標准輸入傳入的是文本文件,則將其視為 Dockerfile
,並開始構建。這種形式由於直接從標准輸入中讀取 Dockerfile 的內容,它沒有上下文,因此不可以像其他方法那樣可以將本地文件 COPY
進鏡像之類的事情。
-
從標准輸入中讀取上下文壓縮包進行構建
[root@docker ~]# docker build - < context.tar.gz
如果發現標准輸入的文件格式是 gzip
、bzip2
以及 xz
的話,將會使其為上下文壓縮包,直接將其展開,將里面視為上下文,並開始構建。