Docker學習文件

2020-08-12 20:01:26

Docker

Build once,Run anywhere

Docker 使用 Google 公司推出的 Go 語言 進行開發實現,基於 Linux 內核的 cgroup,namespace,以及
AUFS 類的 Union FS 等技術,對進程進行封裝隔離,屬於操作系統層面的虛擬化技術。由於隔離的進程獨立於宿主和其它的隔離的進程,因此也稱其爲容器。最初實現是基於 LXC,從 0.7 版本以後開始去除 LXC,轉而使用自行開發的 libcontainer,從 1.11 開始,則進一步演進爲使用 runC 和 containerd。

什麼是 Docker

Docker 最初是 dotCloud 公司創始人 Solomon Hykes 在法國期間發起的一個公司內部專案,它是基於 dotCloud 公司多年雲服務技術的一次革新,並於 2013 年 3 月以 Apache 2.0 授權協定開源 ,主要專案程式碼在 GitHub 上進行維護。Docker 專案後來還加入了 Linux 基金會,併成立推動 開放容器聯盟(OCI)

Docker 自開源後受到廣泛的關注和討論,至今其 GitHub 專案已經超過 4 萬 6 千個星標和一萬多個 fork。甚至由於 Docker 專案的火爆,在 2013 年底,dotCloud 公司決定改名爲 Docker 。Docker 最初是在 Ubuntu 12.04 上開發實現的;Red Hat 則從 RHEL 6.5 開始對 Docker 進行支援;Google 也在其 PaaS 產品中廣泛應用 Docker。

Docker 使用 Google 公司推出的 Go 語言 進行開發實現,基於 Linux 內核的 cgroup namespace ,以及 AUFS 類的 Union FS 等技術,對進程進行封裝隔離,屬於 操作系統層面的虛擬化技術 。由於隔離的進程獨立於宿主和其它的隔離的進程,因此也稱其爲容器。最初實現是基於 LXC ,從 0.7 版本以後開始去除 LXC,轉而使用自行開發的 libcontainer ,從 1.11 開始,則進一步演進爲使用 runC containerd

Docker 在容器的基礎上,進行了進一步的封裝,從檔案系統、網路互聯到進程隔離等等,極大的簡化了容器的建立和維護。使得 Docker 技術比虛擬機器技術更爲輕便、快捷。

下面 下麪的圖片比較了 Docker 和傳統虛擬化方式的不同之處。傳統虛擬機器技術是虛擬出一套硬體後,在其上執行一個完整操作系統,在該系統上再執行所需應用進程;而容器內的應用進程直接執行於宿主的內核,容器內沒有自己的內核,而且也沒有進行硬體虛擬。因此容器要比傳統虛擬機器更爲輕便。

在这里插入图片描述
在这里插入图片描述

爲什麼要使用 Docker

作爲一種新興的虛擬化方式,Docker 跟傳統的虛擬化方式相比具有衆多的優勢。

更高效的利用系統資源

由於容器不需要進行硬體虛擬以及執行完整操作系統等額外開銷,Docker 對系統資源的利用率更高。無論是應用執行速度、記憶體損耗或者檔案儲存速度,都要比傳統虛擬機器技術更高效。因此,相比虛擬機器技術,一個相同設定的主機,往往可以執行更多數量的應用。

更快速的啓動時間

傳統的虛擬機器技術啓動應用服務往往需要數分鐘,而 Docker 容器應用,由於直接執行於宿主內核,無需啓動完整的操作系統,因此可以做到秒級、甚至毫秒級的啓動時間。大大的節約了開發、測試、部署的時間。

一致的執行環境

開發過程中一個常見的問題是環境一致性問題。由於開發環境、測試環境、生產環境不一致,導致有些 bug 並未在開發過程中被發現。而 Docker 的映象提供了除內核外完整的執行時環境,確保了應用執行環境一致性,從而不會再出現 「這段程式碼在我機器上沒問題啊」 這類問題。

持續交付和部署

對開發和運維(DevOps)人員來說,最希望的就是一次建立或設定,可以在任意地方正常執行。

使用 Docker 可以通過定製應用映象來實現持續整合、持續交付、部署。開發人員可以通過 Dockerfile 來進行映象構建,並結合 持續整合(Continuous Integration)

系統進行整合測試,而運維人員則可以直接在生產環境中快速部署該映象,甚至結合持續部署(Continuous Delivery/Deployment)系統進行自動部署。

而且使用 Dockerfile 使映象構建透明化,不僅僅開發團隊可以理解應用執行環境,也方便運維團隊理解應用執行所需條件,幫助更好的生產環境中部署該映象。

更輕鬆的遷移

由於 Docker 確保了執行環境的一致性,使得應用的遷移更加容易。Docker 可以在很多平臺上執行,無論是物理機、虛擬機器、公有雲、私有雲,甚至是筆電,其執行結果是一致的。因此使用者可以很輕易的將在一個平臺上執行的應用,遷移到另一個平臺上,而不用擔心執行環境的變化導致應用無法正常執行的情況。

更輕鬆的維護和擴充套件

Docker 使用的分層儲存以及映象的技術,使得應用重複部分的複用更爲容易,也使得應用的維護更新更加簡單,基於基礎映象進一步擴充套件映象也變得非常簡單。此外,Docker 團隊同各個開源專案團隊一起維護了一大批高品質的 官方映象,既可以直接在生產環境使用,又可以作爲基礎進一步定製,大大的降低了應用服務的映象製作成本。

對比傳統虛擬機器總結

特性 容器 虛擬機器
啓動 秒級 分鐘級
硬碟使用 一般爲 MB 一般爲 GB
效能 接近原生 弱於
系統支援量 單機支援上千個容器 一般幾十個

Docker 基本概念

