docker 適用dockerfile 定製docker鏡像

在一個空白目錄中,創建一個文本文件,並命名爲 Dockerfilephp

$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile

其內容爲:html

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

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

FROM 指定基礎鏡像

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

在 Docker Hub[^1] 上有很是多的高質量的官方鏡像, 有能夠直接拿來使用的服務類的鏡像,如 nginxredismongomysqlhttpdphptomcat 等; 也有一些方便開發、構建、運行各類語言應用的鏡像,如 nodeopenjdkpythonrubygolang 等。 能夠在其中尋找一個最符合咱們最終目標的鏡像爲基礎鏡像進行定製。 若是沒有找到對應服務的鏡像,官方鏡像中還提供了一些更爲基礎的操做系統鏡像,如 ubuntudebiancentosfedoraalpine 等,這些操做系統的軟件庫爲咱們提供了更廣闊的擴展空間。mysql

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

FROM scratch
...

若是你以 scratch 爲基礎鏡像的話,意味着你不以任何鏡像爲基礎,接下來所寫的指令將做爲鏡像第一層開始存在。git

不以任何系統爲基礎,直接將可執行文件複製進鏡像的作法並不罕見,好比 swarmcoreos/etcd。對於 Linux 下靜態編譯的程序來講,並不須要有操做系統提供運行時支持,所需的一切庫都已經在可執行文件裏了,所以直接 FROM scratch 會讓鏡像體積更加小巧。使用 Go 語言 開發的應用不少會使用這種方式來製做鏡像,這也是爲何有人認爲 Go 是特別適合容器微服務架構的語言的緣由之一。github

RUN 執行命令

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

  • shell 格式:RUN <命令>,就像直接在命令行中輸入的命令同樣。剛纔寫的 Dockrfile 中的 RUN 指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
  • exec 格式:RUN ["可執行文件", "參數1", "參數2"],這更像是函數調用中的格式。

既然 RUN 就像 Shell 腳本同樣能夠執行命令,那麼咱們是否就能夠像 Shell 腳本同樣把每一個命令對應一個 RUN 呢?好比這樣:redis

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 build 的用法

直接用 Git repo 進行構建

或許你已經注意到了,docker build 還支持從 URL 構建,好比能夠直接從 Git repo 中構建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
docker build https://github.com/twang2218/gitlab-ce-zh.git\#:8.14
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM gitlab/gitlab-ce:8.14.0-ce.0
8.14.0-ce.0: Pulling from gitlab/gitlab-ce
aed15891ba52: Already exists
773ae8583d14: Already exists
...

這行命令指定了構建所需的 Git repo,而且指定默認的 master 分支,構建目錄爲 /8.14/,而後 Docker 就會本身去 git clone 這個項目、切換到指定分支、並進入到指定目錄後開始構建。

用給定的 tar 壓縮包構建

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

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

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

docker build - < Dockerfile

cat Dockerfile | docker build -

若是標準輸入傳入的是文本文件,則將其視爲 Dockerfile,並開始構建。這種形式因爲直接從標準輸入中讀取 Dockerfile 的內容,它沒有上下文,所以不能夠像其餘方法那樣能夠將本地文件 COPY 進鏡像之類的事情。

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

$ docker build - < context.tar.gz

若是發現標準輸入的文件格式是 gzipbzip2 以及 xz 的話,將會使其爲上下文壓縮包,直接將其展開,將裏面視爲上下文,並開始構建。

[^1]: Docker Store是發現公共Docker內容,鏡像發佈和發行軟件的新地方

相關文章
相關標籤/搜索