原文鏈接: http://yangbingdong.com/2017/note-of-dockefile-and-build-hexo-docker-image/
Preface
制作一個(gè)鏡像可以使用
docker commit
和定制Dockerfile,但推薦的是寫Dockerfile。因?yàn)?code>docker commit是一個(gè)暗箱操作,除了制作鏡像的人知道執(zhí)行過什么命令、怎么生成的鏡像,別人根本無從得知,而且會(huì)加入一些沒用的操作導(dǎo)致鏡像臃腫。
此篇記錄構(gòu)建Hexo的鏡像踩坑~
Build Images
首先在當(dāng)前空目錄創(chuàng)建一個(gè)Dockerfile:
FROM ubuntu:latest
ENV BLOG_PATH /root/blog
ENV NODE_VERSION 6
MAINTAINER yangbingdong <yangbingdong1994@gmail.com>
RUN \
apt-get update -y && \
apt-get install -y git curl libpng-dev && \
curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && \
apt-get autoclean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
npm install -g hexo-cli
WORKDIR $BLOG_PATH
VOLUME ["$BLOG_PATH", "/root/.ssh"]
EXPOSE 4000
CMD ['/bin/bash']
然后在當(dāng)前目錄打開終端:
docker build -t <repo-name>/<image-name>:<tag> .
其中<repo-name>
表示倉庫名,與遠(yuǎn)程倉庫(如docker hub)名字要一致,<tag>
表示標(biāo)簽,不給默認(rèn)latest
,都是可選項(xiàng),例如可以寫成這樣:
docker build -t <image-name> .
看到Successfully built
就表示構(gòu)建成功了
注意docker build
命令最后有一個(gè) .
表示構(gòu)建的上下文,鏡像構(gòu)建需要把上下文的東西上傳到Docker引擎去構(gòu)建。
Dockerfile 指令
From 指定基礎(chǔ)鏡像
所謂定制鏡像,那一定是以一個(gè)鏡像為基礎(chǔ),在其上進(jìn)行定制。而 FROM
就是指定基礎(chǔ)鏡像,因此一個(gè) Dockerfile
中 FROM
是必備的指令,并且必須是第一條指令。
在 Docker Hub上有非常多的高質(zhì)量的官方鏡像, 有可以直接拿來使用的服務(wù)類的鏡像,如 nginx
、redis
、mongo
、mysql
、httpd
、php
、tomcat
等; 也有一些方便開發(fā)、構(gòu)建、運(yùn)行各種語言應(yīng)用的鏡像,如 node
、openjdk
、python
、ruby
、golang
等。 可以在其中尋找一個(gè)最符合我們最終目標(biāo)的鏡像為基礎(chǔ)鏡像進(jìn)行定制。 如果沒有找到對(duì)應(yīng)服務(wù)的鏡像,官方鏡像中還提供了一些更為基礎(chǔ)的操作系統(tǒng)鏡像,如 ubuntu
、debian
、centos
、fedora
、alpine
等,這些操作系統(tǒng)的軟件庫為我們提供了更廣闊的擴(kuò)展空間。
除了選擇現(xiàn)有鏡像為基礎(chǔ)鏡像外,Docker 還存在一個(gè)特殊的鏡像,名為 scratch
。這個(gè)鏡像是虛擬的概念,并不實(shí)際存在,它表示一個(gè)空白的鏡像。
RUN 執(zhí)行命令
RUN
指令是用來執(zhí)行命令行命令的。由于命令行的強(qiáng)大能力,RUN
指令在定制鏡像時(shí)是最常用的指令之一。其格式有兩種:
-
shell 格式:
RUN <命令>
,就像直接在命令行中輸入的命令一樣。剛才寫的 Dockrfile 中的RUN
指令就是這種格式。
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
-
exec 格式:
RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
,這更像是函數(shù)調(diào)用中的格式。
注意:
- RUN命令盡量精簡(jiǎn),也就是像上面一樣一個(gè)RUN(使用
$$ \
),如果分開寫很多個(gè)RUN會(huì)導(dǎo)致鏡像鋪了很多層從而臃腫。 - RUN最后記住清理掉沒用的垃圾,很多人初學(xué) Docker 制作出了很臃腫的鏡像的原因之一,就是忘記了每一層構(gòu)建的最后一定要清理掉無關(guān)文件。
COPY 復(fù)制文件
格式:
COPY <源路徑>... <目標(biāo)路徑>
COPY ["<源路徑1>",... "<目標(biāo)路徑>"]
和 RUN
指令一樣,也有兩種格式,一種類似于命令行,一種類似于函數(shù)調(diào)用。
COPY
指令將從構(gòu)建上下文目錄中 <源路徑>
的文件/目錄復(fù)制到新的一層的鏡像內(nèi)的 <目標(biāo)路徑>
位置。比如:
COPY package.json /usr/src/app/
ADD 更高級(jí)的復(fù)制文件
ADD
指令和 COPY
的格式和性質(zhì)基本一致。但是在 COPY
基礎(chǔ)上增加了一些功能。
比如 <源路徑>
可以是一個(gè) URL
,這種情況下,Docker 引擎會(huì)試圖去下載這個(gè)鏈接的文件放到 <目標(biāo)路徑>
去。下載后的文件權(quán)限自動(dòng)設(shè)置為 600
,如果這并不是想要的權(quán)限,那么還需要增加額外的一層 RUN
進(jìn)行權(quán)限調(diào)整,另外,如果下載的是個(gè)壓縮包,需要解壓縮,也一樣還需要額外的一層 RUN
指令進(jìn)行解壓縮。所以不如直接使用 RUN
指令,然后使用 wget
或者 curl
工具下載,處理權(quán)限、解壓縮、然后清理無用文件更合理。因此,這個(gè)功能其實(shí)并不實(shí)用,而且不推薦使用。
如果 <源路徑>
為一個(gè) tar
壓縮文件的話,壓縮格式為 gzip
, bzip2
以及 xz
的情況下,ADD
指令將會(huì)自動(dòng)解壓縮這個(gè)壓縮文件到 <目標(biāo)路徑>
去。
在某些情況下,這個(gè)自動(dòng)解壓縮的功能非常有用,比如官方鏡像 ubuntu
中:
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /
...
但在某些情況下,如果我們真的是希望復(fù)制個(gè)壓縮文件進(jìn)去,而不解壓縮,這時(shí)就不可以使用 ADD
命令了。
在 Docker 官方的最佳實(shí)踐文檔中要求,盡可能的使用 COPY
,因?yàn)?COPY
的語義很明確,就是復(fù)制文件而已,而 ADD
則包含了更復(fù)雜的功能,其行為也不一定很清晰。最適合使用 ADD
的場(chǎng)合,就是所提及的需要自動(dòng)解壓縮的場(chǎng)合。
另外需要注意的是,ADD
指令會(huì)令鏡像構(gòu)建緩存失效,從而可能會(huì)令鏡像構(gòu)建變得比較緩慢。
因此在 COPY
和 ADD
指令中選擇的時(shí)候,可以遵循這樣的原則,所有的文件復(fù)制均使用 COPY
指令,僅在需要自動(dòng)解壓縮的場(chǎng)合使用 ADD
。
CMD 容器啟動(dòng)命令
CMD
指令就是用于指定默認(rèn)的容器主進(jìn)程的啟動(dòng)命令的。
CMD
指令的格式和 RUN
相似,也是兩種格式:
-
shell
格式:CMD <命令>
-
exec
格式:CMD ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"...]
- 參數(shù)列表格式:
CMD ["參數(shù)1", "參數(shù)2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具體的參數(shù)。
在運(yùn)行時(shí)可以指定新的命令來替代鏡像設(shè)置中的這個(gè)默認(rèn)命令,比如,ubuntu
鏡像默認(rèn)的 CMD
是 /bin/bash
,如果我們直接 docker run -it ubuntu
的話,會(huì)直接進(jìn)入 bash
。我們也可以在運(yùn)行時(shí)指定運(yùn)行別的命令,如 docker run -it ubuntu cat /etc/os-release
。這就是用 cat /etc/os-release
命令替換了默認(rèn)的 /bin/bash
命令了,輸出了系統(tǒng)版本信息。
在指令格式上,一般推薦使用 exec
格式,這類格式在解析時(shí)會(huì)被解析為 JSON 數(shù)組,因此一定要使用雙引號(hào) "
,而不要使用單引號(hào)。
如果使用 shell
格式的話,實(shí)際的命令會(huì)被包裝為 sh -c
的參數(shù)的形式進(jìn)行執(zhí)行。比如:
CMD echo $HOME
在實(shí)際執(zhí)行中,會(huì)將其變更為:
CMD [ "sh", "-c", "echo $HOME" ]
所以如果使用shell
格式會(huì)導(dǎo)致容器莫名退出,因?yàn)閷?shí)際上執(zhí)行的事sh
命令,而sh
命令執(zhí)行完時(shí)候容器也就沒有存在的意義。
ENTRYPOINT 入口點(diǎn)
ENTRYPOINT
的格式和 RUN
指令格式一樣,分為 exec
格式和 shell
格式。
ENTRYPOINT
的目的和 CMD
一樣,都是在指定容器啟動(dòng)程序及參數(shù)。ENTRYPOINT
在運(yùn)行時(shí)也可以替代,不過比 CMD
要略顯繁瑣,需要通過 docker run
的參數(shù) --entrypoint
來指定。
當(dāng)指定了 ENTRYPOINT
后,CMD
的含義就發(fā)生了改變,不再是直接的運(yùn)行其命令,而是將 CMD
的內(nèi)容作為參數(shù)傳給 ENTRYPOINT
指令,換句話說實(shí)際執(zhí)行時(shí),將變?yōu)椋?/p>
<ENTRYPOINT> "<CMD>"
這個(gè)指令非常有用,例如可以把命令后面的參數(shù)傳進(jìn)來或啟動(dòng)容器前準(zhǔn)備一些環(huán)境然后執(zhí)行啟動(dòng)命令(通過腳本exec "$@"
)。
ENV 設(shè)置環(huán)境變量
格式有兩種:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
這個(gè)指令很簡(jiǎn)單,就是設(shè)置環(huán)境變量而已,無論是后面的其它指令,如 RUN
,還是運(yùn)行時(shí)的應(yīng)用,都可以直接使用這里定義的環(huán)境變量。
ex:
ENV NODE_VERSION 6
...
RUN curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - && \
...
ARG 構(gòu)建參數(shù)
格式:ARG <參數(shù)名>[=<默認(rèn)值>]
構(gòu)建參數(shù)和 ENV
的效果一樣,都是設(shè)置環(huán)境變量。所不同的是,ARG
所設(shè)置的構(gòu)建環(huán)境的環(huán)境變量,在將來容器運(yùn)行時(shí)是不會(huì)存在這些環(huán)境變量的。但是不要因此就使用 ARG
保存密碼之類的信息,因?yàn)?docker history
還是可以看到所有值的。
Dockerfile
中的 ARG
指令是定義參數(shù)名稱,以及定義其默認(rèn)值。該默認(rèn)值可以在構(gòu)建命令 docker build
中用 --build-arg <參數(shù)名>=<值>
來覆蓋。
在 1.13 之前的版本,要求 --build-arg
中的參數(shù)名,必須在 Dockerfile
中用 ARG
定義過了,換句話說,就是 --build-arg
指定的參數(shù),必須在 Dockerfile
中使用了。如果對(duì)應(yīng)參數(shù)沒有被使用,則會(huì)報(bào)錯(cuò)退出構(gòu)建。從 1.13 開始,這種嚴(yán)格的限制被放開,不再報(bào)錯(cuò)退出,而是顯示警告信息,并繼續(xù)構(gòu)建。這對(duì)于使用 CI 系統(tǒng),用同樣的構(gòu)建流程構(gòu)建不同的 Dockerfile
的時(shí)候比較有幫助,避免構(gòu)建命令必須根據(jù)每個(gè) Dockerfile 的內(nèi)容修改。
VOLUME 定義匿名卷
格式為:
VOLUME ["<路徑1>", "<路徑2>"...]
VOLUME <路徑>
之前我們說過,容器運(yùn)行時(shí)應(yīng)該盡量保持容器存儲(chǔ)層不發(fā)生寫操作,對(duì)于數(shù)據(jù)庫類需要保存動(dòng)態(tài)數(shù)據(jù)的應(yīng)用,其數(shù)據(jù)庫文件應(yīng)該保存于卷(volume)中,后面的章節(jié)我們會(huì)進(jìn)一步介紹 Docker 卷的概念。為了防止運(yùn)行時(shí)用戶忘記將動(dòng)態(tài)文件所保存目錄掛載為卷,在 Dockerfile
中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運(yùn)行時(shí)如果用戶不指定掛載,其應(yīng)用也可以正常運(yùn)行,不會(huì)向容器存儲(chǔ)層寫入大量數(shù)據(jù)。
VOLUME /data
這里的 /data
目錄就會(huì)在運(yùn)行時(shí)自動(dòng)掛載為匿名卷,任何向 /data
中寫入的信息都不會(huì)記錄進(jìn)容器存儲(chǔ)層,從而保證了容器存儲(chǔ)層的無狀態(tài)化。當(dāng)然,運(yùn)行時(shí)可以覆蓋這個(gè)掛載設(shè)置。比如:
docker run -d -v mydata:/data xxxx
在這行命令中,就使用了 mydata
這個(gè)命名卷掛載到了 /data
這個(gè)位置,替代了 Dockerfile
中定義的匿名卷的掛載配置。
EXPOSE 聲明端口
格式為 EXPOSE <端口1> [<端口2>...]
。
EXPOSE
指令是聲明運(yùn)行時(shí)容器提供服務(wù)端口,這只是一個(gè)聲明,在運(yùn)行時(shí)并不會(huì)因?yàn)檫@個(gè)聲明應(yīng)用就會(huì)開啟這個(gè)端口的服務(wù)。在 Dockerfile 中寫入這樣的聲明有兩個(gè)好處,一個(gè)是幫助鏡像使用者理解這個(gè)鏡像服務(wù)的守護(hù)端口,以方便配置映射;另一個(gè)用處則是在運(yùn)行時(shí)使用隨機(jī)端口映射時(shí),也就是 docker run -P
時(shí),會(huì)自動(dòng)隨機(jī)映射 EXPOSE
的端口。
此外,在早期 Docker 版本中還有一個(gè)特殊的用處。以前所有容器都運(yùn)行于默認(rèn)橋接網(wǎng)絡(luò)中,因此所有容器互相之間都可以直接訪問,這樣存在一定的安全性問題。于是有了一個(gè) Docker 引擎參數(shù) --icc=false
,當(dāng)指定該參數(shù)后,容器間將默認(rèn)無法互訪,除非互相間使用了 --links
參數(shù)的容器才可以互通,并且只有鏡像中 EXPOSE
所聲明的端口才可以被訪問。這個(gè) --icc=false
的用法,在引入了 docker network
后已經(jīng)基本不用了,通過自定義網(wǎng)絡(luò)可以很輕松的實(shí)現(xiàn)容器間的互聯(lián)與隔離。
要將 EXPOSE
和在運(yùn)行時(shí)使用 -p <宿主端口>:<容器端口>
區(qū)分開來。-p
,是映射宿主端口和容器端口,換句話說,就是將容器的對(duì)應(yīng)端口服務(wù)公開給外界訪問,而 EXPOSE
僅僅是聲明容器打算使用什么端口而已,并不會(huì)自動(dòng)在宿主進(jìn)行端口映射。
WORKDIR 指定工作目錄
格式為 WORKDIR <工作目錄路徑>
。
使用 WORKDIR
指令可以來指定工作目錄(或者稱為當(dāng)前目錄),以后各層的當(dāng)前目錄就被改為指定的目錄,如該目錄不存在,WORKDIR
會(huì)幫你建立目錄。
之前提到一些初學(xué)者常犯的錯(cuò)誤是把 Dockerfile
等同于 Shell 腳本來書寫,這種錯(cuò)誤的理解還可能會(huì)導(dǎo)致出現(xiàn)下面這樣的錯(cuò)誤:
RUN cd /app
RUN echo "hello" > world.txt
如果將這個(gè) Dockerfile 進(jìn)行構(gòu)建鏡像運(yùn)行后,會(huì)發(fā)現(xiàn)找不到 /app/world.txt
文件,或者其內(nèi)容不是 hello
。原因其實(shí)很簡(jiǎn)單,在 Shell 中,連續(xù)兩行是同一個(gè)進(jìn)程執(zhí)行環(huán)境,因此前一個(gè)命令修改的內(nèi)存狀態(tài),會(huì)直接影響后一個(gè)命令;而在 Dockerfile 中,這兩行 RUN
命令的執(zhí)行環(huán)境根本不同,是兩個(gè)完全不同的容器。這就是對(duì) Dokerfile 構(gòu)建分層存儲(chǔ)的概念不了解所導(dǎo)致的錯(cuò)誤。
之前說過每一個(gè) RUN
都是啟動(dòng)一個(gè)容器、執(zhí)行命令、然后提交存儲(chǔ)層文件變更。第一層 RUN cd /app
的執(zhí)行僅僅是當(dāng)前進(jìn)程的工作目錄變更,一個(gè)內(nèi)存上的變化而已,其結(jié)果不會(huì)造成任何文件變更。而到第二層的時(shí)候,啟動(dòng)的是一個(gè)全新的容器,跟第一層的容器更完全沒關(guān)系,自然不可能繼承前一層構(gòu)建過程中的內(nèi)存變化。
因此如果需要改變以后各層的工作目錄的位置,那么應(yīng)該使用 WORKDIR
指令。
USER 指定當(dāng)前用戶
格式:USER <用戶名>
USER
指令和 WORKDIR
相似,都是改變環(huán)境狀態(tài)并影響以后的層。WORKDIR
是改變工作目錄,USER
則是改變之后層的執(zhí)行 RUN
, CMD
以及 ENTRYPOINT
這類命令的身份。
當(dāng)然,和 WORKDIR
一樣,USER
只是幫助你切換到指定用戶而已,這個(gè)用戶必須是事先建立好的,否則無法切換。
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
踩坑
- Dockerfile里也需要注意權(quán)限問題(nodejs7版本以上不能正常安裝hexo,需要?jiǎng)?chuàng)建用戶并制定權(quán)限去安裝)
- 在docker容器里如果是root用戶對(duì)掛載的文件進(jìn)行了操作,那么實(shí)際上掛載文件的權(quán)限也變成了root的
- 使用attach進(jìn)入容器,退出的時(shí)候容器也跟著退出了。。。囧
- 每一個(gè)RUN是一個(gè)新的shell
-
su -
之前在啟動(dòng)腳本加了-
,導(dǎo)致環(huán)境變量以及工作目錄都變了
Hexo-Docker
最后獻(xiàn)上踩坑寫的Hexo Dockerfile:
# 使用Ubuntu官方鏡像
FROM ubuntu:latest
# 作者信息
MAINTAINER yangbingdong <yangbingdong1994@gmail.com>
# 設(shè)置環(huán)境變量,使用${變量名}取值
ENV \
USER_NAME=hexo \
NODE_VERSION=8.5.0 \
NODE_DIR=/home/${USER_NAME}/nodejs
# 需要執(zhí)行的命令,使用 `$$ \` 分割多行多個(gè)命令
RUN \
# 安裝基本的依賴以及工具
apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y git && \
apt-get install -y curl && \
apt-get install -y libpng-dev && \
# 清理不必要的垃圾
apt-get clean && \
apt-get autoclean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
# 創(chuàng)建hexo用戶去安裝hexo
useradd -m -U ${USER_NAME} && \
# 創(chuàng)建nodejs目錄
mkdir ${NODE_DIR} && \
# 將nodejs下載解壓到對(duì)應(yīng)目錄
curl -L https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz | tar xvzf - -C ${NODE_DIR} --strip-components=1 && \
# 把nodejs文件賦權(quán)給hexo用戶
chown -R ${USER_NAME}.${USER_NAME} ${NODE_DIR} && \
# 把node相關(guān)命令軟連接到/usr/local/bin目錄下以便我們使用
ln -s ${NODE_DIR}/bin/node /usr/local/bin/node && \
ln -s ${NODE_DIR}/bin/npm /usr/local/bin/npm && \
# 以hexo用戶身份安裝hexo-cli
su - ${USER_NAME} -c "npm install -g hexo-cli" && \
# 同樣把hexo命令放到/usr/local/bin下
ln -s ${NODE_DIR}/bin/hexo /usr/local/bin/hexo && \
# 使用淘寶鏡像
npm config set registry https://registry.npm.taobao.org/
# 切換到此目錄
WORKDIR /home/${USER_NAME}/blog
# 可以掛載進(jìn)來的卷(文件夾)
VOLUME ["/home/${USER_NAME}/blog", "/home/${USER_NAME}/.ssh"]
# 暴露端口
EXPOSE 4000
# 把上下文中的docker-entrypoint.sh復(fù)制進(jìn)來
COPY docker-entrypoint.sh /docker-entrypoint.sh
# 執(zhí)行腳本
ENTRYPOINT ["/docker-entrypoint.sh"]
# 這個(gè)...雞肋操作
CMD ['/bin/bash']
docker-entrypoint.sh :
#!/bin/sh
# 發(fā)生異?;貪L
set -e
# 設(shè)置git相關(guān)信息,不設(shè)置默認(rèn)為博主的=.=
GIT_USER_NAME=${GIT_USER_NAME:-yangbingdong}
GIT_USER_MAIL=${GIT_USER_MAIL:-yangbingdong1994@gmail.com}
# 你想要的用戶名
NEW_USER_NAME=${NEW_USER_NAME:-ybd}
# 由于每次啟動(dòng)容器都會(huì)執(zhí)行這個(gè)腳本,但這個(gè)只需要執(zhí)行一次,在此標(biāo)志一下
if [ $(git config --system user.name)x = ${GIT_USER_NAME}x ]
then
su ${NEW_USER_NAME}
else
# 修改用戶名
/usr/sbin/usermod -l ${NEW_USER_NAME} ${USER_NAME}
/usr/sbin/usermod -c ${NEW_USER_NAME} ${NEW_USER_NAME}
/usr/sbin/groupmod -n ${NEW_USER_NAME} ${USER_NAME}
chown -R ${NEW_USER_NAME}.${NEW_USER_NAME} /home/${USER_NAME}/blog
chmod -R 766 /home/${USER_NAME}/blog
# 設(shè)置git全局信息
git config --system user.name $GIT_USER_NAME
git config --system user.email $GIT_USER_MAIL
su ${NEW_USER_NAME}
fi
# 執(zhí)行腳本之后的命令
exec "$@"
源碼:https://github.com/masteranthoneyd/docker-hexo