【Day 2】Docker Image (映像檔) & Dockerfile


前言

這篇分為「關於 image,常做的幾件事」和「建構 image 的方式」兩大部分,前者藉由介紹 image 基礎的生命週期(?)與其對應指令,來熟悉 image 的使用方式;後者則希望能在學習 image 建構方式的同時,理解 image 的構成。


關於 image,常做的幾件事:

上一篇介紹了 docker 幾個重要元素(Image、Container、Repository和Registry),其中提到「Container 是 Image 的實體」,或說「Image 是 Container 的樣板」。總之,容器以 image 為基礎運行,那麼想要在機器上執行容器前,機器就必須先擁有所需的 image。可以從 Registry(倉庫伺服器,如官方的 DockerHub)上取得 image,再對它進一步操作。

  1. 查詢 registry 有哪些 image ---------------------------- docker search
  2. 從 registry 取得所需 image ---------------------------- docker pull
  3. 檢視本地端有哪些 images(已下載的 images)----- docker image ls
  4. 實體化 image (把 Container 跑起來)--------------- docker run
  5. 把用不到的 image 從本地端刪除 --------------------- docker rmidocker image rm

指令介紹

1. docker search <image_keywords> <搜尋條件>

  • <image_keywords> 不需要是完整的 image_name。
  • 如下例,搜尋官方提供的 含「ubun」關鍵字的 images:
$ docker search ubun -f is-official=true
NAME                 DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
ubuntu               Ubuntu is a Debian-based Linux operating sys…   10542               [OK]
ubuntu-upstart       Upstart is an event-based replacement for th…   105                 [OK]
neurodebian          NeuroDebian provides neuroscience research s…   64                  [OK]
ubuntu-debootstrap   debootstrap --variant=minbase --components=m…   42                  [OK]

2. docker pull <registry>/<image>

※ 註:<image>指<username>/<image_name>:<tag>,若無 username 則從官方提供的 images 中抓取。

  • 作用:從 Registry 取得 Image
  • 若沒提供 tag,則預設為 latest;若沒提供 registry,則預設從 DockerHub 上取得。例如,docker pull ubuntu 就等同於 docker pull registry.hub.docker.com/ubuntu:latest
  • 只要把上面的 registry.hub.docker.com 替換成其他 registry 的位址,就可以從其他 registry 取得 image。
  • Image 採分層存儲方式,故下載 image 也是一層層下載,而非下載一大包打包檔。實際執行 docker pull 可看到所下載的各層的部分 image ID。
$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
423ae2b273f4: Pull complete
de83a2304fa1: Pull complete
f9a83bce3af0: Pull complete
b6b53be908de: Pull complete
Digest: sha256:04d48df82c938587820d7b6006f5071dbbffceb7ca01d2814f81857c631d44df
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

3. docker image ls <image>

  • 作用:列出本地端已下載的 images
  • Image ID 是 image 的唯一識別,但一個 image 可以對應多個 tag。
    • 如下,兩個 image 擁有相同 Image ID,代表它們是同個 image,只是被賦予不同的 tag:
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              isthebest           72300a873c2c        2 days ago          64.2MB
ubuntu              latest              72300a873c2c        2 days ago          64.2MB
  • 加上<image>可篩選列舉,或加上其他條件(如:用 -f 篩選、用--format顯示所需資料、用 -a 列出所有 images),詳見官方文件(docker image ls)
$ docker image ls ubuntu:latest
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              72300a873c2c        2 days ago          64.2MB

$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID            REPOSITORY          TAG
72300a873c2c        ubuntu              isthebest
72300a873c2c        ubuntu              latest

4. docker run <options> <image> <command>

  • 作用:把 image 實體化,即以 image 為基礎創建一個容器(在上頭疊加暫時的容器存儲層,以及相關配置,如網路或 ip 位址等等),並執行指定的程序/命令。
  • 關於 docker run 的細節,詳見之後的 container 篇。

5. docker rmi 或 docker image rm + <repository>:<tag> 或 <image_ID>

(若沒指定<tag>只會嘗試移除 latest)

  • 作用:從本地移除 image。
  • 移除不一定真的會從本地刪掉並釋出空間,image 的移除包含 untagdelete 兩部分:
    • 假設本地有 ubuntu:latest 和 ubuntu:isthebest 兩個 images,可以移除的過程中,先後出現了 Untagged 和 Deleted:
$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              isthebest           72300a873c2c        2 days ago          64.2MB
ubuntu              latest              72300a873c2c        2 days ago          64.2MB

$ docker rmi ubuntu
Untagged: ubuntu:latest

$ docker rmi ubuntu:isthebest
Untagged: ubuntu:isthebest
Untagged: ubuntu@sha256:04d48df82c938587820d7b6006f5071dbbffceb7ca01d2814f81857c631d44df
Deleted: sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c
  • 由於「相同 Image ID 的 images 其實是同一個 image,只是可能被賦予不同 tags」,這邊 ubuntu:latest 和 ubuntu:isthebest 是同一個 image。若下指令 docker rmi ubuntu:latest 就直接移除這個 image、釋出空間,ubuntu:isthebest 就也被消失了,不合理。
  • 為了避免把還需要的 image 也刪掉,每當進行刪除,會先移除該 tag(untag),確認沒有其他 tags 參考到該 image,才會進一步刪除 image、釋出空間(delete)
  • 此例中,刪掉 ubuntu:latest 只移除了 latest 這個 tag(untag),直到第二次 isthebest 也被 untagged,發現沒有 tag 參考此 image,才真正刪掉 image(delete)。

建構 Image 的方式

  1. docker commit [僅適用於少數暫時性的特殊情況]:手動替 image 疊加新的一層,成為新的 image
  2. Dockerfile + docker build [標準!]:完整記錄 image 構建環節(容器執行所需條件)
  3. docker import / export 以及 docker load / save [沒 registry 前的古法]:直接導入應用程式的壓縮包,直接展開保存為 base image

1. 以 docker commit 建構 image [僅適用於少數暫時性的特殊情況]

方法

  • 建構步驟:
    1. docker exec -it <container_name或container_ID> bash:以交互模式執行容器的 bash 命令,對容器進行修改。
    2. docker diff <container_name或container_ID>:查看變更檔案。
    3. docker commit <options> <container_name或container_ID> <repository>:<tag>:把這些變更(當前的容器存儲層)保存為新的 image。
  • 建構結果查看方法:
    • docker image ls:可以看到新增的 image。
    • docker history <repository>:<tag>:可以查看此 image 分層存儲架構的紀錄,包含先前的 images,還有本次疊加上去的新 image。

缺點

  • 手動進行變更,沒有清楚的紀錄,難以追溯、維護,也很難再重建出一個一模一樣的 image。
  • 後續的修改都會以先前 image 為基礎,建立新的 image(先前 image 裡的東西都無法被真正移除)。這樣零碎缺乏組織的修改,會導致 image 越來越臃腫。

2. 以 Dockerfile + docker build 建構 image [標準作法!]

2.(1) 撰寫 Dockerfile

Dockerfile 官方範例

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

Dockerfile 跟 docker commit 層層疊加的建構方式雷同,Dockerfile 裡的每個指令(FROM、COPY、RUN、CMD等等)都會建造一個新的 image。但它清楚紀錄了完整的建構過程,解決了 docker commit 的問題:

  • Dockerfile 易於追溯、維護 image。
  • 只須建構一次,就能輕易複用,

不過也不要把 Dockerfile 跟一般 shell 指令腳本混淆。書中寫到:

撰寫 Dockerfile 時,要提醒自己,不是在寫 Shell 腳本,而是在定義如何構建每一層 image。

引用書的一段範例碼:

FROM debian:jessie

RUN buildDeps='gcc lib6-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 redix.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 可執行檔,沒有必要建立很多層,這只是一層的事情。

  • 另外,在指令結束前,必須把所有暫時/不需要的東西(下載檔案、apt緩存等)都移除掉。一旦指令結束,所有東西就會被建構為一個「唯讀」的 image,就算之後再用指令刪除,無法真正移除它。

