接著文章「系統(tǒng)架構(gòu)」如何使用Dockerfile制作Docker容器?(1)我們繼續(xù)介紹ENV、ARG、VOLUME、EXPOSE、WORKDIR、USER、HEALTHCHECK、ONBUILD幾個(gè)命令。
7、ENV
這個(gè)指令很簡(jiǎn)單,就是設(shè)置環(huán)境變量而已,無(wú)論是后面的其它指令,如 RUN,還是運(yùn)行時(shí)的應(yīng)用,都可以直接使用這里定義的環(huán)境變量。
它的格式有兩種:
- ENV <key> <value>
- ENV <key1>=<value1> <key2>=<value2>...
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
ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD等命令都支持環(huán)境變量展開(kāi)。
8、ARG
和ENV命令一樣,ARG命令也是設(shè)置環(huán)境變量。所不同的是,ARG 所設(shè)置的構(gòu)建環(huán)境的環(huán)境變量,在將來(lái)容器運(yùn)行時(shí)是不會(huì)存在這些環(huán)境變量的。但是不要因此就使用 ARG 保存密碼之類的信息,因?yàn)?Docker history 還是可以看到所有值的。
ARG的格式為:ARG <參數(shù)名>[=<默認(rèn)值>]
Dockerfile 中的 ARG 指令是定義參數(shù)名稱,以及定義其默認(rèn)值。該默認(rèn)值可以在構(gòu)建命令 docker build 中用 --build-arg <參數(shù)名>=<值> 來(lái)覆蓋。
在 1.13 之前的版本,要求 --build-arg 中的參數(shù)名,必須在 Dockerfile 中用 ARG 定義過(guò)了,換句話說(shuō),就是 --build-arg 指定的參數(shù),必須在 Dockerfile 中使用了。如果對(duì)應(yīng)參數(shù)沒(méi)有被使用,則會(huì)報(bào)錯(cuò)退出構(gòu)建。從 1.13 開(kāi)始,這種嚴(yán)格的限制被放開(kāi),不再報(bào)錯(cuò)退出,而是顯示警告信息,并繼續(xù)構(gòu)建。這對(duì)于使用 CI 系統(tǒng),用同樣的構(gòu)建流程構(gòu)建不同的 Dockerfile 的時(shí)候比較有幫助,避免構(gòu)建命令必須根據(jù)每個(gè) Dockerfile 的內(nèi)容修改。
9、VOLUME
此命令用來(lái)定義匿名卷。之前我們說(shuō)過(guò),容器運(yùn)行時(shí)應(yīng)該盡量保持容器存儲(chǔ)層不發(fā)生寫(xiě)操作,對(duì)于數(shù)據(jù)庫(kù)類需要保存動(dòng)態(tài)數(shù)據(jù)的應(yīng)用,其數(shù)據(jù)庫(kù)文件應(yīng)該保存于卷(volume)中。
為了防止運(yùn)行時(shí)用戶忘記將動(dòng)態(tài)文件所保存目錄掛載為卷,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷,這樣在運(yùn)行時(shí)如果用戶不指定掛載,其應(yīng)用也可以正常運(yùn)行,不會(huì)向容器存儲(chǔ)層寫(xiě)入大量數(shù)據(jù)。
它的格式為:
- VOLUME ["<路徑1>", "<路徑2>"...]
- VOLUME <路徑>
VOLUME /data
這里的 /data 目錄就會(huì)在運(yùn)行時(shí)自動(dòng)掛載為匿名卷,任何向 /data 中寫(xiě)入的信息都不會(huì)記錄進(jìn)容器存儲(chǔ)層,從而保證了容器存儲(chǔ)層的無(wú)狀態(tài)化。當(dāng)然,運(yùn)行時(shí)可以覆蓋這個(gè)掛載設(shè)置。比如:
docker run -d -v mydata:/data xxxx
在這行命令中,就使用了 mydata 這個(gè)命名卷掛載到了 /data 這個(gè)位置,替代了 Dockerfile 中定義的匿名卷的掛載配置。
10、EXPOSE
EXPOSE 指令是聲明運(yùn)行時(shí)容器提供服務(wù)端口,這只是一個(gè)聲明,在運(yùn)行時(shí)并不會(huì)因?yàn)檫@個(gè)聲明應(yīng)用就會(huì)開(kāi)啟這個(gè)端口的服務(wù)。在 Dockerfile 中寫(xiě)入這樣的聲明有兩個(gè)好處,一個(gè)是幫助鏡像使用者理解這個(gè)鏡像服務(wù)的守護(hù)端口,以方便配置映射;另一個(gè)用處則是在運(yùn)行時(shí)使用隨機(jī)端口映射時(shí),也就是 docker run -P 時(shí),會(huì)自動(dòng)隨機(jī)映射 EXPOSE 的端口。
要將 EXPOSE 和在運(yùn)行時(shí)使用 -p <宿主端口>:<容器端口> 區(qū)分開(kāi)來(lái)。-p是映射宿主端口和容器端口,換句話說(shuō),就是將容器的對(duì)應(yīng)端口服務(wù)公開(kāi)給外界訪問(wèn),而 EXPOSE 僅僅是聲明容器打算使用什么端口而已,并不會(huì)自動(dòng)在宿主進(jìn)行端口映射。
11、WORKDIR
WORKDIR 指令可以來(lái)指定工作目錄(或者稱為當(dāng)前目錄),以后各層的當(dāng)前目錄就被改為指定的目錄,如該目錄不存在,WORKDIR 會(huì)幫你建立目錄。
之前提到一些初學(xué)者常犯的錯(cuò)誤是把 Dockerfile 等同于 Shell 腳本來(lái)書(shū)寫(xiě),這種錯(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ì) Dockerfile 構(gòu)建分層存儲(chǔ)的概念不了解所導(dǎo)致的錯(cuò)誤。
之前說(shuō)過(guò)每一個(gè) RUN 都是啟動(dòng)一個(gè)容器、執(zhí)行命令、然后提交存儲(chǔ)層文件變更。第一層 RUN cd /app 的執(zhí)行僅僅是當(dāng)前進(jìn)程的工作目錄變更,一個(gè)內(nèi)存上的變化而已,其結(jié)果不會(huì)造成任何文件變更。而到第二層的時(shí)候,啟動(dòng)的是一個(gè)全新的容器,跟第一層的容器更完全沒(méi)關(guān)系,自然不可能繼承前一層構(gòu)建過(guò)程中的內(nèi)存變化。
因此,如果需要改變以后各層的工作目錄的位置,那么應(yīng)該使用 WORKDIR 指令。
它的格式為 WORKDIR <工作目錄路徑>。
12、USER
USER 指令和 WORKDIR 相似,都是改變環(huán)境狀態(tài)并影響以后的層。WORKDIR 是改變工作目錄,USER 則是改變之后層的執(zhí)行 RUN、CMD 以及 ENTRYPOINT 這類命令的身份。
當(dāng)然,和 WORKDIR 一樣,USER 只是幫助你切換到指定用戶而已,這個(gè)用戶必須是事先建立好的,否則無(wú)法切換。
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]
如果以 root 執(zhí)行的腳本,在執(zhí)行期間希望改變身份,比如希望以某個(gè)已經(jīng)建立好的用戶來(lái)運(yùn)行某個(gè)服務(wù)進(jìn)程,不要使用 su 或者 sudo,這些都需要比較麻煩的配置,而且在 TTY 缺失的環(huán)境下經(jīng)常出錯(cuò)。建議使用 gosu。
# 建立 redis 用戶,并使用 gosu 換另一個(gè)用戶執(zhí)行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下載 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64"
&& chmod +x /usr/local/bin/gosu
&& gosu nobody true
# 設(shè)置 CMD,并以另外的用戶執(zhí)行
CMD [ "exec", "gosu", "redis", "redis-server" ]
它的使用格式為:
格式:USER <用戶名>[:<用戶組>]
13、HEALTHCHECK
HEALTHCHECK 指令是告訴 Docker 應(yīng)該如何進(jìn)行判斷容器的狀態(tài)是否正常,這是 Docker 1.12 引入的新指令。
在沒(méi)有 HEALTHCHECK 指令前,Docker 引擎只可以通過(guò)容器內(nèi)主進(jìn)程是否退出來(lái)判斷容器是否狀態(tài)異常。很多情況下這沒(méi)問(wèn)題,但是如果程序進(jìn)入死鎖狀態(tài),或者死循環(huán)狀態(tài),應(yīng)用進(jìn)程并不退出,但是該容器已經(jīng)無(wú)法提供服務(wù)了。在 1.12 以前,Docker 不會(huì)檢測(cè)到容器的這種狀態(tài),從而不會(huì)重新調(diào)度,導(dǎo)致可能會(huì)有部分容器已經(jīng)無(wú)法提供服務(wù)了卻還在接受用戶請(qǐng)求。而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通過(guò)該指令指定一行命令,用這行命令來(lái)判斷容器主進(jìn)程的服務(wù)狀態(tài)是否還正常,從而比較真實(shí)的反應(yīng)容器實(shí)際狀態(tài)。
當(dāng)在一個(gè)鏡像指定了 HEALTHCHECK 指令后,用其啟動(dòng)容器,初始狀態(tài)會(huì)為 starting,在 HEALTHCHECK 指令檢查成功后變?yōu)?healthy,如果連續(xù)一定次數(shù)失敗,則會(huì)變?yōu)?unhealthy。
HEALTHCHECK 支持下列選項(xiàng):
- --interval=<間隔>:兩次健康檢查的間隔,默認(rèn)為 30 秒;
- --timeout=<時(shí)長(zhǎng)>:健康檢查命令運(yùn)行超時(shí)時(shí)間,如果超過(guò)這個(gè)時(shí)間,本次健康檢查就被視為失敗,默認(rèn) 30 秒;
- --retries=<次數(shù)>:當(dāng)連續(xù)失敗指定次數(shù)后,則將容器狀態(tài)視為 unhealthy,默認(rèn) 3 次。
和 CMD, ENTRYPOINT 一樣,HEALTHCHECK 只可以出現(xiàn)一次,如果寫(xiě)了多個(gè),只有最后一個(gè)生效。
它的使用格式為:
- HEALTHCHECK [選項(xiàng)] CMD <命令>:設(shè)置檢查容器健康狀況的命令
- HEALTHCHECK NONE:如果基礎(chǔ)鏡像有健康檢查指令,使用這行可以屏蔽掉其健康檢查指令
假設(shè)我們有個(gè)鏡像是個(gè)最簡(jiǎn)單的 Web 服務(wù),我們希望增加健康檢查來(lái)判斷其 Web 服務(wù)是否在正常工作,我們可以用 curl 來(lái)幫助判斷,其 Dockerfile 的 HEALTHCHECK 可以這么寫(xiě):
FROM Nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s
CMD curl -fs http://localhost/ || exit 1
這里我們?cè)O(shè)置了每 5 秒檢查一次(這里為了試驗(yàn)所以間隔非常短,實(shí)際應(yīng)該相對(duì)較長(zhǎng)),如果健康檢查命令超過(guò) 3 秒沒(méi)響應(yīng)就視為失敗,并且使用 curl -fs http://localhost/ || exit 1 作為健康檢查命令。
14、ONBUILD
ONBUILD 是一個(gè)特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而這些指令,在當(dāng)前鏡像構(gòu)建時(shí)并不會(huì)被執(zhí)行。只有當(dāng)以當(dāng)前鏡像為基礎(chǔ)鏡像,去構(gòu)建下一級(jí)鏡像的時(shí)候才會(huì)被執(zhí)行。
Dockerfile 中的其它指令都是為了定制當(dāng)前鏡像而準(zhǔn)備的,唯有 ONBUILD 是為了幫助別人定制自己而準(zhǔn)備的。
好了,命令介紹完了,下面以使用Dockerfile構(gòu)建lnmp容器的過(guò)程,我們?cè)賮?lái)具體介紹下Dockerfile吧。
如下是Dockerfile的構(gòu)建目錄,這里我們先提前下載好了源碼包:
dockerfile/
├── nginx
│ ├── Dockerfile
│ ├── nginx-1.12.1.tar.gz
│ └── nginx.conf
└── php
├── Dockerfile
├── php-5.6.31.tar.gz
└── php.ini
nginx、php里面分別存放Dockerfile文件、源碼包。nginx目錄下還放了nginx.conf配置文件,php目錄下也放置了php.ini配置文件。
有些人可能會(huì)問(wèn)為什么要把nginx.conf、php.ini配置文件放到這里,有兩個(gè)原因,其一,把這兩個(gè)默認(rèn)的配置文件放在這里可以提前修改好所需要的參數(shù),當(dāng)容器啟動(dòng)后,就不需要在進(jìn)入容器去修改了。當(dāng)然,我這里只是練習(xí)環(huán)境,并未對(duì)這兩個(gè)文件做任何更改。其二,在實(shí)際環(huán)境中,這兩個(gè)文件是經(jīng)常需要修改的,單獨(dú)拿出來(lái)后在啟動(dòng)容器時(shí)你可以把這兩個(gè)文件mount到容器中,便于管理。
一、Nginx構(gòu)建
FROM centos:7 // 以 centos:7 鏡像為基礎(chǔ)鏡像
MAINTAINER yuanma // 指定鏡像制作者
ENV TIME_ZOME Asia/Shanghai // 設(shè)置時(shí)區(qū)環(huán)境變量
RUN yum -y install gcc gcc-c++ make openssl-devel pcre-devel // 使用 RUN 命令下載編譯相關(guān)工具,為后面的編譯做準(zhǔn)備
ADD nginx-1.12.1.tar.gz /tmp // ADD 可以自動(dòng)解壓文件,并將解壓后的文件 COPY 到指定目錄
RUN cd /tmp/nginx-1.12.1 &&
./configure --prefix=/usr/local/nginx &&
make &&
make install // 進(jìn)入到 nginx 臨時(shí)安裝目錄,然后執(zhí)行編譯安裝
RUN rm -rf /tmp/nginx* && yum clean all &&
echo "${TIME_ZOME}" > /etc/timezone &&
ln -sf /usr/share/zoneinfo/${TIME_ZOME} /etc/localtime // 刪除編譯安裝期間產(chǎn)生的臨時(shí)文件,同時(shí)記錄時(shí)區(qū)
COPY nginx.conf /usr/local/nginx/conf/ # 轉(zhuǎn)移構(gòu)建目錄中的配置文件到 nginx 配置文件所在目錄,以達(dá)到修改 nginx 配置文件的目的
WORKDIR /usr/local/nginx/ # 指定工作目錄,以后每一層的當(dāng)前目錄都是此目錄
EXPOSE 80 // 說(shuō)明將對(duì)外暴露80端口,但這不代表會(huì)自動(dòng)映射80端口到宿主機(jī)
CMD ["./sbin/nginx","-g","daemon off;"] // 指定容器啟動(dòng)后,在容器中執(zhí)行的命令,這里表示容器啟動(dòng)后,就啟動(dòng)nginx。
構(gòu)建鏡像
docker build -t nginx:demo(鏡像名稱)
2、PHP構(gòu)建
FROM centos:7 // 以 centos:7 鏡像為基礎(chǔ)鏡像
MAINTAINER yuanma // 指定鏡像制作者
ENV TIME_ZOME Asia/Shanghai // 設(shè)置時(shí)區(qū)環(huán)境變量
RUN yum install -y gcc gcc-c++ make gd-devel libxml2-devel libcurl-devel libjpeg-devel libpng-devel openssl-devel # 使用 RUN 命令下載編譯相關(guān)工具,為后面的編譯做準(zhǔn)備
ADD php-5.6.31.tar.gz /tmp/ // ADD 可以自動(dòng)解壓文件,并將解壓后的文件 COPY 到指定目錄
RUN cd /tmp/php-5.6.31 &&
./configure --prefix=/usr/local/php
--with-config-file-path=/usr/local/php/etc
--with-MySQL --with-mysqli
--with-openssl --with-zlib --with-curl --with-gd
--with-jpeg-dir --with-png-dir --with-iconv
--enable-fpm --enable-zip --enable-mbstring &&
make -j 4 &&
make install // 進(jìn)入到 php 臨時(shí)安裝目錄,然后執(zhí)行編譯安裝
RUN cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf &&
sed -i 's/127.0.0.1/0.0.0.0/g' /usr/local/php/etc/php-fpm.conf &&
sed -i "21a daemonize=no" /usr/local/php/etc/php-fpm.conf &&
echo "${TIME_ZOME}" > /etc/timezone &&
ln -sf /usr/share/zoneinfo/${TIME_ZOME} /etc/localtime // 修改 php-fpm 配置文件
COPY php.ini /usr/local/php/etc/ // 轉(zhuǎn)移構(gòu)建目錄中的配置文件到 PHP 配置文件所在目錄,以達(dá)到修改 PHP 配置文件的目的
RUN rm -rf /tmp/php* && yum clean all // 同時(shí)刪除安裝 PHP 時(shí)產(chǎn)生的一些臨時(shí)文件
WORKDIR /usr/local/php/ // 指定工作目錄,以后每一層的當(dāng)前目錄都是此目錄
EXPOSE 9000 // 說(shuō)明將對(duì)外暴露9000端口,但這不代表會(huì)自動(dòng)映射9000端口到宿主機(jī)
CMD ["./sbin/php-fpm","-c","/usr/local/php/etc/php-fpm.conf"] // 指定容器啟動(dòng)后,在容器中執(zhí)行的命令,這里表示容器啟動(dòng)后,就啟動(dòng)php-fpm。
構(gòu)建鏡像
docker build -t php:demo(鏡像名稱)
3、MySQL構(gòu)建
FROM centos:7 # 以 centos:7 鏡像為基礎(chǔ)鏡像
MAINTAINER yuanma # 指定鏡像制作者
ENV TIME_ZOME Asia/Shanghai # 設(shè)置時(shí)區(qū)環(huán)境變量
RUN yum install mysql mysql-server
RUN /etc/init.d/mysqld start &&
mysql -e "grant all privileges on *.* to 'root'@'%' identified by 'abc123';" &&
mysql -e "grant all privileges on *.* to 'root'@'localhost' identified by 'abc123';" //root在本地,非本地登錄時(shí)都使用abc123密碼
EXPOSE 3306
CMD ["mysqld_safe"]
構(gòu)建鏡像
docker build -t mysql:demo(鏡像名稱)
4、運(yùn)行容器
首先創(chuàng)建自定義網(wǎng)絡(luò)lnmp,然后運(yùn)行ningx、php這些容器的時(shí)候加入到lnmp網(wǎng)絡(luò)中來(lái)
# docker network create lnmp
創(chuàng)建php容器
docker run -itd --name lnmp_php --network lnmp --mount type=bind,src=/app/wwwroot,dst=/usr/local/nginx/html php:demo
參數(shù)說(shuō)明:
-itd: 在容器中打開(kāi)一個(gè)偽終端進(jìn)行交互操作,并在后臺(tái)運(yùn)行;
--name: 為容器分配一個(gè)名字lnmp_php;
--network:為容器指定一個(gè)網(wǎng)絡(luò)環(huán)境為lnmp網(wǎng)絡(luò);
--mout: 把宿主機(jī)的/app/wwwroot目錄掛載到容器的/usr/local/nginx/html目錄,掛載也相當(dāng)于數(shù)據(jù)持久化;
php:demo:指定剛才構(gòu)建的php鏡像來(lái)啟動(dòng)容器;
創(chuàng)建nginx容器
# docker run -itd --name lnmp_nginx --network lnmp -p 80:80 --mount type=bind,src=/app/wwwroot,dst=/usr/local/nginx/html nginx:demo
創(chuàng)建mysql容器
# docker volume create mysql-volume
docker run -itd --name lnmp_mysql --network lnmp -p 3306:3306 --mount src=mysql-volume,dst=/var/lib/mysql -e MYSQL_ROOT_PASSword=123456 mysql --character-set-server=utf8
至此,使用 dockerfile 構(gòu)建 Docker 容器就完成了