Docker 包括三個基本概念

  • 映象(Image
  • 容器(Container
  • 倉庫(Repository

理解了這三個概念,就理解了 Docker 的整個生命週期。

Docker 引擎

Docker 引擎是一個包含以下主要元件的用戶端伺服器應用程式。

  • 一種伺服器,它是一種稱爲守護行程並且長時間執行的程式。
  • REST API用於指定程式可以用來與守護行程通訊的介面,並指示它做什麼。
  • 一個有命令列介面 (CLI) 工具的用戶端。

Docker 引擎元件的流程如下圖所示:

在这里插入图片描述

Docker 系統架構

Docker 使用用戶端-伺服器 (C/S) 架構模式,使用遠端 API 來管理和建立 Docker 容器。

Docker 容器通過 Docker 映象來建立。

容器與映象的關係類似於物件導向程式設計中的物件與類。

Docker 物件導向
容器 物件
映象

在这里插入图片描述

標題 說明
映象(Images) Docker 映象是用於建立 Docker 容器的模板。
容器(Container) 容器是獨立執行的一個或一組應用。
用戶端(Client) Docker 用戶端通過命令列或者其他工具使用 Docker API (https://docs.docker.com/reference/api/docker_remote_api ) 與 Docker 的守護行程通訊。
主機(Host) 一個物理或者虛擬的機器用於執行 Docker 守護行程和容器。
倉庫(Registry) Docker 倉庫用來儲存映象,可以理解爲程式碼控制中的程式碼倉庫。Docker Hub(https://hub.docker.com ) 提供了龐大的映象集合供使用。
Docker Machine Docker Machine是一個簡化Docker安裝的命令列工具,通過一個簡單的命令列即可在相應的平臺上安裝Docker,比如VirtualBox、 Digital Ocean、Microsoft Azure。

Docker 映象

我們都知道,操作系統分爲內核和使用者空間。對於 Linux 而言,內核啓動後,會掛載 root 檔案系統爲其提供使用者空間支援。而 Docker 映象(Image),就相當於是一個 root 檔案系統。比如官方映象 ubuntu:16.04 就包含了完整的一套 Ubuntu 16.04 最小系統的 root 檔案系統。

Docker 映象是一個特殊的檔案系統,除了提供容器執行時所需的程式、庫、資源、設定等檔案外,還包含了一些爲執行時準備的一些設定參數(如匿名卷、環境變數、使用者等)。映象不包含任何動態數據,其內容在構建之後也不會被改變。

分層儲存

因爲映象包含操作系統完整的 root 檔案系統,其體積往往是龐大的,因此在 Docker 設計時,就充分利用 Union FS的技術,將其設計爲分層儲存的架構。所以嚴格來說,映象並非是像一個 ISO 那樣的打包檔案,映象只是一個虛擬的概念,其實際體現並非由一個檔案組成,而是由一組檔案系統組成,或者說,由多層檔案系統聯合組成。

映象構建時,會一層層構建,前一層是後一層的基礎。每一層構建完就不會再發生改變,後一層上的任何改變只發生在自己這一層。比如,刪除前一層檔案的操作,實際不是真的刪除前一層的檔案,而是僅在當前層標記爲該檔案已刪除。在最終容器執行的時候,雖然不會看到這個檔案,但是實際上該檔案會一直跟隨映象。因此,在構建映象的時候,需要額外小心,每一層儘量只包含該層需要新增的東西,任何額外的東西應該在該層構建結束前清理掉。

分層儲存的特徵還使得映象的複用、定製變的更爲容易。甚至可以用之前構建好的映象作爲基礎層,然後進一步新增新的層,以定製自己所需的內容,構建新的映象。

Docker 容器

映象(Image)和容器(Container)的關係,就像是物件導向程式設計中的 範例 一樣,映象是靜態的定義,容器是映象執行時的實體。容器可以被建立、啓動、停止、刪除、暫停等。

容器的實質是進程,但與直接在宿主執行的進程不同,容器進程執行於屬於自己的獨立的 名稱空間 。因此容器可以擁有自己的 root檔案系統、自己的網路設定、自己的進程空間,甚至自己的使用者 ID空間。容器內的進程是執行在一個隔離的環境裡,使用起來,就好像是在一個獨立於宿主的系統下操作一樣。這種特性使得容器封裝的應用比直接在宿主執行更加安全。也因爲這種隔離的特性,很多人初學Docker 時常常會混淆容器和虛擬機器。

前面講過映象使用的是分層儲存,容器也是如此。每一個容器執行時,是以映象爲基礎層,在其上建立一個當前容器的儲存層,我們可以稱這個爲容器執行時讀寫而準備的儲存層爲容器儲存層

容器儲存層的生存週期和容器一樣,容器消亡時,容器儲存層也隨之消亡。因此,任何儲存於容器儲存層的資訊都會隨容器刪除而丟失。

按照 Docker 最佳實踐的要求,容器不應該向其儲存層內寫入任何數據,容器儲存層要保持無狀態化。所有的檔案寫入操作,都應該使用 數據卷(Volume)、或者系結宿主目錄,在這些位置的讀寫會跳過容器儲存層,直接對宿主(或網路儲存)發生讀寫,其效能和穩定性更高。

數據卷的生存週期獨立於容器,容器消亡,數據卷不會消亡。因此,使用數據卷後,容器刪除或者重新執行之後,數據卻不會丟失。

Docker 倉庫

映象構建完成後,可以很容易的在當前宿主機上執行,但是,如果需要在其它伺服器上使用這個映象,我們就需要一個集中的儲存、分發映象的服務,Docker Registry 就是這樣的服務。

一個 Docker Registry 中可以包含多個倉庫Repository);每個倉庫可以包含多個標籤Tag);每個標籤對應一個映象。

通常,一個倉庫會包含同一個軟體不同版本的映象,而標籤就常用於對應該軟體的各個版本。我們可以通過 <倉庫名>:<標籤> 的格式來指定具體是這個軟體哪個版本的映象。如果不給出標籤,將以 latest 作爲預設標籤。

Ubuntu 映象 爲例,ubuntu 是倉庫的名字,其內包含有不同的版本標籤,如,14.04, 16.04。我們可以通過 ubuntu:14.04,或者 ubuntu:16.04 來具體指定所需哪個版本的映象。如果忽略了標籤,比如 ubuntu,那將視爲 ubuntu:latest

倉庫名經常以 兩段式路徑 形式出現,比如 jwilder/nginx-proxy,前者往往意味着 Docker Registry 多使用者環境下的使用者名稱,後者則往往是對應的軟體名。但這並非絕對,取決於所使用的具體 Docker Registry 的軟體或服務。

公有 Docker Registry

Docker Registry 公開服務是開放給使用者使用、允許使用者管理映象的 Registry 服務。一般這類公開服務允許使用者免費上傳、下載公開的映象,並可能提供收費服務供使用者管理私有映象。

最常使用的 Registry 公開服務是官方的 Docker Hub ,這也是預設的 Registry,並擁有大量的高品質的官方映象。除此以外,還有 CoreOS Quay.io ,CoreOS 相關的映象儲存在這裏;Google 的 Google Container Registry Kubernetes 的映象使用的就是這個服務。

由於某些原因,在國內存取這些服務可能會比較慢。國內的一些雲服務商提供了針對 Docker Hub 的映象服務(Registry Mirror),這些映象服務被稱爲加速器。常見的有 阿裡雲加速器 DaoCloud 加速器 等。使用加速器會直接從國內的地址下載 Docker Hub 的映象,比直接從 Docker Hub 下載速度會提高很多。

國內也有一些雲服務商提供類似於 Docker Hub 的公開服務。比如 時速雲映象倉庫 網易雲映象服務 DaoCloud 映象市場 阿裡雲映象庫 等。

私有 Docker Registry

除了使用公開服務外,使用者還可以在本地搭建私有 Docker Registry。Docker 官方提供了 Docker Registry 映象,可以直接使用做爲私有 Registry 服務。

開源的 Docker Registry 映象只提供了 Docker Registry API 的伺服器端實現,足以支援 docker 命令,不影響使用。但不包含圖形介面,以及映象維護、使用者管理、存取控制等高階功能。在官方的商業化版本 Docker Trusted Registry 中,提供了這些高階功能。

除了官方的 Docker Registry 外,還有第三方軟體實現了 Docker Registry API,甚至提供了用戶介面以及一些高階功能。比如,VMWare Harbor Sonatype Nexus

安裝 Docker

Docker 在 1.13 版本之後,從 2017 年的 3 月 1 日開始,版本命名規則變爲如下:

專案 說明
版本格式 YY.MM
Stable 版本 每個季度發行
Edge 版本 每個月發行

同時 Docker 劃分爲 CE 和 EE。CE 即社羣版(免費,支援週期三個月),EE 即企業版,強調安全,付費使用。

Docker CE 每月發佈一個 Edge 版本 (17.03, 17.04, 17.05…),每三個月發佈一個 Stable 版本 (17.03, 17.06, 17.09…),Docker EE 和 Stable 版本號保持一致,但每個版本提供一年維護。

官方網站上有各種環境下的 安裝指南 ,這裏主要介紹 Docker CE 在 Linux 、Windows 10 (PC) 和 macOS 上的安裝。

也可以從Docker中國學習

Centos7 安裝 Docker

1、Docker 要求 CentOS 系統的內核版本高於 3.10 ,檢視本頁面的前提條件來驗證你的CentOS 版本是否支援 Docker 。

通過 uname -r 命令檢視你當前的內核版本

 $ uname -r

2、使用 root 許可權登錄 Centos。確保 yum 包更新到最新。

$ sudo yum update

3、舊版本的 Docker 稱爲 docker 或者 docker-engine,使用以下命令解除安裝舊版本:

$ sudo yum remove docker  docker-common docker-selinux docker-engine

或者

$ sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-selinux \
                  docker-engine-selinux \
                  docker-engine

4、安裝需要的軟體包, yum-util 提供yum-config-manager功能,另外兩個是devicemapper驅動依賴的

$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2

5、設定yum源

$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

或者
$ sudo yum-config-manager \
    --add-repo \
    https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo
    
# 官方源
# $ sudo yum-config-manager \
#     --add-repo \
#     https://download.docker.com/linux/centos/docker-ce.repo

在这里插入图片描述

6、可以檢視所有倉庫中所有docker版本,並選擇特定版本安裝

$ yum list docker-ce --showduplicates | sort -r

在这里插入图片描述

7、安裝docker

$ sudo yum install docker-ce  #由於repo中預設只開啓stable倉庫,故這裏安裝的是最新穩定版17.12.0
$ sudo yum install <FQPN>  # 例如:sudo yum install docker-ce-17.12.0.ce

在这里插入图片描述

8、啓動並加入開機啓動

$ sudo systemctl start docker
$ sudo systemctl enable docker

9、驗證安裝是否成功(有client和service兩部分表示docker安裝啓動都成功了)

$ docker version

在这里插入图片描述

或者

# step 1: 安裝必要的一些系統工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# Step 2: 新增軟體源資訊
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# Step 3: 更新並安裝 Docker-CE
sudo yum makecache fast
sudo yum -y install docker-ce
# Step 4: 開啓Docker服務
sudo service docker start

注意:其他注意事項在下面 下麪的註釋中
# 官方軟體源預設啓用了最新的軟體,您可以通過編輯軟體源的方式獲取各個版本的軟體包。例如官方並沒有將測試版本的軟體源置爲可用,你可以通過以下方式開啓。同理可以開啓各種測試版本等。
# vim /etc/yum.repos.d/docker-ce.repo
#   將 [docker-ce-test] 下方的 enabled=0 修改爲 enabled=1
#
# 安裝指定版本的Docker-CE:
# Step 1: 查詢Docker-CE的版本:
# yum list docker-ce.x86_64 --showduplicates | sort -r
#   Loading mirror speeds from cached hostfile
#   Loaded plugins: branch, fastestmirror, langpacks
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            docker-ce-stable
#   docker-ce.x86_64            17.03.1.ce-1.el7.centos            @docker-ce-stable
#   docker-ce.x86_64            17.03.0.ce-1.el7.centos            docker-ce-stable
#   Available Packages
# Step2 : 安裝指定版本的Docker-CE: (VERSION 例如上面的 17.03.0.ce.1-1.el7.centos)
# sudo yum -y install docker-ce-[VERSION]
# 注意:在某些版本之後,docker-ce安裝出現了其他依賴包,如果安裝失敗的話請關注錯誤資訊。例如 docker-ce 17.03 之後,需要先安裝 docker-ce-selinux。
# yum list docker-ce-selinux- --showduplicates | sort -r
# sudo yum -y install docker-ce-selinux-[VERSION]

# 通過經典網路、VPC網路內網安裝時,用以下命令替換Step 2中的命令
# 經典網路:
# sudo yum-config-manager --add-repo http://mirrors.aliyuncs.com/docker-ce/linux/centos/docker-ce.repo
# VPC網路:
# sudo yum-config-manager --add-repo http://mirrors.could.aliyuncs.com/docker-ce/linux/centos/docker-ce.repo

問題

1、因爲之前已經安裝過舊版本的docker,在安裝的時候報錯如下:

Transaction check error:
  file /usr/bin/docker from install of docker-ce-17.12.0.ce-1.el7.centos.x86_64 conflicts with file from package docker-common-2:1.12.6-68.gitec8512b.el7.centos.x86_64
  file /usr/bin/docker-containerd from install of docker-ce-17.12.0.ce-1.el7.centos.x86_64 conflicts with file from package docker-common-2:1.12.6-68.gitec8512b.el7.centos.x86_64
  file /usr/bin/docker-containerd-shim from install of docker-ce-17.12.0.ce-1.el7.centos.x86_64 conflicts with file from package docker-common-2:1.12.6-68.gitec8512b.el7.centos.x86_64
  file /usr/bin/dockerd from install of docker-ce-17.12.0.ce-1.el7.centos.x86_64 conflicts with file from package docker-common-2:1.12.6-68.gitec8512b.el7.centos.x86_64

2、解除安裝舊版本的包

$ sudo yum erase docker-common-2:1.12.6-68.gitec8512b.el7.centos.x86_64

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iO8mn8Vf-1597232562011)(Docker/1107037-20180128103145287-536100760.png)]

3、再次安裝docker

$ sudo yum install docker-ce

映象加速

鑑於國內網路問題,後續拉取 Docker 映象十分緩慢,強烈建議安裝 Docker 之後設定 國內映象加速

新增內核參數

預設設定下,如果在 CentOS 使用 Docker CE 看到下面 下麪的這些警告資訊:

WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled

請新增內核設定參數以啓用這些功能。

$ sudo tee -a /etc/sysctl.conf <<-EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

然後重新載入 sysctl.conf 即可

$ sudo sysctl -p

Docker 映象加速器

國內從 Docker Hub 拉取映象有時會遇到困難,此時可以設定映象加速器。Docker 官方和國內很多雲服務商都提供了國內加速器服務,例如:

我們以 Docker 官方加速器爲例進行介紹。

Ubuntu 14.04、Debian 7 Wheezy

對於使用 upstart 的系統而言,編輯 /etc/default/docker 檔案,在其中的 DOCKER_OPTS 中設定加速器地址:

DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"

重新啓動服務。

$ sudo service docker restart

Ubuntu 16.04+、Debian 8+、CentOS 7

對於使用 systemd的系統,請在 /etc/docker/daemon.json 中寫入如下內容(如果檔案不存在請新建該檔案)

{
  "registry-mirrors": ["https://1sbmxpab.mirror.aliyuncs.com"]
}

注意,一定要保證該檔案符合 json 規範,否則 Docker 將不能啓動。

之後重新啓動服務。

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker

注意:如果您之前檢視舊教學,修改了 docker.service 檔案內容,請去掉您新增的內容(--registry-mirror=https://registry.docker-cn.com),這裏不再贅述。

Windows 10

對於使用 Windows 10 的系統,在系統右下角托盤 Docker 圖示內右鍵選單選擇 Settings,開啓設定視窗後左側導航選單選擇 Daemon。在 Registry mirrors 一欄中填寫加速器地址 https://registry.docker-cn.com,之後點選 Apply 儲存後 Docker 就會重新啓動並應用設定的映象地址了。

macOS

對於使用 macOS 的使用者,在工作列點選 Docker for mac 應用圖示 -> Perferences… -> Daemon -> Registry mirrors。在列表中填寫加速器地址 https://registry.docker-cn.com。修改完成之後,點選 Apply & Restart 按鈕,Docker 就會重新啓動並應用設定的映象地址了。

檢查加速器是否生效

設定加速器之後,如果拉取映象仍然十分緩慢,請手動檢查加速器設定是否生效,在命令列執行 docker info,如果從結果中看到瞭如下內容,說明設定成功。

Registry Mirrors:
 https://registry.docker-cn.com/

使用 Docker 映象

在之前的介紹中,我們知道映象是 Docker 的三大元件之一。

Docker 執行容器前需要本地存在對應的映象,如果本地不存在該映象,Docker 會從映象倉庫下載該映象。

本章將介紹更多關於映象的內容,包括:

  • 從倉庫獲取映象;
  • 管理本地主機上的映象;
  • 介紹映象實現的基本原理。

Docker 獲取映象

之前提到過,Docker Hub 上有大量的高品質的映象可以用,這裏我們就說一下怎麼獲取這些映象。

在拉去映象之前可以查詢通過docker hub也可以使用命令

docker search mysql

從 Docker 映象倉庫獲取映象的命令是 docker pull。其命令格式爲:

docker pull [選項] [Docker Registry 地址[:埠號]/]倉庫名[:標籤]

具體的選項可以通過 docker pull --help 命令看到,這裏我們說一下映象名稱的格式。

  • Docker 映象倉庫地址:地址的格式一般是 <域名/IP>[:埠號]。預設地址是 Docker Hub。
  • 倉庫名:如之前所說,這裏的倉庫名是兩段式名稱,即 <使用者名稱>/<軟體名>。對於 Docker Hub,如果不給出使用者名稱,則預設爲 library,也就是官方映象。

比如:

$ docker pull ubuntu:16.04
16.04: Pulling from library/ubuntu
bf5d46315322: Pull complete
9f13e0ac480c: Pull complete
e8988b5b3097: Pull complete
40af181810e7: Pull complete
e6f7c7e5c03e: Pull complete
Digest: sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status: Downloaded newer image for ubuntu:16.04

上面的命令中沒有給出 Docker 映象倉庫地址,因此將會從 Docker Hub 獲取映象。而映象名稱是 ubuntu:16.04,因此將會獲取官方映象 library/ubuntu 倉庫中標籤爲 16.04 的映象。

在使用上面命令的時候,你可能會發現,你所看到的層 ID 以及 sha256 的摘要和這裏的不一樣。這是因爲官方映象是一直在維護的,有任何新的 bug,或者版本更新,都會進行修復再以原來的標籤發佈,這樣可以確保任何使用這個標籤的使用者可以獲得更安全、更穩定的映象。

執行

有了映象後,我們就能夠以這個映象爲基礎啓動並執行一個容器。以上面的 ubuntu:16.04 爲例,如果我們打算啓動裏面的 bash 並且進行互動式操作的話,可以執行下面 下麪的命令。

$ docker run -it --rm \
    ubuntu:16.04 \
    bash

root@e7009c6ce357:/# cat /etc/os-release
NAME="Ubuntu"
VERSION="16.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.4 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

docker run 就是執行容器的命令,我們這裏簡要的說明一下上面用到的參數。

  • -it:這是兩個參數,一個是 -i:互動式操作,一個是 -t 終端。我們這裏打算進入 bash 執行一些命令並檢視返回結果,因此我們需要互動式終端。
  • --rm:這個參數是說容器退出後隨之將其刪除。預設情況下,爲了排障需求,退出的容器並不會立即刪除,除非手動 docker rm。我們這裏只是隨便執行個命令,看看結果,不需要排障和保留結果,因此使用 --rm 可以避免浪費空間。
  • ubuntu:16.04:這是指用 ubuntu:16.04 映象爲基礎來啓動容器。
  • bash:放在映象名後的是命令,這裏我們希望有個互動式 Shell,因此用的是 bash

進入容器後,我們可以在 Shell 下操作,執行任何所需的命令。這裏,我們執行了 cat /etc/os-release,這是 Linux 常用的檢視當前系統版本的命令,從返回的結果可以看到容器內是 Ubuntu 16.04.4 LTS 系統。

最後我們通過 exit 退出了這個容器。

Docker 列出鏡像

要想列出已經下載下來的映象,可以使用 docker image ls 命令。

$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
redis                latest              5f515359c7f8        5 days ago          183 MB
nginx                latest              05a60462f8ba        5 days ago          181 MB
mongo                3.2                 fe9198c04d62        5 days ago          342 MB
<none>               <none>              00285df0df87        5 days ago          342 MB
ubuntu               16.04               f753707788c5        4 weeks ago         127 MB
ubuntu               latest              f753707788c5        4 weeks ago         127 MB
ubuntu               14.04               1e0c3dd64ccd        4 weeks ago         188 MB

列表包含了 倉庫名標籤映象 ID建立時間 以及 所佔用的空間

其中倉庫名、標籤在之前的基礎概念章節已經介紹過了。映象 ID 則是映象的唯一標識,一個映象可以對應多個標籤。因此,在上面的例子中,我們可以看到 ubuntu:16.04ubuntu:latest 擁有相同的 ID,因爲它們對應的是同一個映象。

映象體積

如果仔細觀察,會注意到,這裏標識的所佔用空間和在 Docker Hub 上看到的映象大小不同。比如,ubuntu:16.04 映象大小,在這裏是 127 MB,但是在 Docker Hub顯示的卻是 50 MB。這是因爲 Docker Hub 中顯示的體積是壓縮後的體積。在映象下載和上傳過程中映象是保持着壓縮狀態的,因此 Docker Hub 所顯示的大小是網路傳輸中更關心的流量大小。而 docker image ls 顯示的是映象下載到本地後,展開的大小,準確說,是展開後的各層所佔空間的總和,因爲映象到本地後,檢視空間的時候,更關心的是本地磁碟空間佔用的大小。

另外一個需要注意的問題是,docker image ls 列表中的映象體積總和並非是所有映象實際硬碟消耗。由於 Docker 映象是多層儲存結構,並且可以繼承、複用,因此不同映象可能會因爲使用相同的基礎映象,從而擁有共同的層。由於 Docker 使用 Union FS,相同的層只需要儲存一份即可,因此實際映象硬碟佔用空間很可能要比這個列表映象大小的總和要小的多。

你可以通過以下命令來便捷的檢視映象、容器、數據卷所佔用的空間。

$ docker system df
TYPE                TOTAL               ACTIVE              SIZE                RECLAIMABLE
Images              24                  0                   1.992GB             1.992GB (100%)
Containers          1                   0                   62.82MB             62.82MB (100%)
Local Volumes       9                   0                   652.2MB             652.2MB (100%)
Build Cache                                                 0B                  0B

虛懸映象

上面的映象列表中,還可以看到一個特殊的映象,這個映象既沒有倉庫名,也沒有標籤,均爲 <none>。:

<none>               <none>              00285df0df87        5 days ago          342 MB

這個映象原本是有映象名和標籤的,原來爲 mongo:3.2,隨着官方映象維護,發佈了新版本後,重新 docker pull mongo:3.2 時,mongo:3.2 這個映象名被轉移到了新下載的映象身上,而舊的映象上的這個名稱則被取消,從而成爲了 <none>。除了 docker pull 可能導致這種情況,docker build 也同樣可以導致這種現象。由於新舊映象同名,舊映象名稱被取消,從而出現倉庫名、標籤均爲 <none> 的映象。這類無標籤映象也被稱爲 虛懸映象(dangling image) ,可以用下面 下麪的命令專門顯示這類映象:

$ docker image ls -f dangling=true
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              00285df0df87        5 days ago          342 MB

一般來說,虛懸映象已經失去了存在的價值,是可以隨意刪除的,可以用下面 下麪的命令刪除。

$ docker image prune

中間層映象

爲了加速映象構建、重複利用資源,Docker 會利用 中間層映象。所以在使用一段時間後,可能會看到一些依賴的中間層映象。預設的 docker image ls 列表中只會顯示頂層映象,如果希望顯示包括中間層映象在內的所有映象的話,需要加 -a 參數。

$ docker image ls -a

這樣會看到很多無標籤的映象,與之前的虛懸映象不同,這些無標籤的映象很多都是中間層映象,是其它映象所依賴的映象。這些無標籤映象不應該刪除,否則會導致上層映象因爲依賴丟失而出錯。實際上,這些映象也沒必要刪除,因爲之前說過,相同的層只會存一遍,而這些映象是別的映象的依賴,因此並不會因爲它們被列出來而多存了一份,無論如何你也會需要它們。只要刪除那些依賴它們的映象後,這些依賴的中間層映象也會被連帶刪除。

列出部分映象

不加任何參數的情況下,docker image ls 會列出所有頂級映象,但是有時候我們只希望列出部分映象。docker image ls 有好幾個參數可以幫助做到這個事情。

根據倉庫名列出鏡像

$ docker image ls ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              16.04               f753707788c5        4 weeks ago         127 MB
ubuntu              latest              f753707788c5        4 weeks ago         127 MB
ubuntu              14.04               1e0c3dd64ccd        4 weeks ago         188 MB

列出特定的某個映象,也就是說指定倉庫名和標籤

$ docker image ls ubuntu:16.04
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              16.04               f753707788c5        4 weeks ago         127 MB

除此以外,docker image ls 還支援強大的過濾器參數 --filter,或者簡寫 -f。之前我們已經看到了使用過濾器來列出虛懸映象的用法,它還有更多的用法。比如,我們希望看到在 mongo:3.2 之後建立的映象,可以用下面 下麪的命令:

$ docker image ls -f since=mongo:3.2
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
redis               latest              5f515359c7f8        5 days ago          183 MB
nginx               latest              05a60462f8ba        5 days ago          181 MB

想檢視某個位置之前的映象也可以,只需要把 since 換成 before 即可。

此外,如果映象構建時,定義了 LABEL,還可以通過 LABEL 來過濾。

$ docker image ls -f label=com.example.version=0.1
...

以特定格式顯示

預設情況下,docker image ls 會輸出一個完整的表格,但是我們並非所有時候都會需要這些內容。比如,剛纔刪除虛懸映象的時候,我們需要利用 docker image ls 把所有的虛懸映象的 ID 列出來,然後纔可以交給 docker image rm 命令作爲參數來刪除指定的這些映象,這個時候就用到了 -q 參數。

$ docker image ls -q
5f515359c7f8
05a60462f8ba
fe9198c04d62
00285df0df87
f753707788c5
f753707788c5
1e0c3dd64ccd

--filter 配合 -q 產生出指定範圍的 ID 列表,然後送給另一個 docker 命令作爲參數,從而針對這組實體成批的進行某種操作的做法在 Docker 命令列使用過程中非常常見,不僅僅是映象,將來我們會在各個命令中看到這類搭配以完成很強大的功能。因此每次在文件看到過濾器後,可以多注意一下它們的用法。

另外一些時候,我們可能只是對錶格的結構不滿意,希望自己組織列;或者不希望有標題,這樣方便其它程式解析結果等,這就用到了 Go 的模板語法

比如,下面 下麪的命令會直接列出鏡像結果,並且只包含映象ID和倉庫名:

$ docker image ls --format "{{.ID}}: {{.Repository}}"
5f515359c7f8: redis
05a60462f8ba: nginx
fe9198c04d62: mongo
00285df0df87: <none>
f753707788c5: ubuntu
f753707788c5: ubuntu
1e0c3dd64ccd: ubuntu

或者打算以表格等距顯示,並且有標題行,和預設一樣,不過自己定義列:

$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID            REPOSITORY          TAG
5f515359c7f8        redis               latest
05a60462f8ba        nginx               latest
fe9198c04d62        mongo               3.2
00285df0df87        <none>              <none>
f753707788c5        ubuntu              16.04
f753707788c5        ubuntu              latest
1e0c3dd64ccd        ubuntu              14.04

Docker 刪除本地映象

如果要刪除原生的映象,可以使用 docker image rm 命令,其格式爲:

$ docker image rm [選項] <映象1> [<映象2> ...]
#刪除所有容器
$ docker container prune

用 ID、映象名、摘要刪除映象

其中,<映象> 可以是 映象短 ID映象長 ID映象名 或者 映象摘要

比如我們有這麼一些映象:

$ docker image ls
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
centos                      latest              0584b3d2cf6d        3 weeks ago         196.5 MB
redis                       alpine              501ad78535f0        3 weeks ago         21.03 MB
docker                      latest              cf693ec9b5c7        3 weeks ago         105.1 MB
nginx                       latest              e43d811ce2f4        5 weeks ago         181.5 MB

我們可以用映象的完整 ID,也稱爲 長 ID,來刪除映象。使用指令碼的時候可能會用長 ID,但是人工輸入就太累了,所以更多的時候是用 短 ID 來刪除映象。docker image ls 預設列出的就已經是短 ID 了,一般取前3個字元以上,只要足夠區分於別的映象就可以了。

比如這裏,如果我們要刪除 redis:alpine 映象,可以執行:

$ docker image rm 501
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
Deleted: sha256:127227698ad74a5846ff5153475e03439d96d4b1c7f2a449c7a826ef74a2d2fa
Deleted: sha256:1333ecc582459bac54e1437335c0816bc17634e131ea0cc48daa27d32c75eab3
Deleted: sha256:4fc455b921edf9c4aea207c51ab39b10b06540c8b4825ba57b3feed1668fa7c7

我們也可以用映象名,也就是 <倉庫名>:<標籤>,來刪除映象。

$ docker image rm centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

當然,更精確的是使用 映象摘要 刪除映象。

$ docker image ls --digests
REPOSITORY                  TAG                 DIGEST                                                                    IMAGE ID            CREATED             SIZE
node                        slim                sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228   6e0c4c8e3913        3 weeks ago         214 MB

$ docker image rm node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

Untagged 和 Deleted

如果觀察上面這幾個命令的執行輸出資訊的話,你會注意到刪除行爲分爲兩類,一類是 Untagged,另一類是 Deleted。我們之前介紹過,映象的唯一標識是其 ID 和摘要,而一個映象可以有多個標籤。

因此當我們使用上面命令刪除映象的時候,實際上是在要求刪除某個標籤的映象。所以首先需要做的是將滿足我們要求的所有映象標籤都取消,這就是我們看到的 Untagged 的資訊。因爲一個映象可以對應多個標籤,因此當我們刪除了所指定的標籤後,可能還有別的標籤指向了這個映象,如果是這種情況,那麼 Delete 行爲就不會發生。所以並非所有的 docker image rm 都會產生刪除映象的行爲,有可能僅僅是取消了某個標籤而已。

當該映象所有的標籤都被取消了,該映象很可能會失去了存在的意義,因此會觸發刪除行爲。映象是多層儲存結構,因此在刪除的時候也是從上層向基礎層方向依次進行判斷刪除。映象的多層結構讓映象複用變動非常容易,因此很有可能某個其它映象正依賴於當前映象的某一層。這種情況,依舊不會觸發刪除該層的行爲。直到沒有任何層依賴當前層時,纔會真實的刪除當前層。這就是爲什麼,有時候會奇怪,爲什麼明明沒有別的標籤指向這個映象,但是它還是存在的原因,也是爲什麼有時候會發現所刪除的層數和自己 docker pull 看到的層數不一樣的源。

除了映象依賴以外,還需要注意的是容器對映象的依賴。如果有用這個映象啓動的容器存在(即使容器沒有執行),那麼同樣不可以刪除這個映象。之前講過,容器是以映象爲基礎,再加一層容器儲存層,組成這樣的多層儲存結構去執行的。因此該映象如果被這個容器所依賴的,那麼刪除必然會導致故障。如果這些容器是不需要的,應該先將它們刪除,然後再來刪除映象。

用 docker image ls 命令來配合

像其它可以承接多個實體的命令一樣,可以使用 docker image ls -q 來配合使用 docker image rm,這樣可以成批的刪除希望刪除的映象。我們在「映象列表」章節介紹過很多過濾映象列表的方式都可以拿過來使用。

比如,我們需要刪除所有倉庫名爲 redis 的映象:

$ docker image rm $(docker image ls -q redis)

或者刪除所有在 mongo:3.2 之前的映象:

$ docker image rm $(docker image ls -q -f before=mongo:3.2)

充分利用你的想象力和 Linux 命令列的強大,你可以完成很多非常讚的功能。

CentOS/RHEL 的使用者需要注意的事項

在 Ubuntu/Debian 上有 UnionFS 可以使用,如 aufs 或者 overlay2,而 CentOS 和 RHEL 的內核中沒有相關驅動。因此對於這類系統,一般使用 devicemapper 驅動利用 LVM 的一些機制 機製來模擬分層儲存。這樣的做法除了效能比較差外,穩定性一般也不好,而且設定相對複雜。Docker 安裝在 CentOS/RHEL 上後,會預設選擇 devicemapper,但是爲了簡化設定,其 devicemapper 是跑在一個稀疏檔案模擬的塊裝置上,也被稱爲 loop-lvm。這樣的選擇是因爲不需要額外設定就可以執行 Docker,這是自動設定唯一能做到的事情。但是 loop-lvm 的做法非常不好,其穩定性、效能更差,無論是日誌還是 docker info 中都會看到警告資訊。官方文件有明確的文章講解瞭如何設定塊裝置給 devicemapper 驅動做儲存層的做法,這類做法也被稱爲設定 direct-lvm

除了前面說到的問題外,devicemapper + loop-lvm 還有一個缺陷,因爲它是稀疏檔案,所以它會不斷增長。使用者在使用過程中會注意到 /var/lib/docker/devicemapper/devicemapper/data 不斷增長,而且無法控制。很多人會希望刪除映象或者可以解決這個問題,結果發現效果並不明顯。原因就是這個稀疏檔案的空間釋放後基本不進行垃圾回收的問題。因此往往會出現即使刪除了檔案內容,空間卻無法回收,隨着使用這個稀疏檔案一直在不斷增長。

所以對於 CentOS/RHEL 的使用者來說,在沒有辦法使用 UnionFS 的情況下,一定要設定 direct-lvmdevicemapper,無論是爲了效能、穩定性還是空間利用率。

或許有人注意到了 CentOS 7 中存在被 backports 回來的 overlay 驅動,不過 CentOS 裡的這個驅動達不到生產環境使用的穩定程度,所以不推薦使用。

利用 commit 理解映象構成

注意: docker commit 命令除了學習之外,還有一些特殊的應用場合,比如被入侵後儲存現場等。但是,不要使用 docker commit 定製映象,定製映象應該使用 Dockerfile 來完成。如果你想要定製映象請檢視下一小節。

映象是容器的基礎,每次執行 docker run 的時候都會指定哪個映象作爲容器執行的基礎。在之前的例子中,我們所使用的都是來自於 Docker Hub 的映象。直接使用這些映象是可以滿足一定的需求,而當這些映象無法直接滿足需求時,我們就需要定製這些映象。接下來的幾節就將講解如何定製映象。

回顧一下之前我們學到的知識,映象是多層儲存,每一層是在前一層的基礎上進行的修改;而容器同樣也是多層儲存,是在以映象爲基礎層,在其基礎上加一層作爲容器執行時的儲存層。

現在讓我們以定製一個 Web 伺服器爲例子,來講解映象是如何構建的。

$ docker run --name webserver -d -p 80:80 nginx

這條命令會用 nginx 映象啓動一個容器,命名爲 webserver,並且對映了 80 埠,這樣我們可以用瀏覽器去存取這個 nginx 伺服器。

如果是在 Linux 本機執行的 Docker,或者如果使用的是 Docker for Mac、Docker for Windows,那麼可以直接存取:http://localhost;如果使用的是 Docker Toolbox,或者是在虛擬機器、雲伺服器上安裝的 Docker,則需要將 localhost 換爲虛擬機器地址或者實際雲伺服器地址。

直接用瀏覽器存取的話,我們會看到預設的 Nginx 歡迎頁面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mLHXZzuR-1597232562012)(Docker/images-mac-example-nginx.png)]

