docker入門——構建鏡像

前面咱們已經介紹瞭如何拉取已經構建好的帶有定製內容的Docker鏡像,那麼如何構建本身的鏡像呢?html

構建Docker鏡像有如下兩種方法:python

  • 使用docker commit命令。
  • 使用docker build命令和 Dockerfile 文件。

在這裏並不推薦使用docker commit來構建鏡像,而應該使用更靈活、更強大的Dockerfile來構建Docker鏡像。可是爲了對Docker有一個更全面的瞭解,仍是會先介紹如下如何使用docker commit構建Docker鏡像。以後將重點介紹Docker所推薦的鏡像構建方法:編寫Dockerfile以後使用docker build命令。nginx

通常來講,咱們不是真正的「建立」新鏡像,而是基於一個已有的基礎鏡像,如ubuntu或centos等,構建新鏡像而已。若是真的想從零構建一個全新的鏡像,也能夠參考https://docs.docker.com/engine/userguide/eng-image/baseimages/。git

經過commit命令建立鏡像

docker commit 構建鏡像能夠想象爲是在往版本控制系統裏提交變動。咱們先建立一個容器,並在容器裏作出修改,就像修改代碼同樣,最後再將修改提交爲一個鏡像。github

# 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命令:web

# 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

也能夠在提交鏡像時指定更多的數據(包括標籤)來詳細描述所作的修改。shell

# docker commit -m="A new custom image" --author="Bourbon Tian" b437ffe4d630 test/apache2:webserver
27fc508c41d1180b1a421380d755cf00f9dfb6b0d354b9eccaec94ae58a06675

這條命令裏,咱們指定了更多的信息選項:apache

  • -m 用來指定建立鏡像的提交信息;
  • --author 用來列出該鏡像的做者信息;
  • 最後在test/apache2後面增長了一個webserver標籤。

經過使用docker inspect命令來查看新建立的鏡像的詳細信息:ubuntu

# 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:/#
相關文章
相關標籤/搜索