Dockerfile 最佳實踐
本文由Vikings(http://www.cnblogs.com/vikings-blog/) 原創,轉載請標明.謝謝!
寫在前面的話
如果要研究和使用Docker,那么一定要使用Dockerfile來build自己的image。但docker的原理限定了image的layer不能太多,因此不能肆意妄為的進行build,一定要控制image的layer數量。同樣也要關注image的大小,如果build出來的image動輒就是7,8GB,那么實用性就很差了。因此在參考官方的Dockerfile Best Practices之后,結合本人平時的經驗寫出下面的文章。
Overview
Docker通過讀取Dockerfile里面的內容自動build image。Dockerfile是一個包含了build過程中需要執行的所有命令的文本文件。Dockerfile有特定的數據格式,關於Dockerfile的基礎可以參考 Dockerfile Reference ,如果您對Dockerfile還沒有感覺,建議從Reference開始。
這篇文檔包含了Docker推薦的最佳實踐,Docker官方建議您按照以下規則編寫Dockerfile。事實上,如果您准備build一個官方版本的image,那么有一些規則,是必須遵守的。
如果您在查看以下內容時,需要深入理解某些命令的使用方法,建議查看Dockerfile Reference。
General guidelines and recommendations
Containers should be ephemeral
這些通過您的Dockerfile所構建的image而產生的container,越精簡越好(ephemeral 意為短暫,我認為這里翻譯為精簡較為合適)。這個"精簡",我們的意思是:這個container可以很容易的stop或者destroy,也很容易的build出一個新的container,同時通過很少的步驟或者配置就可以部署使用。
Use a .dockerignore file
如果想要快速的進行upload,或者想要提高docker build的效率。那么建議你使用 .dockerignore 文件。例如:除非在build過程中需要.git文件,否則你應該將.git添加到.dockerignore中,這將減少最終image的大小也會提高upload的效率。
Avoid installing unnecessary packages
為了減少build復雜度,軟件依賴度,image尺寸和build時間,你應該盡量回避那些非必要安裝的軟件包,因為這些軟件包僅屬於錦上添花那一列,非必須那一列。比如:你就沒有必要在Database image中安裝一個文本編輯器。
Run only one process per container
在幾乎所有的case里面,就盡量是一個container只運行一個單獨的實例。將具有耦合度的application分別安裝到不同的container里面,將很容易進行橫向擴展和復用container。如果某個service依賴其他service,推薦使用container linking 技術。
Minimize the number of layers
在Dockerfile可讀性和保持最少數據層之間找到平衡。一定要慎重引入新的數據層。
Sort multi-line arguments
如果可能的話,將你准備安裝的軟件包安裝字母順序排列。這樣可以回避重復安裝軟件包的情況,同時也有助於進行軟件更新。通過添加"\"進行分割,將增強代碼的可讀性。
下面列出了在build-deps中的一段代碼:
RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
Build cache
Docker在build image過程中,會按照Dockerfile中規定的步驟依次執行。Docker當執行每一條命令時都會查找有沒有已存在的數據層或者可以服用的數據層,而不是每次都是傻傻的重新執行。當然如果你不想使用cache中的數據層,那么在執行docker build時添加 --no-cache=true即可。
如果你准備使用cache中的數據層,那么有必要了解一下docker什么時候會使用,什么時候不會使用這些數據層。以下是Docker的規則:
-
- 如果cache中存在baseimage,那么遞歸檢查Dockerfile中所有的數據層定義是否和cache中的baseimage數據層定義相同。如果不相同,則cache數據無效
- 在大多數情況下,將Dockerfile中的指令同cache中的image 數據層比對就足夠了。但某一些命令需要每次都執行。
- 針對ADD COPY這些命令,docker會檢查這些文件。每次都會計算這些文件的checksum。如果checksum同cache中的checksum不匹配,那么這些cacha中的文件將會失效。
- 除了ADD COPY這兩個命令,Docker會檢查cache中有沒有匹配的數據,其他的命令Docker都不會匹配cache中的數據。比如當執行RUN apt-get -y update命令時,Docker不會檢查cache中是否有update后的數據,而僅僅是在cache中查找有沒有匹配的命令字符串而已。
一旦cache中的數據無效了,那么這條命令以后的所有命令都不會使用cache中的數據,而是產生一個新的數據層。
The Dockerfile instructions
下面是定義Dockerfile的一些建議。
FROM
如果有可能,建議使用官方提供的image版本作為你的baseimage。我們建議使用Debian image,因為這些image易於控制同時尺寸都很小,大多數在100MB以下,非常適合進行分發。
RUN
為了保持你的Dockerfile可讀性,易於理解,方便維護。建議將多條RUN 命令使用"/"連接起來。
apt-get應該是大多數Dockerfile都會定義的RUN 命令。當使用apt-get,有如下建議可參考:
-
- 不用將RUN apt-get update單獨作為一條命令。如果關聯包發生變化后,在執行apt-get install 命令時,docker 查找cache時有可能會有問題。
- 回避使用 RUN apt-get upgrade 或者 dis-upgrade 命令。因為很多外部的軟件包在未經認證情況執行upgrade會失敗。如果有一些軟件包過期了,那么你應該聯系軟件包的維護者來確定是否需要升級。比如你確定一個第三方的軟件包 foo 可以進行升級。那么執行apt-get install -y foo就可以自動完成升級。
- 可以寫成如下格式:
RUN apt-get update && apt-get install -y package-bar package-foo package-baz
下面是一個包含了上述建議的使用RUN命令的例子。注意最后一個軟件包 s3cmd 特定了版本1.1.0*。 如果image中安裝的是舊版本的s3cmd,那么這條命令將會更新cache中的數據。
RUN apt-get update && apt-get install -y \ aufs-tools \ automake \ btrfs-tools \ build-essential \ curl \ dpkg-sig \ git \ iptables \ libapparmor-dev \ libcap-dev \ libsqlite3-dev \ lxc=1.0* \ mercurial \ parallel \ reprepro \ ruby1.9.1 \ ruby1.9.1-dev \ s3cmd=1.1.0*
按照這個規范定義RUN命令,將使你回避到重復安裝軟件包的情況。因為下面的格式太難維護了。
RUN apt-get install -y package-foo && apt-get install -y package-bar
CMD
CMD命令用來執行image中的所有應用。CMD一般采用CMD [“executable”, “param1”, “param2”…]的格式來運行。所以,如果你的image是用來提供服務的,例如Apache,Rails。你就應該執行類似這樣的命令CMD ["apache2","-DFOREGROUND"]。
在其他的case中,CMD用來執行特定的shell,比如:bash,python,perl等等。比如:CMD ["perl", "-de0"]
, CMD ["python"]
, or CMD [“php”, “-a”]。
當你執行docker run -it python時就可以進入特定的shell中。
CMD經常是配合ENTRYPOINT 來使用的。除非確定你的用戶非常了解
ENTRYPOINT 的特性。否則還是建議你事先設定好
ENTRYPOINT
EXPOSE
EXPOSE命令定義了container用來監聽連接者的端口。因此,你應該為你的image定義一個比較通用的端口。比如一個用來提供Apache web服務的image,你應該expose 80.而提供MongoDB的image,應該提供27017端口。
對於一些外部訪問,你的用戶可以使用docker run -p的形式來進行端口綁定。
ENV
為了保證application可以順利執行,你可以通過ENV來更新PATH環境變量。比如:通過ENV PATH /usr/local/nginx/bin:$PATH 可以確保CMD ["nginx"]順利執行。
ENV也可以用來提供特定的環境變量,比如你可以自定義postgres所需要的PGDATA變量。
最后ENV可以用來定義一些版本信息。這樣維護起來就更容易,比如下例:
ENV PG_MAJOR 9.3 ENV PG_VERSION 9.3.4 RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
ADD和COPY
盡管ADD和COPY用法和作用很相近,但COPY仍是首選建議。因為COPY相對於ADD而言,功能簡單夠用。COPY僅提供本地文件向container的基本拷貝功能。但ADD就有額外的一些功能,比如支持拷貝tar包和URL。因此,ADD比較符合邏輯的使用方式是 ADD roots.tar.gz / 。
如果在你的Dockerfile中每步之間需要使用不用的文件,那么建議使用COPY 一些文件而不是COPY所有文件。比如:
COPY requirements.txt /tmp/ RUN pip install /tmp/requirements.txt COPY . /tmp/
結果就是cache中的數據將最大可能性的復用,比 COPY . /tmp/ 效果要好的多。
因為考慮的image的尺寸問題,現在針對使用ADD 從遠程URL獲取軟件包還有一些爭議。建議你還是使用curl或者wget。這樣當你安裝完畢后,可以再選擇刪除掉,而不是留在數據層中。如果你想用ADD URL,那么是這樣的:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all
使用curl和wget,是這樣的:
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.gz \
| tar -xJC /usr/src/things \ && make -C /usr/src/things all
對於很多文件和目錄,那么就應該使用COPY。
ENTRYPOINT
ENTRYPOINT最好的使用方式是設定image的主命令,允許image通過這個主命令來執行,使用CMD來設定參數。
比如使用s3cmd的例子是這樣的:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
當我們執行 docker run s3cmd或者 docker run s3cmd ls s3://mybucket 時,image就可以執行。
這非常有用,當image執行時s3cmd的reference將會同步顯示出來。
ENTERYPOINT也可以用來允許help 腳本。比如下面是postgres官方image 使用ENTRYPOINT的方式:
#!/bin/bash set -e if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"
if [ -z "$(ls -A "$PGDATA")" ]; then gosu postgres initdb fi exec gosu postgres "$@"
fi exec "$@"
將這個腳本COPY到image里面,並且設定為ENTRYPOINT。
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
當執行docker run postgres 就可以啟動image。如果執行docker run postgres postgres --help 將會啟動postgres,並且顯示reference。
最后當然也可以啟動一個bash,docker run -it --rm postgres bash
VOLUME
VOLUME應該被用來導出數據庫存儲區域,配置文件存儲區域或者container內部app創建的目錄或者文件。
USER
如果app運行不需要root權限,使用USER可以變更為普通用戶。使用RUN groupadd -r postgres && useradd -r -g postgres postgres可以創建一個普通用戶。
你應該回避使用sudo來安裝軟件包。因為在build過程中,TTY是無法使用的。如果在安裝過程中需要使用root權限,就使用gosu。
最后為了減少不必要的數據層和復雜度,回避切換USER的情況。
WORKDIR
為了保持執行過程清晰,你應該經常使用絕對路徑來設定WORKDIR。同樣,你應該使用WORKDIR來替代 RUN cd .. && do-something。