国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

從零開始學習 Docker

lily_wang / 2480人閱讀

摘要:此命令下載測試鏡像并在容器中運行它。國內很多云服務商都提供了加速器服務,例如阿里云加速器注冊用戶并且申請加速器,會獲得如這樣的地址。獲取鏡像阿里云鏡像庫上有大量的高質量的鏡像可以用,這里我們就說一下怎么獲取這些鏡像并運行。

這篇文章是我學習 Docker 的記錄,大部分內容摘抄自 <> 一書,并非本人原創.
學習過程中整理成適合我自己的筆記,其中也包含了我自己的實踐記錄.

最近工作中遇到項目部署的問題,因為原先舊項目還需要繼續在線服役,所以生產環境的一整套東西一直都停留在很低版本的 CentOS 中,很多時候想擴展或想部署一個新功能因為生產環境的問題而不得不花費更多的時間,有時候還不得不放棄.最要命的是我們新項目的開發環境是 Windows 環境,而且都是用較新的開發環境;而測試環境卻又是較新的 CentOS 環境,導致很多時候在這個環境運行沒有問題,在另一個環境卻無緣無故出問題,期間為了這些事浪費了很多時間.還好發現有 Docker 能夠解決這些頭痛的問題,當然 Docker 不單單只能解決以上問題,它還有很多強大的功能.接下來就從零開始講講 Docker.

什么是 Docker

Docker 是 Docker 公司的開源項目,使用 Google 公司推出的?Go 語言開發的,并于 2013 年 3 月以 Apache 2.0 授權協議開源,主要項目代碼在 GitHub 上進行維護。

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

為什么要使用 Docker?

Docker 跟傳統的虛擬化方式相比具有以下優勢:

更高效的利用系統資源

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

更快速的啟動時間

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

一致的運行環境

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

持續交付和部署

對開發和運維人員來說,最希望的就是一次創建或配置,可以在任意地方正常運行。

使用 Docker 可以通過定制應用鏡像來實現持續集成、持續交付、部署。開發人員可以通過 Dockerfile 來進行鏡像構建,并結合 持續集成系統進行集成測試,而運維人員則可以直接在生產環境中快速部署該鏡像,甚至結合持續部署系統進行自動部署。

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

更輕松的遷移

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

更輕松的維護和擴展

Docker 使用的分層存儲以及鏡像的技術,使得應用重復部分的復用更為容易,也使得應用的維護更新更加簡單,基于基礎鏡像進一步擴展鏡像也變得非常簡單。此外,Docker 團隊同各個開源項目團隊一起維護了一大批高質量的官方鏡像,既可以直接在生產環境使用,又可以作為基礎進一步定制,大大的降低了應用服務的鏡像制作成本。

對比傳統虛擬機總結

基本概念

Docker 包括三個基本概念

鏡像(Image)

容器(Container)

倉庫(Repository)

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

Docker 鏡像

我們都知道,操作系統分為內核和用戶空間。對于 Linux 而言,內核啟動后,會掛載 root 文件系統為其提供用戶空間支持。而 Docker 鏡像,就相當于是一個 root 文件系統。比如 Docker 官方鏡像 ubuntu:14.04 就包含了完整的一套 Ubuntu 14.04 最小系統的 root 文件系統。

Docker 鏡像是一個特殊的文件系統,除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些為運行時準備的一些配置參數(如匿名卷、環境變量、用戶等)。鏡像不包含任何動態數據,其內容在構建之后也不會被改變。

Docker 容器

鏡像和容器的關系,就像是面向對象程序設計中的實例一樣,鏡像是靜態的定義,容器是鏡像運行時的實體。容器可以被創建、啟動、停止、刪除、暫停等。

每一個容器運行時,是以鏡像為基礎層,在其上創建一個當前容器的存儲層,我們可以稱這個為容器運行時讀寫而準備的存儲層為容器存儲層

容器存儲層的生存周期和容器一樣,容器消亡時,容器存儲層也隨之消亡。因此,任何保存于容器存儲層的信息都會隨容器刪除而丟失。

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

