本文主要介紹在編寫 docker 鏡像的時候一些須要注意的事項和推薦的作法。php
雖然 Dockerfile 簡化了鏡像構建的過程,而且把這個過程能夠進行版本控制,可是不正當的
Dockerfile 使用也會致使不少問題: python
但願讀者可以對 docker 鏡像有必定的瞭解,閱讀這篇文章至少須要一下前提知識:nginx
當運行 docker build 命令的時候,整個的構建過程是這樣的:git
a. 讀取 Dockerfile 文件發送到 docker daemon
b. 讀取當前目錄的全部文件(context),發送到 docker daemon
c. 對 Dockerfile 進行解析,處理成命令加上對應參數的結構
d. 按照順序循環遍歷全部的命令,對每一個命令調用對應的處理函數進行處理
e. 每一個命令(除了 FROM)都會在一個容器執行,執行的結果會生成一個新的鏡像,爲最後生成的鏡像打上標籤web
編寫 Dockerfile 的一些最佳實踐
1.使用統一的 base 鏡像sql
有些文章講優化鏡像會提倡使用盡可能小的基礎鏡像,目前集團操做系統一級提供統一的基礎鏡像,一些BU也根據本身的技術規範定義了BU級的基礎鏡像,通常的應用只須要FROM本身BU提供的基礎鏡像便可,由於基礎鏡像只須要下載一次能夠共享,並不會形成太多的存儲空間浪費。它的好處是這些鏡像的生態比較完整,方便咱們安裝軟件,除了問題方便調試。docker
2.動靜分離shell
常常變化的內容和基本不會變化的內容要分開,把不怎麼變化的內容放在下層,建立出來不一樣基礎鏡像供上層使用。好比能夠建立各類語言的基礎鏡像,這些鏡像包含了最基本的語言庫,每一個組能夠在上面繼續構建應用級別的鏡像。數據庫
3.最小原則:只安裝必需的東西apache
不少人構建鏡像的時候,都有一種衝動——把可能用到的東西都打包到鏡像中。要遏制這種想法,鏡像中應該只包含必需的東西,任何能夠有也能夠沒有的東西都不要放到裏面。由於鏡像的擴展很容易,並且運行容器的時候也很方便地對其進行修改。這樣能夠保證鏡像儘量小,構建的時候儘量快,也保證將來的更快傳輸、更省網絡資源。
4.一個原則:每一個鏡像只有一個功能
不要在容器裏運行多個不一樣功能的進程,每一個鏡像中只安裝一個應用的軟件包和文件,須要交互的程序經過 容器之間的網絡進行交流。這樣能夠保證模塊化,不一樣的應用能夠分開維護和升級,也能減少單個鏡像的大小。
5.使用更少的層
雖然看起來把不一樣的命令儘可能分開來,寫在多個命令中容易閱讀和理解。可是這樣會致使出現太多的鏡像層,而很差管理和分析鏡像,並且鏡像的層是有限的。儘可能把相關的內容放到同一個層,使用換行符進行分割,這樣能夠進一步減少鏡像大小,而且方便查看鏡像歷史。
6.減小每層的內容
儘管只安裝必須的內容,在這個過程當中也可能會產生額外的內容或者臨時文件,咱們要儘可能讓每層安裝的東西保持最小。
好比使用 --no-install-recommends 參數告訴 apt-get 不要安裝推薦的軟件包
7.不要在 Dockerfile 中單獨修改文件的權限
由於 docker 鏡像是分層的,任何修改都會新增一個層,修改文件或者目錄權限也是如此。若是有一個命令單獨修改大文件或者目錄的權限,會把這些文件複製一份,這樣很容易致使鏡像很大。
解決方案也很簡單,要麼在添加到 Dockerfile 以前就把文件的權限和用戶設置好,要麼在容器啓動腳本(entrypoint)作這些修改,或者拷貝文件和修改權限放在一塊兒作(這樣最終也只是增長一層)。
8.利用 cache 來加快構建速度
若是 Docker 發現某個層已經存在了,它會直接使用已經存在的層,而不會從新運行一次。若是你連續運行 docker build 屢次,會發現第二次運行很快就結束了。
不過從 1.10 版本開始,Content Addressable Storage 的引入致使緩存功能的實效,目前引入了 --cache-from 參數能夠手動指定一個鏡像來使用它的緩存。
9.版本控制和自動構建
最好把 Dockerfile 和對應的應用代碼一塊兒放到版本控制中,而後可以自動構建鏡像。這樣的好處是能夠追蹤各個版本鏡像的內容,方便了解不一樣鏡像有什麼區別,對於調試和回滾都有好處。
另外,若是運行鏡像的參數或者環境變量不少,也要有對應的文檔給予說明,而且文檔要隨着 Dockerfile 變化而更新,這樣任何人都能參考着文檔很容易地使用鏡像,而不是下載了鏡像不知道怎麼用。
10.使用一個.dockerignore文件
在大部分狀況下,最好的作法是將每個Dockerfile文件放到一個空的文件夾裏。接着,把構建Dockerfile所需的文件添加到這個文件下。爲了提升構建的效率,你能夠在這個文夾下添加一個.dockerignore 文件來排除那些沒用的文件和文件夾。這個文件支持相似 .gitignore 文件那樣的排除模式。關於如何建立它,能夠移步到dockerignore 文件。
Dockerfile 指令介紹:
更多信息請參考《Dockerfile 參考》
FROM :
這個設置基本的鏡像,爲後續的命令使用,因此應該做爲Dockerfile的第一條指令。FROM <image>:<tag>
不管何時,儘量使用BU提供的基礎鏡像,有利於技術規範化,簡化你的Dockerfile。
RUN :
RUN命令會在上面FROM指定的鏡像裏執行任何命令,而後提交(commit)結果,提交的鏡像會在後面繼續用到。格式
RUN <command> (the command is run in a shell - `/bin/sh -c`)
通常,爲讓你的 Dockerfile 更加易讀,易懂和便於維護,請將長的或者複雜的 RUN 語句用反斜槓()分割成多行。
RUN 通常都是搭配 apt-get一塊兒使用。當使用 apt-get時,這裏幾個注意事項:
不要在單獨一行上使用RUN apt-get update 。 這樣會引發緩存問題,若是關聯的歸檔文檔被更新了,將會致使後續的 apt-get install 執行失敗而沒有任何提示。
避免 RUN apt-get upgrade 或dist-upgrade, 由於不少來自基礎鏡像的「底層」的包將會更新失敗,在一個無特權的容器裏。若是一個基礎包已通過期,你應該通知它的維護人員。若是你知道這裏一個特定的包,如 foo,它須要更新,能夠直接使用apt-get install -y foo 讓它自動更新。
應該這樣編寫你的指令:
RUN yum update && yum install -y \ package-bar \ package-baz \ package-foo
使用這樣方法編寫指令,不只讓它變得更加易讀和可維護,並且,經過包含 apt-get update,確保繞開本地的緩存,安裝最新的版本而不須要編寫更多的指令和手動的干預。
繞開緩存能夠實現包的版本定位(例如:package-foo=1.3.*)。這將強制去檢索指定的版本,無論緩存裏存儲了什麼。編寫你的 apt-get 代碼,這種方法將大大下降的維護難度和減小由未意料的的包而致使失敗機率。
例子
下面是一段格式良好的 RUN 指令,它演示了上述的建議。注意最後的包 s3cmd,指定了一個版本 1.1.0*。若是這個鏡像以前使用過一箇舊的版本,指定的新版將引發 apt-get update 緩存失效,確保一個新的版本被安裝(在這個應用場景中,須要這個特性)。
RUN yum update && yum 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*;yum clean all
使用這種方法編寫指令也能夠幫助你避免包的重複,由於這樣寫比下面的寫法更加的易讀:
RUN yum install -y package-foo && yum install -y package-bar;yum clean all
EXPOSE
EXPOSE 指令指定容器監聽的端口。所以,你應該使用通用、慣例的端口到你的應用。例如,一個包含着Apacheweb服務端的鏡像將使用80端口,當鏡像包含是一個MangoDB應該使用EXPOSE 27017 等等。
爲了提供外部訪問,你的用戶能夠執行docker run 帶上一個標誌,代表如何映射指定的端口到他們選擇的端口。爲了容器的鏈接,Docker提供了環境變量來指定接受容器到源容器的路徑(如,MYSQL_PORT_3306_TCP)。
ENV
爲了方便新安裝的軟件的運行,你可使用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_MAJOR/bin:$PATH
和在編程時定義常亮相似(而不採用硬編碼),使用這樣方法,你只需修改一個ENV 指令,就能自動更新與之關聯的數據。
ADD 或 COPY
雖然 ADD 和COPY 的功能相似,通常而言,推薦使用COPY 。由於它比ADD更加見名知意。COPY 只支持將本地本件拷貝到容器中,雖然ADD 擁有一些功能(例如,抽取本地tar文件內容和支持遠程URL),可是這些功能不是很經常使用。所以,ADD 的最佳使用場景是,自動抽取一個本地tar的內容到鏡像中,例如:ADD rootfs.tar.xz /。
若是你要執行多個Dockerfile 步驟且使用來自的環境中不一樣的文件,分開COPY 它們,而不是一次性的拷貝它們。這樣能夠確保每一個步驟的構建緩存都是失效的(強制步驟的重作),若是指定須要的的文件更新了。
例如:
COPY requirements.txt /tmp/ RUN pip install /tmp/requirements.txt COPY . /tmp/
這樣,RUN 步驟能夠增長緩存的命中率,若是你把COPY . /tmp/ 放到它前面,反之。
出於鏡像的大小的考慮,使用 ADD 從遠程URL提取內容的方法強烈不推薦。你應該使用curl 或 wget 替代。這種方法容許你在提取完內容後,能夠刪除你不須要的文件。例如,你應該避免這樣作:
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
相反,你應該這樣作:
RUN mkdir -p /usr/src/things \ && curl -SL http://example.com/big.tar.xz
| tar -xJC /usr/src/things \ && make -C /usr/src/things all
除了須要從tar文件中提取內容時使用ADD,其餘時候,你應該老是使用COPY。
COPY指令是以root身份執行的。但集團pouch在啓動應用時會將/home/admin的屬主置爲admin,因此用戶通常不須要額外的指令來處理COPY到/home/admin/目錄下的文件屬主權限。
ENTRYPOINT
ENTRYPOINT 最佳使用場景是設置鏡像的主入口命令,容許鏡像好像命令運行同樣(使用 CMD 做爲默認的標誌)。
讓咱們啓動一個帶命令行工具 s3cmd的鏡像:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
如今,啓動後的鏡像與在命令行中執行命令的幫助相似:
$ docker run s3cmd
或在右邊添加參數來執行一個命令:
$ docker run s3cmd ls s3://mybucket
這很用,如上所述,能夠把鏡像的名字當作一個二進制程序來使用。
ENTRYPOINT 指令也能夠和一個輔助腳本結合使用,容許它和上述的相似方式運行,即便當啓動工具命令超過一行時。
例如,Postgres官方鏡像使用下面的腳本做爲它的ENTRYPOINT:
#!/bin/bashset -eif [ "PGDATA"if [ -z "
PGDATA")" ]; then
gosu postgres initdb
fiexec gosu postgres "@"
注意:這個腳本使用了exec Bash指令,運行時的應用程序會變成容器的PID 1。這將容許應用能夠接收發送到容器的全部Unix信號。 查看ENTRYPOINT 幫助文檔得到更多的信息。
將這個輔助腳本拷貝到容器裏,經過 ENTRYPOINT 來啓動容器:
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]
這個腳本容許用戶使用幾種交互的方法啓動Postgres:
能夠簡單的啓動Postgres:
$ docker run postgres
或者,可使用它去運行帶幾個參數的Postgres:
$ docker run postgres postgres --help
最後,它也能夠用來啓動一個徹底不一樣的工具,如,Bash:
$ docker run --rm -it postgres bash
VOLUME
建立一個掛載點用於共享目錄。
VOLUME 指令應該用於暴露任何的數據庫存儲域、配置存儲、文件/文件夾,在建立容器的時候。任何易變的或鏡像的供用戶使用的部分,建議使用VOLUME 。
docker run時會將宿主機的目錄掛載到VOLUME目錄下,以宿主機某個目錄(此鏡像獨享的目錄)覆蓋docker容器中的對應目錄,使得其中的數據修改在docker重啓時仍然能保持;帶來另外一個後果是,若是你在Dockerfile中往VOLUME目錄中寫入了數據(即docker build階段寫入的數據),在啓動容器的時候你會發現它不見了(由於它寫到編譯機上去了)。
USER
指定運行用戶。
若是一個服務能夠不須要權限就能運行,應該使用 USER 切換到一個非root用戶。使用像這種命令 RUN groupadd -r postgres && useradd -r -g postgres postgres能夠建立一個用戶和用戶組。
注意:鏡像裏的用戶和組的UID/GID都是不肯定的,無論它是否被重建。若是這些信息對你很重要,你應該顯示的指定一個UID/GID。
你應該避免安裝或使用 sudo ,由於這些操做帶來不肯定的TTY和信號的轉發行爲,是一個得不償失的設置。若是你必須要使用相似 sudo 的功能(例如,在非root用戶在初始化一個須要root權限的的守護進程),你可能須要使用「gosu」。
最後,爲了減小層和複雜度,不建議頻繁的來回切換 USER 。
WORKDIR
更多內容請移步《Dockerfile參考》的WORKDIR部分
爲了清晰和可靠,你應該始終爲你的WORKDIR指定一個絕度路徑。另外,你因該使用WORKDIR 來替代相似RUN cd … && do-something指令,這樣能夠下降可讀性、故障排除難度、維護成本。
CMD
CMD 命令應該用來運行包含軟件的鏡像,連同任何參數。CMD 應該老是使用這種格式CMD [「executable」, 「param1」, 「param2」…]。 這樣,若是這個鏡像承載着一個服務(Apache,Rails等),你能夠運行相似CMD ["apache2","-DFOREGROUND"]的指令。 事實上,這種格式的指令,不管那種基於服務的鏡像,都值得推薦。
在大多的其餘場景裏,CMD 應該指定一個交互式的shell (bash, python, perl, 等),例如,CMD ["perl", "-de0"], CMD ["python"], 或 CMD [「php」, 「-a」]。 使用這些格式相似你執行docker run -it python,你將進入一個可用的shell中,準備好了。當CMD 和ENTRYPOINT 協同工做時,應該使用 CMD [「param」, 「param」] 格式。這種方式儘可能少用,除非你和你的用戶對 ENTRYPOINT 實現機制都很瞭解。
ONBUILD
更多內容請移步《Dockerfile參考》的ONBUILD部分
一個ONBUILD 命令在當前的Dockerfile 構建完成後會被執行。當使用 FROM 爲鏡像個派生出子鏡像時,ONBUILD 也會被執行。也能夠簡單的理解爲,實際上是將父Dockerfile 的ONBUILD 中的指令放到子Dockerfile中。
ONBUILD 命令會先於子Dockerfile中全部命令執行。
ONBUILD 對使用 FROM 基於指定鏡像構建頗有幫助。例如, ONBUILD 容許你在 Dockerfile裏,基於某種語言棧構建任意的軟件鏡像,你能夠參考Ruby的 ONBUILD 。.
ONBUILD 因該指定一個指定標誌(tag),例如:ruby:1.9-onbuild 或 ruby:2.0-onbuild。
當你把 ADD 或 COPY 放到ONBUILD要注意。 若是新的構建環境缺乏要添加的資源,會致使鏡像的構建失敗。添加一個分隔標籤,如條建議同樣,以供編寫的 Dockerfile 能夠選擇合適他的構建環境。