Try   HackMD
tags: docker

手把手教你安裝、使用 docker 並快速產生 Anaconda 環境 (1)

Jeff Chan / 鐘致耀 / 劉承泰

tags: PyHUG,Geminiopencloud

緣起

Using Docker as a Python Development Environment

大多數 Python 開發人員在開發中使用 virtualenv。它提供了一種易用的機制讓應用程式使用自己專用的依賴項,這些依賴項可能與在其它應用程式或作業系統存在衝突(尤其是不同的Pyhton版本,還有不同的庫版本等等)。


  • 我經常忘記啟用它,或者在切換工程時忘記切換它,這會遇到含糊的出錯資訊,另人倍感困惑。
  • 它無法提供“純粹的”隔離,只能是Python級別的隔離(系統庫和非python的依賴項仍然會出問題)。
  • 我通常不想在正式產品中執行它,這就意味著在開發環境和正式產品的不一致。
  • 它讓人感覺有點“黑客”作法:它是依靠修改指令碼和設定新路徑實現的。

Why I hate virtualenv and pip


What is a container


  • Container (容器),即作業系統層虛擬化 (Operating-system-level virtualization)
  • Container 的歷史最早可追朔至 chroot (1979)
    • chroot 改變一個 process 及其 subprocess 執行時的根目錄
    • 此 process 不能對這個根目錄以外的檔案進行存取。所以這個根目錄也叫作 chroot jail,保護本機其他檔案不受存取。
    • 範例:FTP service。用 chroot 限制一個 user 可以看到的目錄結構

  • 2007年,LXC (Linux Container) 誕生
    • chroot on steroids:比 chroot 更完整的 process isolation
    • 讓個別 process 之間達成系統、資源與環境上的隔離
      • process 於 filesystem, network, IPC 等的可視範圍
      • Host 的實體資源之存取權和用量 (i.e CPU, MEM)
    • 兩種 Linux Kernel Feature 催生了 LXC
      • namespace: 限制 process 可視的本機資源,讓 process 認為自己有獨立的執行環境 (不同的 namespace 不互相干擾)
      • cgroup: 限制,控制與分離一群 process 可用的實體資源 (i.e CPU, MEM)

VM v.s. Container

  • Container 比 VM 更輕量:不用Hypervisor、不必額外安裝 Guest OS,直接共同分享本機的 Kernel。所以比傳統 VM 更省資源、更快速,效能接近原生主機

Virtualization (i.e. kvm, xen) LXC Containers
Footprint Requires a hypervisor and a full operating system image. Does not require a hypervisor or a separate operating system image.
OS supported Any OS supported by the hypervisor Most Linux distros, uses same kernel as host

Virtualization (i.e. kvm, xen) LXC Containers
Typical server deployment 10 – 100 VMs 100 - 1000 containers
Boot time Less than a minute Seconds
Pysical resources use (i.e. memory, CPU) Each VM has resource reserved for its own use Shared by all containers

  • 但直接使用 Container 是不容易的,因為沒有一個有系統性的管理方式,也有太多細節要去顧慮,容易出錯

What is Docker


  • 2013年,Docker 誕生
  • Docker 是一個讓開發者能方便地利用 Container 建置、移動、執行、移除軟體服務的開源專案
  • Docker 不是 Container。它是以 Container 為基礎衍生出來的服務
    • 以 Golang 實作
    • 最初以 LXC 為基礎,進行進一步的封裝,讓 Container 的管理變得更加方便易用
    • Docker 0.9 起,不再以 LXC 作為預設的 Container。取而代之的是 libcontainer (今已成為 runC 專案)

Why libcontainer?


  • 首先,Docker 定義了 Execution Driver API。透過它,可以抽換底層的容器實作 (driver),例如 LXC、systemd-nspawn 等。

  • libcontainer 則成為預設的 driver。它可以直接呼叫 Linux Kernel 的 container API,直接操作 namespace、cgroup等,不必依賴任何第三方實作 (例如 LXC)

  • libcontainer 讓 Docker 與這些實作解耦合,不再依賴這些實作,進而提升了效能和可維護性

  • 也因 libcontainer 定義了一系列的統一 API,所以任何人可以透過實作這組API,產生新的容器實作去產生自己要的 container,甚至可讓 Docker 達到全平台通用的目標


