[轉]Docker中的鏡像

引言


  這篇文章中咱們主要來探討下Docker鏡像,它是用來啓動容器的構建基石,本文的所用到的Dcoker版本是17.1,API版本是1.33,Go的版本是1.9.2,OS是基於Arch Linux的Manjaro。html

Docker鏡像的概念


  總的來講,Docker鏡像是由文件系統疊加而成的。java

bootfs

  最底端是一個引導文件系統,即bootfs,這很像典型的Linux/Unix的引導文件系統。Docker用戶幾乎永遠不會和引導文件系統有什麼交互。實際上,當一個容器啓動後,它將會被移到內存中,而引導文件系統則會被卸載(unmount),以留出更多的內存供initrd磁盤鏡像使用。mysql

rootfs

  Docker看起來很像一個典型的Linux虛擬化棧。實際上,Docker鏡像的第二層是root文件系統rootfs,它位於引導文件系統之上。rootfs能夠是一種或多種操做系統(如Centos或者Ubuntu系統)。nginx

只讀與聯合加載

  在傳統的Linux引導過程當中,root文件系統會最早以只讀的方式加載,當引導結束並完成了完整性檢查以後,它纔會被切換爲讀寫模式。可是在Docker裏,root文件系統永遠只能是隻讀狀態,而且Docker利用聯合加載(union mount)技術又會在root文件系統層上加載更多的只讀文件系統。聯合加載指的是一次同時加載多個文件系統,可是在外面看起來只能看到一個文件系統。聯合加載會將各層文件系統疊加到一塊兒,這樣最終的文件系統會包含全部底層的文件和目錄。git

鏡像

  Docker將這樣的文件系統稱爲鏡像。一個鏡像能夠放到另外一個鏡像的頂部。位於下面的鏡像稱爲父鏡像(parent image),能夠依次類推,直到鏡像棧的最底部,最底部的鏡像稱爲基礎鏡像(base image)。最後,當從一個鏡像啓動容器時,Docker會在該鏡像的最頂層加載一個讀寫文件系統。咱們想在Docker中運行的程序就是在這個讀寫層中執行的。下圖是一張示意圖:
The docker filesystem layersgithub

寫時複製

  當Docker第一次啓動一個容器時,初始的讀寫層是空的。當文件系統發生變化時,這些變化都會應用到這一層上。好比,若是想修改一個文件,這個文件首先會從該讀寫層下面的只讀層複製到該讀寫層。該文件的只讀版本依然存在,可是已經被讀寫層中的該文件副本所隱藏。   一般這種機制被稱爲寫時複製(copy on write),這也是使Docker如此強大的技術之一。每一個只讀鏡像層都是隻讀的,而且之後永遠不會變化。當建立一個新容器時,Docker會構建出一個鏡像棧,並在棧的最頂端添加一個讀寫層。這個讀寫層再加上其下面的鏡像層以及一些配置數據,就構成了一個容器。在上一章咱們已經知道,容器是能夠修改的,它們都有本身的狀態,而且是能夠啓動和中止的。容器的這種特色加上鏡像分層框架(image-layering framework),使咱們能夠快速構建鏡像並運行包含咱們本身的應用程序和服務的容器。web

列出鏡像


  咱們可使用docker images命令來列出全部鏡像。sql

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB

 

  本地鏡像都保存在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.0四、12.十、13.0四、13.10和14.04的鏡像)。
  咱們能夠用docker pull來拉取ubuntu倉庫中的Ubuntu12.04的鏡像。docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:12.04
12.04: Pulling from library/ubuntu
d8868e50ac4c: Pull complete
83251ac64627: Pull complete
589bba2f1b36: Pull complete
d62ecaceda39: Pull complete
6d93b41cfc6b: Pull complete
Digest: sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
Status: Downloaded newer image for ubuntu:12.04

hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB
ubuntu 12.04 5b117edd0b76 7 months ago 104MB

 

  咱們再用docker images命令來看一下,發現已經獲得了Ubuntu的latest鏡像和12.04鏡像。這代表ubuntu鏡像其實是彙集在一個倉庫下的一系列鏡像。
  咱們看到Docker提供了TAG來區分同一個倉庫中的不一樣鏡像,如12.0四、12.十、quantal或者precise等。每一個標籤對組成特定鏡像的一些鏡像層進行標記,好比,標籤12.04就是對全部Ubuntu 12.04鏡像的層的標記。這種機制使得在同一個倉庫中能夠存儲多個鏡像。shell

