Dockerfile 命令詳解
-
FROM 指定基礎鏡像(必選)
所謂定制鏡像,那一定是以一個鏡像為基礎,在其上進行定制。就像我們之前運行了一個 nginx 鏡像的容器,再進行修改一樣,基礎鏡像是必須指定的。而
FROM
就是指定基礎鏡像,因此一個 Dockerfile 中 FROM 是必備的指令,並且必須是第一條指令。在Docker hub上有非常多的高質量的官方鏡像,有可以直接拿來使用的服務類的鏡像,如
nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等;也有一些方便開發、構建、運行各種語言應用的鏡像,如node
、openjdk
、python
、ruby
、golang
等。如果沒有找到對應服務的鏡像,官方鏡像中還提供了一些更為基礎的操作系統鏡像,如
ubuntu
、debian
、centos
、fedora
、alpine
等。FROM
命令語法:FROM <image>:<tag>
如果
tag
沒有選擇,默認為latest
。除了選擇現有鏡像為基礎鏡像外,Docker 還存在一個特殊的鏡像,名為
scratch
。這個鏡像是虛擬的概念,並不實際存在,它表示一個空白的鏡像。FROM scratch ...
如果你以
scratch
為基礎鏡像的話,意味着你不以任何鏡像為基礎,接下來所寫的指令將作為鏡像第一層開始存在。有的同學可能感覺很奇怪,沒有任何基礎鏡像,我怎么去執行我的程序呢,其實對於 Linux 下靜態編譯的程序來說,並不需要有操作系統提供運行時支持,所需的一切庫都已經在可執行文件里了,因此直接FROM scratch
會讓鏡像體積更加小巧。使用 Go 語言 開發的應用很多會使用這種方式來制作鏡像,這也是為什么有人認為 Go 是特別適合容器微服務架構的語言的原因之一。下面我們以一個
go
語言的helloworld
為例:FROM scratch COPY helloworld / COPY hellowold2 / CMD ["./helloworld"]
helloworld
文件就是個go
語言編譯出來的可執行程序,只會打印出hello world
。docker build -t hello-go:v1 . docker run hello-go:v1
-
LABEL 設置鏡像元數據
使用
LABEL
指令,可以為鏡像設置元數據,例如鏡像創建者或者鏡像說明。舊版的Dockerfile
語法使用MAINTAINER
指令指定鏡像創建者,但是它已經被棄用了。LABEL
命令語法:LABEL <key>=<value> <key>=<value> <key>=<value> ...
一個Dockerfile種可以有多個
LABEL
,如下:LABEL maintainer="cerberus43@gmail.com" LABEL version="1.0" LABEL description="This is a test dockerfile"
但是並不建議這樣寫,最好就寫成一行,如太長需要換行的話則使用
\
符號。如下:
LABEL maintainer="cerberus43@gmail.com" \ version="1.0" \ description="This is a test dockerfile"
說明:
LABEL
會繼承基礎鏡像種的LABEL
,如遇到key相同,則值覆蓋。 -
RUN 運行命令
使用
RUN
指令,可以用來執行命令行的命令。RUN
命令有兩種語法:-
shell
格式:在linux操作系統上默認 /bin/sh -c
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec
格式:RUN ["可執行文件", "參數1", "參數2"]
注意:多行命令不要寫多個RUN,原因是Dockerfile中每一個指令都會建立一層,多少個RUN就構建了多少層鏡像,會造成鏡像的臃腫、多層,不僅僅增加了構件部署的時間,還容易出錯。
下面是一個使用
apt-get
安裝多個包的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
-
COPY 復制文件
COPY
命令有兩種語法格式:-
COPY [--chown=<user>:<group>] <源路徑>... <目標路徑>
-
COPY [--chown=<user>:<group>] ["<源路徑1>",... "<目標路徑>"]
和
RUN
指令一樣,也有兩種格式,一種類似於命令行,一種類似於函數調用。說明:
- 目標路徑可以是容器內的絕對路徑,也可以是相對於工作目錄的相對路徑(工作目錄可以用
WORKDIR
指令來指定)。 - 目標路徑不需要事先創建,如果目錄不存在會在復制文件前先行創建缺失目錄。
- 使用
COPY
指令,源文件的各種元數據都會保留。比如讀、寫、執行權限、文件變更時間等。
復制單個文件示例:
COPY package.json /usr/src/app/
<源路徑>可以是多個,甚至可以是通配符,其通配符規則要滿足 Go 的 filepath.Match 規則,如:
COPY hom* /mydir/ COPY hom?.txt /mydir/
復制src目錄下內容到 /tmp 目錄下:
COPY src/ /tmp
復制多個目錄下內容到 /tmp 目錄下:
COPY src1/ src2/ /tmp
上面的命令只會將文件夾內容復制到鏡像目錄下,復制整個src目錄到/tmp目錄下,如果源目錄名不存在將自動逐級創建:
COPY src/ /tmp/src
指定文件權限
在使用該指令的時候還可以加上 --chown=: 選項來改變文件的所屬用戶及所屬組。
COPY --chown=devuser:devgroup files* /mydir/
-
-
ADD 更高級的復制文件
ADD
命令和COPY
的格式和性質基本一致。但是在COPY
基礎上增加了一些功能。-
解壓壓縮文件並把它們添加到鏡像中:
WORKDIR /app ADD nginx.tar.gz .
-
從 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
但是在Dockerfile 最佳實踐官方文檔中卻強烈建議不要這么用!官方建議我們當需要從遠程復制文件時,最好使用curl或wget命令來代替ADD命令。原因是,當使用ADD命令時,會創建更多的鏡像層,當然鏡像也會變的更大。
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
在 Docker 官方的 Dockerfile 最佳實踐官方文檔 中要求,盡可能的使用
COPY
,因為COPY
的語義很明確,就是復制文件而已,而ADD
則包含了更復雜的功能,其行為也不一定很清晰。最適合使用ADD
的場合,就是所提及的需要自動解壓縮的場合。因此在 COPY和 ADD指令中選擇的時候,可以遵循這樣的原則,所有的文件復制均使用COPY指令,僅在需要自動解壓縮的場合使用ADD。
-
-
WORKDIR 指定工作目錄
使用
WORKDIR
指令可以來指定工作目錄(或者稱為當前目錄),以后各層的當前目錄就被改為指定的目錄,如該目錄不存在,WORKDIR
會幫你建立目錄。語法格式為:
WORKDIR <工作目錄路徑>
FROM centos:7.2 #創建/usr/local/tomcat目錄 RUN mkdir /usr/local/tomcat #定位到tomcat下載目錄 WORKDIR /usr/local/tomcat #wget tomcat到/usr/local/tomcat目錄 RUN wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-7/v7.0.86/bin/apache-tomcat-7.0.86.tar.gz
-
ENV 指定容器的環境變量
使用
ENV
指令,可以設置環境變量,無論是后面的其它指令,如RUN
,還是運行時的應用,都可以直接使用這里定義的環境變量。語法格式有兩種:
-
ENV <key> <value>
-
ENV <key1>=<value1> <key2>=<value2>...
定義了環境變量,那么在后續的指令中,就可以使用這個環境變量。比如在官方
node
鏡像Dockerfile
中,就有類似這樣的代碼: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
在這里先定義了環境變量
NODE_VERSION
,其后的RUN
這層里,多次使用$NODE_VERSION
來進行操作定制。可以看到,將來升級鏡像構建版本的時候,只需要更新7.2.0
即可,Dockerfile
構建維護變得更輕松了。 -
-
ARG 指定Dockerfile中的環境變量
ARG
:ARG
定義的變量用於構建Docker
鏡像,在把Dockerfile
構建成鏡像后,ARG
定義的變量便不在起作用;ENV
:ENV
定義的變量用於容器的環境變量,在Dockerfile
里定義后,在容器的運行時是可以使用這個變量的;上面可能讀起來比較繞,看下這個實例就明白了:
ARG VAR_A=1 ENV VAR_B ${VAR_A}
通過構建鏡像並啟動容器后,查看環境變量如下:
$ docker exec ContainerID env VAR_B=1
從實例可看出,
ARG
定義的變量在Dockerfile中
使用,構建完鏡像后,就下崗;而ENV
定義的變量會帶入容器的環境變量。通常可以把ARG與ENV結合使用:
ARG buildtime_variable=default_value ENV env_var_name=$buildtime_variable
使用這種方式可以解決Dockerfile硬編碼的問題,比如在微服務下很多服務的情況下,構建一個鏡像修改一次Dockerfile,而使用這種方式Dockerfile是不變的,只需要在docker build的時候加上參數值就可以。
-
CMD 指定鏡像啟動時的命令
首先我們看官網對
CMD
的定義:The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.
意思是,
CMD
給出的是一個容器的默認的可執行體。也就是容器啟動以后,默認的執行的命令。重點就是這個默認。意味着,如果docker run
沒有指定任何的執行命令或者Dockerfile
里面也沒有ENTRYPOINT
,那么,就會使用CMD
指定的默認的執行命令執行。同時也從側面說明了ENTRYPOINT
的含義,它才是真正的容器啟動以后要執行命令。所以這句話就給出了
CMD
命令的一個角色定位,它主要作用是默認的容器啟動執行命令。(注意不是“全部”作用)這也是為什么大多數網上博客論壇說的“
CMD
會被覆蓋”,其實為什么會覆蓋?因為CMD
的角色定位就是默認,如果你不額外指定,那么就執行CMD
的命令,否則呢?只要你指定了,那么就不會執行CMD
,也就是CMD
會被覆蓋。比如,
ubuntu
鏡像默認的CMD
是/bin/bash
,如果我們直接docker run -it ubuntu
的話,會直接進入bash
。我們也可以在運行時指定運行別的命令,如docker run -it ubuntu cat /etc/os-release
。這就是用cat /etc/os-release
命令替換了默認的/bin/bash
命令了,輸出了系統版本信息。明白了
CMD
命令的主要用途。下面就看看具體用法:The CMD instruction has three forms: CMD ["executable","param1","param2"] (exec form, this is the preferred form) #exec格式,首選方法 CMD ["param1","param2"] (as default parameters to ENTRYPOINT) #為ENTRYPOINT傳參用法 CMD command param1 param2 (shell form) #shell格式
因為還沒有講
ENTRYPOINT
,所以先不用看第二種用法。在指令格式上,一般推薦使用 exec 格式,這類格式在解析時會被解析為 JSON 數組,因此一定要使用雙引號
"
,而不要使用單引號。如果使用
shell
格式的話,實際的命令會被包裝為sh -c
的參數的形式進行執行。比如:CMD echo $HOME
在實際執行中,會將其變更為:
CMD [ "sh", "-c", "echo $HOME" ]
這就是為什么我們可以使用環境變量的原因,因為這些環境變量會被 shell 進行解析處理。
提到
CMD
就不得不提容器中應用在前台執行和后台執行的問題。這是常出現的一個混淆。Docker 不是虛擬機,容器中的應用都應該以前台執行,而不是像虛擬機、物理機里面那樣,用
systemd
去啟動后台服務,容器內沒有后台服務的概念。如有人會把寫成這樣:
CMD service nginx start
然后發現容器執行后就立即退出了。這就是因為沒有搞明白前台、后台的概念,沒有區分容器和 虛擬機的差異,依舊在以傳統虛擬機的角度去理解容器。
對於容器而言,其啟動程序就是容器應用進程,容器就是為了主進程而存在的,主進程退出,容器就失去了存在的意義,從而退出,其它輔助進程不是它需要關心的東西。
而使用
service nginx start
命令,則是希望以后台守護進程形式啟動nginx
服務。而剛才說了CMD service nginx start
會被理解為CMD [ "sh", "-c", "service nginx start"]
,因此主進程實際上是sh
。那么當service nginx start
命令結束后,sh
也就結束了,sh
作為主進程退出了,自然就會令容器退出。正確的做法是直接執行
nginx
可執行文件,並且要求以前台形式運行:CMD ["nginx", "-g", "daemon off;"]
-
ENTRYPOINT 指定容器入口命令
首先我們看官網對
ENTRYPOINT
的定義:An ENTRYPOINT allows you to configure a container that will run as an executable.
也就是說
ENTRYPOINT
才是正統地用於定義容器啟動以后的執行體的,其實我們從名字也可以理解,這個是容器的“入口”。它有兩種用法:
ENTRYPOINT has two forms: ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) #exec格式,首選方法 ENTRYPOINT command param1 param2 (shell form) #shell格式
先看
exec
命令行模式,也就是帶中括號的。如果docker run
命令后面有東西,那么后面的全部都會作為ENTRYPOINT
的參數。如果docker run
后面沒有額外的東西,但是CMD
有,那么CMD
的全部內容會作為ENTRYPOINT
的參數,這同時是CMD
的第二種用法。這也是網上說的ENTRYPOINT
不會被覆蓋。當然如果要在docker run
里面覆蓋,也是有辦法的,使用--entrypoint
即可。可能光看文字有點迷糊,下面看個例子:
FROM alpine ENTRYPOINT ["echo"] CMD ["CMD"]
docker build -t entrypoint-test:v1 . #會打印出CMD中定義的輸出“CMD” docker run --rm entrypoint-test:v1 $CMD #會打印出docker run中傳入的“docker run”覆蓋CMD中的定義 docker run --rm entrypoint-test:v1 docker run $docker run
第二種是
shell
模式的。在這種模式下,任何docker run
和CMD
的參數都無法被傳入到ENTRYPOINT
里。所以官網推薦第一種用法。FROM alpine ENTRYPOINT echo CMD ["CMD"]
docker build -t entrypoint-test:v2 . #不會打印出CMD中定義的“CMD” docker run --rm entrypoint-test:v2 $ #不會打印出docker run中傳入的“docker run” docker run --rm entrypoint-test:v2 docker run $
最后總結下一般該怎么使用:
一般還是會用ENTRYPOINT的中括號形式作為docker 容器啟動以后的默認執行命令,里面放的是不變的部分,可變部分比如命令參數可以使用CMD的形式提供默認版本,也就是執行docker run里面沒有任何參數時使用的默認參數。如果我們想用默認參數,就直接docker run,如果想用其他參數,就在docker run后面加想要的參數。
ENTRYPOINT ["python3", "manage.py", "runserver"] CMD ["0.0.0.0:8000"]
-
EXPOSE 暴露端口
格式為
EXPOSE <端口1> [<端口2>...]
。EXPOSE
指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時並不會因為這個聲明應用就會開啟這個端口的服務。在Dockerfile
中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;另一個用處則是在運行時使用隨機端口映射時,也就是docker run -P
時,會自動隨機映射EXPOSE
的端口。要將
EXPOSE
和在運行時使用-p <宿主端口>:<容器端口>
區分開來。-p
,是映射宿主端口和容器端口,換句話說,就是將容器的對應端口服務公開給外界訪問,而EXPOSE
僅僅是聲明容器打算使用什么端口而已,並不會自動在宿主進行端口映射。 -
VOLUME 定義匿名卷
VOLUME
指令用於暴露任何數據庫存儲文件,配置文件,或容器創建的文件和目錄。強烈建議使用 VOLUME來管理鏡像中的可變部分和用戶可以改變的部分。兩種使用方法的格式為:
VOLUME ["<路徑1>", "<路徑2>"...] VOLUME <路徑>
之前我們說過,容器運行時應該盡量保持容器存儲層不發生寫操作,對於數據庫類需要保存動態數據的應用,其數據庫文件應該保存於卷中。為了防止運行時用戶忘記將動態文件所保存目錄掛載為卷,在
Dockerfile
中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運行時如果用戶不指定掛載,其應用也可以正常運行,不會向容器存儲層寫入大量數據。VOLUME /data
這里的
/data
目錄就會在運行時自動掛載為匿名卷,任何向/data
中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態化。 -
ONBUILD
ONBUILD
指令可以為鏡像添加觸發器。其參數是任意一個Dockerfile
指令。當我們在一個
Dockerfile
文件中加上ONBUILD
指令,該指令對利用該Dockerfile
構建鏡像(A鏡像)不會產生實質性影響。但是當我們編寫一個新的
Dockerfile
文件來基於A鏡像構建一個鏡像(比如為B鏡像)時,這時構造A鏡像的Dockerfile
文件中的ONBUILD
指令就生效了,在構建B鏡像的過程中,首先會執行ONBUILD
指令指定的指令,然后才會執行其它指令。需要注意的是,如果是再利用B鏡像構造新的鏡像時,那個
ONBUILD
指令就無效了,也就是說只能再構建子鏡像中執行,對孫子鏡像構建無效。其實想想是合理的,因為在構建子鏡像中已經執行了,如果孫子鏡像構建還要執行,相當於重復執行,這就有問題了。利用
ONBUILD
指令,實際上就是相當於創建一個模板鏡像,后續可以根據該模板鏡像創建特定的子鏡像,需要在子鏡像構建過程中執行的一些通用操作就可以在模板鏡像對應的Dockerfile
文件中用ONBUILD
指令指定。 從而減少Dockerfile
文件的重復內容編寫。例如:
先編寫個
onbuild-test:a
鏡像:FROM alpine LABEL maintainer="cerberus43@gmail.com" ONBUILD RUN echo "onbuild" >> test.txt CMD ["cat", "test.txt"]
$docker build -t onbuild-test:a . $docker run --rm onbuild-test:a
再編寫個
onbuild-test:b
鏡像:FROM onbuild-test:a
$docker build -t onbuild-test:b . $docker run --rm onbuild-test:b
Dockerfile最佳實踐:
官方原文:Dockerfile最佳實踐
-
容器應該是短暫的
通過
Dockerfile
構建的鏡像所啟動的容器應該盡可能短暫(生命周期短)。「短暫」意味着可以停止和銷毀容器,並且創建一個新容器並部署好所需的設置和配置工作量應該是極小的。我們可以查看下12 Factor(12要素)應用程序方法的進程部分,可以讓我們理解這種無狀態方式運行容器的動機。 -
理解上下文context
如果注意,會看到
docker build
命令最后有一個.
。.
表示當前目錄,而Dockerfile
就在當前目錄,因此不少人以為這個路徑是在指定Dockerfile
所在路徑,這么理解其實是不准確的。如果對應上面的命令格式,你可能會發現,這是在指定上下文路徑context
。那么什么是上下文呢?首先我們要理解
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 ...
理解構建上下文對於鏡像構建是很重要的。
context
過大會造成docker build
很耗時,鏡像過大則會造成docker pull/push
性能變差以及運行時容器體積過大浪費空間資源。一般來說,應該會將
Dockerfile
置於一個空目錄下,或者項目根目錄下。如果該目錄下沒有所需文件,那么應該把所需文件復制一份過來。如果目錄下有些東西確實不希望構建時傳給Docker
引擎,那么可以用.gitignore
一樣的語法寫一個.dockerignore
,該文件是用於剔除不需要作為上下文傳遞給Docker
引擎的。那么為什么會有人誤以為
.
是指定Dockerfile
所在目錄呢?這是因為在默認情況下,如果不額外指定Dockerfile
的話,會將上下文目錄下的名為Dockerfile
的文件作為Dockerfile
。這只是默認行為,實際上
Dockerfile
的文件名並不要求必須為Dockerfile
,而且並不要求必須位於上下文目錄中,比如可以用-f ../Dockerfile.php
參數指定某個文件作為Dockerfile
。 -
使用
.dockerignore
文件使用
Dockerfile
構建鏡像時最好是將Dockerfile
放置在一個新建的空目錄下。然后將構建鏡像所需要的文件添加到該目錄中。為了提高構建鏡像的效率,你可以在目錄下新建一個.dockerignore
文件來指定要忽略的文件和目錄。.dockerignore
文件的排除模式語法和Git
的.gitignore
文件相似。 -
使用多段構建
多階段構建從
Docker 17.05
及更高版本的守護進程與客戶端的新功能, 對於那些努力優化Dockerfile
同時保持可閱讀性和可維護性的人來說,多階段構建是非常有用的。一個
Dockerfile
用於開發環境,其中包含構建應用程序所需的一切, 另一個精簡版的Dockerfile
,只包含你的應用程序及運行所需的內容,用於生產環境, 這種情況實際上非常普遍,這被稱為”構建器模式”。維護兩個Dockerfile
並不理想。下面是一個
Dockerfile.build
與Dockerfile
的示例,采用上面的構建器模式:Dockerfile.build
FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN go get -d -v golang.org/x/net/html \ && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
Dockerfile
FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY app . CMD ["./app"]
build.sh
#!/bin/sh echo Building alexellis2/href-counter:build docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \ -t alexellis2/href-counter:build . -f Dockerfile.build docker create --name extract alexellis2/href-counter:build docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app docker rm -f extract echo Building alexellis2/href-counter:latest docker build --no-cache -t alexellis2/href-counter:latest . rm ./app
運行
build.sh
時,你需要先構建第一個鏡像,創建一個容器以便將結果復制出來,然后構建第二個鏡像。 兩個鏡像都會占用你的系統空間,並且在你的本地磁盤上依然有應用程序。在多階段構建下,你可以在
Dockerfile
中使用多個FROM
聲明,每個FROM
聲明可以使用不同的基礎鏡像, 並且每個FROM
都使用一個新的構建階段。你可以選擇性的將文件從一個階段復制到另一個階段, 刪除你不想保留在最終鏡像中的一切。我們來調整上面的Dockerfile
以使用多階段構建做個示例。FROM golang:1.7.3 WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
你只需要一個Dockerfile文件即可,也不需要單獨的構建腳本,只需要運行
docker build
。docker build -t alexellis2/href-counter:latest .
最終的結果是與前面一樣的極小的結果,但是復雜性大大降低,你不需要創建任何中間鏡像, 也根本不需要將任何文件提取到本地系統。
它是如何工作的?第二個
FROM
指令使用alpine:latest
鏡像作為基礎開始一個新的構建階段,COPY --from=0
的行將前一個階段的結果復制到新的階段,GO SDK
及所有中間產物被拋棄,並沒有保存在最終鏡像中。默認情況下,構建階段沒有命名,使用它們的整數編號引用它們,從第一個
FORM
以0
開始計數。 但是你可以使用給FORM
指令添加一個as <NAME>
為其構建階段命名。FROM golang:1.7.3 as builder WORKDIR /go/src/github.com/alexellis/href-counter/ RUN go get -d -v golang.org/x/net/html COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=builder /go/src/github.com/alexellis/href-counter/app . CMD ["./app"]
-
避免安裝不需要的包
為了降低復雜性、減少依賴、減小文件大小和構建時間,應該避免安裝額外的或者不必要的軟件包。例如,不要在數據庫鏡像中包含一個文本編輯器。
-
一個容器只做一件事
應該保證在一個容器中只運行一個進程。將多個應用解耦到不同容器中,保證了容器的橫向擴展和復用。例如一個
web
應用程序可能包含三個獨立的容器:web
應用、數據庫、緩存,每個容器都是獨立的鏡像,分開運行。但這並不是說一個容器就只跑一個進程,因為有的程序可能會自行產生其他進程,比如Celery
就可以有很多個工作進程。雖然“每個容器跑一個進程”是一條很好的法則,但這並不是一條硬性的規定。我們主要是希望一個容器只關注意見事情,盡量保持干凈和模塊化。如果容器互相依賴,你可以使用Docker 容器網絡來把這些容器連接起來,我們前面已經跟大家講解過
Docker
的容器網絡模式了。 -
最小化鏡像層數
在
Docker 17.05
甚至更早1.10
之 前,盡量減少鏡像層數是非常重要的,不過現在的版本已經有了一定的改善了:- 在
1.10
以后,只有RUN、COPY和ADD指令會創建層,其他指令會創建臨時的中間鏡像,但是不會直接增加構建的鏡像大小了。 - 到了
17.05
版本以后增加了多階段構建的支持,允許我們把需要的數據直接復制到最終的鏡像中,這就允許我們在中間階段包含一些工具或者調試信息了,而且不會增加最終的鏡像大小。
當然減少
RUN
、COPY
、ADD
的指令仍然是很有必要的,但是我們也需要在Dockerfile
可讀性(也包括長期的可維護性)和減少層數之間做一個平衡。 - 在
-
對多行參數排序
只要有可能,就將多行參數按字母順序排序(比如要安裝多個包時)。這可以幫助你避免重復包含同一個包,更新包列表時也更容易,也更容易閱讀和審查。建議在反斜杠符號
\
之前添加一個空格,可以增加可讀性。 下面是來自buildpack-deps
鏡像的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
構建緩存
在鏡像的構建過程中
docker
會遍歷Dockerfile
文件中的所有指令,順序執行。對於每一條指令,docker
都會在緩存中查找是否已存在可重用的鏡像,否則會創建一個新的鏡像我們可以使用
docker build --no-cache
跳過緩存ADD
和COPY
將會計算文件的checksum
是否改變來決定是否利用緩存RUN
僅僅查看命令字符串是否命中緩存,如RUN apt-get -y update
可能會有問題
如一個
node
應用,可以先拷貝package.json
進行依賴安裝,然后再添加整個目錄,可以做到充分利用緩存的目的。FROM node:10-alpine as builder WORKDIR /code ADD package.json /code # 此步將可以充分利用 node_modules 的緩存 RUN npm install --production ADD . /code RUN npm run build