VM v.s. Docker Containers

vm


Docker Containers


架構與一般 Container 大同小異,但不同的是,Docker Container 多了 Docker Engine 這層,去管理眾多的 Container。


安裝 Docker

官方文件已詳盡地介紹各平台的安裝過程,在此不贅述。請至以下網站閱讀文件並按照說明進行操作。
Get Docker for Ubuntu
Get started with Docker for Windows
Get started with Docker for macOS


Docker 官方為了簡化安裝流程,提供了一套安裝指令碼,Ubuntu 和 Debian 系統可以使用這套指令碼安裝:

curl -sSL https://get.docker.com/ | sh

執行這個指令後,指令碼就會自動的將一切準備工作做好,並且把 Docker 安裝在系統中。


基本概念

Docker 包括三個基本概念

  • 映像檔(Image)
  • 容器(Container)
  • 倉庫(Repository)

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


Docker 映像檔


  • 一個 Docker 映像檔是包含了一個 Container 執行時所需要的所有程式、檔案系統、函式庫、環境變數等資料的二元檔。因此也可以看作是一個 Container 的快照 (Snapshot)
  • 我們可以用同一個映像檔產生多個 Container;這些 Container 在 filesystem 上做的任何改變都不會影響原本的映像檔
  • 映像檔的好處:讓 Container 透過建立映像檔,成為真正可移動的容器 (Shipping Containers),可以在任何地方產生同樣的 Container。
  • 可以透過 Docker Hub 或 Private Registry 發佈映像檔

映像檔的分層儲存

映像檔的體積有可能會很龐大,所以 Docker 利用 Union Filesystem 將它設計成分層儲存的架構。


以下例子是一個 ubuntu 映像檔的結構,由四個 layer 組成,他們都是 read-only 的。


每個一 layer,基本上都是一個 filesystem。新的 layer 會繼承前一個 layer。

因為使用這種分層儲存的架構,我們可以用之前建立好的映像檔作為 base layer,加入新的 layer,客製化自己的映像檔。而且不同映像檔間也可共享同樣的 layer,節省了空間與效能。

當使用這個映像檔開啟 Container 後,會即時產生一個可寫的 layer。



Container 在 filesystem 上做的改變,例如增加新檔案,都是紀錄在這第一層。當 Container 被刪除,這個 layer 也會被刪除,因此不會影響到原本的映像檔。


因此每個 Container 有自己的 writable-layer,彼此互不影響,所以可以共享同一份映像檔


Docker basic image operations

取得映像檔

Docker Hub 上有大量的高品質的映像檔可以用,這裡我們就說一下怎麼取得這些映像檔並執行。
從 Docker Registry 取得映像檔的指令是 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
c60055a51d74: Pull complete
755da0cdb7d2: Pull complete
969d017f67e6: Pull complete
37c9a9113595: Pull complete
a3d9f8479786: Pull complete
Digest: sha256:8f5f12335124c1b78e4cf2f8860d395f75ba279bae70a3c18dd470e910e38ec5
Status: Downloaded newer image for ubuntu:14.04

上面的指令中沒有給出 Docker Registry 位址,因此將會從 Docker Hub 取得映像檔。而映像檔名稱是 ubuntu:14.04,因此將會取得官方映像檔 library/ubuntu 倉庫中標籤為 14.04 的映像檔。


從下載程序中可以看到我們之前提及的分層儲存的概念,映像檔是由多層儲存所構成。下載也是一層層的去下載,並非單一檔案。下載程序中給出了每一層的 ID 的前 12 位。並且下載結束後,給出該映像檔完整的 sha256 的摘要,以確保下載一致性。

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


執行

有了映像檔後,我們就可以以這個映像檔為基礎啟動一個容器來執行。以上面的 ubuntu:14.04 為例,如果我們打算啟動裡面的 bash 並且進行交談式作業的話,可以執行下面的指令。


