Docker入門實踐之dokerfile編寫(1)

   Dockerfile是一個文本格式的配置文件,經過dockerfile能夠快速建立自定義鏡像以適應測試,預發佈,生產環境等各類應用部署鏡像,一個好的dockerfile可讓咱們的鏡像更方便管理以及應用。php

一. Dokerfile的基本結構

Dockfile是由一行行命令語句組成,而且遲滯以#開頭的註釋行;通常而言,Dockerfiel分爲四部分:
1.基礎鏡像信息 ;2.維護者信息;3.鏡像操做指令;4.容器啓動時指令,以下爲一個標準的dockfile鏡像模板:html

# This dockerfile uses the ubutu image
# VERSION 2 - EDITON 1
# Author docker_user
# Command format: Instruction [arguments / command ] ..

# (第一部分) 基礎鏡像信息 
FROM ubuntu

# (第二部分) 維護者信息
MAINTAINER docker_user docker_user@email.com

#(第三部分)鏡像操做指令 
RUN yum install apr-devel -y

    #(第四部分) 容器啓動時指令
CMD /usr/sbin/echo

從上面能夠看到一個基本的dockerfile實例包含:1.基礎鏡像信息, 2.維護者信息,3.鏡像操做指令,4容器啓動時指令這四個要素node

二. Dokerfile鏡像操做指令

dockerfiel鏡像操做指令通常格式爲: Instruction arguments 其指令包括:FORM MAINTAINER RUN 等等指令python

1. FROM 引用基礎鏡像

格式爲:FORM <image> 或FORM <image>:<tag>
第一條指令必須爲FORM指令,若是在同一個dockerfile文件中建立多個鏡像時,可使用多個FROM指令(每一個鏡像一次)mysql

在 Docker Store 上有很是多的高質量的官方鏡像,有能夠直接拿來使用的服務類的鏡像,如 nginx、redis、mongo、mysql、httpd、php、tomcat 等;也有一些方便開發、構建、運行各類語言應用的鏡像,如 node、openjdk、python、ruby、golang 等。能夠在其中尋找一個最符合咱們最終目標的鏡像爲基礎鏡像進行定製。若是沒有找到對應服務的鏡像,官方鏡像中還提供了一些更爲基礎的操做系統鏡像,如 ubuntu、debian、centos、fedora、alpine 等,這些操做系統的軟件庫爲咱們提供了更廣闊的擴展空間。linux

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

FROM scratch
...

若是你以 scratch 爲基礎鏡像的話,意味着你不以任何鏡像爲基礎,接下來所寫的指令將做爲鏡像第一層開始存在。不以任何系統爲基礎,直接將可執行文件複製進鏡像的作法並不罕見,好比 swarm、coreos/etcd。對於 Linux 下靜態編譯的程序來講,並不須要有操做系統提供運行時支持,所需的一切庫都已經在可執行文件裏了。git

所以直接 FROM scratch 會讓鏡像體積更加小巧。使用 Go 語言 開發的應用不少會使用這種方式來製做鏡像,這也是爲何有人認爲 Go 是特別適合容器微服務架構的語言的緣由之一。github

2. MAINTAINER 維護者信息

格式爲MAINTAINER <name>, 指定維護者信息golang

3. RUN 執行命令

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

3.1.shell 格式:

RUN <命令>,就像直接在命令行中輸入的命令同樣。剛纔寫的 Dockerfile 中的 RUN 指令就是這種格式。

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
3.2.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製做出了很臃腫的鏡像的緣由之一,就是忘記了每一層構建的最後必定要清理掉無關文件。

4. COPY 複製文件

複製本地主機<src> (爲dockerfile所在的目錄的相對路徑,文件或目錄) <dest>爲容器中的路徑,當目標路徑不存在時,會自動建立,通常格式爲:

COPY <源路徑>... <目標路徑>
COPY ["<源路徑1>",... "<目標路徑>"]

和 RUN 指令同樣,也有兩種格式,一種相似於命令行,一種相似於函數調用。
COPY 指令將從構建上下文目錄中 <源路徑> 的文件/目錄複製到新的一層的鏡像內的 <目標路徑> 位置。好比:

COPY package.json /usr/src/app/

<源路徑> 能夠是多個,甚至能夠是通配符,其通配符規則要知足 Go 的 filepath.Match 規則,如:

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目標路徑> 能夠是容器內的絕對路徑,也能夠是相對於工做目錄的相對路徑(工做目錄能夠用 WORKDIR 指令來指定)。目標路徑不須要事先建立,若是目錄不存在會在複製文件前先行建立缺失目錄。

此外,還須要注意一點,使用 COPY 指令,源文件的各類元數據都會保留。好比讀、寫、執行權限、文件變動時間等。這個特性對於鏡像定製頗有用。特別是構建相關文件都在使用 Git 進行管理的時候。

5. ADD更高級的複製文件

好比 <源路徑> 是一個 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 命令了。儘量的使用 COPY,由於 COPY 的語義很明確,就是複製文件而已,而 ADD 則包含了更復雜的功能,其行爲也不必定很清晰。最適合使用 ADD 的場合,就是所說起的須要自動解壓縮的場合。所以在 COPY 和 ADD 指令中選擇的時候,能夠遵循這樣的原則,全部的文件複製均使用 COPY 指令,僅在須要自動解壓縮的場合使用 ADD。

6 .ENV設置環境變量

格式有兩種:

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

env就是設置環境變量,不管是後面的其它指令,如 RUN,仍是運行時的應用,均可以直接使用env定義的環境變量。以下所示定義了環境變量,那麼在後續的指令中,就可使用這個環境變量:

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

這和 Shell 下的行爲是一致的。

7 . VOLUME 定義匿名卷

格式爲:

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

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

VOLUME /data

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

docker run -d -v mydata:/data xxxx

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

8. WORKDIR 指定工做目錄

格式爲 : WORKDIR <工做目錄路徑>。

使用 WORKDIR 指令能夠來指定工做目錄(或者稱爲當前目錄),之後各層的當前目錄就被改成指定的目錄,如該目錄不存在,WORKDIR 會幫你創建目錄。常犯的錯誤是把 Dockerfile 等同於 Shell 腳原本書寫,這種錯誤的理解還可能會致使出現下面這樣的錯誤:

RUN cd /app
RUN echo "hello" > world.txt

若是將這個 Dockerfile 進行構建鏡像運行後,會發現找不到 /app/world.txt 文件,或者其內容不是 hello。緣由其實很簡單,在 Shell 中,連續兩行是同一個進程執行環境,所以前一個命令修改的內存狀態,會直接影響後一個命令;而在 Dockerfile 中,這兩行 RUN 命令的執行環境根本不一樣,是兩個徹底不一樣的容器。這就是對 Dockerfile 構建分層存儲的概念不瞭解所致使的錯誤。以前說過每個 RUN 都是啓動一個容器、執行命令、而後提交存儲層文件變動。第一層 RUN cd /app 的執行僅僅是當前進程的工做目錄變動,一個內存上的變化而已,其結果不會形成任何文件變動。而到第二層的時候,啓動的是一個全新的容器,跟第一層的容器更徹底不要緊,天然不可能繼承前一層構建過程當中的內存變化,所以若是須要改變之後各層的工做目錄的位置,那麼應該使用 WORKDIR 指令。例如:

WORKDIR /a
WORKDIR b
WORKDIR c
則最終路徑爲:/a/b/c

注意可使用多個WORKDIR指令,後續指令若是爲相對路徑,則會基於以前命令指定路徑,述上所示。

9. 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。以下所示:

# 創建 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" ]
相關文章
相關標籤/搜索