從零開始學習 Docker

Docker

這篇文章是我學習 Docker 的記錄,大部份內容摘抄自 <<Docker — 從入門到實踐>> 一書,並不是本人原創.
學習過程當中整理成適合我本身的筆記,其中也包含了我本身的實踐記錄.

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

什麼是 Docker

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

下面的圖片比較了 Docker 和傳統虛擬化方式的不一樣之處。傳統虛擬機技術是虛擬出一套硬件後,在其上運行一個完整操做系統,在該系統上再運行所需應用進程;而容器內的應用進程直接運行於宿主的內核,容器內沒有本身的內核,並且也沒有進行硬件虛擬。所以容器要比傳統虛擬機更爲輕便。
傳統虛擬化node

Docker

爲何要使用 Docker?

Docker 跟傳統的虛擬化方式相比具備如下優點:python

更高效的利用系統資源

因爲容器不須要進行硬件虛擬以及運行完整操做系統等額外開銷,Docker 對系統資源的利用率更高。不管是應用執行速度、內存損耗或者文件存儲速度,都要比傳統虛擬機技術更高效。所以,相比虛擬機技術,一個相同配置的主機,每每能夠運行更多數量的應用。mysql

更快速的啓動時間

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

一致的運行環境

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

持續交付和部署

對開發和運維人員來講,最但願的就是一次建立或配置,能夠在任意地方正常運行。git

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

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

更輕鬆的遷移

因爲 Docker 確保了執行環境的一致性,使得應用的遷移更加容易。Docker 能夠在不少平臺上運行,不管是物理機、虛擬機、公有云、私有云,甚至是筆記本,其運行結果是一致的。所以用戶能夠很輕易的將在一個平臺上運行的應用,遷移到另外一個平臺上,而不用擔憂運行環境的變化致使應用沒法正常運行的狀況。

更輕鬆的維護和擴展

Docker 使用的分層存儲以及鏡像的技術,使得應用重複部分的複用更爲容易,也使得應用的維護更新更加簡單,基於基礎鏡像進一步擴展鏡像也變得很是簡單。此外,Docker 團隊同各個開源項目團隊一塊兒維護了一大批高質量的官方鏡像,既能夠直接在生產環境使用,又能夠做爲基礎進一步定製,大大的下降了應用服務的鏡像製做成本。

對比傳統虛擬機總結

image.png

基本概念

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 版本,而且內核也更新到最新的穩定版本.更新的方法能夠看看個人<<CentOS 7. × 系統及內核升級指南>>

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

爲了簡化 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 歡迎頁面。

Nginx 歡迎頁面

如今,改動這個歡迎頁面,改爲Hello, Docker!,咱們可使用 docker exec 命令進入容器,修改其內容。

$ docker exec -it webserver bash

root@f532879089c6:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@f532879089c6:/# exit
exit

咱們以交互式終端方式進入 webserver 容器,並執行了 bash 命令,也就是得到一個可操做的 Shell。

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

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

Nginx 歡迎頁面

咱們修改了容器的文件,也就是改動了容器的存儲層。咱們能夠經過 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
  <missing>           5 days ago          /bin/sh -c #(nop)  STOPSIGNAL [SIGTERM]         0B
  <missing>           5 days ago          /bin/sh -c #(nop)  EXPOSE 80/tcp                0B
  <missing>           5 days ago          /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22B
  <missing>           5 days ago          /bin/sh -c apt-get update  && apt-get inst...   52.2MB
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NJS_VERSION=1.13.2....   0B
  <missing>           5 days ago          /bin/sh -c #(nop)  ENV NGINX_VERSION=1.13....   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0B
  <missing>           2 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
  <missing>           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 '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

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

FROM 指定基礎鏡像

所謂定製鏡像,那必定是以一個鏡像爲基礎,在其上進行定製。就像咱們以前運行了一個 nginx 鏡像的容器,再進行修改同樣,基礎鏡像是必須指定的。而 FROM 就是指定基礎鏡像,所以一個 DockerfileFROM 是必備的指令,而且必須是第一條指令。

RUN 執行命令

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

  • shell 格式:RUN <命令>,就像直接在命令行中輸入的命令同樣。剛纔寫的 Dockrfile 中的 RUN 指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /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 '<h1>Hello, Docker!</h1>' > /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 指令,換句話說實際執行時,將變爲:

<ENTRYPOINT> "<CMD>"

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

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

假設咱們須要一個得知本身當前公網 IP 的鏡像,那麼能夠先用 CMD 來實現:

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

假如咱們使用 docker build -t myip . 來構建鏡像的話,若是咱們須要查詢當前公網 IP,只須要執行:

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

嗯,這麼看起來好像能夠直接把鏡像當作命令使用了,不過命令總有參數,若是咱們但願加參數呢?好比從上面的 CMD 中能夠看到實質的命令是 curl,那麼若是咱們但願顯示 HTTP 頭信息,就須要加上 -i 參數。那麼咱們能夠直接加 -i 參數給 docker run myip 麼?

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

咱們能夠看到可執行文件找不到的報錯,executable file not found。以前咱們說過,跟在鏡像名後面的是 command,運行時會替換 CMD 的默認值。所以這裏的 -i 替換了原來的 CMD,而不是添加在原來的 curl -s http://ip.cn 後面。而 -i 根本不是命令,因此天然找不到。

那麼若是咱們但願加入 -i 這參數,咱們就必須從新完整的輸入這個命令:

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

這顯然不是很好的解決方案,而使用 ENTRYPOINT 就能夠解決這個問題。如今咱們從新用 ENTRYPOINT 來實現這個鏡像:

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

此次咱們再來嘗試直接使用 docker run myip -i

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

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

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

能夠看到,此次成功了。這是由於當存在 ENTRYPOINT 後,CMD 的內容將會做爲參數傳給 ENTRYPOINT,而這裏 -i 就是新的 CMD,所以會做爲參數傳給 curl,從而達到了咱們預期的效果。