# docker run -it --rm ubuntu:14.04 bash
root@93b501913281:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@93b501913281:/# 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@93b501913281:/#

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 系統。


列出映像檔

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

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              16.04               f49eec89601e        2 weeks ago         129.5 MB
ubuntu              latest              f49eec89601e        2 weeks ago         129.5 MB
ubuntu              14.04               b969ab9f929b        2 weeks ago         188 MB
busybox             latest              7968321274dc        3 weeks ago         1.11 MB

清單包含了倉庫名、標籤、映像檔 ID、建立日期以及所佔用的空間。
其中倉庫名、標籤在之前的基礎概念章節已經介紹過了。映像檔 ID 則是映像檔的唯一識別,一個映像檔可以對應多個標籤。


映像檔體積

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


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


中間層映像檔

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

# docker images -a

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


Docker basic container operations

啟動容器


啟動容器有兩種方式,一種是從映像檔建立一個新的容器,一種是將 stopped 狀態的容器重新啟動


新建並啟動

docker run

以下範例為從 busybox 標記為 latest 這個 image 去產生一個容器,並在容器起來後 執行 /bin/echo 'Hello world'

# docker run busybox:latest /bin/echo 'Hello world'
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
4b0bc1c4050b: Pull complete 
Digest: sha256:817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e
Status: Downloaded newer image for busybox:latest
Hello world

上述輸出中間的部份為當本機沒有 busybox:latest 這個 image 時會去網路(docker hub)上拉下來到本機中


如果你希望它操作起來像是進入 VM 的話,可以使用下面的指令

# docker run -it busybox:latest /bin/sh 
/ #

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

你會發現執行完後你已經進入 shell 中,可以執行許多 image 本來就包含的指令如 ls,ps,ping,ifconfig 等


容器的核心為所執行的應用程式,所需要的資源都是應用程式執行所必需的。除此之外,並沒有其它的資源。可以在虛擬終端中利用 ps 或 top 來查看程式訊息。

/ # ps
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh
    5 root       0:00 ps

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


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

  • 檢查本地是否存在指定的映像檔,不存在就從公有倉庫下載
  • 利用映像檔建立並啟動一個容器
  • 分配一個檔案系統,並在唯讀的映像檔層外面掛載一層可讀寫層
  • 從宿主主機設定的網路橋界面中橋接一個虛擬埠到容器中去
  • 從位址池中設定一個 ip 位址給容器
  • 執行使用者指定的應用程式
  • 執行完畢後容器被終止

啟動已終止容器

利用 docker start 命令,直接將一個已經終止的容器啟動執行

# docker start <container_name or uuid>

背景執行

更多的時候,需要讓 Docker 容器在後臺以守護態(Daemonized)形式執行。此時,可以透過新增 -d 參數來實作。
例以下面的命令會在後臺執行容器。

$ docker run -d busybox:latest /bin/sh -c "while true; do echo hello world; date ; sleep 3; done"
12faa9656634db51097199680944463b6dbfff33a24f5452cabb79e8159d2a27

容器啟動後會返回一個唯一的 id


也可以透過 docker ps 命令來查看容器訊息

# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
12faa9656634        busybox:latest      "/bin/sh -c 'while..."   5 minutes ago       Up 7 seconds                            sad_mcnulty

要取得容器的輸出訊息,可以透過 docker logs 命令。

# docker logs <container_name or uuid>

終止容器

可以使用 docker stop 來終止一個執行中的容器。
此外,當Docker容器中指定的應用終結時,容器也自動終止。 例如對於上一章節中只啟動了一個終端機的容器,使用者透過 exit 命令或 Ctrl+d 來退出終端時,所建立的容器立刻終止。
終止狀態的容器可以用 docker ps -a 命令看到。例如

# docker ps -a
CONTAINER ID        IMAGE                          COMMAND                  CREATED             STATUS                        PORTS                                              NAMES
886c80ac0130        busybox:latest                 "/bin/sh"                14 minutes ago      Exited (0) 14 minutes ago

