使用Docker 鏡像和倉庫
上一篇文章中,我們學習了包括 docker run 在內的許多對容器進行操作的基本指令,那么在本節中,我們主要探討 Docker 鏡像的一些概念,比如什么是鏡像,如何對鏡像進行管理,如何修改鏡像,如何創建、存儲、共享自己創建的鏡像等,那么就開始我們的學習
什么是 Docker 鏡像
Docker 鏡像是由文件系統疊加而成,最底端是一個引導文件系統,也就是bootfs
,這很像典型的 Linux/Unix 的引導文件系統。Docker 用戶永遠不會和引導文件系統有什么交互。實際上,一個容器啟動后,它就會被移入內容,而引導文件系統則會被卸載,從而留出更多的空間。(感覺有點像古代的餐館招待?負責引導顧客進入餐館,自己的工作就算是完成了)
傳統的Linux 引導過程中,root文件系統最先以只讀的方式加載,當引導結束后,會切換為讀寫模式。但是在Docker 中,root文件系統永遠只是只讀狀態,並且使用聯合加載的技術一次同時加載多個文件系統。聯合加載會將各層系統文件疊加在一起,最終的文件系統包含底層的文件和目錄。
聯合加載:聯合加載指的是一次同時加載多個文件系統,但是外面看起來只有一個文件系統。
Docker 將這樣的文件系統成為鏡像。一個鏡像可以放到另一個鏡像的頂部。位於下面的鏡像稱為父鏡像,一次類推,知道鏡像棧的最底部,最底部的鏡像稱為基礎鏡像。最后,當一個鏡像啟動容器時,Docker會在鏡像的最頂層加載一個文件系統。我們想在 Docker 中運行的程序就是在這個讀寫層中執行的。
用一幅圖來表示一下:
列出 Docker 鏡像
我們先從如何列出系統中的 Docker 鏡像來開始,可以使用 docker images 命令來實現,如下
可以看到,我們已經獲取了一個鏡像列表。那么,這些鏡像是從哪來的呢?我們執行 docker run 命令時,同時進行了鏡像下載
鏡像從倉庫下載下來。鏡像保存在倉庫中,而倉庫存在於 Registry 中。默認的 Registry 是由 Docker 公司運行的公共 Registry 服務,即 Docker Hub。需要進行ID的注冊
Docker Registry 的代碼是開源的,你也可以擁有自己的Registry。
在 Docker Hub (或者是你自己運營的 Docker Registry)中,鏡像是保存在倉庫中的,可以將鏡像倉庫想象成類似於Git 倉庫的東西。它包括鏡像、層、以及包括鏡像的元數據。
倉庫可以包含很多鏡像,你可以使用docker pull
來拉取倉庫中的鏡像,如下
Git 拉取代碼的指令是 git pull ,這樣就很相似了。
再來使用 docker images 看一下現在有哪些鏡像
因為我的 Docker Hub 倉庫中只有一個 ubuntu 的鏡像,所以圖中標紅的這個鏡像是我們剛從 Docker Hub 上下載下來的。
tag 標簽
為了區分同一個倉庫中的不同鏡像,Docker 為我們提供了 tag 這個標簽,每個鏡像在列出來的時候都帶有一個標簽,如12.10、 12.04等,這種標簽機制使得一個倉庫中允許存儲多個鏡像。
我們可以在倉庫后面加一個冒號:標簽名
的方式來指定該倉庫中的某一個鏡像,例如 docker run -t -i --name new_container ubuntu:12.04 /bin/bash
Docker 會自動幫我們切換到 Ubuntu 的環境下,當然,這種方式創建了一個交互式任務。
在構建容器時指定倉庫的標簽也是一個好習慣,這樣便可以准確的指定容器來源於哪里。
Docker Hub
Docker Hub 有兩種倉庫,一種是用戶倉庫,一種是頂層倉庫。用戶倉庫是由開發人員自己創建的,頂層倉庫是由Docker Hub 內部人員管理。
用戶倉庫的命名由兩部分構成,如 cxuan/ubuntu
- 用戶名 例如 : cxuan
- 倉庫名 例如 : ubuntu
相對的,頂層倉庫的命名就比較嚴謹,如 ubuntu 倉庫。頂層倉庫由 Docker 公司和選定的優質基礎鏡像廠商來管理,用戶可以基於這些鏡像構建自己的鏡像。
用戶鏡像都是由愛好者社區自己提供的,沒有經過 Docker 公司的認證,所以需要自己承擔相應的風險。
拉取鏡像
還記得docker run 的啟動過程嗎?再來一下這張圖回顧一下
其實也可以通過 docker pull 命令先預先拉取鏡像到本地,使用 docker pull 命令可以節省從一個新鏡像啟動一個容器所需要的時間。下面就來領取一下fedora
基礎鏡像( fedora 是 Fedora 優質廠商提供的基礎鏡像 )
可以使用 docker images 查看新鏡像是否拉取到本地,不過我們這次只希望看到 fedora 的鏡像,那么你可以使用這個命令: docker images fedora
可以看到我們已經把 fedora 鏡像拉取到了本地
查找鏡像
我們可以通過 docker search 命令來查找所有 Docker Hub 上公共可用的鏡像,如下
下面還有很多鏡像,我們主要看一下每條鏡像都返回了哪些內容
- 倉庫名稱
- 鏡像描述
- 用戶評價 --- 反應一個鏡像的受歡迎程度
- 是否官方 --- 是否由Docker 公司及其指定廠商開發的鏡像
- 是否自動構建 --- 表示這個鏡像是由 Docker Hub 自動構建的
從上面查詢的結果中選擇一個鏡像進行拉取,docker pull jamtur01/puppetmaster
這條命令將會下載 jamtur01/puppetmaster鏡像到本地。
接下來就可以使用這個鏡像來構建一個容器,下面就用 docker run 命令構建一個容器。
...
查看版本號
構建鏡像
上面我們看到如何拉取並且構建好帶有定制內容的 Docker 鏡像,那么我們如何修改自己的鏡像,並且管理和更新這些鏡像呢?
- 使用 docker commit 命令
- 使用 docker build 命令和 Dockerfile 文件
現在我們不推薦使用 docker commit 命令,相反應該使用更靈活更強大的 Dockerfile 來構建鏡像。不過,為了對 Docker 又一個更深的了解,我們還是會先介紹一下 docker commit 構建鏡像。之后,我們重點介紹Docker 所推薦的構建方法:編寫 Dockerfile 之后使用 docker build
命令。
創建Docker Hub 賬號
構建鏡像中很重要的一環就是如何共享和發布鏡像。可以將鏡像推送到 Docker Hub中或者自己的私有 Registry 中。為了完成這項工作,需要在 Docker Hub上創建一個賬號
如果你還沒有Docker 通行證,在 hub.docker.com 注冊一個,記下你的用戶名,登錄本地計算機上的Docker公共注冊表。使用docker login
,輸入用戶名和密碼進行登錄
你的個人信息會保存在 $HOME/.dockercfg 文件中
使用 Docker 的commit 命令創建鏡像
創建 Docker鏡像的第一種方式是使用 docker commit 命令。可以將此想象為我們是在版本控制系統里面提交變更,畢竟這和 git commit 命令真是太像了。
我們先從創建一個容器開始,這個容器基於我們前面見過的 ubuntu 鏡像。如下
接下來,我們在 ubuntu 中安裝 apache 服務器,使用apt-get -yqq update
和 apt-get -y install apache2
命令。
我們啟動了一個容器,並安裝了 Apache 服務器,我們會將這個服務器作為 Web 服務器運行,所以我們想把它當前狀態保存起來。這樣下次啟動就不用重新安裝了。為了完成這項工作,需要先使用 exit 從 ubuntu 中退出,之后再運行 docker commit 命令。如下
我們看到,在上圖所示的 docker commit 命令中,指定了要提交修改過的容器ID(可以通過 docker ps -l -q 命令得到剛剛創建的容器 ID),以及一個鏡像倉庫和鏡像名,這里是 jamtur01/puppetmaster
可以使用 docker images jamtur01/puppetmaster
命令查看剛剛創建的鏡像。
可以在提交時指定更多的數據,就和 git 的命令是一樣的,使用 docker commit -m
命令
這條命令中我們使用 -m(message) 指定提交信息,同時指定了 --authro 選項,列出鏡像作者信息。接着列出了想要提交的容器ID, 最后指定了 jamtur01/puppetmaster ,並為其打上了 webserver 的tag 標簽。
可以使用 docker inspect
命令來查看新創建的鏡像的詳細信息。
使用 Dockerfile 構建鏡像
我們並不推薦使用 docker commit 方法來構建鏡像。相反,我們推薦使用 Dockerfile
和 docker build
的命令來構建鏡像。Dockerfile 使用基於 DSL 語法的指令來構建一個 Docker 鏡像,之后使用 docker build 命令基於 Dockerfile 中的指令構建一個新的鏡像。
我們的第一個 Dockerfile
下面我們創建一個目錄並初始化 Dockerfile,我們創建一個包含簡單web服務器的Docker鏡像
如上圖所示,我們在 /usr/local/docker 目錄下創建了一個 static_web
的目錄,再創建了一個 Dockerfile 文件。那么這個 static_web 目錄就是我們的構建環境。Docker 稱此環境為上下文(context)
或者 構建上下文(build context)
,Docker 會在構建鏡像時將構建上下文和該上下文中的文件和目錄上傳到 Docker 守護進程。這樣 Docker 守護進程就可以直接訪問你鏡像中的 代碼、文件和數據。
下面是一個通過 Dockerfile 來構建 web 服務器的 Docker 鏡像
# Version: 0.0.1
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' \
>/usr/share/nginx/html/index.html
EXPOSE 80
該 Dockerfile 由一系列指令和參數組成。每條指令,如FROM,都必須為大寫字母,而且后面要跟隨一個參數: FROM ubuntu:14.04。Dockerfile 中的指令會按照順序由上向下
執行,所以編寫 Dockerfile 時,請注意它的順序。
如果不能使用
:wq
來進行保存的話,請首先使用sudo su
切換到管理員模式,然后就可以保存啦。
每條指令都會創建一個新的鏡像層並對鏡像進行提交。Docker 大體按照如下流程執行 Dockerfile 指令
- Docker 從基礎鏡像運行一個容器
- 執行一條指令,對容器作出修改
- 執行類似docker commit 操作,提交一個新的鏡像層
- Docker 再基於剛提交的鏡像運行一個新容器
- 執行 Dockerfile 中的下一條指令,直到所有指令都執行完畢
從上面可以看出,如果你的Dockerfile 由於某些原因(例如指令失敗了)沒有正常結束,那么你將得到了一個可以使用的鏡像。這對調試很有幫助: 可以基於鏡像運行一個具備交互功能的容器,使用最后創建的鏡像對你最后失敗的指令做出調試
Dockerfile 也支持中文注釋,以 # 開頭的行都會被認為是注釋。Dockerfile 中的第一行就是注釋的例子
每個 Dockerfile 的第一行指令都應該是 FROM。FROM指令指定一個已經存在的鏡像,后續指令都將基於該鏡像進行,這個鏡像被稱為基礎鏡像(base image)。
-
在上面的示例中,我們使用了
ubuntu:14.04
作為新鏡像基礎鏡像。基於這個 Dockerfile 構建的新鏡像以 Ubuntu 14.04 操作系統為基礎。再運行一個容器時,必須要指明基於哪個基礎鏡像進行構建。 -
接着指定了
MAINTAINER
指令,這條指令會告訴 Docker 該鏡像的作者是誰,以及作者的電子郵件地址,這有助於標示鏡像的所有者以及聯系方式。 -
在這些指令之后,我們指定了三條
RUN
指令。RUN指令會在當前的鏡像中運行指定的命令。在這個例子中,我們通過 RUN 指令更新了已經安裝的 APT 倉庫,安裝了 nginx 包,之后創建了 /usr/share/nginx/html/index.html 文件,該文件由一些簡單的示例文本。像前面說的那樣,每條RUN指令都會創建一個新的鏡像層,如果該命令執行成功,就會將此鏡像提交,繼續執行下一條指令。默認情況下,RUN指令會在shell里使用命令包裝器 /bin/sh -c 來執行。如果是在一個不支持 shell 的平台上運行或者不希望在 shell 中運行,也可以使用 exec 格式的 RUN 指令
如下 : RUN["apt-get", "install", "-y", "nginx"]
在這種方式中,我們使用數組的方式來指定要運行的命令和要傳遞的參數。
-
接着設置了
EXPOSE
命令,這條執行告訴 Docker 容器內的應用程序將會使用容器的指定接口。這並不意味着可以自動訪問任意容器運行中的服務端口,可以指定多個 EXPOSE 指令向外公開多個端口。
基於 Dockerfile 構建新鏡像
執行 docker build
命令時,Dockerfile 中的所有指令都會被執行並且提交,並且在命令成功結束后返回一個新鏡像,下面就來看看如何構建一個新鏡像。
一定不要忘記最后面有個空格 和 .
,也可以在構建鏡像的過程中為鏡像設置一個標簽: 使用方法為“鏡像名 : 標簽”,如下所示
指令失敗時呢?
之前大致介紹了一下指令失敗時的執行過程,下面來看一個例子: 假設我們在上面的 Dockerfile 中把 nginx 拼成了 ngnx ,再來構建一遍
我們可以看到,程序出錯了,這個時候我們希望去調試一下這次失敗。我們使用 docker run 命令來基於這次構建到目前為止已經成功的最后一步創建一個容器,這里它的ID 是 dee85a65a396
,我們可以使用如下命令
docker run -t -i dee85a65a396 /bin/bash
,來恢復到出錯之前的鏡像,然后重新運行出錯的指令apt-get install -y ngnx
,可以看到哪里出錯了
但是感覺這個步驟是多余了一些,如果 Dockerfile 中出現了錯誤,那么 Docker 就會給你提示,用不着重新運行命令來找出問題原因。
Dockerfile 和構建緩存
由於每一步的結果都會作為下一步的基礎鏡像,所以Docker 構建鏡像的過程非常聰明,它會將之前的鏡像層作為緩存。
正如上面 Dockerfile 來舉例,比如,在我們調試過程中,不需要在第一步和第三步之間做任何修改,因此 Docker 會將之前構建時創建的鏡像當作緩存並作為新的開始點。再次構建時,Docker 會直接從第四步開始。當之前的構建步驟沒有變化時,這會節省大量的時間。如果第一步到第三步之間有什么變化,則回到第一步開始。
然而,有的時候不希望有緩存的功能,這個時候你需要使用 apt-get update
,那么 Docker 將不會刷新 APT 包的緩存,要想略過緩存,可以使用 docker build
的 --no-cache 標志。
基於構建緩存的 Dockerfile 模版
構建緩存的一個好處就是,我們可以實現簡單的 Dockerfile 模版,一般會在 Dockerfile 文件頂部使用相同的指令集模版,比如對 ubuntu ,使用下面的模版
FROM ubuntu:14.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2019-08-15
RUN apt-get -qq update
我們來分析一下這個新的 Dockerfile :
- 首先,通過 FROM 指令為新鏡像設置了一個基礎鏡像 ubuntu:14.04。
- 接着,使用 MAINTAINER 指令添加了自己的詳細信息
- 然后,通過 ENV 指令設置了一個名為 REFRESHED_AT 的環境變量,用來表示最后一次的更新時間
- 最后,使用 RUN 指令運行 apt-get -qq update 命令,該指令會刷新 APT 包的緩存,用來確保每個安裝的軟件包都在最新版本。
查看新鏡像
現在來看一下新構建的鏡像,使用 docker images 命令來完成
如果想要了解鏡像是如何構建出來的,可以使用 docker history 命令,如下
從結果可以看出鏡像構建的每一層都是哪些指令構成的
從新鏡像啟動容器
我們可以基於新構建的鏡像啟動新容器,來檢查我們的構建工作是否正常
在這里,我們使用 docker run 命令,啟動一個 static_web 的容器, -d
表示的是以分離(detached) 的方式在后台運行。這種方式適合 nginx守護進程 這種需要長時間運行的進程。我們也指定了需要在 容器中運行的命令: nginx -g "daemon off;"
,將以前台方式運行 nginx 作為我們的服務器。
我們這里也使用了一個新的 -p 標志,用來控制 Docker 再運行時應該給外部開放哪些端口
- Docker 可以在宿主機上隨機選擇 49153 --- 65535 之間的一個比較大的端口映射到 80 端口上
- 可以在 Docker 宿主機指定一個具體的端口來映射到 80 端口上
使用 docker ps
查看一下端口分配情況
Docker 把 32769 端口映射到了 80 端口上
也可以通過 docker port
查看端口的映射情況
Dockerfile 指令
Dockerfile 指令比較多,這里我們會對 Dockerfile 單獨列一個章節進行說明
將鏡像推送至 Docker Hub
鏡像構建完畢之后,我們也可以將它上傳到 Docker Hub 上面去,這樣其他人就能使用這個鏡像了。
Docker Hub 的私有倉庫是需要收費的
我們可以使用 docker push 命令將鏡像推送至 Docker Hub。命令如下
為什么推送不上去?
網上搜索了一下,大概是鏡像標簽的問題,重新為鏡像設置一個標簽
然后把這個標簽推送上去,相當於就是把鏡像推送上去
我們可以在 Docker Hub 上看到我們推送的鏡像了
刪除鏡像
如果不再需要一個鏡像了,也可以將它刪除,使用 docker rmi
命令來刪除一個鏡像
該操作只能刪除本地鏡像,如果你已經推送至 Docker Hub 上,那么你還需要在 Docker Hub 上將其刪除
登錄 Docker Hub ,直接點下面的鏈接刪除
docker rmi 刪除多個容器的方式直接在后面枚舉容器即可,中間用空格隔開
總結
本篇文章主要講述了 Docker 中的鏡像和倉庫的一些概念和基本用法,那么你是否能回顧起來下面這些內容呢?
- 什么是鏡像
- 如何列出Docker中的鏡像,tag標簽是干什么用的
- 如何拉取遠程倉庫中的鏡像
- 如何查找鏡像
- 對於鏡像構建,你能想到哪些內容
- 如何推送鏡像至 Docker Hub
- 如何刪除鏡像