1
2
3
4
5
6
7
8
9
10
hazzacheng@hazzacheng-pc ~> sudo docker pull ubuntu:precise
precise: Pulling from library/ubuntu
Digest: sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
Status: Downloaded newer image for ubuntu:precise

hazzacheng@hazzacheng-pc ~> sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 20c44cd7596f 2 weeks ago 123MB
ubuntu 12.04 5b117edd0b76 7 months ago 104MB
ubuntu precise 5b117edd0b76 7 months ago 104MB

 

  咱們看一下上面的例子,咱們能夠經過在倉庫名後面加上一個冒號和標籤名來指定該倉庫中的某一鏡像,在咱們的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
2
3
4
5
hazzacheng@hazzacheng-pc ~> sudo docker pull fedora:20
20: Pulling from library/fedora
4abd98c7489c: Pull complete
Digest: sha256:5d5a02b873d298da9bca4b84440c5cd698b0832560c850d92cf389cef58bc549
Status: Downloaded newer image for fedora:20

 

  咱們也能夠經過docker images命令來只查看fedora的鏡像:

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker images fedora
REPOSITORY TAG IMAGE ID CREATED SIZE
fedora 20 ba74bddb630e 15 months ago 291MB

 

  由於Docker Hub實在太慢了,還常常被牆,因此咱們通常從國內的鏡像源pull,例如網易蜂巢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo docker pull hub.c.163.com/public/centos:7.2.1511
7.2.1511: Pulling from public/centos
a3ed95caeb02: Pull complete
545d7a0a5d69: Pull complete
391b1a24f697: Pull complete
7925fb64b42b: Pull complete
66edcdbd7fd5: Pull complete
0046acf831b2: Pull complete
b9bc9910c14f: Pull complete
3965c1da099d: Pull complete
2192a5a11821: Pull complete
38535f20ee00: Pull complete
d74d10b0ba21: Pull complete
1da8ab1a04c1: Pull complete
Digest: sha256:0bc43868ac6553fce2c2af378dc370ccab92bf68edc45224216447f58f651fd4
Status: Downloaded newer image for hub.c.163.com/public/centos:7.2.1511

 

  咱們也能夠用阿里雲的鏡像加速來進行加速,具體作法請Google。

查找鏡像


  能夠經過docker search查找全部Docker Hub上的公共的可用鏡像,例如咱們搜索下mysql:

1
2
3
4
5
6
hazzacheng@hazzacheng-pc ~> sudo docker search mysql
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relati... 5327 [OK]
mariadb MariaDB is a community-developed fork of M... 1657 [OK]
mysql/mysql-server Optimized MySQL Server Docker images. Crea... 370 [OK]
...

 

  它返回了以下信息:

  • 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.jsonHOME/.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
2
3
hazzacheng@hazzacheng-pc ~> mkdir static_web
hazzacheng@hazzacheng-pc ~> cd static_web/
hazzacheng@hazzacheng-pc ~/static_web> touch Dockerfile

 

  咱們建立了一個名爲static_web的目錄用來保存Dockerfile,這個目錄就是咱們的構建環境(build environment),Docker則稱此環境爲上下文(context)或者構建上下文(build context)。Docker會在構建鏡像時將構建上下文和該上下文中的文件和目錄上傳到Docker守護進程。這樣Docker守護進程就能直接訪問用戶想在鏡像中存儲的任何代碼、文件或者其餘數據。
  輸入Dockerfile的內容:

1
2
3
4
5
6
# Version: 0.0.1  
FROM ubuntu:latest
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
RUN apt-get update && apt-get install -y nginx 
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html 
EXPOSE 80

 

  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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