處於終止狀態的容器,可以透過 docker start 命令來重新啟動。
此外,docker restart 命令會將一個執行中的容器終止,然後再重新啟動它。

示範:開任一 container ,在 host 主機用 ps -aux |grep <container uuid> 找出 process ,手動用 kill -9 <process_id> ,會發現container 異常中斷了!


進入容器

在使用 -d 參數時,容器啟動後會進入後臺。 某些時候需要進入容器進行操作,有很多種方法,包括使用 docker exec 或 docker attach 工具等。


exec 命令

docker exec 是Docker內建的命令。下面示範如何使用該命令。

先隨意開個 container 並開啟交互介面但用背景執行

# docker run -itd busybox:latest
91996223e26c52461256bc4db1ada2335a029c8c1e1b8414502691c73664042b

使用 exec 並使用交互介面,指定該 container 執行 sh (shell)

# docker exec -it 91996223e26c sh
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ #

attach 命令

docker attach 亦是Docker內建的命令。下面示例如何使用該命令。

# docker attach 91996223e26c
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ #

但是使用 attach 命令有時候並不方便。當多個窗口同時 attach 到同一個容器的時候,所有窗口都會同步顯示。當某個窗口因命令阻塞時,其他窗口也無法執行操作了。

示範: 開一個 container 然後開數個 console 去 attach,會發現一個視窗操作,其他視窗也會同步顯示。但當其中一個 console 下指令 exit ,會因為當前 process 1 被中斷了,也就是container 生存的根本 process 被中斷,而導致 container 關閉。


若 attach 進入容器中,想要中 detach 又不終止容器的運行,可在 /home/<user>/.docker/config.json 中作設定。下面範例就是設定 ctrl-p,ctrl-q 作為 detach 指令。

{
    "auths": {
            "amz": {
                "auth": key"
            },
            "amz2": {
                "auth": key2"
            },
            "amz3": {
                "auth": "key3" }
         },
    "detachKeys": "ctrl-p,ctrl-q"
}

匯出和匯入容器

匯出容器

如果要匯出本地某個容器,可以使用 docker export 命令。

# docker export 17fea42a33ef > busyboxdemo.tar

這樣將匯出容器快照到本地檔案。


匯入容器快照

可以使用 docker import 從容器快照檔案中再匯入為映像檔,例如

# cat busyboxdemo.tar | docker import - test/busybox:v0.1
# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
test/busybox        v0.1                65f426b5ba0e        About a minute ago   1.11 MB

此外,也可以透過指定 URL 或者某個目錄來匯入,例如

# docker import http://example.com/exampleimage.tgz example/imagerepo

註:使用者既可以使用 docker load 來匯入映像檔儲存檔案到本地映像檔庫,也可以使用 docker import 來匯入一個容器快照到本地映像檔庫。這兩者的區別在於容器快照檔案將丟棄所有的歷史記錄和原始資料訊息(即僅保存容器當時的快照狀態),而映像檔儲存檔案將保存完整記錄,檔案體積也跟著變大。此外,從容器快照檔案匯入時可以重新指定標籤等原始資料訊息。


刪除容器

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

# docker rm c17fc107e075
c17fc107e075

如果要刪除一個執行中的容器,可以新增 -f 參數。Docker 會發送 SIGKILL 信號給容器

如果想要讓容器停止後自行刪除,可以在 docker run 後面加上參數 rm,例如

# docker run -it --rm busybox:latest
/ # exit

結束後使用 docker ps -a 去看,會發現剛剛停掉的容器自行刪除了。不過此參數與 -d 衝突,所以通常在你要開個有交互介面隨開即用的容器,離開後自行刪除的狀況。


How to build a docker image

利用 commit 理解映像檔構成

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

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

現在讓我們以訂製一個 Web 伺服器為例子,來講解映像檔是如何建構的。

docker run --name webserver -d -p 80:80 nginx
  • name 為參數指定這個容器的名稱(alias name)
  • -p 指定本機網路的 port 與容器的某個 port 對應

這條指令會用 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@7a299b3e3fab:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@7a299b3e3fab:/# 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
A /run/nginx.pid
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 "Jeff Chan <pm751211@gmail.com>" \
    --message "modify default page" \
    webserver \
    nginx:v2

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

