前面我們已經介紹了如何拉取已經建構好的帶有定制内容的Docker鏡像,那麼如何建構自己的鏡像呢?
建構Docker鏡像有以下兩種方法:
使用docker commit指令。
使用docker build指令和 Dockerfile 檔案。
在這裡并不推薦使用docker commit來建構鏡像,而應該使用更靈活、更強大的Dockerfile來建構Docker鏡像。但是為了對Docker有一個更全面的了解,還是會先介紹以下如何使用docker commit建構Docker鏡像。之後将重點介紹Docker所推薦的鏡像建構方法:編寫Dockerfile之後使用docker build指令。
一般來說,我們不是真正的“建立”新鏡像,而是基于一個已有的基礎鏡像,如ubuntu或centos等,建構新鏡像而已。如果真的想從零建構一個全新的鏡像,也可以參考https://docs.docker.com/engine/userguide/eng-image/baseimages/。
docker commit 建構鏡像可以想象為是在往版本控制系統裡送出變更。我們先建立一個容器,并在容器裡做出修改,就像修改代碼一樣,最後再将修改送出為一個鏡像。
?
1
2
3
<code># docker run -i -t ubuntu /bin/bash</code>
<code>root@b437ffe4d630:</code><code>/</code><code># apt-get -yqq update</code>
<code>root@b437ffe4d630:</code><code>/</code><code># apt-get -y install apache2</code>
我們啟動了一個容器,并在裡面安裝了Apache。我們會将這個容器作為一個Web伺服器來運作,是以我們想把它的目前狀态儲存下來。這樣我們就不必每次都建立一個新容器并再次在裡面安裝Apache了。為了完成此項工作,需要先使用exit指令從容器裡退出,之後再運作docker commit指令:
4
5
6
7
8
9
10
11
<code># docker ps -a</code>
<code>CONTAINER</code><code>ID</code> <code>IMAGE COMMAND CREATED STATUS PORTS NAMES</code>
<code>b437ffe4d630 ubuntu </code><code>"/bin/bash"</code> <code>45</code> <code>minutes ago Exited (</code><code>0</code><code>)</code><code>10</code> <code>seconds ago clever_pare </code>
<code>b87f9dde62b0 devopsil</code><code>/</code><code>puppet </code><code>"/bin/bash"</code> <code>2</code> <code>days ago Up</code><code>2</code> <code>days evil_archimedes </code>
<code># docker commit b437ffe4d630 test/apache2</code>
<code>9c30616364f44a519571709690e3c92a5cad4ad01c007d8126eb6d63670d33f4</code>
<code># docker images test/apache2</code>
<code>REPOSITORY TAG IMAGE</code><code>ID</code> <code>CREATED VIRTUAL SIZE</code>
<code>test</code><code>/</code><code>apache2 latest </code><code>9c30616364f4</code> <code>36</code> <code>seconds ago </code><code>254.4</code> <code>MB</code>
在使用docker commit指令中,指定了要送出的修改過的容器的ID(可以通過docker ps指令得到剛建立的容器ID),以及一個目标鏡像倉庫和鏡像名,這裡是test/apahce2。需要注意的是,docker commit送出的隻是建立容器的鏡像與容器的目前狀态之間有差異的部分,這使得該更新非常輕量。通過docker images 可以檢視新建立的鏡像資訊。
也可以在送出鏡像時指定更多的資料(包括标簽)來較長的描述所做的修改。
<code># docker commit -m="A new custom image" --author="Bourbon Tian" b437ffe4d630 test/apache2:webserver</code>
<code>27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675</code>
這條指令裡,我們指定了更多的資訊選項:
-m 用來指定建立鏡像的送出資訊;
--author 用來列出該鏡像的作者資訊;
最後在test/apache2後面增加了一個webserver标簽。
通過使用docker inspect指令來檢視新建立的鏡像的詳細資訊:
<code># docker inspect test/apache2:webserver</code>
<code>[</code>
<code>{</code>
<code> </code><code>"Id"</code><code>:</code><code>"27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675"</code><code>,</code>
<code> </code><code>"Parent"</code><code>:</code><code>"f5bb94a8fac47aaf15fb4e4ceb138d59ac2fcf004cd3f277cebe2174fd7a6c70"</code><code>,</code>
<code> </code><code>"Comment"</code><code>:</code><code>"A new custom image"</code><code>,</code>
<code> </code><code>"Created"</code><code>:</code><code>"2017-05-17T07:29:46.000512241Z"</code><code>,</code>
<code> </code><code>"Container"</code><code>:</code><code>"b437ffe4d63047dd34653f5256bb6eda54acfd3db99f72f2262a9b9af7f31334"</code><code>,</code>
<code>...</code>
如果想從剛建立的鏡像運作一個容器,可以使用docker run指令:
<code># docker run -t -i test/apache2:webserver /bin/bash</code>
下面将介紹如何通過Dockerfile的定義檔案和docker build指令來建構鏡像。
Dockerfile使用基本的基于DSL文法的指令來建構一個Docker鏡像,之後使用docker build指令基于該Dockerfile中的指令建構一個新的鏡像。
<code># mkdir /opt/static_web</code>
<code># cd /opt/static_web/</code>
<code># vim Dockerfile</code>
首先建立一個名為static_web的目錄用來儲存Dockerfile,這個目錄就是我們的建構環境(build environment),Docker則稱此環境為上下文(context)或者建構上下文(build context)。Docker會在建構鏡像時将建構上下文和該上下文中的檔案和目錄上傳到Docker守護程序。這樣Docker守護程序就能直接通路你想在鏡像中存儲的任何代碼、檔案或者其他資料。這裡我們還建立了一個Dockerfile檔案,我們将用它建構一個能作為Web伺服器的Docker鏡像。
<code># Version: 0.0.1</code>
<code>FROM ubuntu:latest</code>
<code>MAINTAINER Bourbon Tian</code><code>"[email protected]"</code>
<code>RUN apt</code><code>-</code><code>get update</code>
<code>RUN apt</code><code>-</code><code>get install</code><code>-</code><code>y nginx</code>
<code>RUN echo</code><code>'Hi, I am in your container'</code> <code>></code><code>/</code><code>usr</code><code>/</code><code>share</code><code>/</code><code>nginx</code><code>/</code><code>html</code><code>/</code><code>index.html</code>
<code>EXPOSE</code><code>80</code>
Dockerfile由一系列指令和參數組成。每條指令都必須為大寫字母,切後面要跟随一個參數。Dockerfile中的指令會按照順序從上到下執行,是以應該根據需要合理安排指令的順序。每條指令都會建立一個新的鏡像層并對鏡像進行送出。Docker大體上按照如下流程執行Dockerfile中的指令。
Docker從基礎鏡像運作一個容器。
執行第一條指令,對容器進行修改。
執行類似docker commit的操作,送出一個新的鏡像層。
Docker再基于剛送出的鏡像運作一個新的容器。
執行Dockerfile中的下一條指令,直到所有指令都執行完畢。
從上面可以看出,如果你的Dockerfile由于某些原因(如某條指令失敗了)沒有正常結束,那你也可以得到一個可以使用的鏡像。這對調試非常有幫助:可以基于該鏡像運作一個具備互動功能的容器,使用最後建立的鏡像對為什麼你的指令會失敗進行調試。
Dockerfile也支援注釋。以#開頭的行都會被認為是注釋,# Version: 0.0.1這就是個注釋
FROM:
每個Dockerfile的第一條指令都應該是FROM。FROM指令指定一個已經存在的鏡像,後續指令都是将基于該鏡像進行,這個鏡像被稱為基礎鏡像(base iamge)。在這裡ubuntu:latest就是作為新鏡像的基礎鏡像。也就是說Dockerfile建構的新鏡像将以ubuntu:latest作業系統為基礎。在運作一個容器時,必須要指明是基于哪個基礎鏡像在進行建構。
MAINTAINER:
MAINTAINER指令,這條指令會告訴Docker該鏡像的作者是誰,以及作者的郵箱位址。這有助于表示鏡像的所有者和聯系方式
RUN:
在這些指令之後,我們指定了三條RUN指令。RUN指令會在目前鏡像中運作指定的指令。這裡我們通過RUN指令更新了APT倉庫,安裝nginx包,并建立了一個index.html檔案。像前面說的那樣,每條RUN指令都會建立一個新的鏡像層,如果該指令執行成功,就會将此鏡像層送出,之後繼續執行Dockerfile中的下一個指令。
預設情況下,RUN指令會在shell裡使用指令包裝器/bin/sh -c 來執行。如果是在一個不支援shell的平台上運作或者不希望在shell中運作(比如避免shell字元串篡改),也可以使用exec格式的RUN指令,通過一個數組的方式指定要運作的指令和傳遞給該指令的每個參數:
<code>RUN [</code><code>"apt-get"</code><code>,</code><code>"install"</code><code>,</code><code>"-y"</code><code>,</code><code>"nginx"</code><code>]</code>
EXPOSE:
EXPOSE指令是告訴Docker該容器内的應用程式将會使用容器的指定端口。這并不意味着可以自動通路任意容器運作中服務的端口。出于安全的原因,Docker并不會自動打開該端口,而是需要你在使用docker run運作容器時來指定需要打開哪些端口。
可以指定多個EXPOSE指令來向外部公開多個端口,Docker也使用EXPOSE指令來幫助将多個容器連結,在後面的學習過程中我們會接觸到。
執行docker build指令時,Dockerfile中的所有指令都會被執行并且送出,并且在該指令成功結束後傳回一個新鏡像。
<code># cd static_web</code>
<code># docker build -t="test/static_web" .</code>
<code>Sending build context to Docker daemon</code><code>2.048</code> <code>kB</code>
<code>Sending build context to Docker daemon</code>
<code>Successfully built</code><code>94728651ce15</code>
-t選項為新鏡像設定了倉庫和名稱,這裡倉庫為test,鏡像名為static_web。建議為自己的鏡像設定合适的名字友善以後追蹤和管理
也可以在建構鏡像的過程當中為鏡像設定一個标簽:
<code># docker build -t="test/static_web:v1" .</code>
上面指令中最後的“.”告訴Docker到目前目錄中去找Dockerfile檔案。也可以指定一個Git倉庫位址來指定Dockerfile的位置,這裡Docker假設在Git倉庫的根目錄下存在Dockerfile檔案:
<code># docker build -t="test/static_web:v1" [email protected]:test/static_web</code>
再回到docker build過程。可以看到建構上下文已經上傳到Docker守護程序:
提示:如果在建構上下文的根目錄下存在以.dockerignore命名的檔案的話,那麼該檔案内容會被按行進行分割,每一行都是一條檔案過濾比對模式。這非常像.gitignore檔案,該檔案用來設定哪些檔案不會被上傳到建構上下文中去。該檔案中模式的比對規則采用了Go語言中的filepath。
之後,可以看到Dockerfile中的每條指令會被順序執行,而作為建構過程中最終結果,傳回了新鏡像的ID,即94728651ce15。建構的每一步及其對應指令都會獨立運作,并且在輸出最終鏡像ID之前,Docker會送出每步的建構結果。
指令失敗時會怎樣?
假設我們将安裝的軟體包名字弄錯,比如寫成ngin,再次運作docker build:
12
13
14
15
16
17
18
<code>Step</code><code>0</code> <code>: FROM ubuntu:latest</code>
<code> </code><code>-</code><code>-</code><code>-</code><code>> f5bb94a8fac4</code>
<code>Step</code><code>1</code> <code>: MAINTAINER Bourbon Tian</code><code>"[email protected]"</code>
<code> </code><code>-</code><code>-</code><code>-</code><code>> Using cache</code>
<code> </code><code>-</code><code>-</code><code>-</code><code>> ce64f2e75a74</code>
<code>Step</code><code>2</code> <code>: RUN apt</code><code>-</code><code>get update</code>
<code> </code><code>-</code><code>-</code><code>-</code><code>> e98d2c152d1d</code>
<code>Step</code><code>3</code> <code>: RUN apt</code><code>-</code><code>get install</code><code>-</code><code>y ngin</code>
<code> </code><code>-</code><code>-</code><code>-</code><code>> Running</code><code>in</code> <code>2f16c5f11250</code>
<code>Reading package lists...</code>
<code>Building dependency tree...</code>
<code>Reading state information...</code>
<code>E: Unable to locate package ngin</code>
<code>The command</code><code>'/bin/sh -c apt-get install -y ngin'</code> <code>returned a non</code><code>-</code><code>zero code:</code><code>100</code>
這時我們需要調試一下這次失敗,我們可以通過docker run指令來基于這次建構到目前為止已經成功的最後一步建立一個容器,這裡它的ID是e98d2c152d1d:
<code># docker run -t -i e98d2c152d1d /bin/bash</code>
<code>root@</code><code>55aee4322f77</code><code>:</code><code>/</code><code># apt-get install -y ngin</code>
<code>Reading package lists... Done</code>
<code>Building dependency tree </code>
<code>Reading state information... Done</code>
再次運作出錯的指令apt-get install -y ngin,發現這裡沒有找到ngin包,我們執行安裝nginx包時,包命輸錯了。這時退出容器使用正确的包名修改Dockerfile檔案,之後再嘗試進行建構。
建構緩存:
在上面執行建構鏡像的過程中,我們發現當執行apt-get update時,傳回Using cache。Docker會将之前的鏡像層看做緩存,因為在安裝nginx前并沒有做其他的修改,是以Docker會将之前建構時建立的鏡像當做緩存并作為新的開始點。然後,有些時候需要確定建構過程不會使用緩存。可以使用docker build 的 --no-cache标志。
<code># docker build --no-cache -t="test/static_web" .</code>
建構緩存帶來的一個好處就是,我們可以實作簡單的Dockerfile模闆(比如在Dockerfile檔案頂部增加包倉庫或者更新包,進而盡可能確定緩存命中)。
<code># cat Dockerfile</code>
<code>ENV REFRESHED_AT</code><code>2017</code><code>-</code><code>05</code><code>-</code><code>18</code>
ENV 在鏡像中設定環境變量,在這裡設定了一個名為REFRESHED_AT的環境變量,這個環境變量用來表明該鏡像模闆最後的更新時間,這樣隻需要修改ENV指令中的日期,這使Docker在命中ENV指令時開始重置這個緩存,并運作後續指令而無需依賴該緩存。也就是說,RUN apt-get update這條指令将會被再次執行,包緩存也将會被重新整理為最新内容。
檢視新鏡像:
現在來看一下新建構的鏡像,使用docker image指令,如果想深入探求鏡像如何建構出來的,可以使用docker history指令看到新建構的test/static_web鏡像的每一層,以及建立這些層的Dockerfile指令。
<code># docker images test/static_web</code>
<code>test</code><code>/</code><code>static_web latest </code><code>94728651ce15</code> <code>20</code> <code>hours ago </code><code>212.1</code> <code>MB</code>
<code># docker history 94728651ce15</code>
<code>IMAGE CREATED CREATED BY SIZE COMMENT</code>
<code>94728651ce15</code> <code>20</code> <code>hours ago </code><code>/</code><code>bin</code><code>/</code><code>sh</code><code>-</code><code>c</code><code>#(nop) EXPOSE 80/tcp 0 B </code>
<code>09e999b131f4</code> <code>20</code> <code>hours ago </code><code>/</code><code>bin</code><code>/</code><code>sh</code><code>-</code><code>c echo</code><code>'Hi, I am in your container'</code> <code>27</code> <code>B </code>
<code>4af2ef04fb91</code> <code>20</code> <code>hours ago </code><code>/</code><code>bin</code><code>/</code><code>sh</code><code>-</code><code>c apt</code><code>-</code><code>get install</code><code>-</code><code>y nginx </code><code>56.52</code> <code>MB </code>
<code>e98d2c152d1d </code><code>20</code> <code>hours ago </code><code>/</code><code>bin</code><code>/</code><code>sh</code><code>-</code><code>c apt</code><code>-</code><code>get update </code><code>38.29</code> <code>MB </code>
下面基于新建構的鏡像啟動一個新容器,來檢查之前的建構工作是否一切正常:
<code># docker run -d -p 80 --name static_web test/static_web nginx -g "daemon off;"</code>
<code>a4ad951b2ef91275bb918d11964d7d60889608efa3958e699030d38a681ba35e</code>
-d選項,告訴Docker以分離(detached)的方式在背景運作。這種方式非常适合運作類似Nginx守護程序這樣的需要長時間運作的程序。
這裡也指定了需要在容器中運作的指令:nginx -g "daemon off;"。這将以前台運作的方式啟動Nginx,來作為我們的Web伺服器。
-p選項,控制Docker在運作時應該公開哪些網絡端口給外部(主控端)。運作一個容器時,Docker可通過兩種方法在主控端上配置設定端口。
Docker可以在主控端上通過/proc/sys/net/ipv4/ip_local_port_range檔案随機一個端口映射到容器的80端口。
可以在Docker主控端中指定一個具體的端口号來映射到容器的80端口上。
這将在Docker主控端上随機打開一個端口,這個端口會連接配接到容器中的80端口上。可以使用docker ps指令檢視容得的端口配置設定情況:
<code># docker ps -l</code>
<code>CONTAINER</code><code>ID</code> <code>IMAGE COMMAND CREATED STATUS PORTS NAMES</code>
<code>0b422bbcce10</code> <code>test</code><code>/</code><code>static_web "nginx</code><code>-</code><code>g 'daemon of </code><code>5</code> <code>seconds ago Up</code><code>5</code> <code>seconds </code><code>0.0</code><code>.</code><code>0.0</code><code>:</code><code>32772</code><code>-</code><code>></code><code>80</code><code>/</code><code>tcp static_web</code>
如果沒有啟動成功,則通過互動的方式進入我們新建立的鏡像中,嘗試啟動nginx,通過分析錯誤日志查出不能正常啟動的原因,在這裡我遇到的問題是:
nginx: [emerg] socket() [::]:80 failed (97: Address family not supported by protocol)
我們需要删除/etc/nginx/sites-enabled/default 中 listen [::]:80 ipv6only=on default_server;定位到問題,我們退出容器,重新修改我們的Dockerfile:
<code>RUN sed</code><code>-</code><code>i</code><code>'22d'</code> <code>/</code><code>etc</code><code>/</code><code>nginx</code><code>/</code><code>sites</code><code>-</code><code>enabled</code><code>/</code><code>default</code>
重新嘗試建構我們的容器,再次啟動我們建立的容器,通過docker ps -l檢視是否正常啟動了。
我們也可以通過docker port 來檢視容器的端口映射情況:
<code># docker port 0b422bbcce10 80</code>
<code>0.0</code><code>.</code><code>0.0</code><code>:</code><code>32772</code>
在上面的指令中我們指定了想要檢視映射情況的容器ID和容器的端口号,這裡是80。該指令傳回了主控端中映射的端口,即32772。
-p選項還讓我們可以靈活地管理容器和主控端之間的端口映射關系。比如,可以指定将容器中的端口映射到Docker主控端的某一個特定的端口上:
<code># docker run -d -p 80:80 --name static_web test/static_web nginx -g "daemon off;"</code>
<code>ee09ef811a9865d9bd50c71b3ddcbd414194031f14145fdbaf339d92e3ccd1bd</code>
上面的指令會将容器内的80端口綁定到本地主控端的80端口上。我們也可以将端口綁定限制在特定的網絡接口(即ip位址)上:
<code># docker run -d -p 127.0.0.1:80:80 --name static_web test/static_web nginx -g "daemon off;"</code>
我們也可以使用類似的方法将容器内的80端口綁定到一個特定網絡接口的随機端口上:
<code># docker run -d -p 127.0.0.1::80 --name static_web test/static_web nginx -g "daemon off;"</code>
Docker還提供了一個更簡單的方式,即-P參數,該參數可以用來對外公開在Dockfile中的EXPOSE指令中設定的所有端口:
<code># docker run -d -P --name static_web test/static_web nginx -g "daemon off;"</code>
<code>4fd632e975ad5e47a487e5e23790124da0826886dc24b2497a561d274e4e698e</code>
<code>4fd632e975ad</code> <code>test</code><code>/</code><code>static_web "nginx</code><code>-</code><code>g 'daemon of </code><code>4</code> <code>seconds ago Up</code><code>3</code> <code>seconds </code><code>0.0</code><code>.</code><code>0.0</code><code>:</code><code>32773</code><code>-</code><code>></code><code>80</code><code>/</code><code>tcp static_web</code>
該指令會将容器内的80端口對本地主控端公開,并且綁定到主控端的一個随機端口上。該指令會将用來建構該鏡像的Dockerfile檔案中EXPOSE指令指定的其他端口也一并公開。
<code># curl localhost:32773</code>
<code>Hi, I am</code><code>in</code> <code>your container</code>
到這,就完成了一個非常簡單的基于Docker的Web伺服器。
我們可以通過docker rmi指令來删除一個鏡像
<code># docker rmi test/webapp</code>
<code>Untagged: test</code><code>/</code><code>webapp:latest</code>
<code>Deleted:</code><code>36ae30d2e972f6651b29127266d68783290e3a861b974c5a491e04ae7e9a9d3d</code>
<code>Deleted:</code><code>8cecce09465bc0f2679fd96e1c6e1af03af9c4589b62113d319f24ca969d9164</code>
<code>Deleted:</code><code>29c803cce363f84801bd8b8c768bba8767c37947e803c8ae58541d163622ccfa</code>
<code>Deleted:</code><code>92a79034071552c09f45ffb1afc455150edc438d4c7da48b28ca6a2dba44d15b</code>
這裡我們删除了test/webapp(在附錄Dockerfile指令這個章節中建構的)鏡像。在這裡也可以看到Docker的分層檔案系統:每個Deleted都代表一個鏡像層被删除。該操作隻會将本地的鏡像删除。如果我們想删除本地的所有鏡像可以像這樣:
<code># docker rmi `docker images -a -q`</code>
前面我們已經介紹了Docker有公共的Docker Registry就是Docker Hub。但是有時我們可能希望建構和存儲包含不想被公開的資訊或資料的鏡像。這時候我們有以下兩種選擇:
利用Docker Hub上的私有倉庫;
在防火牆後面運作自己的Registry。
從Docker容器安裝一個Registry非常簡單
<code>## 拉去registry鏡像</code>
<code># docker pull registry</code>
<code>## 搭建本地鏡像源</code>
<code># docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest</code>
<code>## 檢視容器狀态</code>
<code>CONTAINER</code><code>ID</code> <code>IMAGE COMMAND CREATED STATUS PORTS NAMES</code>
<code>f570fab5d67d registry:latest "</code><code>/</code><code>entrypoint.sh</code><code>/</code><code>etc </code><code>3</code> <code>seconds ago Up</code><code>3</code> <code>seconds </code><code>0.0</code><code>.</code><code>0.0</code><code>:</code><code>5000</code><code>-</code><code>></code><code>5000</code><code>/</code><code>tcp registry</code>
接下來将我們的鏡像上傳到本地的Docker Registry
19
20
21
22
23
<code>## 找到我們要上傳的鏡像</code>
<code>test</code><code>/</code><code>apache2 latest </code><code>9c30616364f4</code> <code>7</code> <code>days ago </code><code>254.4</code> <code>MB</code>
<code>## 使用新的Registry給該鏡像打上标簽</code>
<code># docker tag 9c30616364f4 docker.example.com:5000/test/apache2</code>
<code>## 通過docker push 指令将它推送到新的Registry中去</code>
<code># docker push docker.example.com:5000/test/apache2</code>
<code>The push refers to a repository [docker.example.com:</code><code>5000</code><code>/</code><code>test</code><code>/</code><code>apache2] (</code><code>len</code><code>:</code><code>1</code><code>)</code>
<code>9c30616364f4</code><code>: Image already exists</code>
<code>f5bb94a8fac4: Image successfully pushed</code>
<code>2e36b30057ab</code><code>: Image successfully pushed</code>
<code>0346cecb4e51</code><code>: Image successfully pushed</code>
<code>274da7f89b05</code><code>: Image successfully pushed</code>
<code>b5ce920a148c: Image successfully pushed</code>
<code>576b12d1aa01</code><code>: Image successfully pushed</code>
<code>Digest: sha256:</code><code>0c22a559f8dea881bca046e0ca27a01f73aa5f3c153b08b8bdf3306082e48b72</code>
<code>## 測試我們上傳的鏡像</code>
<code># docker run -it docker.example.com:5000/test/apache2 /bin/bash</code>
<code>root@</code><code>5088a0fd20e8</code><code>:</code><code>/</code><code>#</code>