Docker 使用 Google 公司推出的 Go 語言 進行開發實現,基於 Linux 內核的 cgroup,namespace,以及
AUFS 類的 Union FS 等技術,對進程進行封裝隔離,屬於操作系統層面的虛擬化技術。由於隔離的進程獨立於宿主和其它的隔離的進程,因此也稱其爲容器。最初實現是基於 LXC,從 0.7 版本以後開始去除 LXC,轉而使用自行開發的 libcontainer,從 1.11 開始,則進一步演進爲使用 runC 和 containerd。
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 容器應用,由於直接執行於宿主內核,無需啓動完整的操作系統,因此可以做到秒級、甚至毫秒級的啓動時間。大大的節約了開發、測試、部署的時間。
一致的執行環境
開發過程中一個常見的問題是環境一致性問題。由於開發環境、測試環境、生產環境不一致,導致有些 bug 並未在開發過程中被發現。而 Docker 的映象提供了除內核外完整的執行時環境,確保了應用執行環境一致性,從而不會再出現 「這段程式碼在我機器上沒問題啊」 這類問題。
持續交付和部署
對開發和運維(DevOps)人員來說,最希望的就是一次建立或設定,可以在任意地方正常執行。
使用 Docker 可以通過定製應用映象來實現持續整合、持續交付、部署。開發人員可以通過 Dockerfile
來進行映象構建,並結合 持續整合(Continuous Integration)
系統進行整合測試,而運維人員則可以直接在生產環境中快速部署該映象,甚至結合持續部署(Continuous Delivery/Deployment)系統進行自動部署。
而且使用 Dockerfile
使映象構建透明化,不僅僅開發團隊可以理解應用執行環境,也方便運維團隊理解應用執行所需條件,幫助更好的生產環境中部署該映象。
更輕鬆的遷移
由於 Docker 確保了執行環境的一致性,使得應用的遷移更加容易。Docker 可以在很多平臺上執行,無論是物理機、虛擬機器、公有雲、私有雲,甚至是筆電,其執行結果是一致的。因此使用者可以很輕易的將在一個平臺上執行的應用,遷移到另一個平臺上,而不用擔心執行環境的變化導致應用無法正常執行的情況。
更輕鬆的維護和擴充套件
Docker 使用的分層儲存以及映象的技術,使得應用重複部分的複用更爲容易,也使得應用的維護更新更加簡單,基於基礎映象進一步擴充套件映象也變得非常簡單。此外,Docker 團隊同各個開源專案團隊一起維護了一大批高品質的 官方映象,既可以直接在生產環境使用,又可以作爲基礎進一步定製,大大的降低了應用服務的映象製作成本。
對比傳統虛擬機器總結
特性 | 容器 | 虛擬機器 |
---|---|---|
啓動 | 秒級 | 分鐘級 |
硬碟使用 | 一般爲 MB |
一般爲 GB |
效能 | 接近原生 | 弱於 |
系統支援量 | 單機支援上千個容器 | 一般幾十個 |
Docker 包括三個基本概念
Image
)Container
)Repository
)理解了這三個概念,就理解了 Docker 的整個生命週期。
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。 |
我們都知道,操作系統分爲內核和使用者空間。對於 Linux 而言,內核啓動後,會掛載 root
檔案系統爲其提供使用者空間支援。而 Docker 映象(Image),就相當於是一個 root
檔案系統。比如官方映象 ubuntu:16.04
就包含了完整的一套 Ubuntu 16.04 最小系統的 root
檔案系統。
Docker 映象是一個特殊的檔案系統,除了提供容器執行時所需的程式、庫、資源、設定等檔案外,還包含了一些爲執行時準備的一些設定參數(如匿名卷、環境變數、使用者等)。映象不包含任何動態數據,其內容在構建之後也不會被改變。
分層儲存
因爲映象包含操作系統完整的 root 檔案系統,其體積往往是龐大的,因此在 Docker 設計時,就充分利用 Union FS的技術,將其設計爲分層儲存的架構。所以嚴格來說,映象並非是像一個 ISO 那樣的打包檔案,映象只是一個虛擬的概念,其實際體現並非由一個檔案組成,而是由一組檔案系統組成,或者說,由多層檔案系統聯合組成。
映象構建時,會一層層構建,前一層是後一層的基礎。每一層構建完就不會再發生改變,後一層上的任何改變只發生在自己這一層。比如,刪除前一層檔案的操作,實際不是真的刪除前一層的檔案,而是僅在當前層標記爲該檔案已刪除。在最終容器執行的時候,雖然不會看到這個檔案,但是實際上該檔案會一直跟隨映象。因此,在構建映象的時候,需要額外小心,每一層儘量只包含該層需要新增的東西,任何額外的東西應該在該層構建結束前清理掉。
分層儲存的特徵還使得映象的複用、定製變的更爲容易。甚至可以用之前構建好的映象作爲基礎層,然後進一步新增新的層,以定製自己所需的內容,構建新的映象。
映象(Image
)和容器(Container
)的關係,就像是物件導向程式設計中的 類
和 範例
一樣,映象是靜態的定義,容器是映象執行時的實體。容器可以被建立、啓動、停止、刪除、暫停等。
容器的實質是進程,但與直接在宿主執行的進程不同,容器進程執行於屬於自己的獨立的 名稱空間 。因此容器可以擁有自己的 root
檔案系統、自己的網路設定、自己的進程空間,甚至自己的使用者 ID空間。容器內的進程是執行在一個隔離的環境裡,使用起來,就好像是在一個獨立於宿主的系統下操作一樣。這種特性使得容器封裝的應用比直接在宿主執行更加安全。也因爲這種隔離的特性,很多人初學Docker 時常常會混淆容器和虛擬機器。
前面講過映象使用的是分層儲存,容器也是如此。每一個容器執行時,是以映象爲基礎層,在其上建立一個當前容器的儲存層,我們可以稱這個爲容器執行時讀寫而準備的儲存層爲容器儲存層。
容器儲存層的生存週期和容器一樣,容器消亡時,容器儲存層也隨之消亡。因此,任何儲存於容器儲存層的資訊都會隨容器刪除而丟失。
按照 Docker 最佳實踐的要求,容器不應該向其儲存層內寫入任何數據,容器儲存層要保持無狀態化。所有的檔案寫入操作,都應該使用 數據卷(Volume)
、或者系結宿主目錄,在這些位置的讀寫會跳過容器儲存層,直接對宿主(或網路儲存)發生讀寫,其效能和穩定性更高。
數據卷的生存週期獨立於容器,容器消亡,數據卷不會消亡。因此,使用數據卷後,容器刪除或者重新執行之後,數據卻不會丟失。
映象構建完成後,可以很容易的在當前宿主機上執行,但是,如果需要在其它伺服器上使用這個映象,我們就需要一個集中的儲存、分發映象的服務,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 公開服務是開放給使用者使用、允許使用者管理映象的 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 官方提供了 Docker Registry 映象,可以直接使用做爲私有 Registry 服務。
開源的 Docker Registry 映象只提供了 Docker Registry API 的伺服器端實現,足以支援 docker
命令,不影響使用。但不包含圖形介面,以及映象維護、使用者管理、存取控制等高階功能。在官方的商業化版本 Docker Trusted Registry 中,提供了這些高階功能。
除了官方的 Docker Registry 外,還有第三方軟體實現了 Docker Registry API,甚至提供了用戶介面以及一些高階功能。比如,VMWare Harbor 和 Sonatype Nexus 。
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中國學習
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
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 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 Hub 上有大量的高品質的映象可以用,這裏我們就說一下怎麼獲取這些映象。
在拉去映象之前可以查詢通過docker hub也可以使用命令
docker search mysql
從 Docker 映象倉庫獲取映象的命令是 docker pull
。其命令格式爲:
docker pull [選項] [Docker Registry 地址[:埠號]/]倉庫名[:標籤]
具體的選項可以通過 docker pull --help
命令看到,這裏我們說一下映象名稱的格式。
<域名/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 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.04
和 ubuntu: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 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-lvm
給 devicemapper
,無論是爲了效能、穩定性還是空間利用率。
或許有人注意到了 CentOS 7 中存在被 backports 回來的 overlay 驅動,不過 CentOS 裡的這個驅動達不到生產環境使用的穩定程度,所以不推薦使用。
注意: 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 歡迎頁面。
現在,假設我們非常不喜歡這個歡迎頁面,我們希望改成歡迎 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
的內容。
現在我們再重新整理瀏覽器的話,會發現內容被改變了。
我們修改了容器的檔案,也就是改動了容器的儲存層。我們可以通過 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
製作映象,以及後期修改的話,每一次修改都會讓映象更加臃腫一次,所刪除的上一層的東西並不會丟失,會一直如影隨形的跟着這個映象,即使根本無法存取到。這會讓映象更加臃腫。
從剛纔的 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 很簡單,一共就兩行。涉及到了兩條指令,FROM
和 RUN
。
FROM 指定基礎映象
所謂定製映象,那一定是以一個映象爲基礎,在其上進行定製。就像我們之前執行了一個 nginx
映象的容器,再進行修改一樣,基礎映象是必須指定的。而 FROM
就是指定基礎映象,因此一個 Dockerfile
中 FROM
是必備的指令,並且必須是第一條指令。
在 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
指令在定製映象時是最常用的指令之一。其格式有兩種:
RUN <命令>
,就像直接在命令列中輸入的命令一樣。剛纔寫的 Dockerfile 中的 RUN
指令就是這種格式。RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
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
如果發現標準輸入的檔案格式是 gzip
、bzip2
以及 xz
的話,將會使其爲上下文壓縮包,直接將其展開,將裏面視爲上下文,並開始構建。
我們已經介紹了 FROM
,RUN
,還提及了 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
指令會令映象構建快取失效,從而可能會令映象構建變得比較緩慢。
因此在 COPY
和 ADD
指令中選擇的時候,可以遵循這樣的原則,所有的檔案複製均使用 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 使用者,並在最後指定了 ENTRYPOINT
爲 docker-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
構建維護變得更輕鬆了。
下列指令可以支援環境變數展開: ADD
、COPY
、ENV
、EXPOSE
、LABEL
、USER
、WORKDIR
、VOLUME
、STOPSIGNAL
、ONBUILD
。
可以從這個指令列表裏感覺到,環境變數可以使用的地方很多,很強大。通過環境變數,我們可以讓一份 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
來幫助判斷,其 Dockerfile
的 HEALTHCHECK
可以這麼寫:
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
,生成應用映象。
參考文件
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
來進行編排管理。
Compose
支援 Linux、macOS、Windows 10 三大平臺。
Compose
可以通過 Python 的包管理工具 pip
進行安裝,也可以直接下載編譯好的二進制檔案使用,甚至能夠直接在 Docker 容器中執行。
前兩種方式是傳統方式,適合本地環境下安裝使用;最後一種方式則不破壞系統環境,更適合雲端計算場景。
Docker for Mac
、Docker 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
術語
首先介紹幾個術語。
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。
命令物件與格式
對於 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 官方三劍客專案之一,提供 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 1.12 Swarm mode 已經內嵌入 Docker 引擎,成爲了 docker 子命令 docker swarm
。請注意與舊的 Docker Swarm
區分開來。
Swarm mode
內建 kv 儲存功能,提供了衆多的新特性,比如:具有容錯能力的去中心化設計、內建服務發現、負載均衡、路由網格、動態伸縮、卷動更新、安全傳輸等。使得 Docker 原生的 Swarm
叢集具備與 Mesos、Kubernetes 競爭的實力。
Swarm
是使用 SwarmKit
構建的 Docker 引擎內建(原生)的叢集管理和編排工具。
使用 Swarm
叢集之前需要瞭解以下幾個概念。
節點
執行 Docker 的主機可以主動初始化一個 Swarm
叢集或者加入一個已存在的 Swarm
叢集,這樣這個執行 Docker 的主機就成爲一個 Swarm
叢集的節點 (node
) 。
節點分爲管理 (manager
) 節點和工作 (worker
) 節點。
管理節點用於 Swarm
叢集的管理,docker swarm
命令基本只能在管理節點執行(節點退出叢集命令 docker swarm leave
可以在工作節點執行)。一個 Swarm
叢集可以有多個管理節點,但只有一個管理節點可以成爲 leader
,leader
通過 raft
協定實現。
工作節點是任務執行節點,管理節點將服務 (service
) 下發至工作節點執行。管理節點預設也作爲工作節點。你也可以通過設定讓服務只執行在管理節點。
來自 Docker 官網的這張圖片形象的展示了叢集中管理節點與工作節點的關係。
服務和任務
任務 (Task
)是 Swarm
中的最小的排程單位,目前來說就是一個單一的容器。
服務 (Services
) 是指一組任務的集合,服務定義了任務的屬性。服務有兩種模式:
replicated services
按照一定規則在各個工作節點上執行指定個數的任務。global services
每個工作節點上執行一個任務兩種模式通過 docker service create
的 --mode
參數指定。
來自 Docker 官網的這張圖片形象的展示了容器、任務、服務的關係。
Docker Machine 是 Docker 官方編排(Orchestration)專案之一,負責在多種平臺上快速安裝 Docker 環境。
Docker Machine 專案基於 Go 語言實現,目前在 Github上進行維護。本章將介紹 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 支援多種後端驅動,包括虛擬機器、本地主機和雲平臺等。
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
是 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
選項可以選擇支援的驅動型別。
第三方驅動
請到 第三方驅動列表檢視
操作命令
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
獲取主機的 URLversion
輸出 docker-machine 版本資訊help
輸出幫助資訊每個命令,又帶有不同的參數,可以通過
$ docker-machine COMMAND --help
來檢視具體的用法。
官方網站
實踐參考
技術交流
其它
目標主機。
$ docker-machine env test
後續根據提示在命令列輸入命令之後就可以操作 test 主機。
也可以通過 SSH
登錄到主機。
$ docker-machine ssh test
docker@test:~$ docker --version
Docker version 17.10.0-ce, build f4ffd25
連線到主機之後你就可以在其上使用 Docker 了。
官方支援驅動
通過 -d
選項可以選擇支援的驅動型別。
第三方驅動
請到 第三方驅動列表檢視
操作命令
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
獲取主機的 URLversion
輸出 docker-machine 版本資訊help
輸出幫助資訊每個命令,又帶有不同的參數,可以通過
$ docker-machine COMMAND --help
來檢視具體的用法。
官方網站
實踐參考
技術交流
其它