數據卷的生存周期獨立于容器,容器消亡,數據卷不會消亡。因此,使用數據卷后,容器可以隨意刪除、重新 run,數據卻不會丟失。

Docker 倉庫

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

安裝 Docker

官方網站上有各種環境下的 安裝指南,這里主要介紹下 CentOS 的安裝。

CentOS 操作系統安裝 Docker 系統要求

Docker 需要安裝在 CentOS 7 64 位的平臺,并且內核版本不低于 3.10. CentOS 7.× 滿足要求的最低內核版本要求,但由于 CentOS 7 內核版本比較低,部分功能(如 overlay2 存儲層驅動)無法使用,并且部分功能可能不太穩定。所以建議大家升級到最新的 CentOS 版本,并且內核也更新到最新的穩定版本.更新的方法可以看看我的<>

使用阿里云的安裝腳本自動安裝

為了簡化 Docker 安裝流程,我們可以使用阿里云提供的一套安裝腳本,CentOS 系統上可以使用這套腳本安裝 Docker :

curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -

執行這個命令后,腳本就會自動的將一切準備工作做好,并且把 Docker 安裝在系統中。

Docker 通過運行 hello-world 映像驗證是否正確安裝。

$ docker run hello-world

> Unable to find image "hello-world:latest" locally
> latest: Pulling from library/hello-world
> b04784fba78d: Pull complete 
> Digest: sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f
> Status: Downloaded newer image for hello-world:latest
 
> Hello from Docker!
> This message shows that your installation appears to be working correctly.

> To generate this message, Docker took the following steps:
>  1. The Docker client contacted the Docker daemon.
>  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
>  3. The Docker daemon created a new container from that image which runs the
>     executable that produces the output you are currently reading.
>  4. The Docker daemon streamed that output to the Docker client, which sent it
>     to your terminal.

> To try something more ambitious, you can run an Ubuntu container with:
>  $ docker run -it ubuntu bash

> Share images, automate workflows, and more with a free Docker ID:
>  https://cloud.docker.com/

> For more examples and ideas, visit:
>  https://docs.docker.com/engine/userguide/

此命令下載測試鏡像并在容器中運行它。當容器運行時,它打印一條信息消息并退出。如果你沒有配置鏡像加速器的話,運行 hello-world 映像驗證也是不會成功的.因為國內網絡的原因,無法下載測試鏡像,更別說運行測試鏡像了,所以這一步可以先跳過,繼續往下看,等一下配置完鏡像加速器再來驗證.

查看當前 Docker 的版本
$ docker -v

> Docker version 17.05.0-ce, build 89658be

可以看出當前的 Docker 為 Docker CE 17.05.0 版本,CE 代表 Docker 社區版,EE 代表 Docker 企業版.

卸載 Docker CE

卸載Docker軟件包:

$ yum remove docker-ce
卸載舊版本 Docker

較老版本的 Docker 被稱為 docker 或 docker-engine。如果這些已安裝,請卸載它們以及關聯的依賴關系。

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

主機上的圖像,容器,卷或自定義配置文件不會自動刪除。必須手動刪除任何已編輯的配置文件。刪除所有圖像,容器和卷:

$ rm -rf /var/lib/docker
參考文檔

參見 Docker 官方 CentOS 安裝文檔.

鏡像加速器

國內訪問 Docker Hub 有時會遇到困難,此時可以配置鏡像加速器。國內很多云服務商都提供了加速器服務,例如:

阿里云加速器

注冊用戶并且申請加速器,會獲得如 https://jxus37ad.mirror.aliyuncs.com 這樣的地址。我們需要將其配置給 Docker 引擎。

systemctl enable docker 啟用服務后,編輯 /etc/systemd/system/multi-user.target.wants/docker.service 文件,找到 ExecStart= 這一行,在這行最后添加加速器地址 --registry-mirror=<加速器地址>,如:

ExecStart=/usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com

注:對于 1.12 以前的版本,dockerd 換成 docker daemon