現在,假設我們非常不喜歡這個歡迎頁面,我們希望改成歡迎 Docker 的文字,我們可以使用 docker exec 命令進入容器,修改其內容。

$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit

我們以互動式終端方式進入 webserver 容器,並執行了 bash 命令,也就是獲得一個可操作的 Shell。

然後,我們用 <h1>Hello, Docker!</h1> 覆蓋了 /usr/share/nginx/html/index.html 的內容。

現在我們再重新整理瀏覽器的話,會發現內容被改變了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3oAZe137-1597232562013)(Docker/images-create-nginx-docker.png)]
我們修改了容器的檔案,也就是改動了容器的儲存層。我們可以通過 docker diff 命令看到具體的改動。

$ docker diff webserver
C /root
A /root/.bash_history
C /run
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
C /usr/share/nginx/html/index.html
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp

現在我們定製好了變化,我們希望能將其儲存下來形成映象。

要知道,當我們執行一個容器的時候(如果不使用卷的話),我們做的任何檔案修改都會被記錄於容器儲存層裡。而 Docker 提供了一個 docker commit 命令,可以將容器的儲存層儲存下來成爲映象。換句話說,就是在原有映象的基礎上,再疊加上容器的儲存層,並構成新的映象。以後我們執行這個新映象的時候,就會擁有原有容器最後的檔案變化。

