前面我們已經介紹了如何拉取已經構建好的帶有定制內容的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/。
通過commit命令創建鏡像
docker commit 構建鏡像可以想象為是在往版本控制系統里提交變更。我們先創建一個容器,並在容器里做出修改,就像修改代碼一樣,最后再將修改提交為一個鏡像。
# docker run -i -t ubuntu /bin/bash root@b437ffe4d630:/# apt-get -yqq update root@b437ffe4d630:/# apt-get -y install apache2
我們啟動了一個容器,並在里面安裝了Apache。我們會將這個容器作為一個Web服務器來運行,所以我們想把它的當前狀態保存下來。這樣我們就不必每次都創建一個新容器並再次在里面安裝Apache了。為了完成此項工作,需要先使用exit命令從容器里退出,之后再運行docker commit命令:
# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b437ffe4d630 ubuntu "/bin/bash" 45 minutes ago Exited (0) 10 seconds ago clever_pare b87f9dde62b0 devopsil/puppet "/bin/bash" 2 days ago Up 2 days evil_archimedes # docker commit b437ffe4d630 test/apache2 9c30616364f44a519571709690e3c92a5cad4ad01c007d8126eb6d63670d33f4 # docker images test/apache2 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE test/apache2 latest 9c30616364f4 36 seconds ago 254.4 MB
在使用docker commit命令中,指定了要提交的修改過的容器的ID(可以通過docker ps命令得到剛創建的容器ID),以及一個目標鏡像倉庫和鏡像名,這里是test/apahce2。需要注意的是,docker commit提交的只是創建容器的鏡像與容器的當前狀態之間有差異的部分,這使得該更新非常輕量。通過docker images 可以查看新創建的鏡像信息。
也可以在提交鏡像時指定更多的數據(包括標簽)來詳細描述所做的修改。
# docker commit -m="A new custom image" --author="Bourbon Tian" b437ffe4d630 test/apache2:webserver 27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675
這條命令里,我們指定了更多的信息選項:
- -m 用來指定創建鏡像的提交信息;
- --author 用來列出該鏡像的作者信息;
- 最后在test/apache2后面增加了一個webserver標簽。
通過使用docker inspect命令來查看新創建的鏡像的詳細信息:
# docker inspect test/apache2:webserver [ { "Id": "27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675", "Parent": "f5bb94a8fac47aaf15fb4e4ceb138d59ac2fcf004cd3f277cebe2174fd7a6c70", "Comment": "A new custom image", "Created": "2017-05-17T07:29:46.000512241Z", "Container": "b437ffe4d63047dd34653f5256bb6eda54acfd3db99f72f2262a9b9af7f31334", ...
如果想從剛創建的鏡像運行一個容器,可以使用docker run命令:
# docker run -t -i test/apache2:webserver /bin/bash
創建Dockerfile文件
下面將介紹如何通過Dockerfile的定義文件和docker build命令來構建鏡像。
Dockerfile使用基本的基於DSL語法的指令來構建一個Docker鏡像,之后使用docker build命令基於該Dockerfile中的指令構建一個新的鏡像。
# mkdir /opt/static_web # cd /opt/static_web/ # vim Dockerfile
首先創建一個名為static_web的目錄用來保存Dockerfile,這個目錄就是我們的構建環境(build environment),Docker則稱此環境為上下文(context)或者構建上下文(build context)。Docker會在構建鏡像時將構建上下文和該上下文中的文件和目錄上傳到Docker守護進程。這樣Docker守護進程就能直接訪問你想在鏡像中存儲的任何代碼、文件或者其他數據。這里我們還創建了一個Dockerfile文件,我們將用它構建一個能作為Web服務器的Docker鏡像。
# Version: 0.0.1 FROM ubuntu:latest MAINTAINER Bourbon Tian "bourbon@1mcloud.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由一系列指令和參數組成。每條指令都必須為大寫字母,切后面要跟隨一個參數。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指令,通過一個數組的方式指定要運行的命令和傳遞給該命令的每個參數:
RUN ["apt-get", "install", "-y", "nginx"]
EXPOSE:
EXPOSE指令是告訴Docker該容器內的應用程序將會使用容器的指定端口。這並不意味着可以自動訪問任意容器運行中服務的端口。出於安全的原因,Docker並不會自動打開該端口,而是需要你在使用docker run運行容器時來指定需要打開哪些端口。
可以指定多個EXPOSE指令來向外部公開多個端口,Docker也使用EXPOSE指令來幫助將多個容器鏈接,在后面的學習過程中我們會接觸到。
基於Dockerfile構建新鏡像
執行docker build命令時,Dockerfile中的所有指令都會被執行並且提交,並且在該命令成功結束后返回一個新鏡像。
# cd static_web # docker build -t="test/static_web" . Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon ... Successfully built 94728651ce15
- -t選項為新鏡像設置了倉庫和名稱,這里倉庫為test,鏡像名為static_web。建議為自己的鏡像設置合適的名字方便以后追蹤和管理
也可以在構建鏡像的過程當中為鏡像設置一個標簽:
# docker build -t="test/static_web:v1" .
上面命令中最后的“.”告訴Docker到當前目錄中去找Dockerfile文件。也可以指定一個Git倉庫地址來指定Dockerfile的位置,這里Docker假設在Git倉庫的根目錄下存在Dockerfile文件:
# docker build -t="test/static_web:v1" git@github.com:test/static_web
再回到docker build過程。可以看到構建上下文已經上傳到Docker守護進程:
Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon
提示:如果在構建上下文的根目錄下存在以.dockerignore命名的文件的話,那么該文件內容會被按行進行分割,每一行都是一條文件過濾匹配模式。這非常像.gitignore文件,該文件用來設置哪些文件不會被上傳到構建上下文中去。該文件中模式的匹配規則采用了Go語言中的filepath。
之后,可以看到Dockerfile中的每條指令會被順序執行,而作為構建過程中最終結果,返回了新鏡像的ID,即94728651ce15。構建的每一步及其對應指令都會獨立運行,並且在輸出最終鏡像ID之前,Docker會提交每步的構建結果。
指令失敗時會怎樣?
假設我們將安裝的軟件包名字弄錯,比如寫成ngin,再次運行docker build:
# docker build -t="test/static_web" . Sending build context to Docker daemon 2.048 kB Sending build context to Docker daemon Step 0 : FROM ubuntu:latest ---> f5bb94a8fac4 Step 1 : MAINTAINER Bourbon Tian "bourbon@1mcloud.com" ---> Using cache ---> ce64f2e75a74 Step 2 : RUN apt-get update ---> Using cache ---> e98d2c152d1d Step 3 : RUN apt-get install -y ngin ---> Running in 2f16c5f11250 Reading package lists... Building dependency tree... Reading state information... E: Unable to locate package ngin The command '/bin/sh -c apt-get install -y ngin' returned a non-zero code: 100
這時我們需要調試一下這次失敗,我們可以通過docker run命令來基於這次構建到目前為止已經成功的最后一步創建一個容器,這里它的ID是e98d2c152d1d:
# docker run -t -i e98d2c152d1d /bin/bash root@55aee4322f77:/# apt-get install -y ngin Reading package lists... Done Building dependency tree Reading state information... Done E: Unable to locate package ngin
再次運行出錯的指令apt-get install -y ngin,發現這里沒有找到ngin包,我們執行安裝nginx包時,包命輸錯了。這時退出容器使用正確的包名修改Dockerfile文件,之后再嘗試進行構建。
構建緩存:
在上面執行構建鏡像的過程中,我們發現當執行apt-get update時,返回Using cache。Docker會將之前的鏡像層看做緩存,因為在安裝nginx前並沒有做其他的修改,因此Docker會將之前構建時創建的鏡像當做緩存並作為新的開始點。然后,有些時候需要確保構建過程不會使用緩存。可以使用docker build 的 --no-cache標志。
# docker build --no-cache -t="test/static_web" .
構建緩存帶來的一個好處就是,我們可以實現簡單的Dockerfile模板(比如在Dockerfile文件頂部增加包倉庫或者更新包,從而盡可能確保緩存命中)。
# cat Dockerfile # Version: 0.0.1 FROM ubuntu:latest MAINTAINER Bourbon Tian "bourbon@1mcloud.com" ENV REFRESHED_AT 2017-05-18 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
- ENV 在鏡像中設置環境變量,在這里設置了一個名為REFRESHED_AT的環境變量,這個環境變量用來表明該鏡像模板最后的更新時間,這樣只需要修改ENV指令中的日期,這使Docker在命中ENV指令時開始重置這個緩存,並運行后續指令而無需依賴該緩存。也就是說,RUN apt-get update這條指令將會被再次執行,包緩存也將會被刷新為最新內容。
查看新鏡像:
現在來看一下新構建的鏡像,使用docker image命令,如果想深入探求鏡像如何構建出來的,可以使用docker history命令看到新構建的test/static_web鏡像的每一層,以及創建這些層的Dockerfile指令。
# docker images test/static_web REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE test/static_web latest 94728651ce15 20 hours ago 212.1 MB # docker history 94728651ce15 IMAGE CREATED CREATED BY SIZE COMMENT 94728651ce15 20 hours ago /bin/sh -c #(nop) EXPOSE 80/tcp 0 B 09e999b131f4 20 hours ago /bin/sh -c echo 'Hi, I am in your container' 27 B 4af2ef04fb91 20 hours ago /bin/sh -c apt-get install -y nginx 56.52 MB e98d2c152d1d 20 hours ago /bin/sh -c apt-get update 38.29 MB ...
從新鏡像啟動容器
下面基於新構建的鏡像啟動一個新容器,來檢查之前的構建工作是否一切正常:
# docker run -d -p 80 --name static_web test/static_web nginx -g "daemon off;" a4ad951b2ef91275bb918d11964d7d60889608efa3958e699030d38a681ba35e
- -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命令查看容得的端口分配情況:
# docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0b422bbcce10 test/static_web "nginx -g 'daemon of 5 seconds ago Up 5 seconds 0.0.0.0:32772->80/tcp static_web
如果沒有啟動成功,則通過交互的方式進入我們新創建的鏡像中,嘗試啟動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:
# Version: 0.0.1 FROM ubuntu:latest MAINTAINER Bourbon Tian "bourbon@1mcloud.com" ENV REFRESHED_AT 2017-05-18 RUN apt-get update RUN apt-get install -y nginx RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html RUN sed -i '22d' /etc/nginx/sites-enabled/default EXPOSE 80
重新嘗試構建我們的容器,再次啟動我們新建的容器,通過docker ps -l查看是否正常啟動了。
我們也可以通過docker port 來查看容器的端口映射情況:
# docker port 0b422bbcce10 80 0.0.0.0:32772
在上面的命令中我們指定了想要查看映射情況的容器ID和容器的端口號,這里是80。該命令返回了宿主機中映射的端口,即32772。
-p選項還讓我們可以靈活地管理容器和宿主機之間的端口映射關系。比如,可以指定將容器中的端口映射到Docker宿主機的某一個特定的端口上:
# docker run -d -p 80:80 --name static_web test/static_web nginx -g "daemon off;" ee09ef811a9865d9bd50c71b3ddcbd414194031f14145fdbaf339d92e3ccd1bd
上面的命令會將容器內的80端口綁定到本地宿主機的80端口上。我們也可以將端口綁定限制在特定的網絡接口(即ip地址)上:
# docker run -d -p 127.0.0.1:80:80 --name static_web test/static_web nginx -g "daemon off;"
我們也可以使用類似的方法將容器內的80端口綁定到一個特定網絡接口的隨機端口上:
# docker run -d -p 127.0.0.1::80 --name static_web test/static_web nginx -g "daemon off;"
Docker還提供了一個更簡單的方式,即-P參數,該參數可以用來對外公開在Dockfile中的EXPOSE指令中設置的所有端口:
# docker run -d -P --name static_web test/static_web nginx -g "daemon off;" 4fd632e975ad5e47a487e5e23790124da0826886dc24b2497a561d274e4e698e # docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4fd632e975ad test/static_web "nginx -g 'daemon of 4 seconds ago Up 3 seconds 0.0.0.0:32773->80/tcp static_web
該命令會將容器內的80端口對本地宿主機公開,並且綁定到宿主機的一個隨機端口上。該命令會將用來構建該鏡像的Dockerfile文件中EXPOSE指令指定的其他端口也一並公開。
# curl localhost:32773 Hi, I am in your container
到這,就完成了一個非常簡單的基於Docker的Web服務器。
刪除鏡像
我們可以通過docker rmi命令來刪除一個鏡像
# docker rmi test/webapp Untagged: test/webapp:latest Deleted: 36ae30d2e972f6651b29127266d68783290e3a861b974c5a491e04ae7e9a9d3d Deleted: 8cecce09465bc0f2679fd96e1c6e1af03af9c4589b62113d319f24ca969d9164 Deleted: 29c803cce363f84801bd8b8c768bba8767c37947e803c8ae58541d163622ccfa Deleted: 92a79034071552c09f45ffb1afc455150edc438d4c7da48b28ca6a2dba44d15b
這里我們刪除了test/webapp(在附錄Dockerfile指令這個章節中構建的)鏡像。在這里也可以看到Docker的分層文件系統:每個Deleted都代表一個鏡像層被刪除。該操作只會將本地的鏡像刪除。如果我們想刪除本地的所有鏡像可以像這樣:
# docker rmi `docker images -a -q`
運行自己的Docker Registry
前面我們已經介紹了Docker有公共的Docker Registry就是Docker Hub。但是有時我們可能希望構建和存儲包含不想被公開的信息或數據的鏡像。這時候我們有以下兩種選擇:
- 利用Docker Hub上的私有倉庫;
- 在防火牆后面運行自己的Registry。
從Docker容器安裝一個Registry非常簡單
## 拉去registry鏡像 # docker pull registry ## 搭建本地鏡像源 # docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest ## 查看容器狀態 # docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f570fab5d67d registry:latest "/entrypoint.sh /etc 3 seconds ago Up 3 seconds 0.0.0.0:5000->5000/tcp registry
接下來將我們的鏡像上傳到本地的Docker Registry
## 找到我們要上傳的鏡像 # docker images test/apache2 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE test/apache2 latest 9c30616364f4 7 days ago 254.4 MB ## 使用新的Registry給該鏡像打上標簽 # docker tag 9c30616364f4 docker.example.com:5000/test/apache2 ## 通過docker push 命令將它推送到新的Registry中去 # docker push docker.example.com:5000/test/apache2 The push refers to a repository [docker.example.com:5000/test/apache2] (len: 1) 9c30616364f4: Image already exists f5bb94a8fac4: Image successfully pushed 2e36b30057ab: Image successfully pushed 0346cecb4e51: Image successfully pushed 274da7f89b05: Image successfully pushed b5ce920a148c: Image successfully pushed 576b12d1aa01: Image successfully pushed Digest: sha256:0c22a559f8dea881bca046e0ca27a01f73aa5f3c153b08b8bdf3306082e48b72 ## 測試我們上傳的鏡像 # docker run -it docker.example.com:5000/test/apache2 /bin/bash root@5088a0fd20e8:/#