重新加載配置并且重新啟動。

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
檢查加速器是否生效

Linux系統下配置完加速器需要檢查是否生效,在命令行執行 ps -ef | grep dockerd,如果從結果中看到了配置的 --registry-mirror 參數說明配置成功。

$ sudo ps -ef | grep dockerd

> root      5346     1  0 19:03 ?        00:00:00 /usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com
使用 Docker 鏡像

Docker 運行容器前需要本地存在對應的鏡像,如果鏡像不存在本地,Docker 會從鏡像倉庫下載(默認是 Docker Hub 公共注冊服務器中的倉庫)。

獲取鏡像

阿里云鏡像庫 上有大量的高質量的鏡像可以用,這里我們就說一下怎么獲取這些鏡像并運行。

獲取鏡像的命令是 docker pull。其命令格式為:

docker pull [選項] [Docker Registry地址]<倉庫名>:<標簽>

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

Docker Registry地址:地址的格式一般是 <域名/IP>[:端口號]。默認地址是 Docker Hub。

倉庫名:如之前所說,這里的倉庫名是兩段式名稱,既 <用戶名>/<軟件名>。對于 Docker Hub,如果不給出用戶名,則默認為 library,也就是官方鏡像.一定要配置鏡像加速器,不然下載速度很慢。

比如:

$ docker pull ubuntu:14.04

14.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:14.04

上面的命令中沒有給出 Docker Registry 地址,因此將會從 Docker Hub 獲取鏡像。而鏡像名稱是 ubuntu:14.04,因此將會獲取官方鏡像 library/ubuntu 倉庫中標簽為 14.04 的鏡像。

查看已下載的鏡像

要想列出已經下載下來的鏡像,可以使用 docker images 命令。

$ docker images

REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hello-world          latest              1815c82652c0        3 weeks ago         1.84kB
ubuntu               14.04               4a2820e686c4        2 weeks ago         188 MB

列表包含了倉庫名、標簽、鏡像 ID、創建時間以及所占用的空間。

運行

有了鏡像后,我們就可以以這個鏡像為基礎啟動一個容器來運行。以上面的 ubuntu:14.04 為例,如果我們打算啟動里面的 bash 并且進行交互式操作的話,可以執行下面的命令。

$ docker run -it --rm ubuntu:14.04 bash

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

docker run 就是運行容器的命令,具體格式我們會在后面的章節講解,我們這里簡要的說明一下上面用到的參數。

-it:這是兩個參數,一個是 -i:交互式操作,一個是 -t 終端。我們這里打算進入 bash 執行一些命令并查看返回結果,因此我們需要交互式終端。

--rm:這個參數是說容器退出后隨之將其刪除。默認情況下,為了排障需求,退出的容器并不會立即刪除,除非手動 docker rm。我們這里只是隨便執行個命令,看看結果,不需要排障和保留結果,因此使用 --rm 可以避免浪費空間。

ubuntu:14.04:這是指用 ubuntu:14.04 鏡像為基礎來啟動容器。

bash:放在鏡像名后的是命令,這里我們希望有個交互式 Shell,因此用的是 bash

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

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

定制鏡像

現在讓我們以定制一個 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 歡迎頁面。

現在,改動這個歡迎頁面,改成Hello, Docker!,我們可以使用 docker exec 命令進入容器,修改其內容。

$ docker exec -it webserver bash

root@f532879089c6:/# echo "

Hello, Docker!

" > /usr/share/nginx/html/index.html root@f532879089c6:/# exit exit

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

然后,我們用

Hello, Docker!

覆蓋了 /usr/share/nginx/html/index.html 的內容。

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

我們修改了容器的文件,也就是改動了容器的存儲層。我們可以通過 docker diff 命令看到具體的改動。

$ docker diff webserver

C /root
A /root/.bash_history
C /run
A /run/nginx.pid
C /usr/share/nginx/html/index.html
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 "longhui <653155073@qq.com>" --message "修改了Nginx 歡迎頁面"  webserver nginx:v2