docker commit 的語法格式爲:

docker commit [選項] <容器ID或容器名> [<倉庫名>[:<標籤>]]

我們可以用下面 下麪的命令將容器儲存爲映象:

$ docker commit \
    --author "Tao Wang <[email protected]>" \
    --message "修改了預設網頁" \
    webserver \
    nginx:v2
sha256:07e33465974800ce65751acc279adc6ed2dc5ed4e0838f8b86f0c87aa1795214

其中 --author 是指定修改的作者,而 --message 則是記錄本次修改的內容。這點和 git 版本控制相似,不過這裏這些資訊可以省略留空。

我們可以在 docker image ls 中看到這個新定製的映象:

$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               v2                  07e334659748        9 seconds ago       181.5 MB
nginx               1.11                05a60462f8ba        12 days ago         181.5 MB
nginx               latest              e43d811ce2f4        4 weeks ago         181.5 MB```

我們還可以用 `docker history` 具體檢視映象內的歷史記錄,如果比較 `nginx:latest` 的歷史記錄,我們會發現新增了我們剛剛提交的這一層。

```bash
$ docker history nginx:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
07e334659748        54 seconds ago      nginx -g daemon off;                            95 B                修改了預設網頁
e43d811ce2f4        4 weeks ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon    0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  EXPOSE 443/tcp 80/tcp        0 B
<missing>           4 weeks ago         /bin/sh -c ln -sf /dev/stdout /var/log/nginx/   22 B
<missing>           4 weeks ago         /bin/sh -c apt-key adv --keyserver hkp://pgp.   58.46 MB
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.11.5-1   0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker Ma   0 B
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:23aa4f893e3288698c   123 MB

新的映象定製好後,我們可以來執行這個映象。

docker run --name web2 -d -p 81:80 nginx:v2

這裏我們命名爲新的服務爲 web2,並且對映到 81 埠。如果是 Docker for Mac/Windows 或 Linux 桌面的話,我們就可以直接存取 http://localhost:81 看到結果,其內容應該和之前修改後的 webserver 一樣。

至此,我們第一次完成了定製映象,使用的是 docker commit 命令,手動操作給舊的映象新增了新的一層,形成新的映象,對映象多層儲存應該有了更直觀的感覺。

**慎用 docker commit**

使用 docker commit 命令雖然可以比較直觀的幫助理解映象分層儲存的概念,但是實際環境中並不會這樣使用。

首先,如果仔細觀察之前的 docker diff webserver 的結果,你會發現除了真正想要修改的 /usr/share/nginx/html/index.html 檔案外,由於命令的執行,還有很多檔案被改動或新增了。這還僅僅是最簡單的操作,如果是安裝軟體包、編譯構建,那會有大量的無關內容被新增進來,如果不小心清理,將會導致映象極爲臃腫。

此外,使用 docker commit 意味着所有對映象的操作都是黑箱操作,生成的映象也被稱爲黑箱映象,換句話說,就是除了製作映象的人知道執行過什麼命令、怎麼生成的映象,別人根本無從得知。而且,即使是這個製作映象的人,過一段時間後也無法記清具體在操作的。雖然 docker diff 或許可以告訴得到一些線索,但是遠遠不到可以確保生成一致映象的地步。這種黑箱映象的維護工作是非常痛苦的。

而且,回顧之前提及的映象所使用的分層儲存的概念,除當前層外,之前的每一層都是不會發生改變的,換句話說,任何修改的結果僅僅是在當前層進行標記、新增、修改,而不會改動上一層。如果使用 docker commit 製作映象,以及後期修改的話,每一次修改都會讓映象更加臃腫一次,所刪除的上一層的東西並不會丟失,會一直如影隨形的跟着這個映象,即使根本無法存取到。這會讓映象更加臃腫。

使用 Dockerfile 定製映象

Dockerfile 定製映象

從剛纔的 docker commit 的學習中,我們可以瞭解到,映象的定製實際上就是定製每一層所新增的設定、檔案。如果我們可以把每一層修改、安裝、構建、操作的命令都寫入一個指令碼,用這個指令碼來構建、定製映象,那麼之前提及的無法重複的問題、映象構建透明性的問題、體積的問題就都會解決。這個指令碼就是 Dockerfile。

Dockerfile 是一個文字檔案,其內包含了一條條的指令(Instruction),每一條指令構建一層,因此每一條指令的內容,就是描述該層應當如何構建。

還以之前定製 nginx 映象爲例,這次我們使用 Dockerfile 來定製。

在一個空白目錄中,建立一個文字檔案,並命名爲 Dockerfile

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其內容爲:

FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

這個 Dockerfile 很簡單,一共就兩行。涉及到了兩條指令,FROMRUN

FROM 指定基礎映象

所謂定製映象,那一定是以一個映象爲基礎,在其上進行定製。就像我們之前執行了一個 nginx 映象的容器,再進行修改一樣,基礎映象是必須指定的。而 FROM 就是指定基礎映象,因此一個 DockerfileFROM 是必備的指令,並且必須是第一條指令。

Docker Store 上有非常多的高品質的官方映象,有可以直接拿來使用的服務類的映象,如 nginx redis mongo mysql httpd php tomcat 等;也有一些方便開發、構建、執行各種語言應用的映象,如 node openjdk python ruby golang 等。可以在其中尋找一個最符合我們最終目標的映象爲基礎映象進行定製。

如果沒有找到對應服務的映象,官方映象中還提供了一些更爲基礎的操作系統映象,如 ubuntu debian centos fedora alpine 等,這些操作系統的軟體庫爲我們提供了更廣闊的擴充套件空間。

除了選擇現有映象爲基礎映象外,Docker 還存在一個特殊的映象,名爲 scratch。這個映象是虛擬的概念,並不實際存在,它表示一個空白的映象。

FROM scratch
...

如果你以 scratch 爲基礎映象的話,意味着你不以任何映象爲基礎,接下來所寫的指令將作爲映象第一層開始存在。

不以任何系統爲基礎,直接將可執行檔案複製進映象的做法並不罕見,比如 swarm coreos/etcd 。對於 Linux 下靜態編譯的程式來說,並不需要有操作系統提供執行時支援,所需的一切庫都已經在可執行檔案裡了,因此直接 FROM scratch 會讓映象體積更加小巧。使用 Go 語言 開發的應用很多會使用這種方式來製作映象,這也是爲什麼有人認爲 Go 是特別適合容器微服務架構的語言的原因之一。

RUN 執行命令

RUN 指令是用來執行命令列命令的。由於命令列的強大能力,RUN 指令在定製映象時是最常用的指令之一。其格式有兩種:

  • shell 格式:RUN <命令>,就像直接在命令列中輸入的命令一樣。剛纔寫的 Dockerfile 中的 RUN 指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可執行檔案", "參數1", "參數2"],這更像是函數呼叫中的格式。

既然 RUN 就像 Shell 指令碼一樣可以執行命令,那麼我們是否就可以像 Shell 指令碼一樣把每個命令對應一個 RUN 呢?比如這樣:

FROM debian:jessie

RUN apt-get update
RUN apt-get install -y gcc libc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install

之前說過,Dockerfile 中每一個指令都會建立一層,RUN 也不例外。每一個 RUN 的行爲,就和剛纔我們手工建立映象的過程一樣:新建立一層,在其上執行這些命令,執行結束後,commit 這一層的修改,構成新的映象。

而上面的這種寫法,建立了 7 層映象。這是完全沒有意義的,而且很多執行時不需要的東西,都被裝進了映象裡,比如編譯環境、更新的軟體包等等。結果就是產生非常臃腫、非常多層的映象,不僅僅增加了構建部署的時間,也很容易出錯。 這是很多初學 Docker 的人常犯的一個錯誤。

Union FS 是有最大層數限制的,比如 AUFS,曾經是最大不得超過 42 層,現在是不得超過 127 層。

上面的 Dockerfile 正確的寫法應該是這樣:

FROM debian:jessie