場景二:應用運行前的準備工做

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

好比 mysql 類的數據庫,可能須要一些數據庫配置、初始化的工做,這些工做要在最終的 mysql 服務器運行以前解決。

此外,可能但願避免使用 root 用戶去啓動服務,從而提升安全性,而在啓動服務前還須要以 root 身份執行一些必要的準備工做,最後切換到服務用戶身份啓動服務。或者除了服務外,其它命令依舊可使用 root 身份執行,方便調試等。

這些準備工做是和容器 CMD 無關的,不管 CMD 爲何,都須要事先進行一個預處理的工做。這種狀況下,能夠寫一個腳本,而後放入 ENTRYPOINT 中去執行,而這個腳本會將接到的參數(也就是 <CMD>)做爲命令,在腳本最後執行。好比官方鏡像 redis 中就是這麼作的:

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

EXPOSE 6379
CMD [ "redis-server" ]

能夠看到其中爲了 redis 服務建立了 redis 用戶,並在最後指定了 ENTRYPOINTdocker-entrypoint.sh 腳本。

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

exec "$@"

該腳本的內容就是根據 CMD 的內容來判斷,若是是 redis-server 的話,則切換到 redis 用戶身份啓動服務器,不然依舊使用 root 身份執行。好比:

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

ENV 設置環境變量

格式有兩種:

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

這個指令很簡單,就是設置環境變量而已,不管是後面的其它指令,如 RUN,仍是運行時的應用,均可以直接使用這裏定義的環境變量。

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

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

定義了環境變量,那麼在後續的指令中,就可使用這個環境變量。好比在官方 node 鏡像 Dockerfile 中,就有相似這樣的代碼:

ENV NODE_VERSION 7.2.0

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

在這裏先定義了環境變量 NODE_VERSION,其後的 RUN 這層裏,屢次使用 $NODE_VERSION 來進行操做定製。能夠看到,未來升級鏡像構建版本的時候,只須要更新 7.2.0 便可,Dockerfile 構建維護變得更輕鬆了。

下列指令能夠支持環境變量引用: ADDCOPYENVEXPOSELABELUSERWORKDIRVOLUMESTOPSIGNALONBUILD

能夠從這個指令列表裏感受到,環境變量可使用的地方不少,很強大。經過環境變量,咱們可讓一份 Dockerfile 製做更多的鏡像,只需使用不一樣的環境變量便可。

ARG 構建參數

格式:ARG <參數名>[=<默認值>]

構建參數和 ENV 的效果同樣,都是設置環境變量。所不一樣的是,ARG 所設置的構建環境的環境變量,在未來容器運行時是不會存在這些環境變量的。可是不要所以就使用 ARG 保存密碼之類的信息,由於 docker history 仍是能夠看到全部值的。

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

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

VOLUME 定義匿名卷

格式爲:

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

以前說過,容器運行時應該儘可能保持容器存儲層不發生寫操做,對於數據庫類須要保存動態數據的應用,其數據庫文件應該保存於卷(volume)中,後面的章節咱們會進一步介紹 Docker 卷的概念。爲了防止運行時用戶忘記將動態文件所保存目錄掛載爲卷,在 Dockerfile 中,咱們能夠事先指定某些目錄掛載爲匿名卷,這樣在運行時若是用戶不指定掛載,其應用也能夠正常運行,不會向容器存儲層寫入大量數據。

VOLUME /data

這裏的 /data 目錄就會在運行時自動掛載爲匿名卷,任何向 /data 中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。固然,運行時能夠覆蓋這個掛載設置。好比:

docker run -d -v mydata:/data xxxx

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

EXPOSE 聲明端口

格式爲 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時並不會由於這個聲明應用就會開啓這個端口的服務。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另外一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口。

要將 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": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n    body {\n        width: 35em;\n        margin: 0 auto;\n        font-family: Tahoma, Verdana, Arial, sans-serif;\n    }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n",
            "Start": "2016-11-25T14:35:37.780192565Z"
        }
    ],
    "Status": "healthy"
}

ONBUILD 鏡像複用及項目環境管理

格式:ONBUILD <其它指令>

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

Dockerfile 中的其它指令都是爲了定製當前鏡像而準備的,惟有 ONBUILD 是爲了幫助別人定製本身而準備的。

假設咱們要製做 Node.js 所寫的應用的鏡像。咱們都知道 Node.js 使用 npm 進行包管理,全部依賴、配置、啓動信息等會放到 package.json 文件裏。在拿到程序代碼後,須要先進行 npm install 才能夠得到全部須要的依賴。而後就能夠經過 npm start 來啓動應用。所以,通常來講會這樣寫 Dockerfile

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

把這個 Dockerfile 放到 Node.js 項目的根目錄,構建好鏡像後,就能夠直接拿來啓動容器運行。可是若是咱們還有第二個 Node.js 項目也差很少呢?好吧,那就再把這個 Dockerfile 複製到第二個項目裏。那若是有第三個項目呢?再複製麼?文件的副本越多,版本控制就越困難,讓咱們繼續看這樣的場景維護的問題。

若是第一個 Node.js 項目在開發過程當中,發現這個 Dockerfile 裏存在問題,好比敲錯字了、或者須要安裝額外的包,而後開發人員修復了這個 Dockerfile,再次構建,問題解決。第一個項目沒問題了,可是第二個項目呢?雖然最初 Dockerfile 是複製、粘貼自第一個項目的,可是並不會由於第一個項目修復了他們的 Dockerfile,而第二個項目的 Dockerfile 就會被自動修復。

那麼咱們可不能夠作一個基礎鏡像,而後各個項目使用這個基礎鏡像呢?這樣基礎鏡像更新,各個項目不用同步 Dockerfile 的變化,從新構建後就繼承了基礎鏡像的更新?好吧,能夠,讓咱們看看這樣的結果。那麼上面的這個 Dockerfile 就會變爲:

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