> sha256:ed889f9d550dd84d81b58eb9e340d49ecbb012b40f5b6507bd388dc335c0d4f5

其中 --author 是指定修改的作者,而 --message 則是記錄本次修改的內容。

可以用 docker images 命令看到這個新定制的鏡像:

$ docker images

  REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
  nginx               v2                  ed889f9d550d        4 minutes ago       108MB
  nginx               latest              2f7f7bce8929        5 days ago          108MB
  hello-world         latest              1815c82652c0        3 weeks ago         1.84kB

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

$ docker history nginx:v2

  IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
  ed889f9d550d        20 minutes ago      nginx -g daemon off;                            164B                修改了Nginx 歡迎頁面
  2f7f7bce8929        5 days ago          /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0B
             5 days ago          /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B
             5 days ago          /bin/sh -c #(nop)  EXPOSE 80/tcp                0B
             5 days ago          /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B
             5 days ago          /bin/sh -c apt-get update  && apt-get inst...   52.2MB
             5 days ago          /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.2....   0B
             5 days ago          /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13....   0B
             2 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B
             2 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
             2 weeks ago         /bin/sh -c #(nop) ADD file:54d82a3a8fe8d47...   55.3MB

新的鏡像定制好后,我們可以來運行這個鏡像。

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 命令除了學習之外,還有一些特殊的應用場合,比如被入侵后保存現場等。但是,不要使用 docker commit 定制鏡像,定制行為應該使用 Dockerfile 來完成。

使用 Dockerfile 定制鏡像

從剛才的學習中,我們可以了解到,鏡像的定制實際上就是定制每一層所添加的配置、文件。如果我們可以把每一層修改、安裝、構建、操作的命令都寫入一個腳本,用這個腳本來構建、定制鏡像,那么之前提及的無法重復的問題、鏡像構建透明性的問題、體積的問題就都會解決。這個腳本就是 Dockerfile。

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

還以之前定制 nginx 鏡像為例,這次我們使用 Dockerfile 來定制。

在一個空白目錄中,建立一個文本文件,并命名為 Dockerfile

$ mkdir mynginx
$ cd mynginx/
$ touch Dockerfile

添加以下內容:

FROM nginx
RUN echo "

Hello, Docker!

" > /usr/share/nginx/html/index.html

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

FROM 指定基礎鏡像

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

RUN 執行命令

RUN 指令是用來執行命令行命令的。由于命令行的強大能力,RUN 指令在定制鏡像時是最常用的指令之一。其格式有兩種:

shell 格式:RUN <命令>,就像直接在命令行中輸入的命令一樣。剛才寫的 Dockrfile 中的 RUN 指令就是這種格式。

RUN echo "

Hello, Docker!

" > /usr/share/nginx/html/index.html

exec 格式:RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。

既然 RUN 就像 Shell 腳本一樣可以執行命令,那么我們是否就可以像 Shell 腳本一樣把每一層構建需要的命令寫出來,比如這樣:

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

僅僅使用一個 RUN 指令,并使用 && 將各個所需命令串聯起來。在撰寫 Dockerfile 的時候,要經常提醒自己,這并不是在寫 Shell 腳本,而是在定義每一層該如何構建。

并且,這里為了格式化還進行了換行。Dockerfile 支持 Shell 類的行尾添加 的命令換行方式,以及行首 # 進行注釋的格式。良好的格式,比如換行、縮進、注釋等,會讓維護、排障更為容易,這是一個比較好的習慣。

此外,還可以看到這一組命令的最后添加了清理工作的命令,刪除了為了編譯構建所需要的軟件,清理了所有下載、展開的文件,并且還清理了 apt 緩存文件。這是很重要的一步,我們之前說過,鏡像是多層存儲,每一層的東西并不會在下一層被刪除,會一直跟隨著鏡像。因此鏡像構建時,一定要確保每一層只添加真正需要添加的東西,任何無關的東西都應該清理掉。