RUN buildDeps='gcc libc6-dev make' \
    && apt-get update \
    && apt-get install -y $buildDeps \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
    && mkdir -p /usr/src/redis \
    && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
    && make -C /usr/src/redis \
    && make -C /usr/src/redis install \
    && rm -rf /var/lib/apt/lists/* \
    && rm redis.tar.gz \
    && rm -r /usr/src/redis \
    && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一個目的,就是編譯、安裝 redis 可執行檔案。因此沒有必要建立很多層,這只是一層的事情。因此,這裏沒有使用很多個 RUN 對一一對應不同的命令,而是僅僅使用一個 RUN 指令,並使用 && 將各個所需命令串聯起來。將之前的 7 層,簡化爲了 1 層。在撰寫 Dockerfile 的時候,要經常提醒自己,這並不是在寫 Shell 指令碼,而是在定義每一層該如何構建。

並且,這裏爲了格式化還進行了換行。Dockerfile 支援 Shell 類的行尾新增 \ 的命令換行方式,以及行首 # 進行註釋的格式。良好的格式,比如換行、縮排、註釋等,會讓維護、排障更爲容易,這是一個比較好的習慣。

此外,還可以看到這一組命令的最後新增了清理工作的命令,刪除了爲了編譯構建所需要的軟體,清理了所有下載、展開的檔案,並且還清理了 apt 快取檔案。這是很重要的一步,我們之前說過,映象是多層儲存,每一層的東西並不會在下一層被刪除,會一直跟隨着映象。因此映象構建時,一定要確保每一層只新增真正需要新增的東西,任何無關的東西都應該清理掉。

很多人初學 Docker 製作出了很臃腫的映象的原因之一,就是忘記了每一層構建的最後一定要清理掉無關檔案。

構建映象

好了,讓我們再回到之前定製的 nginx 映象的 Dockerfile 來。現在我們明白了這個 Dockerfile 的內容,那麼讓我們來構建這個映象吧。

Dockerfile 檔案所在目錄執行:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
 ---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9cdc27646c7b
 ---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c

從命令的輸出結果中,我們可以清晰的看到映象的構建過程。在 Step 2 中,如同我們之前所說的那樣,RUN 指令啓動了一個容器 9cdc27646c7b,執行了所要求的命令,並最後提交了這一層 44aa4490ce2c,隨後刪除了所用到的這個容器 9cdc27646c7b

這裏我們使用了 docker build 命令進行映象構建。其格式爲:

docker build [選項] <上下文路徑/URL/->

在這裏我們指定了最終映象的名稱 -t nginx:v3,構建成功後,我們可以像之前執行 nginx:v2 那樣來執行這個映象,其結果會和 nginx:v2 一樣。

映象構建上下文(Context)

如果注意,會看到 docker build 命令最後有一個 .. 表示當前目錄,而 Dockerfile 就在當前目錄,因此不少初學者以爲這個路徑是在指定 Dockerfile 所在路徑,這麼理解其實是不準 不準確的。如果對應上面的命令格式,你可能會發現,這是在指定上下文路徑。那麼什麼是上下文呢?

首先我們要理解 docker build 的工作原理。Docker 在執行時分爲 Docker 引擎(也就是伺服器端守護行程)和用戶端工具。Docker 的引擎提供了一組 REST API,被稱爲 Docker Remote API,而如 docker 命令這樣的用戶端工具,則是通過這組 API 與 Docker 引擎互動,從而完成各種功能。因此,雖然表面上我們好像是在本機執行各種 docker 功能,但實際上,一切都是使用的遠端呼叫形式在伺服器端(Docker 引擎)完成。也因爲這種 C/S 設計,讓我們操作遠端伺服器的 Docker 引擎變得輕而易舉。

當我們進行映象構建的時候,並非所有定製都會通過 RUN 指令完成,經常會需要將一些本地檔案複製進映象,比如通過 COPY 指令、ADD 指令等。而 docker build 命令構建映象,其實並非在本地構建,而是在伺服器端,也就是 Docker 引擎中構建的。那麼在這種用戶端/伺服器端的架構中,如何才能 纔能讓伺服器端獲得本地檔案呢?

這就引入了上下文的概念。當構建的時候,使用者會指定構建映象上下文的路徑,docker build 命令得知這個路徑後,會將路徑下的所有內容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包後,展開就會獲得構建映象所需的一切檔案。

如果在 Dockerfile 中這麼寫:

COPY ./package.json /app/

這並不是要複製執行 docker build 命令所在的目錄下的 package.json,也不是複製 Dockerfile 所在目錄下的 package.json,而是複製 上下文(context) 目錄下的 package.json

因此,COPY 這類指令中的原始檔的路徑都是相對路徑。這也是初學者經常會問的爲什麼 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法運作的原因,因爲這些路徑已經超出了上下文的範圍,Docker 引擎無法獲得這些位置的檔案。如果真的需要那些檔案,應該將它們複製到上下文目錄中去。

現在就可以理解剛纔的命令 docker build -t nginx:v3 . 中的這個 .,實際上是在指定上下文的目錄,docker build 命令會將該目錄下的內容打包交給 Docker 引擎以幫助構建映象。

如果觀察 docker build 輸出,我們其實已經看到了這個發送上下文的過程:

$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
...

理解構建上下文對於映象構建是很重要的,避免犯一些不應該的錯誤。比如有些初學者在發現 COPY /opt/xxxx /app 不工作後,於是乾脆將 Dockerfile 放到了硬碟根目錄去構建,結果發現 docker build 執行後,在發送一個幾十 GB 的東西,極爲緩慢而且很容易構建失敗。那是因爲這種做法是在讓 docker build 打包整個硬碟,這顯然是使用錯誤。

一般來說,應該會將 Dockerfile 置於一個空目錄下,或者專案根目錄下。如果該目錄下沒有所需檔案,那麼應該把所需檔案複製一份過來。如果目錄下有些東西確實不希望構建時傳給 Docker 引擎,那麼可以用 .gitignore 一樣的語法寫一個 .dockerignore,該檔案是用於剔除不需要作爲上下文傳遞給 Docker 引擎的。

那麼爲什麼會有人誤以爲 . 是指定 Dockerfile 所在目錄呢?這是因爲在預設情況下,如果不額外指定 Dockerfile 的話,會將上下文目錄下的名爲 Dockerfile 的檔案作爲 Dockerfile。

這只是預設行爲,實際上 Dockerfile 的檔名並不要求必須爲 Dockerfile,而且並不要求必須位於上下文目錄中,比如可以用 -f ../Dockerfile.php 參數指定某個檔案作爲 Dockerfile

當然,一般大家習慣性的會使用預設的檔名 Dockerfile,以及會將其置於映象構建上下文目錄中。

其它 docker build 的用法

直接用 Git repo 進行構建

或許你已經注意到了,docker build 還支援從 URL 構建,比如可以直接從 Git repo 中構建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
docker build https://github.com/twang2218/gitlab-ce-zh.git\#:8.14
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:8.14.0-ce.0
8.14.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...

這行命令指定了構建所需的 Git repo,並且指定預設的 master 分支,構建目錄爲 /8.14/,然後 Docker 就會自己去 git clone 這個專案、切換到指定分支、並進入到指定目錄後開始構建。

用給定的 tar 壓縮包構建

$ docker build http://server/context.tar.gz

如果所給出的 URL 不是個 Git repo,而是個 tar 壓縮包,那麼 Docker 引擎會下載這個包,並自動解壓縮,以其作爲上下文,開始構建。

從標準輸入中讀取 Dockerfile 進行構建

docker build - < Dockerfile

cat Dockerfile | docker build -

如果標準輸入傳入的是文字檔案,則將其視爲 Dockerfile,並開始構建。這種形式由於直接從標準輸入中讀取 Dockerfile 的內容,它沒有上下文,因此不可以像其他方法那樣可以將本地檔案 COPY 進映象之類的事情。

從標準輸入中讀取上下文壓縮包進行構建

$ docker build - < context.tar.gz

如果發現標準輸入的檔案格式是 gzipbzip2 以及 xz 的話,將會使其爲上下文壓縮包,直接將其展開,將裏面視爲上下文,並開始構建。

Dockerfile 指令詳解

我們已經介紹了 FROMRUN,還提及了 COPY, ADD,其實 Dockerfile 功能很強大,它提供了十多個指令。下面 下麪我們繼續講解其他的指令。

COPY 複製檔案

格式:

  • COPY <源路徑>... <目標路徑>
  • COPY ["<源路徑1>",... "<目標路徑>"]

RUN 指令一樣,也有兩種格式,一種類似於命令列,一種類似於函數呼叫。

COPY 指令將從構建上下文目錄中 <源路徑> 的檔案/目錄複製到新的一層的映象內的 <目標路徑> 位置。比如:

COPY package.json /usr/src/app/

<源路徑> 可以是多個,甚至可以是萬用字元,其萬用字元規則要滿足 Go 的 filepath.Match 規則,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目標路徑> 可以是容器內的絕對路徑,也可以是相對於工作目錄的相對路徑(工作目錄可以用 WORKDIR 指令來指定)。目標路徑不需要事先建立,如果目錄不存在會在複製檔案前先行建立缺失目錄。

此外,還需要注意一點,使用 COPY 指令,原始檔的各種元數據都會保留。比如讀、寫、執行許可權、檔案變更時間等。這個特性對於映象定製很有用。特別是構建相關檔案都在使用 Git 進行管理的時候。

ADD 更高階的複製檔案

ADD 指令和 COPY 的格式和性質基本一致。但是在 COPY 基礎上增加了一些功能。

比如 <源路徑> 可以是一個 URL,這種情況下,Docker 引擎會試圖去下載這個鏈接的檔案放到 <目標路徑> 去。下載後的檔案許可權自動設定爲 600,如果這並不是想要的許可權,那麼還需要增加額外的一層 RUN 進行許可權調整,另外,如果下載的是個壓縮包,需要解壓縮,也一樣還需要額外的一層 RUN 指令進行解壓縮。所以不如直接使用 RUN 指令,然後使用 wget 或者 curl 工具下載,處理許可權、解壓縮、然後清理無用檔案更合理。因此,這個功能其實並不實用,而且不推薦使用。

如果 <源路徑> 爲一個 tar 壓縮檔案的話,壓縮格式爲 gzip, bzip2 以及 xz 的情況下,ADD 指令將會自動解壓縮這個壓縮檔案到 <目標路徑> 去。

在某些情況下,這個自動解壓縮的功能非常有用,比如官方映象 ubuntu 中:

FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...

但在某些情況下,如果我們真的是希望複製個壓縮檔案進去,而不解壓縮,這時就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳實踐文件 中要求,儘可能的使用 COPY,因爲 COPY 的語意很明確,就是複製檔案而已,而 ADD 則包含了更復雜的功能,其行爲也不一定很清晰。最適合使用 ADD 的場合,就是所提及的需要自動解壓縮的場合。

另外需要注意的是,ADD 指令會令映象構建快取失效,從而可能會令映象構建變得比較緩慢。

因此在 COPYADD 指令中選擇的時候,可以遵循這樣的原則,所有的檔案複製均使用 COPY 指令,僅在需要自動解壓縮的場合使用 ADD

CMD 容器啓動命令

CMD 指令的格式和 RUN 相似,也是兩種格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD ["可執行檔案", "參數1", "參數2"...]
  • 參數列表格式:CMD ["參數1", "參數2"...]。在指定了 ENTRYPOINT 指令後,用 CMD 指定具體的參數。

之前介紹容器的時候曾經說過,Docker 不是虛擬機器,容器就是進程。既然是進程,那麼在啓動容器的時候,需要指定所執行的程式及參數。CMD 指令就是用於指定預設的容器主進程的啓動命令的。

在執行時可以指定新的命令來替代映象設定中的這個預設命令,比如,ubuntu 映象預設的 CMD/bin/bash,如果我們直接 docker run -it ubuntu 的話,會直接進入 bash。我們也可以在執行時指定執行別的命令,如 docker run -it ubuntu cat /etc/os-release。這就是用 cat /etc/os-release 命令替換了預設的 /bin/bash 命令了,輸出了系統版本資訊。

在指令格式上,一般推薦使用 exec 格式,這類格式在解析時會被解析爲 JSON 陣列,因此一定要使用雙引號 ",而不要使用單引號。

如果使用 shell 格式的話,實際的命令會被包裝爲 sh -c 的參數的形式進行執行。比如:

CMD echo $HOME

在實際執行中,會將其變更爲:

CMD [ "sh", "-c", "echo $HOME" ]

這就是爲什麼我們可以使用環境變數的原因,因爲這些環境變數會被 shell 進行解析處理。

提到 CMD 就不得不提容器中應用在前臺執行和後臺執行的問題。這是初學者常出現的一個混淆。

Docker 不是虛擬機器,容器中的應用都應該以前臺執行,而不是像虛擬機器、物理機裏面那樣,用 upstart/systemd 去啓動後臺服務,容器內沒有後台服務的概念。

一些初學者將 CMD 寫爲:

CMD service nginx start

然後發現容器執行後就立即退出了。甚至在容器內去使用 systemctl 命令結果卻發現根本執行不了。這就是因爲沒有搞明白前臺、後臺的概念,沒有區分容器和虛擬機器的差異,依舊在以傳統虛擬機器的角度去理解容器。

對於容器而言,其啓動程式就是容器應用進程,容器就是爲了主進程而存在的,主進程退出,容器就失去了存在的意義,從而退出,其它輔助進程不是它需要關心的東西。

而使用 service nginx start 命令,則是希望 upstart 來以後台守護行程形式啓動 nginx 服務。而剛纔說了 CMD service nginx start 會被理解爲 CMD [ "sh", "-c", "service nginx start"],因此主進程實際上是 sh。那麼當 service nginx start 命令結束後,sh 也就結束了,sh 作爲主進程退出了,自然就會令容器退出。

正確的做法是直接執行 nginx 可執行檔案,並且要求以前臺形式執行。比如:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口點

ENTRYPOINT 的格式和 RUN 指令格式一樣,分爲 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一樣,都是在指定容器啓動程式及參數。ENTRYPOINT 在執行時也可以替代,不過比 CMD 要略顯繁瑣,需要通過 docker run 的參數 --entrypoint 來指定。

當指定了 ENTRYPOINT 後,CMD 的含義就發生了改變,不再是直接的執行其命令,而是將 CMD 的內容作爲參數傳給 ENTRYPOINT 指令,換句話說實際執行時,將變爲:

<ENTRYPOINT> "<CMD>"

那麼有了 CMD 後,爲什麼還要有 ENTRYPOINT 呢?這種 <ENTRYPOINT> "<CMD>" 有什麼好處麼?讓我們來看幾個場景。

場景一:讓映象變成像命令一樣使用

假設我們需要一個得知自己當前公網 IP 的映象,那麼可以先用 CMD 來實現:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "http://ip.cn" ]

假如我們使用 docker build -t myip . 來構建映象的話,如果我們需要查詢當前公網 IP,只需要執行:

$ docker run myip
當前 IP:61.148.226.66 來自:北京市 聯通

嗯,這麼看起來好像可以直接把映象當做命令使用了,不過命令總有參數,如果我們希望加參數呢?比如從上面的 CMD 中可以看到實質的命令是 curl,那麼如果我們希望顯示 HTTP 頭資訊,就需要加上 -i 參數。那麼我們可以直接加 -i 參數給 docker run myip 麼?

$ docker run myip -i
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我們可以看到可執行檔案找不到的報錯,executable file not found。之前我們說過,跟在映象名後面的是 command,執行時會替換 CMD 的預設值。因此這裏的 -i 替換了原來的 CMD,而不是新增在原來的 curl -s http://ip.cn 後面。而 -i 根本不是命令,所以自然找不到。

那麼如果我們希望加入 -i 這參數,我們就必須重新完整的輸入這個命令:

$ docker run myip curl -s http://ip.cn -i

這顯然不是很好的解決方案,而使用 ENTRYPOINT 就可以解決這個問題。現在我們重新用 ENTRYPOINT 來實現這個映象:

FROM ubuntu:16.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

這次我們再來嘗試直接使用 docker run myip -i

$ docker run myip
當前 IP:61.148.226.66 來自:北京市 聯通

$ docker run myip -i
HTTP/1.1 200 OK
Server: nginx/1.8.0
Date: Tue, 22 Nov 2016 05:12:40 GMT
Content-Type: text/html; charset=UTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb+7.1
X-Cache: MISS from cache-2
X-Cache-Lookup: MISS from cache-2:80
X-Cache: MISS from proxy-2_6
Transfer-Encoding: chunked
Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006
Connection: keep-alive

當前 IP:61.148.226.66 來自:北京市 聯通

可以看到,這次成功了。這是因爲當存在 ENTRYPOINT 後,CMD 的內容將會作爲參數傳給 ENTRYPOINT,而這裏 -i 就是新的 CMD,因此會作爲參數傳給 curl,從而達到了我們預期的效果。

場景二:應用執行前的準備工作

啓動容器就是啓動主進程,但有些時候,啓動主進程前,需要一些準備工作。

比如 mysql 類的數據庫,可能需要一些數據庫設定、初始化的工作,這些工作要在最終的 mysql 伺服器執行之前解決。

此外,可能希望避免使用 root 使用者去啓動服務,從而提高安全性,而在啓動服務前還需要以 root 身份執行一些必要的準備工作,最後切換到服務使用者身份啓動服務。或者除了服務外,其它命令依舊可以使用 root 身份執行,方便偵錯等。

這些準備工作是和容器 CMD 無關的,無論 CMD 爲什麼,都需要事先進行一個預處理的工作。這種情況下,可以寫一個指令碼,然後放入 ENTRYPOINT 中去執行,而這個指令碼會將接到的參數(也就是 <CMD>)作爲命令,在指令碼最後執行。比如官方映象 redis 中就是這麼做的:

FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

可以看到其中爲了 redis 服務建立了 redis 使用者,並在最後指定了 ENTRYPOINTdocker-entrypoint.sh 指令碼。

#!/bin/sh
...
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	chown -R redis .
	exec su-exec redis "$0" "$@"
fi

exec "$@"

該指令碼的內容就是根據 CMD 的內容來判斷,如果是 redis-server 的話,則切換到 redis 使用者身份啓動伺服器,否則依舊使用 root 身份執行。比如:

$ docker run -it redis id
uid=0(root) gid=0(root) groups=0(root)

ENV 設定環境變數

格式有兩種:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

這個指令很簡單,就是設定環境變數而已,無論是後面的其它指令,如 RUN,還是執行時的應用,都可以直接使用這裏定義的環境變數。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

這個例子中演示瞭如何換行,以及對含有空格的值用雙引號括起來的辦法,這和 Shell 下的行爲是一致的。

定義了環境變數,那麼在後續的指令中,就可以使用這個環境變數。比如在官方 node 映象 Dockerfile 中,就有類似這樣的程式碼:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在這裏先定義了環境變數 NODE_VERSION,其後的 RUN 這層裡,多次使用 $NODE_VERSION 來進行操作定製。可以看到,將來升級映象構建版本的時候,只需要更新 7.2.0 即可,Dockerfile 構建維護變得更輕鬆了。

下列指令可以支援環境變數展開: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

可以從這個指令列表裏感覺到,環境變數可以使用的地方很多,很強大。通過環境變數,我們可以讓一份 Dockerfile 製作更多的映象,只需使用不同的環境變數即可。

ARG 構建參數

格式:ARG <參數名>[=<預設值>]

構建參數和 ENV 的效果一樣,都是設定環境變數。所不同的是,ARG 所設定的構建環境的環境變數,在將來容器執行時是不會存在這些環境變數的。但是不要因此就使用 ARG 儲存密碼之類的資訊,因爲 docker history 還是可以看到所有值的。

Dockerfile 中的 ARG 指令是定義參數名稱,以及定義其預設值。該預設值可以在構建命令 docker build 中用 --build-arg <參數名>=<值> 來覆蓋。

在 1.13 之前的版本,要求 --build-arg 中的參數名,必須在 Dockerfile 中用 ARG 定義過了,換句話說,就是 --build-arg 指定的參數,必須在 Dockerfile 中使用了。如果對應參數沒有被使用,則會報錯退出構建。從 1.13 開始,這種嚴格的限制被放開,不再報錯退出,而是顯示警告資訊,並繼續構建。這對於使用 CI 系統,用同樣的構建流程構建不同的 Dockerfile 的時候比較有幫助,避免構建命令必須根據每個 Dockerfile 的內容修改。

VOLUME 定義匿名卷

格式爲:

  • VOLUME ["<路徑1>", "<路徑2>"...]
  • VOLUME <路徑>

之前我們說過,容器執行時應該儘量保持容器儲存層不發生寫操作,對於數據庫類需要儲存動態數據的應用,其數據庫檔案應該儲存於卷(volume)中,後面的章節我們會進一步介紹 Docker 卷的概念。爲了防止執行時使用者忘記將動態檔案所儲存目錄掛載爲卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載爲匿名卷,這樣在執行時如果使用者不指定掛載,其應用也可以正常執行,不會向容器儲存層寫入大量數據。

VOLUME /data

這裏的 /data 目錄就會在執行時自動掛載爲匿名卷,任何向 /data 中寫入的資訊都不會記錄進容器儲存層,從而保證了容器儲存層的無狀態化。當然,執行時可以覆蓋這個掛載設定。比如:

docker run -d -v mydata:/data xxxx

在這行命令中,就使用了 mydata 這個命名卷掛載到了 /data 這個位置,替代了 Dockerfile 中定義的匿名卷的掛載設定。

EXPOSE 暴露埠

格式爲 EXPOSE <埠1> [<埠2>...]

EXPOSE 指令是宣告執行時容器提供伺服器端口,這只是一個宣告,在執行時並不會因爲這個宣告應用就會開啓這個埠的服務。在 Dockerfile 中寫入這樣的宣告有兩個好處,一個是幫助映象使用者理解這個映象服務的守護埠,以方便設定對映;另一個用處則是在執行時使用隨機埠對映時,也就是 docker run -P 時,會自動隨機對映 EXPOSE 的埠。

此外,在早期 Docker 版本中還有一個特殊的用處。以前所有容器都執行於預設橋接網路中,因此所有容器互相之間都可以直接存取,這樣存在一定的安全性問題。於是有了一個 Docker 引擎參數 --icc=false,當指定該參數後,容器間將預設無法互訪,除非互相間使用了 --links 參數的容器纔可以互通,並且只有映象中 EXPOSE 所宣告的埠纔可以被存取。這個 --icc=false 的用法,在引入了 docker network 後已經基本不用了,通過自定義網路可以很輕鬆的實現容器間的互聯與隔離。

要將 EXPOSE 和在執行時使用 -p <宿主埠>:<容器埠> 區分開來。-p,是對映宿主埠和容器埠,換句話說,就是將容器的對應埠服務公開給外界存取,而 EXPOSE 僅僅是宣告容器打算使用什麼埠而已,並不會自動在宿主進行埠對映。

WORKDIR 指定工作目錄

格式爲 WORKDIR <工作目錄路徑>

使用 WORKDIR 指令可以來指定工作目錄(或者稱爲當前目錄),以後各層的當前目錄就被改爲指定的目錄,如該目錄不存在,WORKDIR 會幫你建立目錄。

之前提到一些初學者常犯的錯誤是把 Dockerfile 等同於 Shell 指令碼來書寫,這種錯誤的理解還可能會導致出現下面 下麪這樣的錯誤:

RUN cd /app
RUN echo "hello" > world.txt

如果將這個 Dockerfile 進行構建映象執行後,會發現找不到 /app/world.txt 檔案,或者其內容不是 hello。原因其實很簡單,在 Shell 中,連續兩行是同一個進程執行環境,因此前一個命令修改的記憶體狀態,會直接影響後一個命令;而在 Dockerfile 中,這兩行 RUN 命令的執行環境根本不同,是兩個完全不同的容器。這就是對 Dockerfile 構建分層儲存的概念不瞭解所導致的錯誤。

之前說過每一個 RUN 都是啓動一個容器、執行命令、然後提交儲存層檔案變更。第一層 RUN cd /app 的執行僅僅是當前進程的工作目錄變更,一個記憶體上的變化而已,其結果不會造成任何檔案變更。而到第二層的時候,啓動的是一個全新的容器,跟第一層的容器更完全沒關係,自然不可能繼承前一層構建過程中的記憶體變化。

因此如果需要改變以後各層的工作目錄的位置,那麼應該使用 WORKDIR 指令。

USER 指定當前使用者

格式:USER <使用者名稱>

USER 指令和 WORKDIR 相似,都是改變環境狀態並影響以後的層。WORKDIR 是改變工作目錄,USER 則是改變之後層的執行 RUN, CMD 以及 ENTRYPOINT 這類命令的身份。

當然,和 WORKDIR 一樣,USER 只是幫助你切換到指定使用者而已,這個使用者必須是事先建立好的,否則無法切換。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以 root 執行的指令碼,在執行期間希望改變身份,比如希望以某個已經建立好的使用者來執行某個服務進程,不要使用 su 或者 sudo,這些都需要比較麻煩的設定,而且在 TTY 缺失的環境下經常出錯。建議使用 gosu

# 建立 redis 使用者,並使用 gosu 換另一個使用者執行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下載 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 設定 CMD,並以另外的使用者執行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康檢查

格式:

  • HEALTHCHECK [選項] CMD <命令>:設定檢查容器健康狀況的命令
  • HEALTHCHECK NONE:如果基礎映象有健康檢查指令,使用這行可以遮蔽掉其健康檢查指令

HEALTHCHECK 指令是告訴 Docker 應該如何進行判斷容器的狀態是否正常,這是 Docker 1.12 引入的新指令。

在沒有 HEALTHCHECK 指令前,Docker 引擎只可以通過容器內主進程是否退出來判斷容器是否狀態異常。很多情況下這沒問題,但是如果程式進入死鎖狀態,或者死回圈狀態,應用進程並不退出,但是該容器已經無法提供服務了。在 1.12 以前,Docker 不會檢測到容器的這種狀態,從而不會重新排程,導致可能會有部分容器已經無法提供服務了卻還在接受使用者請求。

而自 1.12 之後,Docker 提供了 HEALTHCHECK 指令,通過該指令指定一行命令,用這行命令來判斷容器主進程的服務狀態是否還正常,從而比較真實的反應容器實際狀態。

當在一個映象指定了 HEALTHCHECK 指令後,用其啓動容器,初始狀態會爲 starting,在 HEALTHCHECK 指令檢查成功後變爲 healthy,如果連續一定次數失敗,則會變爲 unhealthy

HEALTHCHECK 支援下列選項:

  • --interval=<間隔>:兩次健康檢查的間隔,預設爲 30 秒;
  • --timeout=<時長>:健康檢查命令執行超時時間,如果超過這個時間,本次健康檢查就被視爲失敗,預設 30 秒;
  • --retries=<次數>:當連續失敗指定次數後,則將容器狀態視爲 unhealthy,預設 3 次。

CMD, ENTRYPOINT 一樣,HEALTHCHECK 只可以出現一次,如果寫了多個,只有最後一個生效。

HEALTHCHECK [選項] CMD 後面的命令,格式和 ENTRYPOINT 一樣,分爲 shell 格式,和 exec 格式。命令的返回值決定了該次健康檢查的成功與否:0:成功;1:失敗;2:保留,不要使用這個值。

假設我們有個映象是個最簡單的 Web 服務,我們希望增加健康檢查來判斷其 Web 服務是否在正常工作,我們可以用 curl 來幫助判斷,其 DockerfileHEALTHCHECK 可以這麼寫:

FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
  CMD curl -fs http://localhost/ || exit 1

這裏我們設定了每 5 秒檢查一次(這裏爲了試驗所以間隔非常短,實際應該相對較長),如果健康檢查命令超過 3 秒沒響應就視爲失敗,並且使用 curl -fs http://localhost/ || exit 1 作爲健康檢查命令。

使用 docker build 來構建這個映象:

$ docker build -t myweb:v1 .

構建好了後,我們啓動一個容器:

$ docker run -d --name web -p 80:80 myweb:v1

當執行該映象後,可以通過 docker container ls 看到最初的狀態爲 (health: starting)

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待幾秒鐘後,再次 docker container ls,就會看到健康狀態變化爲了 (healthy)

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

如果健康檢查連續失敗超過了重試次數,狀態就會變爲 (unhealthy)

爲了幫助排障,健康檢查命令的輸出(包括 stdout 以及 stderr)都會被儲存於健康狀態裡,可以用 docker inspect 來檢視。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD 爲他人作嫁衣

格式:ONBUILD <其它指令>

ONBUILD 是一個特殊的指令,它後面跟的是其它指令,比如 RUN, COPY 等,而這些指令,在當前映象構建時並不會被執行。只有當以當前映象爲基礎映象,去構建下一級映象的時候纔會被執行。

Dockerfile 中的其它指令都是爲了定製當前映象而準備的,唯有 ONBUILD 是爲了幫助別人定製自己而準備的。

假設我們要製作 Node.js 所寫的應用的映象。我們都知道 Node.js 使用 npm 進行包管理,所有依賴、設定、啓動資訊等會放到 package.json 檔案裡。在拿到程式程式碼後,需要先進行 npm install 纔可以獲得所有需要的依賴。然後就可以通過 npm start 來啓動應用。因此,一般來說會這樣寫 Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把這個 Dockerfile 放到 Node.js 專案的根目錄,構建好映象後,就可以直接拿來啓動容器執行。但是如果我們還有第二個 Node.js 專案也差不多呢?好吧,那就再把這個 Dockerfile 複製到第二個專案裡。那如果有第三個專案呢?再複製麼?檔案的副本越多,版本控制就越困難,讓我們繼續看這樣的場景維護的問題。

如果第一個 Node.js 專案在開發過程中,發現這個 Dockerfile 裡存在問題,比如敲錯字了、或者需要安裝額外的包,然後開發人員修復了這個 Dockerfile,再次構建,問題解決。第一個專案沒問題了,但是第二個專案呢?雖然最初 Dockerfile 是複製、貼上自第一個專案的,但是並不會因爲第一個專案修復了他們的 Dockerfile,而第二個專案的 Dockerfile 就會被自動修復。

那麼我們可不可以做一個基礎映象,然後各個專案使用這個基礎映象呢?這樣基礎映象更新,各個專案不用同步 Dockerfile 的變化,重新構建後就繼承了基礎映象的更新?好吧,可以,讓我們看看這樣的結果。那麼上面的這個 Dockerfile 就會變爲:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

這裏我們把專案相關的構建指令拿出來,放到子專案裡去。假設這個基礎映象的名字爲 my-node 的話,各個專案內的自己的 Dockerfile 就變爲:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基礎映象變化後,各個專案都用這個 Dockerfile 重新構建映象,會繼承基礎映象的更新。

那麼,問題解決了麼?沒有。準確說,只解決了一半。如果這個 Dockerfile 裏面有些東西需要調整呢?比如 npm install 都需要加一些參數,那怎麼辦?這一行 RUN 是不可能放入基礎映象的,因爲涉及到了當前專案的 ./package.json,難道又要一個個修改麼?所以說,這樣製作基礎映象,只解決了原來的 Dockerfile 的前4條指令的變化問題,而後面三條指令的變化則完全沒辦法處理。

ONBUILD 可以解決這個問題。讓我們用 ONBUILD 重新寫一下基礎映象的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

這次我們回到原始的 Dockerfile,但是這次將專案相關的指令加上 ONBUILD,這樣在構建基礎映象的時候,這三行並不會被執行。然後各個專案的 Dockerfile 就變成了簡單地:

FROM my-node

是的,只有這麼一行。當在各個專案目錄中,用這個只有一行的 Dockerfile 構建映象時,之前基礎映象的那三行 ONBUILD 就會開始執行,成功的將當前專案的程式碼複製進映象、並且針對本專案執行 npm install,生成應用映象。

參考文件

Docker 三劍客 Compose

Docker Compose 簡介

Compose 專案是 Docker 官方的開源專案,負責實現對 Docker 容器叢集的快速編排。從功能上看,跟 OpenStack 中的 Heat 十分類似。

其程式碼目前在 https://github.com/docker/compose上開源。

Compose 定位是 「定義和執行多個 Docker 容器的應用(Defining and running multi-container Docker applications)」,其前身是開源專案 Fig。

通過第一部分中的介紹,我們知道使用一個 Dockerfile 模板檔案,可以讓使用者很方便的定義一個單獨的應用容器。然而,在日常工作中,經常會碰到需要多個容器相互配合來完成某項任務的情況。例如要實現一個 Web 專案,除了 Web 服務容器本身,往往還需要再加上後端的數據庫服務容器,甚至還包括負載均衡容器等。

Compose 恰好滿足了這樣的需求。它允許使用者通過一個單獨的 docker-compose.yml 模板檔案(YAML 格式)來定義一組相關聯的應用容器爲一個專案(project)。

Compose 中有兩個重要的概念:

  • 服務 (service):一個應用的容器,實際上可以包括若幹執行相同映象的容器範例。
  • 專案 (project):由一組關聯的應用容器組成的一個完整業務單元,在 docker-compose.yml 檔案中定義。

Compose 的預設管理物件是專案,通過子命令對專案中的一組容器進行便捷地生命週期管理。

Compose 專案由 Python 編寫,實現上呼叫了 Docker 服務提供的 API 來對容器進行管理。因此,只要所操作的平臺支援 Docker API,就可以在其上利用 Compose 來進行編排管理。

Docker Compose 安裝與解除安裝

Compose 支援 Linux、macOS、Windows 10 三大平臺。

Compose 可以通過 Python 的包管理工具 pip 進行安裝,也可以直接下載編譯好的二進制檔案使用,甚至能夠直接在 Docker 容器中執行。

前兩種方式是傳統方式,適合本地環境下安裝使用;最後一種方式則不破壞系統環境,更適合雲端計算場景。

Docker for MacDocker for Windows 自帶 docker-compose 二進制檔案,安裝 Docker 之後可以直接使用。

$ docker-compose --version

docker-compose version 1.17.1, build 6d101fb

Linux 系統請使用以下介紹的方法安裝。

二進制包

在 Linux 上的也安裝十分簡單,從 官方 GitHub Release

處直接下載編譯好的二進制檔案即可。

例如,在 Linux 64 位系統上直接下載對應的二進制包。

$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose

# 下載1.25.0 docker compose
sudo curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 新增可執行許可權
sudo chmod +x /usr/local/bin/docker-compose
# 測試安裝
sudo docker-compose --version

PIP 安裝

注: x86_64 架構的 Linux 建議按照上邊的方法下載二進制包進行安裝,如果您計算機的架構是 ARM (例如,樹莓派),再使用 pip 安裝。

這種方式是將 Compose 當作一個 Python 應用來從 pip 源中安裝。

執行安裝命令:

$ sudo pip install -U docker-compose

可以看到類似如下輸出,說明安裝成功。

Collecting docker-compose
  Downloading docker-compose-1.17.1.tar.gz (149kB): 149kB downloaded
...
Successfully installed docker-compose cached-property requests texttable websocket-client docker-py dockerpty six enum34 backports.ssl-match-hostname ipaddress

bash 補全命令

$ curl -L https://raw.githubusercontent.com/docker/compose/1.8.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

容器中執行

Compose 既然是一個 Python 應用,自然也可以直接用容器來執行它。

$ curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
$ chmod +x /usr/local/bin/docker-compose

實際上,檢視下載的 run.sh 指令碼內容,如下

set -e

VERSION="1.8.0"
IMAGE="docker/compose:$VERSION"


# Setup options for connecting to docker host
if [ -z "$DOCKER_HOST" ]; then
    DOCKER_HOST="/var/run/docker.sock"
fi
if [ -S "$DOCKER_HOST" ]; then
    DOCKER_ADDR="-v $DOCKER_HOST:$DOCKER_HOST -e DOCKER_HOST"
else
    DOCKER_ADDR="-e DOCKER_HOST -e DOCKER_TLS_VERIFY -e DOCKER_CERT_PATH"
fi


# Setup volume mounts for compose config and context
if [ "$(pwd)" != '/' ]; then
    VOLUMES="-v $(pwd):$(pwd)"
fi
if [ -n "$COMPOSE_FILE" ]; then
    compose_dir=$(dirname $COMPOSE_FILE)
fi
# TODO: also check --file argument
if [ -n "$compose_dir" ]; then
    VOLUMES="$VOLUMES -v $compose_dir:$compose_dir"
fi
if [ -n "$HOME" ]; then
    VOLUMES="$VOLUMES -v $HOME:$HOME -v $HOME:/root" # mount $HOME in /root to share docker.config
fi

# Only allocate tty if we detect one
if [ -t 1 ]; then
    DOCKER_RUN_OPTIONS="-t"
fi
if [ -t 0 ]; then
    DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"
fi

exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@"

可以看到,它其實是下載了 docker/compose 映象並執行。

解除安裝

如果是二進制包方式安裝的,刪除二進制檔案即可。

$ sudo rm /usr/local/bin/docker-compose

如果是通過 pip 安裝的,則執行如下命令即可刪除。

$ sudo pip uninstall docker-compose

Docker Compose 使用

術語

首先介紹幾個術語。

  • 服務 (service):一個應用容器,實際上可以執行多個相同映象的範例。
  • 專案 (project):由一組關聯的應用容器組成的一個完整業務單元。

可見,一個專案可以由多個服務(容器)關聯而成,Compose 面向專案進行管理。

場景

最常見的專案是 web 網站,該專案應該包含 web 應用和快取。

下面 下麪我們用 Python 來建立一個能夠記錄頁面存取次數的 web 網站。

web 應用

新建資料夾,在該目錄中編寫 app.py 檔案

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello World! 該頁面已被存取 {} 次。\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

Dockerfile

編寫 Dockerfile 檔案,內容爲

FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD ["python", "app.py"]

docker-compose.yml

編寫 docker-compose.yml 檔案,這個是 Compose 使用的主模板檔案。

version: '3'
services:

  web:
    build: .
    ports:
     - "5000:5000"
     
  redis:
    image: "redis:alpine"

執行 compose 專案

$ docker-compose up

此時存取本地

5000

埠,每次重新整理頁面,計數就會加 1。

Docker Compose 命令說明

命令物件與格式

對於 Compose 來說,大部分命令的物件既可以是專案本身,也可以指定爲專案中的服務或者容器。如果沒有特別的說明,命令物件將是專案,這意味着專案中所有的服務都會受到命令影響。

執行 docker-compose [COMMAND] --help 或者 docker-compose help [COMMAND] 可以檢視具體某個命令的使用格式。

docker-compose 命令的基本的使用格式是

docker-compose [-f=<arg>...] [options] [COMMAND] [ARGS...]

命令選項

  • -f, --file FILE 指定使用的 Compose 模板檔案,預設爲 docker-compose.yml,可以多次指定。
  • -p, --project-name NAME 指定專案名稱,預設將使用所在目錄名稱作爲專案名。
  • --x-networking 使用 Docker 的可拔插網路後端特性
  • --x-network-driver DRIVER 指定網路後端的驅動,預設爲 bridge
  • --verbose 輸出更多偵錯資訊。
  • -v, --version 列印版本並退出。

命令使用說明

build

格式爲 docker-compose build [options] [SERVICE...]

構建(重新構建)專案中的服務容器。

服務容器一旦構建後,將會帶上一個標記名,例如對於 web 專案中的一個 db 容器,可能是 web_db。

可以隨時在專案目錄下執行 docker-compose build 來重新構建服務。

選項包括:

  • --force-rm 刪除構建過程中的臨時容器。
  • --no-cache 構建映象過程中不使用 cache(這將加長構建過程)。
  • --pull 始終嘗試通過 pull 來獲取更新版本的映象。

config

驗證 Compose 檔案格式是否正確,若正確則顯示設定,若格式錯誤顯示錯誤原因。

down

此命令將會停止 up 命令所啓動的容器,並移除網路

exec

進入指定的容器。

help

獲得一個命令的幫助。

images

列出 Compose 檔案中包含的映象。

kill

格式爲 docker-compose kill [options] [SERVICE...]

通過發送 SIGKILL 信號來強制停止服務容器。

支援通過 -s 參數來指定發送的信號,例如通過如下指令發送 SIGINT 信號。

$ docker-compose kill -s SIGINT

logs

格式爲 docker-compose logs [options] [SERVICE...]

檢視服務容器的輸出。預設情況下,docker-compose 將對不同的服務輸出使用不同的顏色來區分。可以通過 --no-color 來關閉顏色。

該命令在偵錯問題的時候十分有用。

pause

格式爲 docker-compose pause [SERVICE...]

暫停一個服務容器。

port

格式爲 docker-compose port [options] SERVICE PRIVATE_PORT

列印某個容器埠所對映的公共埠。

選項:

  • --protocol=proto 指定埠協定,tcp(預設值)或者 udp。
  • --index=index 如果同一服務存在多個容器,指定命令物件容器的序號(預設爲 1)。

ps

格式爲 docker-compose ps [options] [SERVICE...]

列出項目中目前的所有容器。

選項:

  • -q 只列印容器的 ID 資訊。

pull

格式爲 docker-compose pull [options] [SERVICE...]

拉取服務依賴的映象。

選項:

  • --ignore-pull-failures 忽略拉取映象過程中的錯誤。

push

推播服務依賴的映象到 Docker 映象倉庫。

restart

格式爲 docker-compose restart [options] [SERVICE...]

重新啓動專案中的服務。

選項:

  • -t, --timeout TIMEOUT 指定重新啓動前停止容器的超時(預設爲 10 秒)。

rm

格式爲 docker-compose rm [options] [SERVICE...]

刪除所有(停止狀態的)服務容器。推薦先執行 docker-compose stop 命令來停止容器。

選項:

  • -f, --force 強制直接刪除,包括非停止狀態的容器。一般儘量不要使用該選項。
  • -v 刪除容器所掛載的數據卷。

run

格式爲 docker-compose run [options] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]

在指定服務上執行一個命令。

例如:

$ docker-compose run ubuntu ping docker.com

將會啓動一個 ubuntu 服務容器,並執行 ping docker.com 命令。

預設情況下,如果存在關聯,則所有關聯的服務將會自動被啓動,除非這些服務已經在執行中。

該命令類似啓動容器後執行指定的命令,相關卷、鏈接等等都將會按照設定自動建立。

兩個不同點:

  • 給定命令將會覆蓋原有的自動執行命令;
  • 不會自動建立埠,以避免衝突。

如果不希望自動啓動關聯的容器,可以使用 --no-deps 選項,例如

$ docker-compose run --no-deps web python manage.py shell

將不會啓動 web 容器所關聯的其它容器。

選項:

  • -d 後臺執行容器。
  • --name NAME 爲容器指定一個名字。
  • --entrypoint CMD 覆蓋預設的容器啓動指令。
  • -e KEY=VAL 設定環境變數值,可多次使用選項來設定多個環境變數。
  • -u, --user="" 指定執行容器的使用者名稱或者 uid。
  • --no-deps 不自動啓動關聯的服務容器。
  • --rm 執行命令後自動刪除容器,d 模式下將忽略。
  • -p, --publish=[] 對映容器埠到本地主機。
  • --service-ports 設定伺服器端口並對映到本地主機。
  • -T 不分配僞 tty,意味着依賴 tty 的指令將無法執行。

scale

格式爲 docker-compose scale [options] [SERVICE=NUM...]

設定指定服務執行的容器個數。

通過 service=num 的參數來設定數量。例如:

$ docker-compose scale web=3 db=2

將啓動 3 個容器執行 web 服務,2 個容器執行 db 服務。

一般的,當指定數目多於該服務當前實際執行容器,將新建立並啓動容器;反之,將停止容器。

選項:

  • -t, --timeout TIMEOUT 停止容器時候的超時(預設爲 10 秒)。

start

格式爲 docker-compose start [SERVICE...]

啓動已經存在的服務容器。

stop

格式爲 docker-compose stop [options] [SERVICE...]

停止已經處於執行狀態的容器,但不刪除它。通過 docker-compose start 可以再次啓動這些容器。

選項:

  • -t, --timeout TIMEOUT 停止容器時候的超時(預設爲 10 秒)。

top

檢視各個服務容器內執行的進程。

unpause

格式爲 docker-compose unpause [SERVICE...]

恢復處於暫停狀態中的服務。

up

格式爲 docker-compose up [options] [SERVICE...]

該命令十分強大,它將嘗試自動完成包括構建映象,(重新)建立服務,啓動服務,並關聯服務相關容器的一系列操作。

鏈接的服務都將會被自動啓動,除非已經處於執行狀態。

可以說,大部分時候都可以直接通過該命令來啓動一個專案。

預設情況,docker-compose up 啓動的容器都在前臺,控制檯將會同時列印所有容器的輸出資訊,可以很方便進行偵錯。

當通過 Ctrl-C 停止命令時,所有容器將會停止。

如果使用 docker-compose up -d,將會在後臺啓動並執行所有的容器。一般推薦生產環境下使用該選項。

預設情況,如果服務容器已經存在,docker-compose up 將會嘗試停止容器,然後重新建立(保持使用 volumes-from 掛載的卷),以保證新啓動的服務匹配 docker-compose.yml 檔案的最新內容。如果使用者不希望容器被停止並重新建立,可以使用 docker-compose up --no-recreate。這樣將只會啓動處於停止狀態的容器,而忽略已經執行的服務。如果使用者只想重新部署某個服務,可以使用 docker-compose up --no-deps -d <SERVICE_NAME> 來重新建立服務並後臺停止舊服務,啓動新服務,並不會影響到其所依賴的服務。

選項:

  • -d 在後台執行服務容器。
  • --no-color 不使用顏色來區分不同的服務的控制檯輸出。
  • --no-deps 不啓動服務所鏈接的容器。
  • --force-recreate 強制重新建立容器,不能與 --no-recreate 同時使用。
  • --no-recreate 如果容器已經存在了,則不重新建立,不能與 --force-recreate 同時使用。
  • --no-build 不自動構建缺失的服務映象。
  • -t, --timeout TIMEOUT 停止容器時候的超時(預設爲 10 秒)。

version

格式爲 docker-compose version

列印版本資訊。

Docker 三劍客 Swarm

什麼是 Docker Swarm

Docker Swarm 是 Docker 官方三劍客專案之一,提供 Docker 容器叢集服務,是 Docker 官方對容器雲生態進行支援的核心方案。

使用它,使用者可以將多個 Docker 主機封裝爲單個大型的虛擬 Docker 主機,快速打造一套容器雲平臺。

注意:Docker 1.12.0+ Swarm mode 已經內嵌入 Docker 引擎,成爲了 docker 子命令 docker swarm,絕大多數使用者已經開始使用 Swarm mode,Docker 引擎 API 已經刪除 Docker Swarm。爲避免大家混淆舊的 Docker Swarm 與新的 Swarm mode,舊的 Docker Swarm 內容已經刪除。

Docker Swarm mode

Docker 1.12 Swarm mode 已經內嵌入 Docker 引擎,成爲了 docker 子命令 docker swarm。請注意與舊的 Docker Swarm 區分開來。

Swarm mode 內建 kv 儲存功能,提供了衆多的新特性,比如:具有容錯能力的去中心化設計、內建服務發現、負載均衡、路由網格、動態伸縮、卷動更新、安全傳輸等。使得 Docker 原生的 Swarm 叢集具備與 Mesos、Kubernetes 競爭的實力。

Swarm mode 基本概念

Swarm 是使用 SwarmKit 構建的 Docker 引擎內建(原生)的叢集管理和編排工具。

使用 Swarm 叢集之前需要瞭解以下幾個概念。

節點

執行 Docker 的主機可以主動初始化一個 Swarm 叢集或者加入一個已存在的 Swarm 叢集,這樣這個執行 Docker 的主機就成爲一個 Swarm 叢集的節點 (node) 。

節點分爲管理 (manager) 節點和工作 (worker) 節點。

管理節點用於 Swarm 叢集的管理,docker swarm 命令基本只能在管理節點執行(節點退出叢集命令 docker swarm leave 可以在工作節點執行)。一個 Swarm 叢集可以有多個管理節點,但只有一個管理節點可以成爲 leaderleader 通過 raft 協定實現。

工作節點是任務執行節點,管理節點將服務 (service) 下發至工作節點執行。管理節點預設也作爲工作節點。你也可以通過設定讓服務只執行在管理節點。

來自 Docker 官網的這張圖片形象的展示了叢集中管理節點與工作節點的關係。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LwNYlAJM-1597232562015)(Docker/swarm-diagram.png)]

服務和任務

任務 (Task)是 Swarm 中的最小的排程單位,目前來說就是一個單一的容器。

服務 (Services) 是指一組任務的集合,服務定義了任務的屬性。服務有兩種模式:

  • replicated services 按照一定規則在各個工作節點上執行指定個數的任務。
  • global services 每個工作節點上執行一個任務

兩種模式通過 docker service create--mode 參數指定。

來自 Docker 官網的這張圖片形象的展示了容器、任務、服務的關係。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nnM70KDv-1597232562015)(Docker/services-diagram.png)]

Docker 三劍客 Machine

什麼是 Docker Machine

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HIdiOeyd-1597232562016)(Docker/machine.png)]

Docker Machine 是 Docker 官方編排(Orchestration)專案之一,負責在多種平臺上快速安裝 Docker 環境。

Docker Machine 專案基於 Go 語言實現,目前在 Github上進行維護。本章將介紹 Docker Machine 的安裝及使用。

Docker Machine 安裝

Docker Machine 可以在多種操作系統平臺上安裝,包括 Linux、macOS,以及 Windows。

macOS、Windows

Docker for Mac、Docker for Windows 自帶 docker-machine 二進制包,安裝之後即可使用。

檢視版本資訊。

$ docker-machine -v
docker-machine version 0.13.0, build 9ba6da9

Linux

在 Linux 上的也安裝十分簡單,從 官方 GitHub Release

處直接下載編譯好的二進制檔案即可。

例如,在 Linux 64 位系統上直接下載對應的二進制包。

$ sudo curl -L https://github.com/docker/machine/releases/download/v0.13.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine
$ sudo chmod +x /usr/local/bin/docker-machine

完成後,檢視版本資訊。

$ docker-machine -v
docker-machine version 0.13.0, build 9ba6da9

Docker Machine 使用

Docker Machine 支援多種後端驅動,包括虛擬機器、本地主機和雲平臺等。

建立本地主機範例

Virtualbox 驅動

使用 virtualbox 型別的驅動,建立一臺 Docker 主機,命名爲 test。

$ docker-machine create -d virtualbox test

你也可以在建立時加上如下參數,來設定主機或者主機上的 Docker。

--engine-opt dns=114.114.114.114 設定 Docker 的預設 DNS

--engine-registry-mirror https://registry.docker-cn.com 設定 Docker 的倉庫映象

--virtualbox-memory 2048 設定主機記憶體

--virtualbox-cpu-count 2 設定主機 CPU

更多參數請使用 docker-machine create --driver virtualbox --help 命令檢視。

macOS xhyve 驅動

xhyve 驅動 GitHub: https://github.com/zchee/docker-machine-driver-xhyve

xhyve

是 macOS 上輕量化的虛擬引擎,使用其建立的 Docker Machine 較 VirtualBox 驅動建立的執行效率要高。

$ brew install docker-machine-driver-xhyve

$ docker-machine create \
      -d xhyve \
      # --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso \
      --engine-opt dns=114.114.114.114 \
      --engine-registry-mirror https://registry.docker-cn.com \
      --xhyve-memory-size 2048 \
      --xhyve-rawdisk \
      --xhyve-cpu-count 2 \
      xhyve

注意:非首次建立時建議加上 --xhyve-boot2docker-url ~/.docker/machine/cache/boot2docker.iso 參數,避免每次建立時都從 GitHub 下載 ISO 映象。

更多參數請使用 docker-machine create --driver xhyve --help 命令檢視。

Windows 10

Windows 10 安裝 Docker for Windows 之後不能再安裝 VirtualBox,也就不能使用 virtualbox 驅動來建立 Docker Machine,我們可以選擇使用 hyperv 驅動。

$ docker-machine create --driver hyperv vm

更多參數請使用 docker-machine create --driver hyperv --help 命令檢視。

使用介紹

建立好主機之後,檢視主機

$ docker-machine ls

NAME      ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER       ERRORS
test      -        virtualbox   Running   tcp://192.168.99.187:2376           v17.10.0-ce

建立主機成功後,可以通過 env 命令來讓後續操作物件都是目標主機。

$ docker-machine env test

後續根據提示在命令列輸入命令之後就可以操作 test 主機。

也可以通過 SSH 登錄到主機。

$ docker-machine ssh test

docker@test:~$ docker --version
Docker version 17.10.0-ce, build f4ffd25

連線到主機之後你就可以在其上使用 Docker 了。

官方支援驅動

通過 -d 選項可以選擇支援的驅動型別。

  • amazonec2
  • azure
  • digitalocean
  • exoscale
  • generic
  • google
  • hyperv
  • none
  • openstack
  • rackspace
  • softlayer
  • virtualbox
  • vmwarevcloudair
  • vmwarefusion
  • vmwarevsphere

第三方驅動

請到 第三方驅動列表檢視

操作命令

  • active 檢視活躍的 Docker 主機
  • config 輸出連線的設定資訊
  • create 建立一個 Docker 主機
  • env 顯示連線到某個主機需要的環境變數
  • inspect 輸出主機更多資訊
  • ip 獲取主機地址
  • kill 停止某個主機
  • ls 列出所有管理的主機
  • provision 重新設定一個已存在的主機
  • regenerate-certs 爲某個主機重新生成 TLS 認證資訊
  • restart 重新啓動主機
  • rm 刪除某台主機
  • ssh SSH 到主機上執行命令
  • scp 在主機之間複製檔案
  • mount 掛載主機目錄到本地
  • start 啓動一個主機
  • status 檢視主機狀態
  • stop 停止一個主機
  • upgrade 更新主機 Docker 版本爲最新
  • url 獲取主機的 URL
  • version 輸出 docker-machine 版本資訊
  • help 輸出幫助資訊

每個命令,又帶有不同的參數,可以通過

$ docker-machine COMMAND --help

來檢視具體的用法。

附錄

Docker 資源鏈接

官方網站

  • Docker 官方主頁:https://www.docker.com
  • Docker 官方部落格:https://blog.docker.com/
  • Docker 官方文件:https://docs.docker.com/
  • Docker Store:https://store.docker.com
  • Docker Cloud:https://cloud.docker.com
  • Docker Hub:https://hub.docker.com
  • Docker 的原始碼倉庫:https://github.com/moby/moby
  • Docker 發佈版本歷史:https://docs.docker.com/release-notes/
  • Docker 常見問題:https://docs.docker.com/engine/faq/
  • Docker 遠端應用 API:https://docs.docker.com/develop/sdk/

實踐參考

  • Dockerfile 參考:https://docs.docker.com/engine/reference/builder/
  • Dockerfile 最佳實踐:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/

技術交流

  • Docker 郵寄清單: https://groups.google.com/forum/#!forum/docker-user
  • Docker 的 IRC 頻道:https://chat.freenode.net#docker
  • Docker 的 Twitter 主頁:https://twitter.com/docker

其它

目標主機。

$ docker-machine env test

後續根據提示在命令列輸入命令之後就可以操作 test 主機。

也可以通過 SSH 登錄到主機。

$ docker-machine ssh test

docker@test:~$ docker --version
Docker version 17.10.0-ce, build f4ffd25

連線到主機之後你就可以在其上使用 Docker 了。

官方支援驅動

通過 -d 選項可以選擇支援的驅動型別。

  • amazonec2
  • azure
  • digitalocean
  • exoscale
  • generic
  • google
  • hyperv
  • none
  • openstack
  • rackspace
  • softlayer
  • virtualbox
  • vmwarevcloudair
  • vmwarefusion
  • vmwarevsphere

第三方驅動

請到 第三方驅動列表檢視

操作命令

  • active 檢視活躍的 Docker 主機
  • config 輸出連線的設定資訊
  • create 建立一個 Docker 主機
  • env 顯示連線到某個主機需要的環境變數
  • inspect 輸出主機更多資訊
  • ip 獲取主機地址
  • kill 停止某個主機
  • ls 列出所有管理的主機
  • provision 重新設定一個已存在的主機
  • regenerate-certs 爲某個主機重新生成 TLS 認證資訊
  • restart 重新啓動主機
  • rm 刪除某台主機
  • ssh SSH 到主機上執行命令
  • scp 在主機之間複製檔案
  • mount 掛載主機目錄到本地
  • start 啓動一個主機
  • status 檢視主機狀態
  • stop 停止一個主機
  • upgrade 更新主機 Docker 版本爲最新
  • url 獲取主機的 URL
  • version 輸出 docker-machine 版本資訊
  • help 輸出幫助資訊

每個命令,又帶有不同的參數,可以通過

$ docker-machine COMMAND --help

來檢視具體的用法。

附錄

Docker 資源鏈接

官方網站

  • Docker 官方主頁:https://www.docker.com
  • Docker 官方部落格:https://blog.docker.com/
  • Docker 官方文件:https://docs.docker.com/
  • Docker Store:https://store.docker.com
  • Docker Cloud:https://cloud.docker.com
  • Docker Hub:https://hub.docker.com
  • Docker 的原始碼倉庫:https://github.com/moby/moby
  • Docker 發佈版本歷史:https://docs.docker.com/release-notes/
  • Docker 常見問題:https://docs.docker.com/engine/faq/
  • Docker 遠端應用 API:https://docs.docker.com/develop/sdk/

實踐參考

  • Dockerfile 參考:https://docs.docker.com/engine/reference/builder/
  • Dockerfile 最佳實踐:https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/

技術交流

  • Docker 郵寄清單: https://groups.google.com/forum/#!forum/docker-user
  • Docker 的 IRC 頻道:https://chat.freenode.net#docker
  • Docker 的 Twitter 主頁:https://twitter.com/docker

其它

  • Docker 的 StackOverflow 問答主頁:https://stackoverflow.com/search?q=docker