這裏咱們把項目相關的構建指令拿出來,放到子項目裏去。假設這個基礎鏡像的名字爲 my-node 的話,各個項目內的本身的 Dockerfile 就變爲:

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

基礎鏡像變化後,各個項目都用這個 Dockerfile 從新構建鏡像,會繼承基礎鏡像的更新。

那麼,問題解決了麼?沒有。準確說,只解決了一半。若是這個 Dockerfile 裏面有些東西須要調整呢?好比 npm install 都須要加一些參數,那怎麼辦?這一行 RUN 是不可能放入基礎鏡像的,由於涉及到了當前項目的 ./package.json,難道又要一個個修改麼?因此說,這樣製做基礎鏡像,只解決了原來的 Dockerfile 的前4條指令的變化問題,然後面三條指令的變化則徹底沒辦法處理。

ONBUILD 能夠解決這個問題。讓咱們用 ONBUILD 從新寫一下基礎鏡像的 Dockerfile:

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

此次咱們回到原始的 Dockerfile,可是此次將項目相關的指令加上 ONBUILD,這樣在構建基礎鏡像的時候,這三行並不會被執行。而後各個項目的 Dockerfile 就變成了簡單地:

FROM my-node

是的,只有這麼一行。當在各個項目目錄中,用這個只有一行的 Dockerfile 構建鏡像時,以前基礎鏡像的那三行 ONBUILD 就會開始執行,成功的將當前項目的代碼複製進鏡像、而且針對本項目執行 npm install,生成應用鏡像。

刪除本地鏡像

若是要刪除本地的鏡像,可使用 docker 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個字符以上,只要足夠區分於別的鏡像就能夠了。

好比這裏,若是咱們要刪除 redis:alpine 鏡像,能夠執行:

$ docker rmi 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 rmi centos
Untagged: centos:latest
Untagged: centos@sha256:b2f9d1c0ff5f87a4743104d099a3d561002ac500db1b9bfa02a783a46e0d366c
Deleted: sha256:0584b3d2cf6d235ee310cf14b54667d889887b838d3f3d3033acd70fc3c48b8a
Deleted: sha256:97ca462ad9eeae25941546209454496e1d66749d53dfa2ee32bf1faabd239d38

固然,更精確的是使用 鏡像摘要 刪除鏡像。

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

$ docker rmi node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228
Untagged: node@sha256:b4f0e0bdeb578043c1ea6862f0d40cc4afe32a4a582f3be235a3b164422be228

用 docker images 命令來配合

像其它能夠承接多個實體的命令同樣,可使用 docker images -q 來配合使用 docker rmi,這樣能夠成批的刪除但願刪除的鏡像。好比以前咱們介紹過的,刪除虛懸鏡像的指令是:

$ docker rmi $(docker images -q -f dangling=true)

咱們在「鏡像列表」章節介紹過不少過濾鏡像列表的方式均可以拿過來使用。

好比,咱們須要刪除全部倉庫名爲 redis 的鏡像:

$ docker rmi $(docker images -q redis)

或者刪除全部在 mongo:3.2 以前的鏡像:

$ docker rmi $(docker images -q -f before=mongo:3.2)

充分利用你的想象力和 Linux 命令行的強大,你能夠完成不少很是讚的功能。

參考文檔

操做 Docker 容器

容器是 Docker 又一核心概念。

簡單的說,容器是獨立運行的一個或一組應用,以及它們的運行態環境。對應的,虛擬機能夠理解爲模擬運行的一整套操做系統(提供了運行態環境和其餘系統環境)和跑在上面的應用。

啓動容器

啓動容器有兩種方式,一種是基於鏡像新建一個容器並啓動,另一個是將在終止狀態(stopped)的容器從新啓動。

由於 Docker 的容器實在過輕量級了,不少時候用戶都是隨時刪除和新建立容器。

新建並啓動

所須要的命令主要爲 docker run

例如,下面的命令輸出一個 「Hello World」,以後終止容器。

$ docker run ubuntu:14.04 /bin/echo 'Hello world'

Unable to find image 'ubuntu:14.04' locally
14.04: Pulling from library/ubuntu
cb56c90f0b30: Pull complete
0acc551e5716: Pull complete
8956dcd35143: Pull complete
908242721214: Pull complete
b44ff14dd3bb: Pull complete
Digest: sha256:5faf6cb681da2be979a177b60d8c18497f962e3d82268c49db6c74008d0c294d
Status: Downloaded newer image for ubuntu:14.04
Hello world

這跟在本地直接執行 /bin/echo 'hello world' 幾乎感受不出任何區別。

下面的命令則啓動一個 bash 終端,容許用戶進行交互。

$ docker run -t -i ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#

其中,-t 選項讓Docker分配一個僞終端(pseudo-tty)並綁定到容器的標準輸入上, -i 則讓容器的標準輸入保持打開。

在交互模式下,用戶能夠經過所建立的終端來輸入命令,例如

root@af8bae53bdd3:/# pwd
/
root@af8bae53bdd3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

容器的核心爲所執行的應用程序,所須要的資源都是應用程序運行所必需的。除此以外,並無其它的資源。能夠在僞終端中利用 pstop 來查看進程信息。

root@ba267838cc1b:/# ps
  PID TTY          TIME CMD
    1 ?        00:00:00 bash
   11 ?        00:00:00 ps

可見,容器中僅運行了指定的 bash 應用。這種特色使得 Docker 對資源的利用率極高,是貨真價實的輕量級虛擬化。

當利用 docker run 來建立容器時,Docker 在後臺運行的標準操做包括:

  • 檢查本地是否存在指定的鏡像,不存在就從公有倉庫下載
  • 利用鏡像建立並啓動一個容器
  • 分配一個文件系統,並在只讀的鏡像層外面掛載一層可讀寫層
  • 從宿主主機配置的網橋接口中橋接一個虛擬接口到容器中去
  • 從地址池配置一個 ip 地址給容器
  • 執行用戶指定的應用程序
  • 執行完畢後容器被終止

