1. Dockerfile 命令詳解:
-
FROM 指定基礎(chǔ)鏡像(必選)
所謂定制鏡像冀值,那一定是以一個鏡像為基礎(chǔ)箕般,在其上進行定制纯赎。就像我們之前運行了一個 nginx 鏡像的容器巡莹,再進行修改一樣泰讽,基礎(chǔ)鏡像是必須指定的。而
FROM
就是指定基礎(chǔ)鏡像薪贫,因此一個 Dockerfile 中 FROM 是必備的指令恍箭,并且必須是第一條指令。在Docker hub上有非常多的高質(zhì)量的官方鏡像瞧省,有可以直接拿來使用的服務(wù)類的鏡像扯夭,如
nginx
、redis
鞍匾、mongo
交洗、mysql
、httpd
橡淑、php
构拳、tomcat
等;也有一些方便開發(fā)梁棠、構(gòu)建隐圾、運行各種語言應(yīng)用的鏡像,如node
掰茶、openjdk
暇藏、python
、ruby
濒蒋、golang
等盐碱。如果沒有找到對應(yīng)服務(wù)的鏡像,官方鏡像中還提供了一些更為基礎(chǔ)的操作系統(tǒng)鏡像沪伙,如
ubuntu
瓮顽、debian
、centos
围橡、fedora
暖混、alpine
等。FROM
命令語法:FROM <image>:<tag>
如果
tag
沒有選擇翁授,默認(rèn)為latest
拣播。除了選擇現(xiàn)有鏡像為基礎(chǔ)鏡像外,Docker 還存在一個特殊的鏡像收擦,名為
scratch
贮配。這個鏡像是虛擬的概念,并不實際存在塞赂,它表示一個空白的鏡像泪勒。FROM scratch ...
如果你以
scratch
為基礎(chǔ)鏡像的話,意味著你不以任何鏡像為基礎(chǔ),接下來所寫的指令將作為鏡像第一層開始存在圆存。有的同學(xué)可能感覺很奇怪叼旋,沒有任何基礎(chǔ)鏡像,我怎么去執(zhí)行我的程序呢沦辙,其實對于 Linux 下靜態(tài)編譯的程序來說夫植,并不需要有操作系統(tǒng)提供運行時支持,所需的一切庫都已經(jīng)在可執(zhí)行文件里了怕轿,因此直接FROM scratch
會讓鏡像體積更加小巧偷崩。使用 Go 語言 開發(fā)的應(yīng)用很多會使用這種方式來制作鏡像,這也是為什么有人認(rèn)為 Go 是特別適合容器微服務(wù)架構(gòu)的語言的原因之一撞羽。下面我們以一個
go
語言的helloworld
為例:FROM scratch COPY helloworld / COPY hellowold2 / CMD ["./helloworld"]
helloworld
文件就是個go
語言編譯出來的可執(zhí)行程序阐斜,只會打印出hello world
。docker build -t hello-go:v1 . docker run hello-go:v1
-
LABEL 設(shè)置鏡像元數(shù)據(jù)
使用
LABEL
指令诀紊,可以為鏡像設(shè)置元數(shù)據(jù)谒出,例如鏡像創(chuàng)建者或者鏡像說明。舊版的Dockerfile
語法使用MAINTAINER
指令指定鏡像創(chuàng)建者邻奠,但是它已經(jīng)被棄用了笤喳。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
會繼承基礎(chǔ)鏡像種的LABEL
贰镣,如遇到key相同呜象,則值覆蓋。
-
RUN 運行命令
使用
RUN
指令碑隆,可以用來執(zhí)行命令行的命令恭陡。RUN
命令有兩種語法:-
shell
格式:在linux操作系統(tǒng)上默認(rèn) /bin/sh -c
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec
格式:RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
注意:多行命令不要寫多個RUN,原因是Dockerfile中每一個指令都會建立一層上煤,多少個RUN就構(gòu)建了多少層鏡像休玩,會造成鏡像的臃腫、多層劫狠,不僅僅增加了構(gòu)件部署的時間拴疤,還容易出錯。
下面是一個使用
apt-get
安裝多個包的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
-
COPY 復(fù)制文件
COPY
命令有兩種語法格式:COPY [--chown=<user>:<group>] <源路徑>... <目標(biāo)路徑>
COPY [--chown=<user>:<group>] ["<源路徑1>",... "<目標(biāo)路徑>"]
和
RUN
指令一樣嘉熊,也有兩種格式遥赚,一種類似于命令行,一種類似于函數(shù)調(diào)用阐肤。說明:
- 目標(biāo)路徑可以是容器內(nèi)的絕對路徑,也可以是相對于工作目錄的相對路徑(工作目錄可以用
WORKDIR
指令來指定)。 - 目標(biāo)路徑不需要事先創(chuàng)建孕惜,如果目錄不存在會在復(fù)制文件前先行創(chuàng)建缺失目錄愧薛。
- 使用
COPY
指令,源文件的各種元數(shù)據(jù)都會保留衫画。比如讀毫炉、寫、執(zhí)行權(quán)限削罩、文件變更時間等瞄勾。
復(fù)制單個文件示例:
COPY package.json /usr/src/app/
<源路徑>可以是多個,甚至可以是通配符弥激,其通配符規(guī)則要滿足 Go 的 filepath.Match 規(guī)則进陡,如:
COPY hom* /mydir/ COPY hom?.txt /mydir/
復(fù)制src目錄下內(nèi)容到 /tmp 目錄下:
COPY src/ /tmp
復(fù)制多個目錄下內(nèi)容到 /tmp 目錄下:
COPY src1/ src2/ /tmp
上面的命令只會將文件夾內(nèi)容復(fù)制到鏡像目錄下,復(fù)制整個src目錄到/tmp目錄下微服,如果源目錄名不存在將自動逐級創(chuàng)建:
COPY src/ /tmp/src
指定文件權(quán)限
在使用該指令的時候還可以加上 --chown=: 選項來改變文件的所屬用戶及所屬組趾疚。
COPY --chown=devuser:devgroup files* /mydir/
-
ADD 更高級的復(fù)制文件
ADD
命令和COPY
的格式和性質(zhì)基本一致。但是在COPY
基礎(chǔ)上增加了一些功能以蕴。-
解壓壓縮文件并把它們添加到鏡像中:
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 最佳實踐官方文檔中卻強烈建議不要這么用糙麦!官方建議我們當(dāng)需要從遠程復(fù)制文件時,最好使用curl或wget命令來代替ADD命令丛肮。原因是赡磅,當(dāng)使用ADD命令時,會創(chuàng)建更多的鏡像層宝与,當(dāng)然鏡像也會變的更大焚廊。
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
的語義很明確节值,就是復(fù)制文件而已,而ADD
則包含了更復(fù)雜的功能榜聂,其行為也不一定很清晰搞疗。最適合使用ADD
的場合,就是所提及的需要自動解壓縮的場合须肆。因此在 COPY和 ADD指令中選擇的時候匿乃,可以遵循這樣的原則,所有的文件復(fù)制均使用COPY指令豌汇,僅在需要自動解壓縮的場合使用ADD幢炸。
-
-
WORKDIR 指定工作目錄
使用
WORKDIR
指令可以來指定工作目錄(或者稱為當(dāng)前目錄),以后各層的當(dāng)前目錄就被改為指定的目錄拒贱,如該目錄不存在宛徊,WORKDIR
會幫你建立目錄佛嬉。語法格式為:
WORKDIR <工作目錄路徑>
FROM centos:7.2 #創(chuàng)建/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 指定容器的環(huán)境變量
使用
ENV
指令,可以設(shè)置環(huán)境變量闸天,無論是后面的其它指令暖呕,如RUN
,還是運行時的應(yīng)用苞氮,都可以直接使用這里定義的環(huán)境變量湾揽。語法格式有兩種:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
定義了環(huán)境變量,那么在后續(xù)的指令中笼吟,就可以使用這個環(huán)境變量库物。比如在官方
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
在這里先定義了環(huán)境變量
NODE_VERSION
贷帮,其后的RUN
這層里戚揭,多次使用$NODE_VERSION
來進行操作定制∶笊#可以看到毫目,將來升級鏡像構(gòu)建版本的時候,只需要更新7.2.0
即可诲侮,Dockerfile
構(gòu)建維護變得更輕松了镀虐。
-
ARG 指定Dockerfile中的環(huán)境變量
ARG
:ARG
定義的變量用于構(gòu)建Docker
鏡像,在把Dockerfile
構(gòu)建成鏡像后沟绪,ARG
定義的變量便不在起作用刮便;ENV
:ENV
定義的變量用于容器的環(huán)境變量,在Dockerfile
里定義后绽慈,在容器的運行時是可以使用這個變量的恨旱;上面可能讀起來比較繞,看下這個實例就明白了:
ARG VAR_A=1 ENV VAR_B ${VAR_A}
通過構(gòu)建鏡像并啟動容器后坝疼,查看環(huán)境變量如下:
$ docker exec ContainerID env VAR_B=1
從實例可看出搜贤,
ARG
定義的變量在Dockerfile中
使用,構(gòu)建完鏡像后钝凶,就下崗仪芒;而ENV
定義的變量會帶入容器的環(huán)境變量。image通掣荩可以把ARG與ENV結(jié)合使用:
ARG buildtime_variable=default_value ENV env_var_name=$buildtime_variable
使用這種方式可以解決Dockerfile硬編碼的問題掂名,比如在微服務(wù)下很多服務(wù)的情況下,構(gòu)建一個鏡像修改一次Dockerfile哟沫,而使用這種方式Dockerfile是不變的饺蔑,只需要在docker build的時候加上參數(shù)值就可以。
-
CMD 指定鏡像啟動時的命令
首先我們看官網(wǎng)對
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
給出的是一個容器的默認(rèn)的可執(zhí)行體猾警。也就是容器啟動以后孔祸,默認(rèn)的執(zhí)行的命令。重點就是這個默認(rèn)肿嘲。意味著融击,如果docker run
沒有指定任何的執(zhí)行命令或者Dockerfile
里面也沒有ENTRYPOINT
筑公,那么雳窟,就會使用CMD
指定的默認(rèn)的執(zhí)行命令執(zhí)行。同時也從側(cè)面說明了ENTRYPOINT
的含義匣屡,它才是真正的容器啟動以后要執(zhí)行命令封救。所以這句話就給出了
CMD
命令的一個角色定位,它主要作用是默認(rèn)的容器啟動執(zhí)行命令捣作。(注意不是“全部”作用)這也是為什么大多數(shù)網(wǎng)上博客論壇說的“
CMD
會被覆蓋”誉结,其實為什么會覆蓋?因為CMD
的角色定位就是默認(rèn)券躁,如果你不額外指定惩坑,那么就執(zhí)行CMD
的命令,否則呢也拜?只要你指定了以舒,那么就不會執(zhí)行CMD
,也就是CMD
會被覆蓋慢哈。比如蔓钟,
ubuntu
鏡像默認(rèn)的CMD
是/bin/bash
,如果我們直接docker run -it ubuntu
的話卵贱,會直接進入bash
滥沫。我們也可以在運行時指定運行別的命令,如docker run -it ubuntu cat /etc/os-release
键俱。這就是用cat /etc/os-release
命令替換了默認(rèn)的/bin/bash
命令了兰绣,輸出了系統(tǒng)版本信息。明白了
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 數(shù)組杯瞻,因此一定要使用雙引號
"
镐牺,而不要使用單引號。如果使用
shell
格式的話魁莉,實際的命令會被包裝為sh -c
的參數(shù)的形式進行執(zhí)行睬涧。比如:CMD echo $HOME
在實際執(zhí)行中募胃,會將其變更為:
CMD [ "sh", "-c", "echo $HOME" ]
這就是為什么我們可以使用環(huán)境變量的原因,因為這些環(huán)境變量會被 shell 進行解析處理畦浓。
提到
CMD
就不得不提容器中應(yīng)用在前臺執(zhí)行和后臺執(zhí)行的問題痹束。這是常出現(xiàn)的一個混淆。Docker 不是虛擬機讶请,容器中的應(yīng)用都應(yīng)該以前臺執(zhí)行祷嘶,而不是像虛擬機、物理機里面那樣夺溢,用
systemd
去啟動后臺服務(wù)论巍,容器內(nèi)沒有后臺服務(wù)的概念。如有人會把寫成這樣:
CMD service nginx start
然后發(fā)現(xiàn)容器執(zhí)行后就立即退出了风响。這就是因為沒有搞明白前臺嘉汰、后臺的概念,沒有區(qū)分容器和 虛擬機的差異状勤,依舊在以傳統(tǒng)虛擬機的角度去理解容器鞋怀。
對于容器而言,其啟動程序就是容器應(yīng)用進程持搜,容器就是為了主進程而存在的密似,主進程退出,容器就失去了存在的意義朵诫,從而退出辛友,其它輔助進程不是它需要關(guān)心的東西。
而使用
service nginx start
命令剪返,則是希望以后臺守護進程形式啟動nginx
服務(wù)废累。而剛才說了CMD service nginx start
會被理解為CMD [ "sh", "-c", "service nginx start"]
,因此主進程實際上是sh
脱盲。那么當(dāng)service nginx start
命令結(jié)束后邑滨,sh
也就結(jié)束了,sh
作為主進程退出了钱反,自然就會令容器退出掖看。正確的做法是直接執(zhí)行
nginx
可執(zhí)行文件,并且要求以前臺形式運行:CMD ["nginx", "-g", "daemon off;"]
-
ENTRYPOINT 指定容器入口命令
首先我們看官網(wǎng)對
ENTRYPOINT
的定義:An ENTRYPOINT allows you to configure a container that will run as an executable.
也就是說
ENTRYPOINT
才是正統(tǒng)地用于定義容器啟動以后的執(zhí)行體的面哥,其實我們從名字也可以理解哎壳,這個是容器的“入口”。它有兩種用法:
ENTRYPOINT has two forms: ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred) #exec格式尚卫,首選方法 ENTRYPOINT command param1 param2 (shell form) #shell格式
先看
exec
命令行模式归榕,也就是帶中括號的。如果docker run
命令后面有東西,那么后面的全部都會作為ENTRYPOINT
的參數(shù)旁赊。如果docker run
后面沒有額外的東西趟大,但是CMD
有喷众,那么CMD
的全部內(nèi)容會作為ENTRYPOINT
的參數(shù),這同時是CMD
的第二種用法挖函。這也是網(wǎng)上說的ENTRYPOINT
不會被覆蓋巷怜。當(dāng)然如果要在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
的參數(shù)都無法被傳入到ENTRYPOINT
里。所以官網(wǎng)推薦第一種用法兰粉。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 $
最后總結(jié)下一般該怎么使用:
一般還是會用ENTRYPOINT的中括號形式作為docker 容器啟動以后的默認(rèn)執(zhí)行命令,里面放的是不變的部分顶瞳,可變部分比如命令參數(shù)可以使用CMD的形式提供默認(rèn)版本玖姑,也就是執(zhí)行docker run里面沒有任何參數(shù)時使用的默認(rèn)參數(shù)。如果我們想用默認(rèn)參數(shù)慨菱,就直接docker run焰络,如果想用其他參數(shù),就在docker run后面加想要的參數(shù)符喝。
ENTRYPOINT ["python3", "manage.py", "runserver"] CMD ["0.0.0.0:8000"]
-
EXPOSE 暴露端口
格式為
EXPOSE <端口1> [<端口2>...]
闪彼。EXPOSE
指令是聲明運行時容器提供服務(wù)端口,這只是一個聲明协饲,在運行時并不會因為這個聲明應(yīng)用就會開啟這個端口的服務(wù)畏腕。在Dockerfile
中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務(wù)的守護端口茉稠,以方便配置映射描馅;另一個用處則是在運行時使用隨機端口映射時,也就是docker run -P
時而线,會自動隨機映射EXPOSE
的端口铭污。要將
EXPOSE
和在運行時使用-p <宿主端口>:<容器端口>
區(qū)分開來。-p
膀篮,是映射宿主端口和容器端口嘹狞,換句話說,就是將容器的對應(yīng)端口服務(wù)公開給外界訪問誓竿,而EXPOSE
僅僅是聲明容器打算使用什么端口而已磅网,并不會自動在宿主進行端口映射。
-
VOLUME 定義匿名卷
VOLUME
指令用于暴露任何數(shù)據(jù)庫存儲文件烤黍,配置文件知市,或容器創(chuàng)建的文件和目錄傻盟。強烈建議使用 VOLUME來管理鏡像中的可變部分和用戶可以改變的部分。兩種使用方法的格式為:
VOLUME ["<路徑1>", "<路徑2>"...] VOLUME <路徑>
之前我們說過嫂丙,容器運行時應(yīng)該盡量保持容器存儲層不發(fā)生寫操作娘赴,對于數(shù)據(jù)庫類需要保存動態(tài)數(shù)據(jù)的應(yīng)用,其數(shù)據(jù)庫文件應(yīng)該保存于卷中跟啤。為了防止運行時用戶忘記將動態(tài)文件所保存目錄掛載為卷诽表,在
Dockerfile
中,我們可以事先指定某些目錄掛載為匿名卷隅肥,這樣在運行時如果用戶不指定掛載竿奏,其應(yīng)用也可以正常運行,不會向容器存儲層寫入大量數(shù)據(jù)腥放。VOLUME /data
這里的
/data
目錄就會在運行時自動掛載為匿名卷泛啸,任何向/data
中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態(tài)化秃症。
-
ONBUILD
ONBUILD
指令可以為鏡像添加觸發(fā)器候址。其參數(shù)是任意一個Dockerfile
指令。當(dāng)我們在一個
Dockerfile
文件中加上ONBUILD
指令种柑,該指令對利用該Dockerfile
構(gòu)建鏡像(A鏡像)不會產(chǎn)生實質(zhì)性影響岗仑。但是當(dāng)我們編寫一個新的
Dockerfile
文件來基于A鏡像構(gòu)建一個鏡像(比如為B鏡像)時,這時構(gòu)造A鏡像的Dockerfile
文件中的ONBUILD
指令就生效了聚请,在構(gòu)建B鏡像的過程中荠雕,首先會執(zhí)行ONBUILD
指令指定的指令,然后才會執(zhí)行其它指令驶赏。需要注意的是炸卑,如果是再利用B鏡像構(gòu)造新的鏡像時,那個
ONBUILD
指令就無效了母市,也就是說只能再構(gòu)建子鏡像中執(zhí)行矾兜,對孫子鏡像構(gòu)建無效。其實想想是合理的患久,因為在構(gòu)建子鏡像中已經(jīng)執(zhí)行了椅寺,如果孫子鏡像構(gòu)建還要執(zhí)行,相當(dāng)于重復(fù)執(zhí)行蒋失,這就有問題了返帕。利用
ONBUILD
指令,實際上就是相當(dāng)于創(chuàng)建一個模板鏡像,后續(xù)可以根據(jù)該模板鏡像創(chuàng)建特定的子鏡像篙挽,需要在子鏡像構(gòu)建過程中執(zhí)行的一些通用操作就可以在模板鏡像對應(yīng)的Dockerfile
文件中用ONBUILD
指令指定荆萤。 從而減少Dockerfile
文件的重復(fù)內(nèi)容編寫。例如:
先編寫個
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
2. Dockerfile最佳實踐:
官方原文:Dockerfile最佳實踐
-
容器應(yīng)該是短暫的
通過
Dockerfile
構(gòu)建的鏡像所啟動的容器應(yīng)該盡可能短暫(生命周期短)×淳拢「短暫」意味著可以停止和銷毀容器偏竟,并且創(chuàng)建一個新容器并部署好所需的設(shè)置和配置工作量應(yīng)該是極小的。我們可以查看下12 Factor(12要素)應(yīng)用程序方法的進程部分敞峭,可以讓我們理解這種無狀態(tài)方式運行容器的動機踊谋。
-
理解上下文context
如果注意,會看到
docker build
命令最后有一個.
旋讹。.
表示當(dāng)前目錄殖蚕,而Dockerfile
就在當(dāng)前目錄,因此不少人以為這個路徑是在指定Dockerfile
所在路徑沉迹,這么理解其實是不準(zhǔn)確的睦疫。如果對應(yīng)上面的命令格式,你可能會發(fā)現(xiàn)鞭呕,這是在指定上下文路徑context
蛤育。那么什么是上下文呢?首先我們要理解
docker build
的工作原理琅拌。Docker
在運行時分為Docker
引擎(也就是服務(wù)端守護進程)和客戶端工具缨伊。Docker
的引擎提供了一組REST API
,被稱為Docker Remote API
进宝,而如docker
命令這樣的客戶端工具,則是通過這組API
與Docker
引擎交互枷恕,從而完成各種功能党晋。因此,雖然表面上我們好像是在本機執(zhí)行各種docker
功能徐块,但實際上未玻,一切都是使用的遠程調(diào)用形式在服務(wù)端(Docker
引擎)完成。也因為這種C/S
設(shè)計胡控,讓我們操作遠程服務(wù)器的Docker
引擎變得輕而易舉扳剿。當(dāng)我們進行鏡像構(gòu)建的時候,并非所有定制都會通過
RUN
指令完成昼激,經(jīng)常會需要將一些本地文件復(fù)制進鏡像庇绽,比如通過COPY
指令、ADD
指令等橙困。而docker build
命令構(gòu)建鏡像瞧掺,其實并非在本地構(gòu)建,而是在服務(wù)端凡傅,也就是Docker
引擎中構(gòu)建的辟狈。那么在這種客戶端/服務(wù)端的架構(gòu)中,如何才能讓服務(wù)端獲得本地文件呢?這就引入了上下文的概念哼转。當(dāng)構(gòu)建的時候明未,用戶會指定構(gòu)建鏡像上下文的路徑,
docker build
命令得知這個路徑后壹蔓,會將路徑下的所有內(nèi)容打包趟妥,然后上傳給Docker
引擎。這樣Docker
引擎收到這個上下文包后庶溶,展開就會獲得構(gòu)建鏡像所需的一切文件煮纵。如果在Dockerfile
中這么寫:COPY ./package.json /app/
這并不是要復(fù)制執(zhí)行
docker build
命令所在的目錄下的package.json
,也不是復(fù)制Dockerfile
所在目錄下的package.json
偏螺,而是復(fù)制 上下文(context
) 目錄下的package.json
行疏。因此,
COPY
這類指令中的源文件的路徑都是相對路徑套像。這也是初學(xué)者經(jīng)常會問的為什么COPY ../package.json /app
或者COPY /opt/xxxx /app
無法工作的原因酿联,因為這些路徑已經(jīng)超出了上下文的范圍,Docker
引擎無法獲得這些位置的文件夺巩。如果真的需要那些文件贞让,應(yīng)該將它們復(fù)制到上下文目錄中去。現(xiàn)在就可以理解剛才的命令
docker build -t nginx:v3 .
中的這個.
柳譬,實際上是在指定上下文的目錄喳张,docker build
命令會將該目錄下的內(nèi)容打包交給Docker
引擎以幫助構(gòu)建鏡像。如果觀察
docker build
輸出美澳,我們其實已經(jīng)看到了這個發(fā)送上下文的過程:$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB ...
理解構(gòu)建上下文對于鏡像構(gòu)建是很重要的销部。
context
過大會造成docker build
很耗時,鏡像過大則會造成docker pull/push
性能變差以及運行時容器體積過大浪費空間資源制跟。一般來說舅桩,應(yīng)該會將
Dockerfile
置于一個空目錄下,或者項目根目錄下雨膨。如果該目錄下沒有所需文件擂涛,那么應(yīng)該把所需文件復(fù)制一份過來。如果目錄下有些東西確實不希望構(gòu)建時傳給Docker
引擎聊记,那么可以用.gitignore
一樣的語法寫一個.dockerignore
撒妈,該文件是用于剔除不需要作為上下文傳遞給Docker
引擎的。那么為什么會有人誤以為
.
是指定Dockerfile
所在目錄呢甥雕?這是因為在默認(rèn)情況下踩身,如果不額外指定Dockerfile
的話,會將上下文目錄下的名為Dockerfile
的文件作為Dockerfile
社露。這只是默認(rèn)行為挟阻,實際上
Dockerfile
的文件名并不要求必須為Dockerfile
,而且并不要求必須位于上下文目錄中,比如可以用-f ../Dockerfile.php
參數(shù)指定某個文件作為Dockerfile
附鸽。
-
使用
.dockerignore
文件使用
Dockerfile
構(gòu)建鏡像時最好是將Dockerfile
放置在一個新建的空目錄下脱拼。然后將構(gòu)建鏡像所需要的文件添加到該目錄中。為了提高構(gòu)建鏡像的效率坷备,你可以在目錄下新建一個.dockerignore
文件來指定要忽略的文件和目錄熄浓。.dockerignore
文件的排除模式語法和Git
的.gitignore
文件相似。
-
使用多段構(gòu)建
多階段構(gòu)建從
Docker 17.05
及更高版本的守護進程與客戶端的新功能省撑, 對于那些努力優(yōu)化Dockerfile
同時保持可閱讀性和可維護性的人來說赌蔑,多階段構(gòu)建是非常有用的。一個
Dockerfile
用于開發(fā)環(huán)境竟秫,其中包含構(gòu)建應(yīng)用程序所需的一切娃惯, 另一個精簡版的Dockerfile
,只包含你的應(yīng)用程序及運行所需的內(nèi)容肥败,用于生產(chǎn)環(huán)境趾浅, 這種情況實際上非常普遍,這被稱為”構(gòu)建器模式”馒稍。維護兩個Dockerfile
并不理想皿哨。下面是一個
Dockerfile.build
與Dockerfile
的示例,采用上面的構(gòu)建器模式: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
時纽谒,你需要先構(gòu)建第一個鏡像证膨,創(chuàng)建一個容器以便將結(jié)果復(fù)制出來,然后構(gòu)建第二個鏡像鼓黔。 兩個鏡像都會占用你的系統(tǒng)空間椎例,并且在你的本地磁盤上依然有應(yīng)用程序。在多階段構(gòu)建下请祖,你可以在
Dockerfile
中使用多個FROM
聲明,每個FROM
聲明可以使用不同的基礎(chǔ)鏡像脖祈, 并且每個FROM
都使用一個新的構(gòu)建階段肆捕。你可以選擇性的將文件從一個階段復(fù)制到另一個階段, 刪除你不想保留在最終鏡像中的一切盖高。我們來調(diào)整上面的Dockerfile
以使用多階段構(gòu)建做個示例。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文件即可席纽,也不需要單獨的構(gòu)建腳本撞蚕,只需要運行
docker build
润梯。docker build -t alexellis2/href-counter:latest .
最終的結(jié)果是與前面一樣的極小的結(jié)果,但是復(fù)雜性大大降低,你不需要創(chuàng)建任何中間鏡像寇钉, 也根本不需要將任何文件提取到本地系統(tǒng)舶赔。
它是如何工作的扫倡?第二個
FROM
指令使用alpine:latest
鏡像作為基礎(chǔ)開始一個新的構(gòu)建階段,COPY --from=0
的行將前一個階段的結(jié)果復(fù)制到新的階段竟纳,GO SDK
及所有中間產(chǎn)物被拋棄,并沒有保存在最終鏡像中缘挑。默認(rèn)情況下揩悄,構(gòu)建階段沒有命名,使用它們的整數(shù)編號引用它們删性,從第一個
FORM
以0
開始計數(shù)。 但是你可以使用給FORM
指令添加一個as <NAME>
為其構(gòu)建階段命名维贺。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"]
-
避免安裝不需要的包
為了降低復(fù)雜性巴帮、減少依賴榕茧、減小文件大小和構(gòu)建時間,應(yīng)該避免安裝額外的或者不必要的軟件包用押。例如,不要在數(shù)據(jù)庫鏡像中包含一個文本編輯器池充。
-
一個容器只做一件事
應(yīng)該保證在一個容器中只運行一個進程缎讼。將多個應(yīng)用解耦到不同容器中血崭,保證了容器的橫向擴展和復(fù)用卧惜。例如一個
web
應(yīng)用程序可能包含三個獨立的容器:web
應(yīng)用厘灼、數(shù)據(jù)庫、緩存序苏,每個容器都是獨立的鏡像,分開運行围来。但這并不是說一個容器就只跑一個進程匈睁,因為有的程序可能會自行產(chǎn)生其他進程航唆,比如Celery
就可以有很多個工作進程。雖然“每個容器跑一個進程”是一條很好的法則粪狼,但這并不是一條硬性的規(guī)定任岸。我們主要是希望一個容器只關(guān)注意見事情,盡量保持干凈和模塊化困鸥。如果容器互相依賴剑按,你可以使用Docker 容器網(wǎng)絡(luò)來把這些容器連接起來艺蝴,我們前面已經(jīng)跟大家講解過
Docker
的容器網(wǎng)絡(luò)模式了。
-
最小化鏡像層數(shù)
在
Docker 17.05
甚至更早1.10
之 前漆诽,盡量減少鏡像層數(shù)是非常重要的锣枝,不過現(xiàn)在的版本已經(jīng)有了一定的改善了:- 在
1.10
以后兰英,只有RUN畦贸、COPY和ADD指令會創(chuàng)建層楞捂,其他指令會創(chuàng)建臨時的中間鏡像趋厉,但是不會直接增加構(gòu)建的鏡像大小了君账。 - 到了
17.05
版本以后增加了多階段構(gòu)建的支持,允許我們把需要的數(shù)據(jù)直接復(fù)制到最終的鏡像中椭蹄,這就允許我們在中間階段包含一些工具或者調(diào)試信息了净赴,而且不會增加最終的鏡像大小。
當(dāng)然減少
RUN
翼馆、COPY
金度、ADD
的指令仍然是很有必要的审姓,但是我們也需要在Dockerfile
可讀性(也包括長期的可維護性)和減少層數(shù)之間做一個平衡。 - 在
-
對多行參數(shù)排序
只要有可能扎筒,就將多行參數(shù)按字母順序排序(比如要安裝多個包時)酬姆。這可以幫助你避免重復(fù)包含同一個包,更新包列表時也更容易骨宠,也更容易閱讀和審查相满。建議在反斜杠符號
\
之前添加一個空格立美,可以增加可讀性。 下面是來自buildpack-deps
鏡像的例子:RUN apt-get update && apt-get install -y \ bzr \ cvs \ git \ mercurial \ subversion
-
構(gòu)建緩存
在鏡像的構(gòu)建過程中
docker
會遍歷Dockerfile
文件中的所有指令碌更,順序執(zhí)行。對于每一條指令嘿棘,docker
都會在緩存中查找是否已存在可重用的鏡像旭绒,否則會創(chuàng)建一個新的鏡像我們可以使用
docker build --no-cache
跳過緩存-
ADD
和COPY
將會計算文件的checksum
是否改變來決定是否利用緩存 -
RUN
僅僅查看命令字符串是否命中緩存快压,如RUN apt-get -y update
可能會有問題
如一個
node
應(yīng)用,可以先拷貝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
-