很多人初學 Docker 制作出了很臃腫的鏡像的原因之一,就是忘記了每一層構建的最后一定要清理掉無關文件。

構建鏡像

再回到之前定制的 nginx 鏡像的 Dockerfile 來。現在我們明白了這個 Dockerfile 的內容,那么讓我們來構建這個鏡像吧。

Dockerfile 文件所在目錄執行:

$ docker build -t nginx:v3 .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM nginx
 ---> 2f7f7bce8929
Step 2/2 : RUN echo "

Hello, Docker!

" > /usr/share/nginx/html/index.html ---> Running in f3f1e0d41576 ---> e189d22f23b5 Removing intermediate container f3f1e0d41576 Successfully built e189d22f23b5 Successfully tagged nginx:v3

從命令的輸出結果中,我們可以清晰的看到鏡像的構建過程。在 Step 2/2 中,如同我們之前所說的那樣,RUN 指令啟動了一個容器 f3f1e0d41576,執行了所要求的命令,并最后提交了這一層 e189d22f23b5,隨后刪除了所用到的這個容器 f3f1e0d41576

這里我們使用了 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,以及會將其置于鏡像構建上下文目錄中。

Dockerfile 指令詳解 COPY 復制文件

格式:

COPY <源路徑>... <目標路徑>

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

RUN 指令一樣,也有兩種格式,一種類似于命令行,一種類似于函數調用。

COPY 指令將從構建上下文目錄中 <源路徑> 的文件/目錄復制到新的一層的鏡像內的 <目標路徑> 位置。比如:

COPY package.json /usr/src/app/

<源路徑> 可以是多個,甚至可以是通配符,如:

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 官方的最佳實踐文檔中要求,盡可能的使用 COPY,因為 COPY 的語義很明確,就是復制文件而已,而 ADD 則包含了更復雜的功能,其行為也不一定很清晰。最適合使用 ADD 的場合,就是所提及的需要自動解壓縮的場合。

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

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

CMD 容器啟動命令

Docker 不是虛擬機,容器就是進程。既然是進程,那么在啟動容器的時候,需要指定所運行的程序及參數。CMD 指令就是用于指定默認的容器主進程的啟動命令的。

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

shell 格式:CMD <命令>

exec 格式:CMD ["可執行文件", "參數1", "參數2"...]

參數列表格式:CMD ["參數1", "參數2"...]。在指定了 ENTRYPOINT 指令后,用 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 指令,換句話說實際執行時,將變為:

 ""

那么有了 CMD 后,為什么還要有 ENTRYPOINT 呢?這種 "" 有什么好處么?讓我們來看幾個場景。

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

假設我們需要一個得知自己當前公網 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"
".

我們可以看到可執行文件找不到的報錯,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 中去執行,而這個腳本會將接到的參數(也就是 )作為命令,在腳本最后執行。比如官方鏡像 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

ENV = =...

這個指令很簡單,就是設置環境變量而已,無論是后面的其它指令,如 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 的端口。

要將 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 命令的執行環境根本不同,是兩個完全不同的容器。這就是對 Dokerfile 構建分層存儲的概念不了解所導致的錯誤。

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

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

HEALTHCHECK 健康檢查

格式:

HEALTHCHECK [選項] CMD <命令>:設置檢查容器健康狀況的命令

HEALTHCHECK NONE:如果基礎鏡像有健康檢查指令,使用這行可以屏蔽掉其健康檢查指令

HEALTHCHECK 指令是告訴 Docker 應該如何進行判斷容器的狀態是否正常.

在沒有 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 ps 看到最初的狀態為 (health: starting)

$ docker ps
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 ps,就會看到健康狀態變化為了 (healthy)

$ docker ps
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": "


Welcome to nginx!



Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

", "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 rmi 命令,其格式為:

docker rmi [選項] <鏡像1> [<鏡像2> ...]

注意 docker rm 命令是刪除容器,不要混淆。

用 ID、鏡像名、摘要刪除鏡像

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

比如我們有這么一些鏡像:

$ docker images
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 images 默認列出的就已經是短 ID 了,一般取前3個字符以上,只要足夠區分于別的鏡像就可

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/27203.html

相關文章

  • 從零構建一個基于Docker的Laravel應用

    摘要:簡介其實在這樣的一個云計算時代早就已被人眾所周知了它改變了傳統物理機的虛擬化方式使得機器的資源得到了高效的利用因為運行在中的應用實際上就是運行在宿主機上的所以它是不需要進行硬件層面的虛擬化以及運行一個完整操作系統來支持于是在應用代碼的執行效 Docker簡介 其實在這樣的一個云計算時代, Docker 早就已被人眾所周知了,它改變了傳統物理機的虛擬化方式,使得機器的資源得到了高效的利用...

    ixlei 評論0 收藏0
  • 從零開始學習部署

    摘要:現在,我們已經有了云服務器了,接下來就是如何把我們能在本地跑起來的代碼部署在服務器上。云服務器設置,是運維同學經常打交道的。討論地址歡迎一起討論,地址從零開始學習部署參考資料誰說前端不需要懂反向代理與負載均衡從入門到實踐 前段時間看到了張云龍的文章 一個程序員的成長之路 - 剖析別人,總結自己,里面有這么一段話 棧外技術,是指棧內技術的上下游,領域外的相關專業知識,包括但不限于服務端技...

    jsdt 評論0 收藏0
  • 從零開始學習部署

    摘要:現在,我們已經有了云服務器了,接下來就是如何把我們能在本地跑起來的代碼部署在服務器上。云服務器設置,是運維同學經常打交道的。討論地址歡迎一起討論,地址從零開始學習部署參考資料誰說前端不需要懂反向代理與負載均衡從入門到實踐 前段時間看到了張云龍的文章 一個程序員的成長之路 - 剖析別人,總結自己,里面有這么一段話 棧外技術,是指棧內技術的上下游,領域外的相關專業知識,包括但不限于服務端技...

    gyl_coder 評論0 收藏0
  • DevOps 從零開始-倉庫環境搭建(Docker,Nginx,Nexus,Gitlab,免費Htt

    摘要:本文章用于描述如何從零開始進行阿里云倉庫的搭建,以及過程中遇到的相關問題。涉及的內容包括,,,,。目標硬盤掛載阿里云額外購買的硬盤服務,需要僅掛載后才可以正常使用。域名代理免費代理的域名通過阿里云域名解析暴露到外網。 本文章用于描述如何從零開始進行阿里云倉庫的搭建,以及過程中遇到的相關問題。涉及的內容包括Docker,Nginx,Nexus,Gitlab,Https。 背景 需要搭建一...

    FWHeart 評論0 收藏0
  • WebAssembly 初體驗:從零開始重構計算模塊

    摘要:初體驗從零開始重構計算模塊從屬于筆者的前端入門與工程實踐,更多相關資料文章參考學習與實踐資料索引和學習與實踐資料索引。不過筆者也只是了解其概念而未真正付諸實踐,本文即是筆者在將我司某個簡單項目中的計算模塊重構為過程中的總結。 WebAssembly 初體驗:從零開始重構計算模塊從屬于筆者的 Web 前端入門與工程實踐,更多相關資料文章參考WebAssembly 學習與實踐資料索引和 ...

    netmou 評論0 收藏0
  • 從零構建前后分離 WEB 項目》 序 - 開源的意義

    摘要:盡量按照前端后端部署運維來講,當然中途涉及到跨域這種前后協調的還是無法避免捎帶一筆。關于我目前在寫從零構建前后分離項目系列,修正和補充以此為準不斷更新的項目實踐地址彩蛋提前預覽下一章傳送門 序: 開源的意義 本系列提前首發地址 背景 從事了近4年的互聯網行業,逐漸擔當過團隊的前端到后端的負責人,和大家一樣從小白逐漸的成長起來,回首望去幾年前的博客還是那么稚嫩。 回首這幾年: 從一個ja...

    seasonley 評論0 收藏0

發表評論

0條評論

lily_wang

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<