查看正在運行中的容器

利用 docker ps 命令能夠查看正在運行中的容器

$ docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours         0.0.0.0:81->80/tcp   webserver

查看全部容器

利用 docker ps -a 命令能夠查看全部容器

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   25 minutes ago      Exited (0) 25 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                          peaceful_brown

啓動已終止的容器

能夠利用 docker start 命令和上面使用 docker ps -a 查看到的 CONTAINER IDNAMES,直接將一個已經終止的容器啓動運行。

$ docker start relaxed_kilby

relaxed_kilby

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   45 minutes ago      Exited (0) 3 seconds ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:82->80/tcp   web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 47 hours ago        Exited (0) 47 hours ago                         peaceful_brown

這裏把 新建並啓動 章節中的容器又啓動了一次,此次這個容器和以前不同,他啓動以後就會被終止,不會輸出一個 「Hello World」,以後才終止容器。能夠看 STATUS 輸出,這個容器的確被啓動過.

容器後臺運行

更多的時候,須要讓 Docker在後臺運行而不是直接把執行命令的結果輸出在當前宿主機下。此時,能夠經過添加 -d 參數來實現。

下面舉兩個例子來講明一下。

若是不使用 -d 參數運行容器。

$ sudo docker run ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
hello world
hello world
hello world
hello world

容器會把輸出的結果(STDOUT)打印到宿主機上面

若是使用了 -d 參數運行容器。

$ sudo docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"
77b2dc01fe0f3f1265df143181e7b9af5e05279a884f4776ee75350ea9d8017a

此時容器會在後臺運行並不會把輸出的結果(STDOUT)打印到宿主機上面(輸出結果能夠用docker logs 查看)。

注: 容器是否會長久運行,是和docker run指定的命令有關,和 -d 參數無關。

使用 -d 參數啓動後會返回一個惟一的 id,也能夠經過 docker ps 命令來查看容器信息。

$ sudo docker ps
CONTAINER ID  IMAGE         COMMAND               CREATED        STATUS       PORTS NAMES
77b2dc01fe0f  ubuntu:14.04  /bin/sh -c 'while tr  2 minutes ago  Up 1 minute        agitated_wright

要獲取容器的輸出信息,能夠經過 docker logs 命令。

$ sudo docker logs [container ID or NAMES]
hello world
hello world
hello world
. . .

終止容器

可使用 docker stop 命令和上面使用的 docker ps -a 查看到的 CONTAINER IDNAMES,來終止一個運行中的容器。

$ docker stop web2

web2

$ docker ps -a

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS                NAMES
1b6890b715ec        ubuntu:14.04        "/bin/echo 'Hello ..."   About an hour ago   Exited (0) 15 minutes ago                        relaxed_kilby
9dea98e12fc0        nginx:v2            "nginx -g 'daemon ..."   47 hours ago        Exited (0) 3 seconds ago                         web2
71e33c548d3d        nginx               "nginx -g 'daemon ..."   47 hours ago        Up 47 hours                 0.0.0.0:81->80/tcp   webserver
e708a9002164        hello-world         "/hello"                 2 days ago          Exited (0) 2 days ago                            peaceful_brown

此外,當 Docker 容器中指定的應用終結時,容器也自動終止。例如啓動了一個終端的容器,用戶經過 exit 命令或 Ctrl+d 來退出終端時,所建立的容器馬上終止。

重啓容器

docker restart 命令會將一個運行態的容器終止,而後再從新啓動它。

進入容器

在使用 -d 參數時,容器啓動後會進入後臺。
某些時候須要進入容器進行操做,有不少種方法,包括使用 docker attach 命令或 nsenter 工具等。

attach 命令

docker attach 是Docker自帶的命令。下面示例如何使用該命令。

$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia
$sudo docker attach nostalgic_hypatia
root@243c32535da7:/#

可是使用 attach 命令有時候並不方便。當多個窗口同時 attach 到同一個容器的時候,全部窗口都會同步顯示。當某個窗口因命令阻塞時,其餘窗口也沒法執行操做了。

nsenter 命令

安裝

nsenter 工具在 util-linux 包2.23版本後包含。
可使用 nsenter -V 查看系統是否安裝了 nsenter 工具.

$ nsenter -V

nsenter from util-linux 2.23.2

若是系統中 util-linux 包沒有該命令,能夠按照下面的方法從源碼安裝。

$ cd /tmp; curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-; cd util-linux-2.24;
$ ./configure --without-ncurses
$ make nsenter && sudo cp nsenter /usr/local/bin

使用

nsenter 啓動一個新的shell進程(默認是/bin/bash), 同時會把這個新進程切換到和目標(target)進程相同的命名空間,這樣就至關於進入了容器內部。nsenter 要正常工做須要有 root 權限。

爲了鏈接到容器,你還須要找到容器的第一個進程的 PID,能夠經過下面的命令獲取。

PID=$(docker inspect --format "{{ .State.Pid }}" <container>)

經過這個 PID,就能夠鏈接到這個容器:

$ nsenter --target $PID --mount --uts --ipc --net --pid

若是沒法經過以上命令鏈接到這個容器,有多是由於宿主的默認 shell 在容器中並不存在,好比zsh,可使用以下命令顯式地使用bash。

$ nsenter --target $pid --mount --uts --ipc --net --pid  -- /usr/bin/env \ 
--ignore-environment HOME=/root /bin/bash --login

下面給出一個完整的例子。