hazzacheng@hazzacheng-pc ~/static_web> sudo docker build -t="hazzacheng/static_web:v1" .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu:latest
16.04: Pulling from library/ubuntu
Digest: sha256:7c67a2206d3c04703e5c23518707bdd4916c057562dd51c74b99b2ba26af0f79
Status: Downloaded newer image for ubuntu:16.04
---> 20c44cd7596f
Step 2/5 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 35ead1a7e3eb
---> a3c3c3fa91b7
Step 3/5 : RUN apt-get update && apt-get install -y nginx
---> Running in 1b386b6b18e2
...
hazzacheng@hazzacheng-pc ~/static_web> sudo docker build -t="hazzacheng/static_web:v1" .
Sending build context to Docker daemon 2.048kB
Step 1/5 : FROM ubuntu:latest
16.04: Pulling from library/ubuntu
Digest: sha256:7c67a2206d3c04703e5c23518707bdd4916c057562dd51c74b99b2ba26af0f79
Status: Downloaded newer image for ubuntu:latest
---> 20c44cd7596f
Step 2/5 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 35ead1a7e3eb
---> a3c3c3fa91b7
Step 3/5 : RUN apt-get update && apt-get install -y nginx
---> Running in 1b386b6b18e2
...
---> 7466d1df3456
Step 4/5 : RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
---> Running in c52e4b9e5e62
---> 95063d4e2b0e
Step 5/5 : EXPOSE 80
---> Running in 841cb52e0856
---> 165e3539bd98
Removing intermediate container 35ead1a7e3eb
Removing intermediate container 1b386b6b18e2
Removing intermediate container c52e4b9e5e62
Removing intermediate container 841cb52e0856
Successfully built 165e3539bd98
Successfully tagged hazzacheng/static_web:v1

 

  如上所示,咱們經過-t選項爲新鏡像設置了倉庫,名稱和一個標籤。若是沒有制定任何標籤,Docker將會自動爲鏡像設置一個latest標籤。
  咱們還能夠經過Git倉庫來構建Docker鏡像:

1
2
$ sudo docker build -t="hazzacheng/static_web:v1" \
git@github.com:hazzacheng/docker-static_web

 

  這裏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
2
3
4
FROM ubuntu:16.04  
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV REFRESHED_AT 2018-01-16
RUN apt-get -qq update

 

  FROMMAINTAINER都與咱們前面所說的同樣,ENV用來在鏡像中設置環境變量。在這個例子裏,我經過ENV指令來設置了一個名爲REFRESHED_AT的環境變量,這個環境變量用來代表該鏡像模板最後的更新時間。最後,我使用了RUN指令來運行apt-get -qq update命令。該指令運行時將會刷新APT包的緩存,用來確保咱們能將要安裝的每一個軟件包都更新到最新版本。
  有了這個模板,若是想刷新一個構建,只需修改ENV指令中的日期。這使Docker在命中ENV指令時開始重置這個緩存,並運行後續指令而無須依賴該緩存。也就是說,RUN apt-get update這條指令將會被再次執行,包緩存也將會被刷新爲最新內容。
  能夠擴展此模板,好比適配到不一樣的平臺或者添加額外的需求。好比,能夠支持一個fedora鏡像:

1
2
3
4
FROM fedora:20 
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV REFRESHED_AT 2018-01-16
RUN yum -q makecache

 

  若是咱們想深刻探究鏡像是如何構建出來的,可使用docker history:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hazzacheng@hazzacheng-pc ~/D/ubuntu_nginx> sudo docker history hazzacheng/static_web:v1
IMAGE CREATED CREATED BY SIZE COMMENT
19ac4c6d5a01 4 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
c559d8d9c6da 4 minutes ago /bin/sh -c echo 'Hi, I am in your container'… 27B
c5ec0eb43d2f 4 minutes ago /bin/sh -c apt-get install -y nginx 56.5MB
04a2d034d655 5 minutes ago /bin/sh -c apt-get -qq update 39.5MB
f6ccf8510418 7 minutes ago /bin/sh -c #(nop) ENV REFRESHED_AT=2018-01-… 0B
0bd1ce7aa2e5 7 minutes ago /bin/sh -c #(nop) MAINTAINER Hazza Cheng "h… 0B
2a4cca5ac898 38 hours ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 38 hours ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 38 hours ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$… 2.76kB
<missing> 38 hours ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 38 hours ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 38 hours ago /bin/sh -c #(nop) ADD file:affda766655e01cbd… 111MB

 

  從上面的結果能夠看到新構建鏡像的每一層,以及建立這些層的Dockerfile指令。