我們可以在 docker images 中看到這個新訂製的映像檔:

# docker images nginx
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
nginx               v2                  bd7a24fb9b8d        About a minute ago   182 MB
nginx               latest              cc1b61406712        2 weeks ago          182 MB

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

docker history nginx:v2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
bd7a24fb9b8d        2 minutes ago       nginx -g daemon off;                            97 B                modify default page
cc1b61406712        2 weeks ago         /bin/sh -c #(nop)  CMD ["nginx" "-g" "daem...   0 B
<missing>           2 weeks ago         /bin/sh -c #(nop)  EXPOSE 443/tcp 80/tcp        0 B
<missing>           2 weeks ago         /bin/sh -c ln -sf /dev/stdout /var/log/ngi...   22 B
<missing>           2 weeks ago         /bin/sh -c apt-key adv --keyserver hkp://p...   58.8 MB
<missing>           2 weeks ago         /bin/sh -c #(nop)  ENV NGINX_VERSION=1.11....   0 B
<missing>           3 weeks ago         /bin/sh -c #(nop)  MAINTAINER NGINX Docker...   0 B
<missing>           3 weeks ago         /bin/sh -c #(nop)  CMD ["/bin/bash"]            0 B
<missing>           3 weeks ago         /bin/sh -c #(nop) ADD file:89ecb642d662ee7...   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 指令除了學習之外,還有一些特殊的應用場合,比如被入侵後儲存現場等。但是,不要使用 docker commit 訂製映像檔,訂製行為應該使用 Dockerfile 來完成。下面的章節我們就來講述一下如何使用 Dockerfile 訂製映像檔。

使用 Dockerfile 訂製映像檔

使用 Dockerfile 訂製映像檔

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

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

還以之前訂製 nginx 映像檔為例,這次我們使用 Dockerfile 來訂製。
在一個空白目錄中,建立一個文字檔案,並命名為 Dockerfile:

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其內容為:

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

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

FROM 指定基礎映像檔

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