$ sudo docker run -idt ubuntu
243c32535da7d142fb0e6df616a3c3ada0b8ab417937c853a9e1c251f499f550
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
243c32535da7        ubuntu:latest       "/bin/bash"         18 seconds ago      Up 17 seconds                           nostalgic_hypatia
$ PID=$(docker-pid 243c32535da7)
10981
$ sudo nsenter --target 10981 --mount --uts --ipc --net --pid
root@243c32535da7:/#

更簡單的,建議你們下載
.bashrc_docker,並將內容放到 .bashrc 中。

$ wget -P ~ https://github.com/yeasy/docker_practice/raw/master/_local/.bashrc_docker;
$ echo "[ -f ~/.bashrc_docker ] && . ~/.bashrc_docker" >> ~/.bashrc; source ~/.bashrc

這個文件中定義了不少方便使用 Docker 的命令,例如 docker-pid 能夠獲取某個容器的 PID;而 docker-enter 能夠進入容器或直接在容器內執行命令。

$ echo $(docker-pid <container>)
$ docker-enter <container> ls

導出和導入容器快照

導出容器快照

若是要導出本地某個容器,可使用 docker export 命令。

$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
7691a814370e        ubuntu:14.04        "/bin/bash"         36 hours ago        Exited (0) 21 hours ago                       test
$ sudo docker export 7691a814370e > ubuntu.tar

這樣將導出容器快照到本地文件。

導入容器快照

可使用 docker import 從容器快照文件中再導入爲鏡像,例如

$ cat ubuntu.tar | sudo docker import - test/ubuntu:v1.0
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
test/ubuntu         v1.0                9d37a6082e97        About a minute ago   171.3 MB

此外,也能夠經過指定 URL 或者某個目錄來導入,例如

$sudo docker import http://example.com/exampleimage.tgz example/imagerepo

*注:用戶既可使用 docker load 來導入鏡像存儲文件到本地鏡像庫,也可使用 docker import 來導入一個容器快照到本地鏡像庫。這二者的區別在於容器快照文件將丟棄全部的歷史記錄和元數據信息(即僅保存容器當時的快照狀態),而鏡像存儲文件將保存完整記錄,體積也要大。此外,從容器快照文件導入時能夠從新指定標籤等元數據信息。

刪除容器

可使用 docker rm 來刪除一個處於終止狀態的容器。
例如

$sudo docker rm  trusting_newton
trusting_newton

若是要刪除一個運行中的容器,能夠添加 -f 參數。Docker 會發送 SIGKILL 信號給容器。

清理全部處於終止狀態的容器(不建議使用)

docker ps -a 命令能夠查看全部已經建立的包括終止狀態的容器,若是數量太多要一個個刪除可能會很麻煩,用 docker rm $(docker ps -a -q) 能夠所有清理掉。

*注意:這個命令其實會試圖刪除全部的包括還在運行中的容器,不過就像上面提過的 docker rm 默認並不會刪除運行中的容器。

私有倉庫

有時候使用阿里雲這樣的公共倉庫可能不方便,用戶能夠建立一個本地倉庫供本身使用。

如何使用本地倉庫。

docker-registry 是官方提供的工具,能夠用於構建私有的鏡像倉庫。

安裝運行 docker-registry

容器中運行 docker-registry

在安裝了 Docker 後,能夠經過獲取官方 registry 鏡像來運行。

$ sudo docker run -d -p 5000:5000 registry

這將使用官方的 registry 鏡像來啓動本地的私有倉庫。
用戶能夠經過指定參數來配置私有倉庫位置,例如配置鏡像存儲到 Amazon S3 服務。

$ sudo docker run \
         -e SETTINGS_FLAVOR=s3 \
         -e AWS_BUCKET=acme-docker \
         -e STORAGE_PATH=/registry \
         -e AWS_KEY=AKIAHSHB43HS3J92MXZ \
         -e AWS_SECRET=xdDowwlK7TJajV1Y7EoOZrmuPEJlHYcNP2k4j49T \
         -e SEARCH_BACKEND=sqlalchemy \
         -p 5000:5000 \
         registry

此外,還能夠指定本地路徑(如 /home/user/registry-conf )下的配置文件。

$ sudo docker run -d -p 5000:5000 -v /home/user/registry-conf:/registry-conf -e DOCKER_REGISTRY_CONFIG=/registry-conf/config.yml registry

默認狀況下,倉庫會被建立在容器的 /var/lib/registry (v1 中是/tmp/registry)下。能夠經過 -v 參數來將鏡像文件存放在本地的指定路徑。
例以下面的例子將上傳的鏡像放到 /opt/data/registry 目錄。

$ sudo docker run -d -p 5000:5000 -v /opt/data/registry:/var/lib/registry registry

本地安裝 docker-registry

對於 CentOS 發行版,能夠直接經過源安裝。

$ sudo yum install -y python-devel libevent-devel python-pip gcc xz-devel
$ sudo python-pip install docker-registry

也能夠從 docker-registry 項目下載源碼進行安裝。

$ sudo apt-get install build-essential python-dev libevent-dev python-pip libssl-dev liblzma-dev libffi-dev
$ git clone https://github.com/docker/docker-registry.git
$ cd docker-registry
$ sudo python setup.py install

而後修改配置文件,主要修改 dev 模板段的 storage_path 到本地的存儲倉庫的路徑。

$ cp config/config_sample.yml config/config.yml

以後啓動 Web 服務。

$ sudo gunicorn -c contrib/gunicorn.py docker_registry.wsgi:application

或者

$ sudo gunicorn --access-logfile - --error-logfile - -k gevent -b 0.0.0.0:5000 -w 4 --max-requests 100 docker_registry.wsgi:application

此時使用 curl 訪問本地的 5000 端口,看到輸出 docker-registry 的版本信息說明運行成功。

*注:config/config_sample.yml 文件是示例配置文件。

在私有倉庫上傳、下載、搜索鏡像

建立好私有倉庫以後,就可使用 docker tag 來標記一個鏡像,而後推送它到倉庫,別的機器上就能夠下載下來了。例如私有倉庫地址爲 192.168.7.26:5000