端口映射

  咱們利用守護方式從鏡像啓動一個容器:

1
2
3
hazzacheng@hazzacheng-pc ~> sudo docker run -d -p 80 --name static_web hazzacheng/static_web:v1 \
nginx -g "daemon off;"
c3ae5b033e273353291c97791f482d005715cdb7a7a7ea8918ae320a11113a84

 

  -p用來控制ocker在運行時應該公開哪些網絡端口給外部(宿主機)。運行一個容器時,Docker能夠經過兩種方法來在宿主機上分配端口。

  • Docker能夠在宿主機上隨機選擇一個位於32768~61000的一個比較大的端口號來映射到容器中的80端口上。
  • 能夠在Docker宿主機中指定一個具體的端口號來映射到容器中的80端口上。

  docker run命令將在Docker宿主機上隨機打開一個端口,這個端口會鏈接到容器中的80端口上。咱們來看一下:

1
2
CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS              PORTS                   NAMES
c3ae5b033e27 hazzacheng/static_web:v1 "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:32768->80/tcp static_web

 

  能夠看到,容器中的80端口映射到了宿主機的32768上,咱們也能夠經過docker port來查看容器的端口映射:

1
2
hazzacheng@hazzacheng-pc ~> sudo docker port static_web 80
0.0.0.0:32768

 

  -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
2
sudo docker run -d -P --name static_web hazzacheng/static_web:v1 \
nginx -g "daemon off;"

 

  該命令會將容器內的80端口對本地宿主機公開,而且綁定到宿主機的一個隨機端口上。該命令會將用來構建該鏡像的Dockerfile文件中EXPOSE指令指定的其餘端口也一併公開。

Dockerfile指令

  咱們介紹一些經常使用的Dockerfile指令,你也能夠經過官方文檔來學習。

CMD

  CMD指令用於指定一個容器啓動時要運行的命令。這有點兒相似於RUN指令,只是RUN指令是指定鏡像被構建時要運行的命令,而CMD是指定容器被啓動時要運行的命令。須要注意的是,要運行的命令是存放在一個數組結構中的。這將告訴Docker按指定的原樣來運行該命令。固然也能夠不使用數組而是指定CMD指令,這時候Docker會在指定的命令前加上/bin/sh -c。這在執行該命令的時候可能會致使意料以外的行爲,因此Docker推薦一直使用以數組語法來設置要執行的命令。例以下面的用法:

1
2
CMD ["/bin/bash"]
CMD ["/bin/bash", "-l"]

 

  使用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
2
ENTRYPOINT ["/usr/sbin/nginx"]  
CMD ["-h"]

 

  若是在啓動容器時不指定任何參數,則在CMD指令中指定的-h參數會被傳遞給Nginx守護進程,即Nginx服務器會以/usr/sbin/nginx -h的方式啓動,該命令用來顯示Nginx的幫助信息。
  這使咱們能夠構建一個鏡像,該鏡像既能夠運行一個默認的命令,同時它也支持經過docker run命令行爲該命令指定可覆蓋的選項或者標誌。
  若是確實須要,用戶也能夠在運行時經過docker run--entrypoint標誌覆蓋ENTRYPOINT指令。

WORKDIR

  WORKDIR指令用來在從鏡像建立一個新容器時,在容器內部設置一個工做目錄,ENTRYPOINTCMD指定的程序會在這個目錄下執行。咱們可使用該指令爲Dockerfile中後續的一系列指令設置工做目錄,也能夠爲特定的指令設置不一樣的工做目錄:

1
2
3
4
WORKDIR /opt/webapp/db  
RUN bundle install 
WORKDIR /opt/webapp 
ENTRYPOINT [ "rackup" ]

 

  能夠經過-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
2
3
4
5
6
USER user  
USER user:group 
USER uid 
USER uid:gid 
USER user:gid 
USER uid:group

 

  也能夠在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、bzip二、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
