引言
這篇文章中我們主要來探討下Docker鏡像,它是用來啟動容器的構建基石,本文的所用到的Dcoker版本是17.1,API版本是1.33,Go的版本是1.9.2,OS是基於Arch Linux的Manjaro。
Docker鏡像的概念
總的來說,Docker鏡像是由文件系統疊加而成的。
bootfs
最底端是一個引導文件系統,即bootfs,這很像典型的Linux/Unix的引導文件系統。Docker用戶幾乎永遠不會和引導文件系統有什么交互。實際上,當一個容器啟動后,它將會被移到內存中,而引導文件系統則會被卸載(unmount),以留出更多的內存供initrd磁盤鏡像使用。
rootfs
Docker看起來很像一個典型的Linux虛擬化棧。實際上,Docker鏡像的第二層是root文件系統rootfs,它位於引導文件系統之上。rootfs可以是一種或多種操作系統(如Centos或者Ubuntu系統)。
只讀與聯合加載
在傳統的Linux引導過程中,root文件系統會最先以只讀的方式加載,當引導結束並完成了完整性檢查之后,它才會被切換為讀寫模式。但是在Docker里,root文件系統永遠只能是只讀狀態,並且Docker利用聯合加載(union mount)技術又會在root文件系統層上加載更多的只讀文件系統。聯合加載指的是一次同時加載多個文件系統,但是在外面看起來只能看到一個文件系統。聯合加載會將各層文件系統疊加到一起,這樣最終的文件系統會包含所有底層的文件和目錄。
鏡像
Docker將這樣的文件系統稱為鏡像。一個鏡像可以放到另一個鏡像的頂部。位於下面的鏡像稱為父鏡像(parent image),可以依次類推,直到鏡像棧的最底部,最底部的鏡像稱為基礎鏡像(base image)。最后,當從一個鏡像啟動容器時,Docker會在該鏡像的最頂層加載一個讀寫文件系統。我們想在Docker中運行的程序就是在這個讀寫層中執行的。下圖是一張示意圖:
寫時復制
當Docker第一次啟動一個容器時,初始的讀寫層是空的。當文件系統發生變化時,這些變化都會應用到這一層上。比如,如果想修改一個文件,這個文件首先會從該讀寫層下面的只讀層復制到該讀寫層。該文件的只讀版本依然存在,但是已經被讀寫層中的該文件副本所隱藏。 通常這種機制被稱為寫時復制(copy on write),這也是使Docker如此強大的技術之一。每個只讀鏡像層都是只讀的,並且以后永遠不會變化。當創建一個新容器時,Docker會構建出一個鏡像棧,並在棧的最頂端添加一個讀寫層。這個讀寫層再加上其下面的鏡像層以及一些配置數據,就構成了一個容器。在上一章我們已經知道,容器是可以修改的,它們都有自己的狀態,並且是可以啟動和停止的。容器的這種特點加上鏡像分層框架(image-layering framework),使我們可以快速構建鏡像並運行包含我們自己的應用程序和服務的容器。
列出鏡像
我們可以使用docker images
命令來列出所有鏡像。
1 |
hazzacheng@hazzacheng-pc ~> sudo docker images |
本地鏡像都保存在Docker宿主機的/var/lib/docker目錄下。每個鏡像都保存在Docker所采用的存儲驅動目錄下面,如aufs或者devicemapper。也可以在/var/lib/docker/containers目錄下面看到所有的容器。
鏡像從倉庫下載下來。鏡像保存在倉庫中,而倉庫存在於Registry中。默認的Registry是由Docker公司運營的公共Registry服務,即Docker Hub,就如GitHub一樣。
在Docker Hub(或者用戶自己運營的Registry)中,鏡像是保存在倉庫中的。可以將鏡像倉庫想象為類似Git倉庫的東西。它包括鏡像、層以及關於鏡像的元數據(metadata)。
每個鏡像倉庫都可以存放很多鏡像(比如,ubuntu倉庫包含了 Ubuntu 12.04、12.10、13.04、13.10和14.04的鏡像)。
我們可以用docker pull
來拉取ubuntu倉庫中的Ubuntu12.04的鏡像。
1 |
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:12.04 |
我們再用docker images
命令來看一下,發現已經得到了Ubuntu的latest鏡像和12.04鏡像。這表明ubuntu鏡像實際上是聚集在一個倉庫下的一系列鏡像。
我們看到Docker提供了TAG來區分同一個倉庫中的不同鏡像,如12.04、12.10、quantal或者precise等。每個標簽對組成特定鏡像的一些鏡像層進行標記,比如,標簽12.04就是對所有Ubuntu 12.04鏡像的層的標記。這種機制使得在同一個倉庫中可以存儲多個鏡像。
1 |
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:precise |
我們看一下上面的例子,我們可以通過在倉庫名后面加上一個冒號和標簽名來指定該倉庫中的某一鏡像,在我們的docker images輸出中新的12.04鏡像以相同的鏡像ID出現了兩次,這是因為一個鏡像可以有多個標簽。這使我們可以方便地對鏡像進行打標簽並且很容易查找鏡像。在這個例子中,ID 5b117edd0b76的鏡像實際上被打上了12.04和precise兩個標簽,分別代表該Ubuntu發布版的版本號和代號。
Docker Hub中有兩種類型的倉庫:用戶倉庫(user repository)和頂層倉庫(top-level repository)。用戶倉庫的鏡像都是由Docker用戶創建的,而頂層倉庫則是由Docker內部的人來管理的。
用戶倉庫的命名由用戶名和倉庫名兩部分組成,如hazzacheng/ubuntu,用戶名:hazzacheng,倉庫名:ubuntu。
與之相對,頂層倉庫只包含倉庫名部分,如ubuntu倉庫。頂層倉庫由Docker公司和由選定的能提供優質基礎鏡像的廠商(如Fedora團隊提供了fedora鏡像)管理,用戶可以基於這些基礎鏡像構建自己的鏡像。同時頂層倉庫也代表了各廠商和Docker公司的一種承諾,即頂層倉庫中的鏡像是架構良好、安全且最新的。
拉取鏡像
用docker run
命令從鏡像啟動一個容器時,如果該鏡像不在本地,Docker會先從Docker Hub下載該鏡像。如果沒有指定具體的鏡像標簽,那么Docker會自動下載latest標簽的鏡像。
我們也可以向前面做的那樣,通過docker pull
來事先將該鏡像拉取到本地。我們來拉取一個fedora的鏡像:
1 |
hazzacheng@hazzacheng-pc ~> sudo docker pull fedora:20 |
我們也可以通過docker images
命令來只查看fedora的鏡像:
1 |
hazzacheng@hazzacheng-pc ~> sudo docker images fedora |
因為Docker Hub實在太慢了,還經常被牆,所以我們一般從國內的鏡像源pull,例如網易蜂巢:
1 |
sudo docker pull hub.c.163.com/public/centos:7.2.1511 |
我們也可以用阿里雲的鏡像加速來進行加速,具體做法請Google。
查找鏡像
可以通過docker search
查找所有Docker Hub上的公共的可用鏡像,例如我們搜索下mysql:
1 |
hazzacheng@hazzacheng-pc ~> sudo docker search mysql |
它返回了如下信息:
- NAME:倉庫名
- DESCRIPTION:鏡像描述
- STARS:用戶評價,類似與GiyHub里的stars
- OFFICIAL:是否官方
- AUTOMATED:表示這個鏡像是由Docker Hub的自動構建(Automated Build)流程創建的。
構建鏡像
構建Docker鏡像有以下兩種方法:
- 使用docker commit命令。
- 使用docker build命令和Dockerfile文件。
登錄到Docker
注冊完帳號之后,我們可以通過docker login
登錄到Docker Hub,因為Docker Hub對於國內實在是太慢了,所以我們登錄到網易的Register,這條命令將會完成登錄到網易蜂巢的工作,並將認證信息保存起來以供后面使用。可以使用docker logout
命令從一個Registry服務器退出。
1 |
sudo docker login -u xxx -p xxx hub.c.163.com |
用戶的個人認證信息將會保存到HOME/.docker/config.json中,這里的HOME/.docker/config.json中,這里的HOME指的應該是/root文件夾下。
因為Docker Hub實在太慢了,所以接下來的操作我們都使用網易蜂巢的服務。
用Dockerfile構建鏡像
不推薦使用docker commit
的方法來構建鏡像,所以我們這里也不介紹那種方法了。推薦使用被稱為Dockerfile的定義文件和docker build
命令來構建鏡像。Dockerfile使用基本的基於DSL(Domain Specific Language))語法的指令來構建一個Docker鏡像,我們推薦使用Dockerfile方法來代替docker commit,因為通過前者來構建鏡像更具備可重復性、透明性以及冪等性。
一旦有了Dockerfile,我們就可以使用docker build
命令基於該Dockerfile中的指令構建一個新的鏡像。
我們創建一個包含簡單Web服務器的Docker鏡像。
我們先創建一個目錄,里面保存初始的Dockerfile:
1 |
hazzacheng@hazzacheng-pc ~> mkdir static_web |
我們創建了一個名為static_web的目錄用來保存Dockerfile,這個目錄就是我們的構建環境(build environment),Docker則稱此環境為上下文(context)或者構建上下文(build context)。Docker會在構建鏡像時將構建上下文和該上下文中的文件和目錄上傳到Docker守護進程。這樣Docker守護進程就能直接訪問用戶想在鏡像中存儲的任何代碼、文件或者其他數據。
輸入Dockerfile的內容:
1 |
# Version: 0.0.1 |
Dockerfile由一系列指令和參數組成。每條指令,如FROM,都必須為大寫字母,我們分別介紹一下它們。
FROM
每個Dockerfile的第一條指令必須是FROM。FROM指令指定一個已經存在的鏡像,后續指令都將基於該鏡像進行,這個鏡像被稱為基礎鏡像(base iamge)。
MAINTAINER
接着指定了MAINTAINER指令,這條指令會告訴Docker該鏡像的作者是誰,以及作者的電子郵件地址。這有助於標識鏡像的所有者和聯系方式。
RUN
然后就是run指令,RUN指令會在當前鏡像中運行指定的命令,Dockerfile中的指令會按順序從上到下執行,所以應該根據需要合理安排指令的順序。每條指令都會創建一個新的鏡像層並對鏡像進行提交。Docker大體上按照如下流程執行Dockerfile中的指令:
- Docker從基礎鏡像運行一個容器。
- 執行一條指令,對容器做出修改。
- 執行類似docker commit的操作,提交一個新的鏡像層。
- Docker再基於剛提交的鏡像運行一個新容器。
- 執行Dockerfile中的下一條指令,直到所有指令都執行完畢。
因為每執行一條指令,就會提交一個新的鏡像層,如果用戶的Dockerfile由於某些原因(如某條指令失敗了)沒有正常結束,那么用戶將得到了一個可以使用的鏡像。這對調試非常有幫助:可以基於該鏡像運行一個具備交互功能的容器,使用最后創建的鏡像對為什么用戶的指令會失敗進行調試。
默認情況下,RUN指令會在shell里使用命令包裝器/bin/sh -c來執行。如果是在一個不支持shell的平台上運行或者不希望在shell中運行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令:
1 |
RUN [ "apt-get", " install", "-y", "nginx" ] |
EXPOSE
最后設置了EXPOSE指令,這條指令告訴Docker該容器內的應用程序將會使用容器的指定端口。這並不意味着可以自動訪問任意容器運行中服務的端口(這里是80)。出於安全的原因,Docker並不會自動打開該端口,而是需要用戶在使用docker run
運行容器時來指定需要打開哪些端口。一會兒我們將會看到如何從這一鏡像創建一個新容器。可以指定多個EXPOSE指令來向外部公開多個端口。
基於Dockerfile構建新鏡像
執行docker build
命令時,Dockerfile中的所有指令都會被執行並且提交,並且在該命令成功結束后返回一個新鏡像,我們來操作一下:
1 |
hazzacheng@hazzacheng-pc ~/static_web> sudo docker build -t="hazzacheng/static_web:v1" . |
如上所示,我們通過-t
選項為新鏡像設置了倉庫,名稱和一個標簽。如果沒有制定任何標簽,Docker將會自動為鏡像設置一個latest標簽。
我們還可以通過Git倉庫來構建Docker鏡像:
1 |
$ sudo docker build -t="hazzacheng/static_web:v1" \ |
這里Docker假設在這個Git倉庫的根目錄下存在Dockerfile文件。
自Docker 1.5.0開始,也可以通過-f標志指定一個區別於標准Dockerfile的構建源的位置:
1 |
dockerbuild-t"hazzacheng/static_- web" -f path/to/file |
這個文件可以不必命名為Dockerfile,但是必須要位於構建上下文之中。
指令失敗
如果一個指令失敗時,例如我們不小心將前面的nginx打成ngin,程序會構建到第四步時錯誤退出,但是我們可以用docker run命令來基於這次構建到目前為止已經成功的最后一步創建一個容器。
Dockerfile和構建緩存
由於每一步的構建過程都會將結果提交為鏡像,所以Docker的構建鏡像過程就顯得非常聰明。它會將之前的鏡像層看作緩存。比如,在我們的調試例子里,我們不需要在第1步到第3步之間進行任何修改,因此Docker會將之前構建時創建的鏡像當做緩存並作為新的開始點。實際上,當再次進行構建時,Docker會直接從第4步開始。當之前的構建步驟沒有變化時,這會節省大量的時間。如果真的在第1步到第3步之間做了什么修改,Docker則會從第一條發生了變化的指令開始。 然而,有些時候需要確保構建過程不會使用緩存。比如,如果已經緩存了前面的第3步,即apt-get update,那么Docker將不會再次刷新APT包的緩存。這時用戶可能需要取得每個包的最新版本。要想略過緩存功能,可以使用docker build
的--no-cache
標志。
基於構建緩存的Dockerfile模板
我們可以利用緩存實現簡單的Dockerfile模板,比如在Dockerfile文件頂部增加包倉庫或者更新包,從而盡可能確保緩存命中。我們可以在自己的Dockerfile文件頂部使用相同的指令集模板,例如對Ubuntu:
1 |
FROM ubuntu:16.04 |
FROM
和MAINTAINER
都與我們前面所說的一樣,ENV
用來在鏡像中設置環境變量。在這個例子里,我通過ENV
指令來設置了一個名為REFRESHED_AT
的環境變量,這個環境變量用來表明該鏡像模板最后的更新時間。最后,我使用了RUN
指令來運行apt-get -qq update
命令。該指令運行時將會刷新APT包的緩存,用來確保我們能將要安裝的每個軟件包都更新到最新版本。
有了這個模板,如果想刷新一個構建,只需修改ENV
指令中的日期。這使Docker在命中ENV指令時開始重置這個緩存,並運行后續指令而無須依賴該緩存。也就是說,RUN apt-get update
這條指令將會被再次執行,包緩存也將會被刷新為最新內容。
可以擴展此模板,比如適配到不同的平台或者添加額外的需求。比如,可以支持一個fedora鏡像:
1 |
FROM fedora:20 |
如果我們想深入探究鏡像是如何構建出來的,可以使用docker history
:
1 |
hazzacheng@hazzacheng-pc ~/D/ubuntu_nginx> sudo docker history hazzacheng/static_web:v1 |
從上面的結果可以看到新構建鏡像的每一層,以及創建這些層的Dockerfile指令。
端口映射
我們利用守護方式從鏡像啟動一個容器:
1 |
hazzacheng@hazzacheng-pc ~> sudo docker run -d -p 80 --name static_web hazzacheng/static_web:v1 \ |
-p
用來控制ocker在運行時應該公開哪些網絡端口給外部(宿主機)。運行一個容器時,Docker可以通過兩種方法來在宿主機上分配端口。
- Docker可以在宿主機上隨機選擇一個位於32768~61000的一個比較大的端口號來映射到容器中的80端口上。
- 可以在Docker宿主機中指定一個具體的端口號來映射到容器中的80端口上。
docker run命令將在Docker宿主機上隨機打開一個端口,這個端口會連接到容器中的80端口上。我們來看一下:
1 |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
可以看到,容器中的80端口映射到了宿主機的32768上,我們也可以通過docker port
來查看容器的端口映射:
1 |
hazzacheng@hazzacheng-pc ~> sudo docker port static_web 80 |
-p
選項還為我們在將容器端口向宿主機公開時提供了一定的靈活性。比如,可以指定將容器中的端口映射到Docker宿主機的某一特定端口上,例如-p 8080:80
會將容器內的80端口綁定到本地宿主機的8080端口上,我們需要小心使用,如果要運行多個容器,只有一個容器能成功地將端口綁定到本地宿主機上,這將會限制Docker的靈活性。 我們也可以將端口綁定限制在特定的IP地址上,如-p 127.0.0.1:80:80
,我們將容器內的80端口綁定到了本地宿主機的127.0.0.1這個IP的80端口上。我們也可以使用類似的方式將容器內的80端口綁定到一個宿主機的隨機端口上,如-p 127.0.0.1::80
。也可以通過在端口綁定時使用/udp
后綴來指定UDP端口。
Docker還提供了一個更簡單的方式,即-P
參數,該參數可以用來對外公開在Dockerfile中通過EXPOSE
指令公開的所有端口:
1 |
sudo docker run -d -P --name static_web hazzacheng/static_web:v1 \ |
該命令會將容器內的80端口對本地宿主機公開,並且綁定到宿主機的一個隨機端口上。該命令會將用來構建該鏡像的Dockerfile文件中EXPOSE指令指定的其他端口也一並公開。
Dockerfile指令
我們介紹一些常用的Dockerfile指令,你也可以通過官方文檔來學習。
CMD
CMD指令用於指定一個容器啟動時要運行的命令。這有點兒類似於RUN
指令,只是RUN
指令是指定鏡像被構建時要運行的命令,而CMD
是指定容器被啟動時要運行的命令。需要注意的是,要運行的命令是存放在一個數組結構中的。這將告訴Docker按指定的原樣來運行該命令。當然也可以不使用數組而是指定CMD
指令,這時候Docker會在指定的命令前加上/bin/sh -c
。這在執行該命令的時候可能會導致意料之外的行為,所以Docker推薦一直使用以數組語法來設置要執行的命令。例如下面的用法:
1 |
CMD ["/bin/bash"] |
使用docker run
命令可以覆蓋CMD
指令。如果我們在Dockerfile里指定了CMD
指令,而同時在docker run
命令行中也指定了要運行的命令,命令行中指定的命令會覆蓋Dockerfile中的CMD
指令。
在Dockerfile中只能指定一條CMD
指令。如果指定了多條CMD指令,也只有最后一條CMD
指令會被使用。如果想在啟動容器時運行多個進程或者多條命令,可以考慮使用類似Supervisor這樣的服務管理工具。
ENTRYPOINT
ENTRYPOINT
指令與CMD
指令非常類似,也很容易和CMD
指令弄混。這兩個指令的區別是,我們可以在docker run
命令行中覆蓋CMD
指令。有時候,我們希望容器會按照我們想象的那樣去工作,這時候CMD
就不太合適了。而ENTRYPOINT
指令提供的命令則不容易在啟動容器時被覆蓋。實際上,docker run
命令行中指定的任何參數都會被當做參數再次傳遞給ENTRYPOINT
指令中指定的命令。例如:
1 |
ENTRYPOINT ["/usr/sbin/nginx", "-g", "daemon off;"] |
ENTRYPOINT
也可以和CMD
一起使用,例如:
1 |
ENTRYPOINT ["/usr/sbin/nginx"] |
如果在啟動容器時不指定任何參數,則在CMD
指令中指定的-h
參數會被傳遞給Nginx守護進程,即Nginx服務器會以/usr/sbin/nginx -h
的方式啟動,該命令用來顯示Nginx的幫助信息。
這使我們可以構建一個鏡像,該鏡像既可以運行一個默認的命令,同時它也支持通過docker run命令行為該命令指定可覆蓋的選項或者標志。
如果確實需要,用戶也可以在運行時通過docker run
的--entrypoint
標志覆蓋ENTRYPOINT
指令。
WORKDIR
WORKDIR
指令用來在從鏡像創建一個新容器時,在容器內部設置一個工作目錄,ENTRYPOINT
和CMD
指定的程序會在這個目錄下執行。我們可以使用該指令為Dockerfile中后續的一系列指令設置工作目錄,也可以為特定的指令設置不同的工作目錄:
1 |
WORKDIR /opt/webapp/db |
可以通過-w
標志在運行時覆蓋工作目錄:
1 |
docker run -ti -w /var/log ubuntu pwd /var/log |
該命令會將容器內的工作目錄設置為/var/log
。
ENV
ENV
指令用來在鏡像構建過程中設置環境變量。
1 |
ENV JAVA_PATH /home/java/ |
這個新的環境變量可以在后續的任何RUN
指令中使用,可以在ENV
指令中指定單個環境變量,也可以指定多個變量。
1 |
ENV JAVA_PATH=/home/java JRE_PATH=/home/java/jre |
也可以使用docker run
命令行的-e
標志來傳遞環境變量。這些變量將只會在運行時有效:
1 |
docker run -ti -e "WEB_PORT=8080" ubuntu env |
在容器中WEB_PORT環境變量被設為了8080。
USER
USER
指令用來指定該鏡像會以什么樣的用戶去運行,我們可以指定用戶名或UID以及組或GID,甚至是兩者的組合。
1 |
USER user |
也可以在docker run
命令中通過-u標志來覆蓋該指令指定的值。
如果不通過USER指令指定用戶,默認用戶為root。
VOLUME
VOLUME
指令用來向基於鏡像創建的容器添加卷。一個卷是可以存在於一個或者多個容器內的特定的目錄,這個目錄可以繞過聯合文件系統,並提供如下共享數據或者對數據進行持久化的功能:
- 卷可以在容器間共享和重用。
- 一個容器可以不是必須和其他容器共享卷。
- 對卷的修改是立時生效的。
- 對卷的修改不會對更新鏡像產生影響。
- 卷會一直存在直到沒有任何容器再使用它。
卷功能讓我們可以將數據(如源代碼)、數據庫或者其他內容添加到鏡像中而不是將這些內容提交到鏡像中,並且允許我們在多個容器間共享這些內容。我們可以利用此功能來測試容器和內部的應用程序代碼,管理日志,或者處理容器內部的數據庫。
1 |
VOLUME ["/opt/project"] |
這條指令將會為基於此鏡像創建的任何容器創建一個名為/opt/project
的掛載點。也可以通過指定數組的方式指定多個卷:
1 |
VOLUME ["/opt/project", "/data" ] |
docker cp
是和VOLUME
指令相關並且也是很實用的命令。該命令允許從容器復制文件和復制文件到容器上。
ADD
ADD
指令用來將構建環境下的文件和目錄復制到鏡像中。例如,在安裝一個應用程序時。ADD
指令需要源文件位置和目的文件位置兩個參數:
1 |
ADD software.lic /opt/application/software.lic |
ADD
指令將會將構建目錄下的software.lic文件復制到鏡像中的/opt/application/software.lic
。指向源文件的位置參數可以是一個URL,或者構建上下文或環境中文件名或者目錄。不能對構建目錄或者上下文之外的文件進行ADD
操作。 在ADD
文件時,Docker通過目的地址參數末尾的字符來判斷文件源是目錄還是文件。如果目的地址以/結尾,那么Docker就認為源位置指向的是目錄。如果目的地址不是以/結尾,那么Docker就認為源位置指向的是文件。 文件源也可以使用URL的格式:
1 |
ADD http://wordpress.org/latest.zip /root/wordpress.zip |
ADD在處理本地壓縮文件(tar archive)時還有一些特殊處理。如果將一個壓縮文件(合法的壓縮文件包括gzip、bzip2、xz)指定為源文件,Docker會自動將壓縮文件解開(unpack):
1 |
ADD latest.tar.gz /var/www/wordpress/ |
這條命令會將壓縮文件latest.tar.gz解開到/var/www/wordpress/目錄下。Docker解開壓縮文件的行為和使用帶-x
選項的tar
命令一樣:該指令執行后的輸出是原目的目錄已經存在的內容加上壓縮文件中的內容。如果目的位置的目錄下已經存在了和壓縮文件同名的文件或者目錄,那么目的位置中的文件或者目錄不會被覆蓋。
如果目的位置不存在的話,Docker將會為我們創建這個全路徑,包括路徑中的任何目錄。新創建的文件和目錄的模式為0755,並且UID和GID都是0。 ADD指令會使得構建緩存變得無效,這一點也非常重要。如果通過ADD指令向鏡像添加一個文件或者目錄,那么這將使Dockerfile中的后續指令都不能繼續使用之前的構建緩存。
COPY
COPY
指令非常類似於ADD
,它們根本的不同是COPY
只關心在構建上下文中復制本地文件,而不會去做文件提取(extraction)和解壓(decompression)的工作
1 |
COPY conf.d/ /etc/apache2/ |
這條指令將會把本地conf.d目錄中的文件復制到/etc/apache2/目錄中。
文件源路徑必須是一個與當前構建環境相對的文件或者目錄,本地文件都放到和Dockerfile同一個目錄下。不能復制該目錄之外的任何文件,因為構建環境將會上傳到Docker守護進程,而復制是在Docker守護進程中進行的。任何位於構建環境之外的東西都是不可用的。COPY
指令的目的位置則必須是容器內部的一個絕對路徑。
任何由該指令創建的文件或者目錄的UID和GID都會設置為0。 如果源路徑是一個目錄,那么這個目錄將整個被復制到容器中,包括文件系統元數據;如果源文件為任何類型的文件,則該文件會隨同元數據一起被復制。在這個例子里,源路徑以/結尾,所以Docker會認為它是目錄,並將它復制到目的目錄中。
如果目的位置不存在,Docker將會自動創建所有需要的目錄結構,就像mkdir -p
命令那樣。
LABEL
LABEL
指令用於為Docker鏡像添加元數據。元數據以鍵值對的形式展現。
1 |
LABEL version="1.0" |
LABEL
指令以label=“value”的形式出現。可以在每一條指令中指定一個元數據,或者指定多個元數據,不同的元數據之間用空格分隔。推薦將所有的元數據都放到一條LABEL指令中,以防止不同的元數據指令創建過多鏡像層。可以通過docker inspect
命令來查看Docker鏡像中的標簽信息。
STOPSIGNAL
STOPSIGNAL
指令用來設置停止容器時發送什么系統調用信號給容器。這個信號必須是內核系統調用表中合法的數,如9,或者SIGNAME格式中的信號名稱,如SIGKILL。
ARG
ARG
指令用來定義可以在docker build
命令運行時傳遞給構建運行時的變量,我們只需要在構建時使用--build-arg
標志即可。用戶只能在構建時指定在Dockerfile文件中定義過的參數。
1 |
ARG build |
上面例子中第二條ARG指令設置了一個默認值,如果構建時沒有為該參數指定值,就會使用這個默認值。我們在docker build
中使用這些參數:
1 |
$ docker build --build-arg build=1234 -t hazzacheng/webapp . |
這里構建hazzacheng/webapp鏡像時,build
變量將會設置為1234,而webapp_user
變量則會繼承設置的默認值user。
請不要使用ARG來傳遞證書或者秘鑰之類的信息,這些機密信息在構建過程中以及鏡像的構建歷史中會被暴露。
Docker預定義了一組ARG
變量,可以在構建時直接使用,而不必再到Dockerfile中自行定義。
預定義ARG變量:
1 |
HTTP_PROXY |
要想使用這些預定義的變量,只需要給docker build
命令傳遞--build-arg <variable>=<value>
標志就可以了。
ONBUILD
ONBUILD
指令能為鏡像添加觸發器(trigger)。當一個鏡像被用做其他鏡像的基礎鏡像時,例如用戶的鏡像需要從某未准備好的位置添加源代碼,或者用戶需要執行特定於構建鏡像的環境的構建腳本,該鏡像中的觸發器將會被執行。
觸發器會在構建過程中插入新指令,我們可以認為這些指令是緊跟在FROM
之后指定的。觸發器可以是任何構建指令:
1 |
ONBUILD ADD . /app/src |
上面的代碼將會在創建的鏡像中加入ONBUILD
觸發器,ONBUILD
指令可以在鏡像上運行docker inspect
命令來查看。 例如,我們為Apache2鏡像構建一個全新的Dockerfile,該鏡像名為hazzacheng/ apache2`:
1 |
FROM ubuntu:14.04 |
構建該鏡像:
1 |
hazzacheng@hazzacheng-pc ~/D/apache2> sudo docker build -t="hazzacheng/apache2:1.0" . |
在新構建的鏡像中包含一條ONBUILD
指令,該指令會使用ADD
指令將構建環境所在的目錄下的內容全部添加到鏡像中的/var/www/目錄下。我們可以輕而易舉地將這個Dockerfile作為一個通用的Web應用程序的模板,可以基於這個模板來構建Web應用程序。
我們可以通過構建一個名為webapp的鏡像來看看如何使用鏡像模板功能。它的Dockerfile如代碼如下:
1 |
FROM hazzacheng/apache2:1.0 |
讓我們看看構建這個鏡像時將會發生什么事情:
1 |
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker build -t="hazzacheng/webapp:1.0" . |
可以清楚地看到,在FROM
指令之后,Docker插入了一條ADD
指令,這條ADD
指令就是在ONBUILD
觸發器中指定的。執行完該ADD
指令后,Docker才會繼續執行構建文件中的后續指令。這種機制使我每次都會將本地源代碼添加到鏡像,就像上面我們做到的那樣,也支持我為不同的應用程序進行一些特定的配置或者設置構建信息。這時,可以將hazzacheng/apache2當做一個鏡像模板。
ONBUILD
觸發器會按照在父鏡像中指定的順序執行,並且只能被繼承一次,也就是說只能在子鏡像中執行,而不會在孫子鏡像中執行。如果我們再基於hazzacheng/apache2構建一個鏡像,則新鏡像是hazzacheng/apache2的孫子鏡像,因此在該鏡像的構建過程中,ONBUILD
觸發器是不會被執行的。
這里有好幾條指令是不能用在ONBUILD
指令中的,包括FROM
、MAINTAINER
和ONBUILD
本身。之所以這么規定是為了防止在 Dockerfile構建過程中產生遞歸調用的問題。
提交鏡像
鏡像構建完畢之后,我們也可以將它上傳到Docker Hub上面去,也可以推送到其他的Registry,例如網易蜂巢,這樣其他人就能使用這個鏡像了。比如,我們可以在組織內共享這個鏡像,或者完全公開這個鏡像。
這里我們將其推送到網易蜂巢上去:
1 |
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker tag hazzacheng/static_web:v1 hub.c.163.com/hazzacheng/static_web:v1 |
再push之前我們必須先打上網易蜂巢的tag,即hub.c.163.com
。
刪除鏡像
我們使用docker rmi
來刪除鏡像,這個命令也支持刪除多個鏡像,我們也可以使用
1 |
docker rmi `docker images -a -q` |
來刪除所有鏡像。
(原文: http://chengfeng96.com/blog/2018/01/19/Docker中的鏡像/ 作者: HazzaCheng)
同時發現其他比較好的資料:
1. https://yeasy.gitbooks.io/docker_practice/repository/dockerhub.html