使用Dockerfile腳本定制鏡像的方法

前言

鏡像的定制實際上就是定制每⼀層所添加的配置、⽂件等信息。

但是命令畢竟隻是命令,一般用 docker commit 每次定制都得去重復執⾏這個命令,⽽且還不夠直觀,如果我們可以把每⼀層修改、安裝、構建、操作的命令都寫⼊⼀個腳本,⽤這個腳本來構建、定制鏡像,那麼這些問題就迎刃而解瞭,而這個腳本就是我們今天要說的 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 很簡單,⼀共就兩⾏。涉及到瞭兩條指令,FROM 和 RUN。

二、FROM指定基礎鏡像

所謂定制鏡像,那⼀定是以⼀個鏡像為基礎,在其上進⾏定制。就像我們之前運⾏瞭⼀個 nginx 鏡像 的容器,再進⾏修改⼀樣,基礎鏡像是必須指定的。⽽ FROM 就是指定基礎鏡像,因此⼀個 Dockerfile 中 FROM 是必備的指令,並且必須是第⼀條指令。

在Docker Store上有⾮常多的⾼質量的官⽅鏡像,有可以直接拿來使⽤的服務類的鏡像,如 nginx、 redis、mongo、mysql、httpd、php、tomcat 等;也有⼀些⽅便開發、構建、運⾏各種語⾔應⽤的鏡 像,如 node、openjdk、python、ruby、golang 等。可以在其中尋找⼀個最符合我們最終⽬標的鏡像 為基礎鏡像進⾏定制。

如果沒有找到對應服務的鏡像,官⽅鏡像中還提供瞭⼀些更為基礎的操作系統鏡像,如 ubuntu、 debian、centos、fedora、alpine 等,這些操作系統的軟件庫為我們提供瞭更⼴闊的擴展空間。

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

FROM scratch 
...

如果你以 scratch 為基礎鏡像的話,意味著你不以任何鏡像為基礎,接下來所寫的指令將作為鏡像第 ⼀層開始存在。有的同學可能感覺很奇怪,沒有任何基礎鏡像,我怎麼去執⾏我的程序呢,其實對於 Linux 下靜態編譯的程序來說,並不需要有操作系統提供運⾏時⽀持,所需的⼀切庫都已經在可執⾏⽂ 件⾥瞭,因此直接 FROM scratch 會讓鏡像體積更加⼩巧。使⽤ Go 語⾔ 開發的應⽤很多會使⽤這種⽅ 式來制作鏡像,這也是為什麼有⼈認為 Go 是特別適合容器微服務架構的語⾔的原因之⼀。

三、RUN執行命令

RUN 指令是⽤來執⾏命令⾏命令的。由於命令⾏的強⼤能⼒, RUN 指令在定制鏡像時是最常⽤的指令 之⼀。其格式有兩種:
shell 格式:RUN <命令>,就像直接在命令⾏中輸⼊的命令⼀樣。剛才寫的 Dockerfile 中的 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 : FROM nginx 
---> e43d811ce2f4 
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html 
---> Running in 9cdc27646c7b 
---> 44aa4490ce2c 
Removing intermediate container 9cdc27646c7b 
Successfully built 44aa4490ce2c

從命令的輸出結果中,我們可以清晰的看到鏡像的構建過程。在 Step 2 中,如同我們之前所說的那 樣,RUN 指令啟動瞭⼀個容器 9cdc27646c7b,執⾏瞭所要求的命令,並最後提交瞭這⼀層 44aa4490ce2c,隨後刪除瞭所⽤到的這個容器 9cdc27646c7b。這⾥我們使⽤瞭 docker build 命令 進⾏鏡像構建。其格式為:

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

在這⾥我們指定瞭最終鏡像的名稱 -t nginx:v3,構建成功後,我們可以像之前運⾏ nginx:v2 那樣來運 ⾏這個鏡像,其結果會和 nginx:v2 ⼀樣。

五、鏡像構建上下文(Context)

如果註意,會看到 docker build 命令最後有⼀個 . 。 . 表示當前⽬錄,⽽ Dockerfile 就在當前⽬錄, 因此不少初學者以為這個路徑是在指定 Dockerfile 所在路徑,這麼理解其實是不準確的。如果對應上⾯的命令格式,你可能會發現,這是在指定上下⽂路徑。那麼什麼是上下⽂呢?

⾸先我們要理解 docker build 的⼯作原理。Docker 在運⾏時分為 Docker 引擎(也就是服務端守護進 程)和客戶端⼯具。Docker 的引擎提供瞭⼀組 REST API,被稱為 Docker Remote API,⽽如 docker 命令這樣的客戶端⼯具,則是通過這組 API 與 Docker 引擎交互,從⽽完成各種功能。因此,雖然表 ⾯上我們好像是在本機執⾏各種 docker 功能,但實際上,⼀切都是使⽤的遠程調⽤形式在服務端 (Docker 引擎)完成。也因為這種 C/S 設計,讓我們操作遠程服務器的 Docker 引擎變得輕⽽易舉。