2
LABEL version="1.0" 
LABEL location="New York" type="Data Center" role="Web Server"

 

  LABEL指令以label=「value」的形式出現。能夠在每一條指令中指定一個元數據,或者指定多個元數據,不一樣的元數據之間用空格分隔。推薦將全部的元數據都放到一條LABEL指令中,以防止不一樣的元數據指令建立過多鏡像層。能夠經過docker inspect命令來查看Docker鏡像中的標籤信息。

STOPSIGNAL

  STOPSIGNAL指令用來設置中止容器時發送什麼系統調用信號給容器。這個信號必須是內核系統調用表中合法的數,如9,或者SIGNAME格式中的信號名稱,如SIGKILL。

ARG

  ARG指令用來定義能夠在docker build命令運行時傳遞給構建運行時的變量,咱們只須要在構建時使用--build-arg標誌便可。用戶只能在構建時指定在Dockerfile文件中定義過的參數。

1
2
ARG build 
ARG webapp_user=user

 

  上面例子中第二條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
2
3
4
5
6
7
8
HTTP_PROXY 
http_proxy
HTTPS_PROXY
https_proxy
FTP_PROXY
ftp_proxy
NO_PROXY
no_proxy

 

  要想使用這些預約義的變量,只須要給docker build命令傳遞--build-arg <variable>=<value>標誌就能夠了。

ONBUILD

  ONBUILD指令能爲鏡像添加觸發器(trigger)。當一個鏡像被用作其餘鏡像的基礎鏡像時,例如用戶的鏡像須要從某未準備好的位置添加源代碼,或者用戶須要執行特定於構建鏡像的環境的構建腳本,該鏡像中的觸發器將會被執行。
  觸發器會在構建過程當中插入新指令,咱們能夠認爲這些指令是緊跟在FROM以後指定的。觸發器能夠是任何構建指令:

1
2
ONBUILD ADD . /app/src  
ONBUILD RUN cd /app/src && make

 

  上面的代碼將會在建立的鏡像中加入ONBUILD觸發器,ONBUILD指令能夠在鏡像上運行docker inspect命令來查看。   例如,咱們爲Apache2鏡像構建一個全新的Dockerfile,該鏡像名爲hazzacheng/ apache2`:

1
2
3
4
5
6
7
8
9
10
FROM ubuntu:14.04  
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
RUN apt-get -qq update && apt-get -qq install -y apache2 
ENV APACHE_RUN_USER www-data 
ENV APACHE_RUN_GROUP www-data 
ENV APACHE_LOG_DIR /var/log/apache2 
ONBUILD ADD . /var/www/ 
EXPOSE 80 
ENTRYPOINT ["/usr/sbin/apache2"] 
CMD ["-D", "FOREGROUND"]

 

  構建該鏡像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
hazzacheng@hazzacheng-pc ~/D/apache2> sudo docker build -t="hazzacheng/apache2:1.0" .
Sending build context to Docker daemon 2.048kB
Step 1/10 : FROM ubuntu:14.04
---> 02a63d8b2bfa
Step 2/10 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Using cache
---> 5958e7d2e6ee
Step 3/10 : RUN apt-get -qq update && apt-get -qq install -y apache2
---> Running in 6602e39c1d1c
...
Removing intermediate container 6602e39c1d1c
---> 27bc656a7f72
Step 4/10 : ENV APACHE_RUN_USER www-data
---> Running in 039181b26294
Removing intermediate container 039181b26294
---> 859f7fde9c1e
Step 5/10 : ENV APACHE_RUN_GROUP www-data
---> Running in 7c8fb7c92363
Removing intermediate container 7c8fb7c92363
---> 38e48e99d7a4
Step 6/10 : ENV APACHE_LOG_DIR /var/log/apache2
---> Running in a9b51450c5f7
Removing intermediate container a9b51450c5f7
---> 13a9b236ab6c
Step 7/10 : ONBUILD ADD . /var/www/
---> Running in a13a07301846
Removing intermediate container a13a07301846
---> 9324a7ba8a37
Step 8/10 : EXPOSE 80
---> Running in fedb23f77928
Removing intermediate container fedb23f77928
---> e3c98bab1099
Step 9/10 : ENTRYPOINT ["/usr/sbin/apache2"]
---> Running in d7a1a4307afc
Removing intermediate container d7a1a4307afc
---> bb2f359867fc
Step 10/10 : CMD ["-D", "FOREGROUND"]
---> Running in 9b5a3977ab5e
Removing intermediate container 9b5a3977ab5e
---> bcf933e3caec
Successfully built bcf933e3caec
Successfully tagged hazzacheng/apache2:1.0

 

  在新構建的鏡像中包含一條ONBUILD指令,該指令會使用ADD指令將構建環境所在的目錄下的內容所有添加到鏡像中的/var/www/目錄下。咱們能夠垂手可得地將這個Dockerfile做爲一個通用的Web應用程序的模板,能夠基於這個模板來構建Web應用程序。
  咱們能夠經過構建一個名爲webapp的鏡像來看看如何使用鏡像模板功能。它的Dockerfile如代碼以下:

1
2
3
4
FROM hazzacheng/apache2:1.0
MAINTAINER Hazza Cheng "hazzacheng@gmail.com" 
ENV APPLICATION_NAME webapp 
ENV ENVIRONMENT development

 

  讓咱們看看構建這個鏡像時將會發生什麼事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker build -t="hazzacheng/webapp:1.0" .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM hazzacheng/apache2:1.0
# Executing 1 build trigger
---> 68b20313194d
Step 2/4 : MAINTAINER Hazza Cheng "hazzacheng@gmail.com"
---> Running in 5834328436f9
Removing intermediate container 5834328436f9
---> 5ae1b436c698
Step 3/4 : ENV APPLICATION_NAME webapp
---> Running in 852154b72d50
Removing intermediate container 852154b72d50
---> 2e3b1b38b4d9
Step 4/4 : ENV ENVIRONMENT development
---> Running in 2e43a07c66a1
Removing intermediate container 2e43a07c66a1
---> 32dfacc87317
Successfully built 32dfacc87317
Successfully tagged hazzacheng/webapp:1.0

 

  能夠清楚地看到,在FROM指令以後,Docker插入了一條ADD指令,這條ADD指令就是在ONBUILD觸發器中指定的。執行完該ADD指令後,Docker纔會繼續執行構建文件中的後續指令。這種機制使我每次都會將本地源代碼添加到鏡像,就像上面咱們作到的那樣,也支持我爲不一樣的應用程序進行一些特定的配置或者設置構建信息。這時,能夠將hazzacheng/apache2當作一個鏡像模板。
  ONBUILD觸發器會按照在父鏡像中指定的順序執行,而且只能被繼承一次,也就是說只能在子鏡像中執行,而不會在孫子鏡像中執行。若是咱們再基於hazzacheng/apache2構建一個鏡像,則新鏡像是hazzacheng/apache2的孫子鏡像,所以在該鏡像的構建過程當中,ONBUILD觸發器是不會被執行的。
  這裏有好幾條指令是不能用在ONBUILD指令中的,包括FROMMAINTAINERONBUILD自己。之因此這麼規定是爲了防止在 Dockerfile構建過程當中產生遞歸調用的問題。

提交鏡像


  鏡像構建完畢以後,咱們也能夠將它上傳到Docker Hub上面去,也能夠推送到其餘的Registry,例如網易蜂巢,這樣其餘人就能使用這個鏡像了。好比,咱們能夠在組織內共享這個鏡像,或者徹底公開這個鏡像。
  這裏咱們將其推送到網易蜂巢上去:

1
2
3
4
5
6
7
8
9
10
11
12
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker tag hazzacheng/static_web:v1 hub.c.163.com/hazzacheng/static_web:v1
hazzacheng@hazzacheng-pc ~/D/webapp> sudo docker push hub.c.163.com/hazzacheng/static_web:v1
The push refers to repository [hub.c.163.com/hazzacheng/static_web]
a37eded3e1c4: Pushed
8c1b6608d69f: Pushed
71e0479ba020: Pushed
8600ee70176b: Pushed
2bbb3cec611d: Pushed
d2bb1fc88136: Pushed
a6a01ad8b53f: Pushed
833649a3e04c: Pushed
v1: digest: sha256:899e70dd39817ea95acc5ae01d335108bbd4cc5969daf4e965deeb30e8f30460 size: 8626

 

  再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

相關文章
相關標籤/搜索