2.(2) 用命令 docker build <options> <context 或 URL> 來構建

Docker 分為 client 端(docker cli)和 server 端(docker engine/docker daemon),雖然總在本地下 docker 指令,事實上都是調用 docker 提供的 REST API(Docker Remote API)來跟 docker engine 溝通,由 docker engine 處理。

Image 建構這個工作也一樣由 docker engine 負責,所以建構 image 所需的檔案也必須傳給 docker engine 才能建構,Context 就是告訴 docker 哪個路徑下的檔案是建構 image 需要的

2.(2) a. 指定 Context(image 構建上下文)來建構

例如,docker build . 說明了所有建構 image 需要的檔案都在當前目錄(context 是當前目錄),docker build 指令就會把 context 路徑下的所有內容打包、上傳給 server(docker engine),server 收到會再展開,以取用所需檔案。

所以,像是 Dockerfile 裡的 COPY ./package.json /app/ 這種指令,源路徑(./package.json)都是基於 context 的相對路徑,而像是 COPY ../package.json /app/COPY /folder/abc /app 這種不在 context 範疇內的路徑/檔案,server 端無法取得。

.dockerignore

承上,不想傳給 docker engine 的本地端文件,就可以像 .gitignore 那樣寫在 .dockerignore 檔裡。

2.(2) b. 指定 URL 來建構

  • URL 若為 git repo:docker engine 會去 clone → 切換到指定 branch(默認為 master) → 進入指定目錄 → 開始構建
  • URL 若為 tar 壓縮包:docker engine 會去下載壓縮包 → 解壓縮,把它當作 context → 開始構建

3. 以 docker export / import 以及 docker load / save 建構 image [沒 registry 前的古法]

由於現有 public / private registry 可以快速 pull / push images,比較不需要利用壓縮檔來建立或搬遷 images,這些變得較少使用。

這兩種方式皆是擷取 image 資料,並在所需的位置載入(自動展開檔案、建立 image)。只是兩者操作的 image 檔案不同。

前者是匯出/匯入的是「容器快照」,後者儲存/載入的是「image 儲存檔」

  • 容器快照:不含歷史紀錄,還有其他 meta data(所以可於 import 時,指定 tag 等資料)。
  • Image 儲存檔:保留完整紀錄。

(1) docker export & docker import

  • export:把容器快照匯出到本地 images。例如:docker export 78hf5sksk6 myubuntu.tardocker export <src_container> > <target_file.tar>)。
  • import:匯入容器快照檔案(本地壓縮包或下載遠端 web 文件),展開保存為 base image。例如:docker import http://example.com/exampleimage.tar myname/imagecreated

(2) docker save & docker load

  • save:將 image 儲存檔案保存為一個 tar 文件。
  • load:加載 image 儲存檔作為 image。

結語

這篇介紹了 image 常用的指令,還有構建 image 的幾種方式。透過 docker commit 手動建構 image,有助理解 image 的分層儲存架構,之後學習 Dockerfile 時,也比較容易理解各指令如何層層疊加 image。

另外,書中提到兩個很受用的 Dockerfile 觀念:

  1. 撰寫 Dockerfile 是在設計層層 images 如何建構,而不是寫一般的 shell script(我以前也這樣想> <),所以每個指令結束前都必須做好清理。
  2. docker build 的時候,context 指定的是要打包送給 server 端的檔案範圍,所有構建 image 需要的檔案都必須涵蓋在 context 內。而 Dockerfile 裡像是 COPY、ADD 之類的指令,源路徑都是基於 context 的相對路徑。

下一篇再來仔細談談 container ~

(是說我真的好不會用 markdown 排版啊,醜得受不了,等忙完這七天挑戰再來研究一下修版好了,嗚嗚)

#docker #dockerfile #image #docker commit #docker build #untagged #deleted #分層存儲