先在本機查看已有的鏡像。

$ sudo docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB

使用docker tagba58 這個鏡像標記爲 192.168.7.26:5000/test(格式爲 docker tag IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG])。

$ sudo docker tag ba58 192.168.7.26:5000/test
root ~ # docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
ubuntu                            14.04               ba5877dc9bec        6 weeks ago         192.7 MB
ubuntu                            latest              ba5877dc9bec        6 weeks ago         192.7 MB
192.168.7.26:5000/test            latest              ba5877dc9bec        6 weeks ago         192.7 MB

使用 docker push 上傳標記的鏡像。

$ sudo docker push 192.168.7.26:5000/test
The push refers to a repository [192.168.7.26:5000/test] (len: 1)
Sending image list
Pushing repository 192.168.7.26:5000/test (1 tags)
Image 511136ea3c5a already pushed, skipping
Image 9bad880da3d2 already pushed, skipping
Image 25f11f5fb0cb already pushed, skipping
Image ebc34468f71d already pushed, skipping
Image 2318d26665ef already pushed, skipping
Image ba5877dc9bec already pushed, skipping
Pushing tag for rev [ba5877dc9bec] on {http://192.168.7.26:5000/v1/repositories/test/tags/latest}

用 curl 查看倉庫中的鏡像。

$ curl http://192.168.7.26:5000/v1/search
{"num_results": 7, "query": "", "results": [{"description": "", "name": "library/miaxis_j2ee"}, {"description": "", "name": "library/tomcat"}, {"description": "", "name": "library/ubuntu"}, {"description": "", "name": "library/ubuntu_office"}, {"description": "", "name": "library/desktop_ubu"}, {"description": "", "name": "dockerfile/ubuntu"}, {"description": "", "name": "library/test"}]}

這裏能夠看到 {"description": "", "name": "library/test"},代表鏡像已經被成功上傳了。

如今能夠到另一臺機器去下載這個鏡像。

$ sudo docker pull 192.168.7.26:5000/test
Pulling repository 192.168.7.26:5000/test
ba5877dc9bec: Download complete
511136ea3c5a: Download complete
9bad880da3d2: Download complete
25f11f5fb0cb: Download complete
ebc34468f71d: Download complete
2318d26665ef: Download complete
$ sudo docker images
REPOSITORY                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
192.168.7.26:5000/test             latest              ba5877dc9bec        6 weeks ago         192.7 MB

可使用 這個腳本 批量上傳本地的鏡像到註冊服務器中,默認是本地註冊服務器 127.0.0.1:5000。例如:

$ wget https://github.com/yeasy/docker_practice/raw/master/_local/push_images.sh; sudo chmod a+x push_images.sh
$ ./push_images.sh ubuntu:latest centos:centos7
The registry server is 127.0.0.1
Uploading ubuntu:latest...
The push refers to a repository [127.0.0.1:5000/ubuntu] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/ubuntu (1 tags)
Image 511136ea3c5a already pushed, skipping
Image bfb8b5a2ad34 already pushed, skipping
Image c1f3bdbd8355 already pushed, skipping
Image 897578f527ae already pushed, skipping
Image 9387bcc9826e already pushed, skipping
Image 809ed259f845 already pushed, skipping
Image 96864a7d2df3 already pushed, skipping
Pushing tag for rev [96864a7d2df3] on {http://127.0.0.1:5000/v1/repositories/ubuntu/tags/latest}
Untagged: 127.0.0.1:5000/ubuntu:latest
Done
Uploading centos:centos7...
The push refers to a repository [127.0.0.1:5000/centos] (len: 1)
Sending image list
Pushing repository 127.0.0.1:5000/centos (1 tags)
Image 511136ea3c5a already pushed, skipping
34e94e67e63a: Image successfully pushed
70214e5d0a90: Image successfully pushed
Pushing tag for rev [70214e5d0a90] on {http://127.0.0.1:5000/v1/repositories/centos/tags/centos7}
Untagged: 127.0.0.1:5000/centos:centos7
Done

Docker 數據管理

數據卷

數據卷是一個可供一個或多個容器使用的特殊目錄,它繞過 UFS,能夠提供不少有用的特性:

  • 數據卷能夠在容器之間共享和重用
  • 對數據卷的修改會立馬生效
  • 對數據卷的更新,不會影響鏡像
  • 數據卷默認會一直存在,即便容器被刪除

*注意:數據卷的使用,相似於 Linux 下對目錄或文件進行 mount,鏡像中的被指定爲掛載點的目錄中的文件會隱藏掉,能顯示看的是掛載的數據卷。

建立一個數據卷

在用 docker run 命令的時候,使用 -v 標記來建立一個數據卷並掛載到容器裏。在一次 run 中屢次使用能夠掛載多個數據卷。

下面建立一個名爲 web 的容器,並加載一個數據捲到容器的 /webapp 目錄。

$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py

*注意:也能夠在 Dockerfile 中使用 VOLUME 來添加一個或者多個新的捲到由該鏡像建立的任意容器。

刪除數據卷

數據卷是被設計用來持久化數據的,它的生命週期獨立於容器,Docker不會在容器被刪除後自動刪除數據卷,而且也不存在垃圾回收這樣的機制來處理沒有任何容器引用的數據卷。若是須要在刪除容器的同時移除數據卷。能夠在刪除容器的時候使用 docker rm -v 這個命令。無主的數據卷可能會佔據不少空間,要清理會很麻煩。Docker官方正在試圖解決這個問題,相關工做的進度能夠查看這個PR

掛載一個主機目錄做爲數據卷

使用 -v 標記也能夠指定掛載一個本地主機的目錄到容器中去。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

上面的命令加載主機的 /src/webapp 目錄到容器的 /opt/webapp
目錄。這個功能在進行測試的時候十分方便,好比用戶能夠放置一些程序到本地目錄中,來查看容器是否正常工做。本地目錄的路徑必須是絕對路徑,若是目錄不存在 Docker 會自動爲你建立它。

*注意:Dockerfile 中不支持這種用法,這是由於 Dockerfile 是爲了移植和分享用的。然而,不一樣操做系統的路徑格式不同,因此目前還不能支持。

Docker 掛載數據卷的默認權限是讀寫,用戶也能夠經過 :ro 指定爲只讀。

$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro
training/webapp python app.py

加了 :ro 以後,就掛載爲只讀了。

查看數據卷的具體信息

在主機裏使用如下命令能夠查看指定容器的信息

$ docker inspect web
...

在輸出的內容中找到其中和數據卷相關的部分,能夠看到全部的數據卷都是建立在主機的/var/lib/docker/volumes/下面的

"Volumes": {
    "/webapp": "/var/lib/docker/volumes/fac362...80535"
},
"VolumesRW": {
    "/webapp": true
}
...

注:從Docker 1.8.0起,數據卷配置在"Mounts"Key下面,能夠看到全部的數據卷都是建立在主機的/mnt/sda1/var/lib/docker/volumes/....下面了。

"Mounts": [
            {
                "Name": "b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29",
                "Source": "/mnt/sda1/var/lib/docker/volumes/b53ebd40054dae599faf7c9666acfe205c3e922fc3e8bc3f2fd178ed788f1c29/_data",
                "Destination": "/webapp",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ]
...

掛載一個本地主機文件做爲數據卷

-v 標記也能夠從主機掛載單個文件到容器中

$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

這樣就能夠記錄在容器輸入過的命令了。

*注意:若是直接掛載一個文件,不少文件編輯工具,包括 vi 或者 sed --in-place,可能會形成文件 inode 的改變,從 Docker 1.1
.0起,這會致使報錯誤信息。因此最簡單的辦法就直接掛載文件的父目錄。

數據卷容器

若是你有一些持續更新的數據須要在容器之間共享,最好建立數據卷容器。

數據卷容器,其實就是一個正常的容器,專門用來提供數據卷供其它容器掛載的。

首先,建立一個名爲 dbdata 的數據卷容器:

$ sudo docker run -d -v /dbdata --name dbdata training/postgres echo Data-only container for postgres

而後,在其餘容器中使用 --volumes-from 來掛載 dbdata 容器中的數據卷。

$ sudo docker run -d --volumes-from dbdata --name db1 training/postgres
$ sudo docker run -d --volumes-from dbdata --name db2 training/postgres

可使用超過一個的 --volumes-from 參數來指定從多個容器掛載不一樣的數據卷。
也能夠從其餘已經掛載了數據卷的容器來級聯掛載數據卷。

$ sudo docker run -d --name db3 --volumes-from db1 training/postgres

*注意:使用 --volumes-from 參數所掛載數據卷的容器本身並不須要保持在運行狀態。

若是刪除了掛載的容器(包括 dbdata、db1 和 db2),數據卷並不會被自動刪除。若是要刪除一個數據卷,必須在刪除最後一個還掛載着它的容器時使用 docker rm -v 命令來指定同時刪除關聯的容器。
這可讓用戶在容器之間升級和移動數據卷。

利用數據卷容器來備份、恢復、遷移數據卷

能夠利用數據卷對其中的數據進行進行備份、恢復和遷移。

備份

首先使用 --volumes-from 標記來建立一個加載 dbdata 容器卷的容器,並從主機掛載當前目錄到容器的 /backup 目錄。命令以下:

$ sudo docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

容器啓動後,使用了 tar 命令來將 dbdata 卷備份爲容器中 /backup/backup.tar 文件,也就是主機當前目錄下的名爲 backup.tar 的文件。

恢復

若是要恢復數據到一個容器,首先建立一個帶有空數據卷的容器 dbdata2。

$ sudo docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

而後建立另外一個容器,掛載 dbdata2 容器卷中的數據卷,並使用 untar 解壓備份文件到掛載的容器卷中。

$ sudo docker run --volumes-from dbdata2 -v $(pwd):/backup busybox tar xvf
/backup/backup.tar

爲了查看/驗證恢復的數據,能夠再啓動一個容器掛載一樣的容器捲來查看

$ sudo docker run --volumes-from dbdata2 busybox /bin/ls /dbdata

 Docker 中的網絡功能介紹

Docker 容許經過外部訪問容器或容器互聯的方式來提供網絡服務。

外部訪問容器

容器中能夠運行一些網絡應用,要讓外部也能夠訪問這些應用,能夠經過 -P-p 參數來指定端口映射。

當使用 -P 標記時,Docker 會隨機映射一個 49000~49900 的端口到內部容器開放的網絡端口。

使用 docker ps 能夠看到,本地主機的 49155 被映射到了容器的 5000 端口。此時訪問本機的 49155 端口便可訪問容器內 web 應用提供的界面。

$ sudo docker run -d -P training/webapp python app.py
$ sudo docker ps -l
CONTAINER ID  IMAGE                   COMMAND       CREATED        STATUS        PORTS                    NAMES
bc533791f3f5  training/webapp:latest  python app.py 5 seconds ago  Up 2 seconds  0.0.0.0:49155->5000/tcp  nostalgic_morse

一樣的,能夠經過 docker logs 命令來查看應用的信息。

$ sudo docker logs -f nostalgic_morse
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -

-p(小寫的)則能夠指定要映射的端口,而且,在一個指定端口上只能夠綁定一個容器。支持的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

映射全部接口地址

使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口,能夠執行

$ sudo docker run -d -p 5000:5000 training/webapp python app.py

此時默認會綁定本地全部接口上的全部地址。

映射到指定地址的指定端口

可使用 ip:hostPort:containerPort 格式指定映射使用一個特定地址,好比 localhost 地址 127.0.0.1

$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

映射到指定地址的任意端口

使用 ip::containerPort 綁定 localhost 的任意端口到容器的 5000 端口,本地主機會自動分配一個端口。

$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py

還可使用 udp 標記來指定 udp 端口

$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

查看映射端口配置

使用 docker port 來查看當前映射的端口配置,也能夠查看到綁定的地址

$ docker port nostalgic_morse 5000
127.0.0.1:49155.

注意:

  • 容器有本身的內部網絡和 ip 地址(使用 docker inspect 能夠獲取全部的變量,Docker 還能夠有一個可變的網絡配置。)
  • -p 標記能夠屢次使用來綁定多個端口

例如

$ sudo docker run -d -p 5000:5000  -p 3000:80 training/webapp python app.py

容器互聯

容器的鏈接(linking)系統是除了端口映射外,另外一種跟容器中應用交互的方式。

該系統會在源和接收容器之間建立一個隧道,接收容器能夠看到源容器指定的信息。

自定義容器命名

鏈接系統依據容器的名稱來執行。所以,首先須要自定義一個好記的容器命名。

雖然當建立容器的時候,系統默認會分配一個名字。自定義命名容器有2個好處:

  • 自定義的命名,比較好記,好比一個web應用容器咱們能夠給它起名叫web
  • 當要鏈接其餘容器時候,能夠做爲一個有用的參考點,好比鏈接web容器到db容器

使用 --name 標記能夠爲容器自定義命名。

$ sudo docker run -d -P --name web training/webapp python app.py

使用 docker ps 來驗證設定的命名。

$ sudo docker ps -l
CONTAINER ID  IMAGE                  COMMAND        CREATED       STATUS       PORTS                    NAMES
aed84ee21bde  training/webapp:latest python app.py  12 hours ago  Up 2 seconds 0.0.0.0:49154->5000/tcp  web

也可使用 docker inspect 來查看容器的名字

$ sudo docker inspect -f "{{ .Name }}" aed84ee21bde
/web

注意:容器的名稱是惟一的。若是已經命名了一個叫 web 的容器,當你要再次使用 web 這個名稱的時候,須要先用docker rm 來刪除以前建立的同名容器。

在執行 docker run 的時候若是添加 --rm 標記,則容器在終止後會馬上刪除。注意,--rm-d 參數不能同時使用。

容器互聯

使用 --link 參數可讓容器之間安全的進行交互。

下面先建立一個新的數據庫容器。

$ sudo docker run -d --name db training/postgres

刪除以前建立的 web 容器

$ docker rm -f web

而後建立一個新的 web 容器,並將它鏈接到 db 容器

$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

此時,db 容器和 web 容器創建互聯關係。

--link 參數的格式爲 --link name:alias,其中 name 是要連接的容器的名稱,alias 是這個鏈接的別名。

使用 docker ps 來查看容器的鏈接

$ docker ps
CONTAINER ID  IMAGE                     COMMAND               CREATED             STATUS             PORTS                    NAMES
349169744e49  training/postgres:latest  su postgres -c '/usr  About a minute ago  Up About a minute  5432/tcp                 db, web/db
aed84ee21bde  training/webapp:latest    python app.py         16 hours ago        Up 2 minutes       0.0.0.0:49154->5000/tcp  web

能夠看到自定義命名的容器,db 和 web,db 容器的 names 列有 db 也有 web/db。這表示 web 容器連接到 db 容器,web 容器將被容許訪問 db 容器的信息。

Docker 在兩個互聯的容器之間建立了一個安全隧道,並且不用映射它們的端口到宿主主機上。在啓動 db 容器的時候並無使用 -p-P 標記,從而避免了暴露數據庫端口到外部網絡上。

Docker 經過 2 種方式爲容器公開鏈接信息:

  • 環境變量
  • 更新 /etc/hosts 文件

使用 env 命令來查看 web 容器的環境變量

$ sudo docker run --rm --name web2 --link db:db training/webapp env
. . .
DB_NAME=/web2/db
DB_PORT=tcp://172.17.0.5:5432
DB_PORT_5000_TCP=tcp://172.17.0.5:5432
DB_PORT_5000_TCP_PROTO=tcp
DB_PORT_5000_TCP_PORT=5432
DB_PORT_5000_TCP_ADDR=172.17.0.5
. . .

其中 DB_ 開頭的環境變量是供 web 容器鏈接 db 容器使用,前綴採用大寫的鏈接別名。

除了環境變量,Docker 還添加 host 信息到父容器的 /etc/hosts 的文件。下面是父容器 web 的 hosts 文件

$ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7  aed84ee21bde
. . .
172.17.0.5  db

這裏有 2 個 hosts,第一個是 web 容器,web 容器用 id 做爲他的主機名,第二個是 db 容器的 ip 和主機名。
能夠在 web 容器中安裝 ping 命令來測試跟db容器的連通。

root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping
root@aed84ee21bde:/opt/webapp# ping db
PING db (172.17.0.5): 48 data bytes
56 bytes from 172.17.0.5: icmp_seq=0 ttl=64 time=0.267 ms
56 bytes from 172.17.0.5: icmp_seq=1 ttl=64 time=0.250 ms
56 bytes from 172.17.0.5: icmp_seq=2 ttl=64 time=0.256 ms

用 ping 來測試db容器,它會解析成 172.17.0.5
*注意:官方的 ubuntu 鏡像默認沒有安裝 ping,須要自行安裝。

用戶能夠連接多個父容器到子容器,好比能夠連接多個 web 到 db 容器上。

資源連接

官方網站

實踐參考

技術交流

其它

常見問題總結

這篇文章是我學習 Docker 的記錄,大部份內容摘抄自 <<Docker — 從入門到實踐>> 一書,並不是本人原創. 學習過程當中整理成適合我本身的筆記,其中也包含了我本身的實踐記錄.
相關文章
相關標籤/搜索