在 Docker Hub (https://hub.docker.com/explore/) 上有非常多的高品質的官方映像檔, 有可以直接拿來使用的服務類的映像檔,如 nginxredismongomysqlhttpdphptomcat 等; 也有一些方便開發、建構、執行各種語言應用的映像檔,如 nodeopenjdkpythonrubygolang 等。 可以在其中尋找一個最符合我們最終目標的映像檔為基礎映像檔進行訂製。 如果沒有找到對應服務的映像檔,官方映像檔中還提供了一些更為基礎的作業系統映像檔,如 ubuntudebiancentosfedoraalpine 等,這些作業系統的軟體庫為我們提供了更廣闊的延伸空間。

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

FROM scratch
...

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

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

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 指令碼一樣把每個指令對應一個 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/2 : FROM nginx
 ---> cc1b61406712
Step 2/2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
 ---> Running in 9968c0206bf4
 ---> c02c48f7c4b1
Removing intermediate container 9968c0206bf4
Successfully built c02c48f7c4b1

從指令的輸出結果中,我們可以清晰的看到映像檔的建構程序。在 Step 2 中,如同我們之前所說的那樣,RUN 指令啟動了一個容器 9968c0206bf4,執行了所要求的指令,並最後送出了這一層 c02c48f7c4b1,隨後刪除了所用到的這個容器 9968c0206bf4。
這裡我們使用了 docker build 指令進行映像檔建構。其格式為:

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

映像檔建構上下文(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 -t="dockerfile/ubuntu" github.com/dockerfile/ubuntu
Sending build context to Docker daemon 184.3 kB
Step 1 : FROM ubuntu:14.04
 ---> b969ab9f929b
Step 2 : RUN sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list &&   apt-get update &&   apt-get -y upgrade &&   apt-get install -y build-essential &&   apt-get install -y software-properties-common &&   apt-get install -y byobu curl git htop man unzip vim wget &&   rm -rf /var/lib/apt/lists/*
 ---> Running in 5f8a7cb88779
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
Get:2 http://archive.ubuntu.com trusty-security InRelease [65.9 kB]
Get:3 http://archive.ubuntu.com trusty Release.gpg [933 B]
Get:4 http://archive.ubuntu.com trusty-updates/main Sources [484 kB]
Get:5 http://archive.ubuntu.com trusty-updates/restricted Sources [5957 B]
Get:6 http://archive.ubuntu.com trusty-updates/universe Sources [219 kB]
Get:7 http://archive.ubuntu.com trusty-updates/main amd64 Packages [1185 kB]
Get:8 http://archive.ubuntu.com trusty-updates/restricted amd64 Packages [20.4 kB]
Get:9 http://archive.ubuntu.com trusty-updates/universe amd64 Packages [514 kB]
···

用設定的 tar 壓縮包建構

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

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

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

docker build - < Dockerfile

or

cat Dockerfile | docker build -

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

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

$ docker build - < context.tar.gz

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

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 官方的最佳實踐文件中要求,盡可能的使用 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", "https://myip.com.tw/" ]

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

$ docker run myip
···

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

$ docker run myip -i
container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH"
docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"-i\": executable file not found in $PATH".
ERRO[0001] error getting events from daemon: net/http: request canceled

我們可以看到可執行檔案找不到的報錯,executable file not found。之前我們說過,跟在映像檔名後面的是 command,執行時會取代 CMD 的預設值。因此這裡的 -i 取代了遠了的 CMD,而不是加入在原來的 curl -s https://myip.com.tw/ 後面。而 -i 根本不是指令,所以自然找不到。
那麼如果我們希望加入 -i 這參數,我們就必須重新完整的輸入這個指令:

$ docker run myip curl -s https://myip.com.tw/ -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://https://myip.com.tw/" ]

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

$ docker run myip -i
HTTP/1.1 200 OK
Date: Tue, 14 Feb 2017 03:52:10 GMT
Server: Apache
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
···

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

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 的連接埠。

要將 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 指令。

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,可以從其項目網站看到進一步的資訊:https://github.com/tianon/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 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

Untagged 和 Deleted

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

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

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

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

用 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 指令行指令列的強大,你可以完成很多非常讚的功能。

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 實際應用

gitlab-ce

開啟一個具有 gitlab 服務的容器

gitlab 主要有兩種版本,社區版與企業版,以下以社區版為例。

docker run --detach \
    --hostname gitlab.example.com \
    --publish 443:443 --publish 80:80 --publish 22:22 \
    --name gitlab \
    --restart always \
    --volume /srv/gitlab/config:/etc/gitlab \
    --volume /srv/gitlab/logs:/var/log/gitlab \
    --volume /srv/gitlab/data:/var/opt/gitlab \
    gitlab/gitlab-ce:latest

上述指令會從 dockerhub 下載最新的 gitlab ,並將本機的 ssh,HTTP,HTTPS 的 port 對應到容器中。且開啟容器卷的功能,將本機的三個資料夾對應到容器中。並設定容器在系統重啟後自動重啟。

在容器完全啟動完畢後,你就可以藉由瀏覽器登入了

ref :

Omnibus GitLab documentation GitLab Docker images

Jenkins

開啟一個完全可用的 Jenkins 服務容器

docker run -p 8080:8080 -p 50000:50000 -v /your/home:/var/jenkins_home jenkins

使用上述指令,所有 Jenkins 的資料會存放在本機上的 /your/home 中

ref :

OFFICIAL REPOSITORY - jenkins


ANACONDA AND DOCKER - BETTER TOGETHER FOR REPRODUCIBLE DATA SCIENCE



What is Anaconda ?

Anaconda概述

  • Anaconda是一個用于科學計算的Python發行版,支持 Linux, Mac, Windows系统,提供了套件管理與環境管理的功能,可以很方便地解決多版本python並存、切換以及各種第三方包安装問題
  • Anaconda利用工具/命令conda来進行package和environment的管理,並且已經包含了Python和相關的配套工具。

  • conda可以理解為一個工具,也是一個可执行命令,其核心功能是套件管理與環境管理。套件管理與pip的使用類似,環境管理則允許使用者方便地安装不同版本的python並可以快速切換。

  • Anaconda則是一個打包的集合,里面預装好了conda、某個版本的python、眾多packages、科學計算工具等等,所以也稱為Python的一种發行版。

  • Miniconda,顧名思義,它只包含最基本的内容——python与conda


如果你只是需要一個有裝 anaconda 的 console ,你可以執行

docker run -i -t continuumio/anaconda3 /bin/bash

一旦容器開好了,我們可以開啟交互式 Python 介面、裝更多 package 或是執行 Python 應用

但如果你是希望有個內含 Anaconda 的 Jupyter notebook ,你可以執行

docker run -i -t -p 8888:8888 continuumio/anaconda3 /bin/bash -c "/opt/conda/bin/conda install jupyter -y --quiet && mkdir -p /opt/notebooks && /opt/conda/bin/jupyter notebook --notebook-dir=/opt/notebooks --ip='*' --port=8888 --no-browser"

經過漫長的下載與開 container ,你會在畫面上看到:


    Copy/paste this URL into your browser when you connect for the first time,
    to login with a token:
        http://localhost:8888/?token=8356c0ed24e8e9a5833eca6f5aeb96c03a420238a461981a

現在可以經由瀏覽器輸入 http://localhost:8888 來開啟 Jupyter Notebook

PS. OSX 環境, ip 參數內容要改成 --ip='0.0.0.0' 並加上 --allow-root

docker run -i -t -p 8888:8888 continuumio/anaconda3 /bin/bash -c "/opt/conda/bin/conda install jupyter -y --quiet && mkdir /opt/notebooks && /opt/conda/bin/jupyter notebook --notebook-dir=/opt/notebooks --ip='0.0.0.0' --port=8888 --no-browser --allow-root"


這種架構的優點:

    1. Quick and easy deployments with Anaconda
    1. Reproducible build and test environments with Anaconda
    1. Collaborative data science workflows with Anaconda
    1. Endless combinations with Anaconda and Docker

ref :

ANACONDA AND DOCKER - BETTER TOGETHER FOR REPRODUCIBLE DATA SCIENCE
docker hub - continuumio/anaconda


docker-hackmd

What is hackmd ?

  • HackMD 是個跨平台的 Markdown 即時協作筆記
  • 所以您可以在電腦、平板甚至是手機與其他人做筆記!
  • 同時也可以在 首頁 透過 Facebook、Twitter、GitHub、Dropbox 登入

  • 開源、由台灣團隊開發
  • 除了有線上服務外,也提供怎麼架設私有服務的範例
  • 前端與後端分成兩個 container

如果你已經安裝 docker-compose 的話

git clone https://github.com/hackmdio/docker-hackmd.git
cd docker-hackmd/
docker-compose up

若你沒有安裝 docker-compose 的話:

# 先開啟 postgresql 的 container
docker run -d --name hackmdPostgres -e POSTGRES_USER=hackmd -e POSTGRES_PASSWORD=hackmdpass -e POSTGRES_DB=hackmd postgres
# 再開啟前端服務的 container
docker run -d -e POSTGRES_USER=hackmd -e POSTGRES_PASSWORD=hackmdpass --link hackmdPostgres -p 3000:3000 hackmdio/hackmd

http://localhost:3000



ref :

docker-hackmd


ref :

Docker 相關



技術文章



對象:

  • 對 docker 有興趣的人
  • 希望開發時有個獨立不怕弄壞的環境的人

時間:

  • 2/15(三) 19:30 ~ 21:30

活動內容:

  • docker 簡介
  • docker 安裝說明
  • docker 基本操作
  • Anaconda 簡介
  • 實際範例
    • 開啟一個 直接可用的 Anaconda 容器

地點:

麥烤杯 Micro Beer
新竹市新竹縣竹北市自強七街40號

備註:

  • 此活動類似 workshop,各位可以帶著筆電實際操作,比較容易進入狀況,學習效果更好

ref:

Anaconda and Docker - Better Together for Reproducible Data Science