當我們進⾏鏡像構建的時候,並⾮所有定制都會通過 RUN 指令完成,經常會需要將⼀些本地⽂件復制 進鏡像,⽐如通過 COPY 指令、ADD 指令等。⽽ docker build 命令構建鏡像,其實並⾮在本地構建, ⽽是在服務端,也就是 Docker 引擎中構建的。那麼在這種客戶端/服務端的架構中,如何才能讓服務 端獲得本地⽂件呢?

這就引⼊瞭上下⽂的概念。當構建的時候,⽤戶會指定構建鏡像上下⽂的路徑,docker build 命令得知 這個路徑後,會將路徑下的所有內容打包,然後上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下 ⽂包後,展開就會獲得構建鏡像所需的⼀切⽂件。如果在 Dockerfile 中這麼寫:

COPY ./package.json /app/

這並不是要復制執⾏ docker build 命令所在的⽬錄下的 package.json,也不是復制 Dockerfile 所在⽬錄下的 package.json,⽽是復制 上下⽂(context) ⽬錄下的 package.json。

因此, COPY 這類指令中的源⽂件的路徑都是相對路徑。這也是初學者經常會問的為什麼 COPY ../package.json /app 或者 COPY /opt/xxxx /app ⽆法⼯作的原因,因為這些路徑已經超出瞭上下⽂的 范圍,Docker 引擎⽆法獲得這些位置的⽂件。如果真的需要那些⽂件,應該將它們復制到上下⽂⽬錄 中去。

現在就可以理解剛才的命令 docker build -t nginx:v3 . 中的這個 . ,實際上是在指定上下⽂的⽬ 錄,docker build 命令會將該⽬錄下的內容打包交給 Docker 引擎以幫助構建鏡像。

如果觀察 docker build 輸出,我們其實已經看到瞭這個發送上下⽂的過程:

$ docker build -t nginx:v3 . 
Sending build context to Docker daemon 2.048 kB 
...

理解構建上下⽂對於鏡像構建是很重要的,可以避免犯⼀些不應該的錯誤。⽐如有些初學者在發現 COPY /opt/xxxx /app 不⼯作後,於是⼲脆將 Dockerfile 放到瞭硬盤根⽬錄去構建,結果發現 docker build 執⾏後,在發送⼀個⼏⼗ GB 的東⻄,極為緩慢⽽且很容易構建失敗。那是因為這種做法是在讓 docker build 打包整個硬盤,這顯然是使⽤錯誤。

⼀般來說,應該會將 Dockerfile 置於⼀個空⽬錄下,或者項⽬根⽬錄下。如果該⽬錄下沒有所需⽂ 件,那麼應該把所需⽂件復制⼀份過來。如果⽬錄下有些東⻄確實不希望構建時傳給 Docker 引擎,那 麼可以⽤ .gitignore ⼀樣的語法寫⼀個 .dockerignore ,該⽂件是⽤於剔除不需要作為上下⽂傳遞給 Docker 引擎的。

那麼為什麼會有⼈誤以為 . 是指定 Dockerfile 所在⽬錄呢?這是因為在默認情況下,如果不額外指定 Dockerfile 的話,會將上下⽂⽬錄下的名為 Dockerfile 的⽂件作為 Dockerfile。

這隻是默認⾏為,實際上 Dockerfile 的⽂件名並不要求必須為 Dockerfile,⽽且並不要求必須位於上下 ⽂⽬錄中,⽐如可以⽤ -f ../Dockerfile.php 參數指定某個⽂件作為 Dockerfile。

當然,⼀般⼤傢習慣性的會使⽤默認的⽂件名 Dockerfile,以及會將其置於鏡像構建上下⽂⽬錄中。

六、遷移鏡像

Docker 還提供瞭 docker load 和 docker save 命令,⽤以將鏡像保存為⼀個 tar ⽂件,然後傳輸到另 ⼀個位置上,再加載進來。這是在沒有 Docker Registry 時的做法,現在已經不推薦,鏡像遷移應該直 接使⽤ Docker Registry,⽆論是直接使⽤ Docker Hub 還是使⽤內⽹私有 Registry 都可以。

使⽤ docker save 命令可以將鏡像保存為歸檔⽂件。⽐如我們希望保存這個 alpine 鏡像。

$ docker image ls alpine 
REPOSITORY TAG IMAGE ID CREATED SIZE 
alpine latest baa5d63471ea 5 weeks ago 4.803 MB

保存鏡像的命令為:

$ docker save alpine | gzip > alpine-latest.tar.gz

然後我們將 alpine-latest.tar.gz ⽂件復制到瞭到瞭另⼀個機器上,可以⽤下⾯這個命令加載鏡像:

$ docker load -i alpine-latest.tar.gz 
Loaded image: alpine:latest

如果我們結合這兩個命令以及 ssh 甚⾄ pv 的話,利⽤ Linux 強⼤的管道,我們可以寫⼀個命令完成從 ⼀個機器將鏡像遷移到另⼀個機器,並且帶進度條的功能:

docker save <鏡像名> | bzip2 | pv | ssh <⽤戶名>@<主機名> 'cat | docker load'

到此這篇關於使用Dockerfile腳本定制鏡像的文章就介紹到這瞭,更多相關Dockerfile定制鏡像內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: