A. dockerfile是什麼類型文件
Dockerfile是由一系列命令和參數構成的腳本,這些命令應用於基礎鏡像並最終創建一個新的鏡像。它們簡化了從頭到尾的流程並極大的簡化了部署工作。Dockerfile從FROM命令開始,緊接著跟隨者各種方法,命令和參數。其產出為一個新的可以用於創建容器的鏡像。
#
#VERSION2-EDITION1
#Author:docker_user
#Commandformat:Instruction[arguments/command]..
#1、第一行必須指定基礎鏡像信息
FROMubuntu
#2、維護者信息
[email protected]
#3、鏡像操作指令
RUNecho"debhttp://archive.ubuntu.com/ubuntu/raringmainuniverse">>/etc/apt/sources.list
RUNapt-getupdate&&apt-getinstall-ynginx
RUNecho" daemonoff;">>/etc/nginx/nginx.conf
#4、容器啟動執行指令
CMD/usr/sbin/nginx
Dockerfile 分為四部分:基礎鏡像信息、維護者信息、鏡像操作指令、容器啟動執行指令。
一開始必須要指明所基於的鏡像名稱,接下來一般會說明維護者信息。
後面則是鏡像操作指令,例如 RUN 指令。RUN 指令將對鏡像執行跟隨的指令。每執行一條RUN 指令,鏡像添加新的一層,並提交
最後是 CMD 指令,來指明運行容器時的操作命令。
FROM 指令:格式: FROM <images> 或者 FROM<image>:<tag>。第一條指令必須是 FROM 指令。並且,如果在同一個Dockerfile中創建多個鏡像時,可以使用多個 FROM 指令。
MAINTAINER 指令:指定維護者信息。
RUN 指令:格式:RUN <command> 或者 RUN ["executable","param1","param2"]
另外通過直接下載程序鏡像(Nginx)也可以創建一個容器,並將容器運行起來。
(1) 從中央倉庫下載鏡像:docker pull nginx:1.9
(2) docker run 命令啟動容器,docker run -d -p 8080:80 nginx:1.9,把容器內的nginx的80埠,映射到當前伺服器(Centos系統的ip地址)的8080埠,我當前伺服器的ip是192.168.1.10,這樣在瀏覽器輸入192.168.1.10:8080/,發現nginx已啟動。
(3) 再啟動多一個容器,docker run -d -p 8085:80 nginx:1.9,瀏覽器輸入http:/192.168.1.10:8085/,就可以看到另外一個nginx已啟動。
B. 如何配置一個 Docker 化持續集成的 PHP 開發環境
首先,我們得知道什麼才是好的開發環境, 對於我而言,一個好的開發環境需要具備以下幾個特點:
可隨意使用。我必須可以隨意刪除和創建新的環境。
快速啟動。我想要用它工作時候,它立馬就能用。
易於更新。在我們行業中,事物發展變化非常快,必須能讓我很容易將我的開發環境更新到新的軟體版本。
而Docker都支持以上這些特點,甚至更多。你幾乎可以即時銷毀和重建容器,而更新環境只需要重建你當前使用的鏡像即可。
什麼是PHP開發環境
目前Web應用錯綜復雜,PHP開發環境需要很多的東西,為了保證環境的簡單性,需要做各種各樣的限制。
我們這次使用Nginx、PHP5-FPM、Mysql來運行Synmfony項目。由於在容器中運行命令行會更復雜,所以這方面的內容我會放到下一篇博客中再說。
Pet 與 Cattle
另一個我們要討論的重點是:我們要把開發環境部署在多容器還是單容器中。 兩種方式各有優點:
單容器易於分發、維護。因為它們是獨立的,所有的東西都運行在同一個容器中,這點就像是一個虛擬機。但這也意味著,當你要升級其中的某樣東西(比如PHP新版本)的時候, 需要重新構建整個容器。
多容器可以在添加組件時提供更好的模塊化。因為每個容器包含了堆棧的一部分:Web、PHP、MySQL等,這樣可以單獨擴展每個服務或者添加服務,並且不需要重建所有的東西。
因為我比較懶,加上我需要在我的筆記本上放點別的內容,所以,這里我們只介紹單個容器的方法。
初始化工程
首先要做的是初始化一個新的Symfony工程. 推薦的方法是用composer的create-project命令。本來可以在工作站上安裝composer,但是那樣太簡單了。這次我們通過Docker來使用它。
我之前發過一篇關於Docker命令的文章:make docker commands(好吧,我說謊了,我本來把它寫在這篇文章中了,然後覺得把它獨立出來會比較好)。
不管怎麼樣,你可以讀一下。接下來如果還沒有composer命令的話,你可以創建一個屬於自己的composer 別名。
$ alias composer="docker run -i -t -v \$PWD:/srv ubermuda/composer"
現在你可以初始化Symfony工程了:
$ composer create-project symfony/framwork-standard-edition SomeProject
帥呆了!下面來點實在的工作。(省略了博主自娛自樂的一堆balabla....原文:Awesome. Give yourself a high-five, get a cup of coffee or whatever is your liquid drug of choice, and get ready for the real work.)
容器
構建一個運行標准Symfony項目且自給自足的容器相當容易,只需要安裝好常用的Nginx、PHP5-FPM和MySQL-Server即可,然後把預先准備好的Nginx的虛擬主機配置文件扔進去,再復制一些配置文件進去就完事了。
本容器的源代碼在GitHub上的 ubermuda/docker-symfony倉庫中可以找到。 Dockerfile 是Docker構建鏡像要用到的配置文件,我們來看一下:
FROM debian:wheezy
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update -y
RUN apt-get install -y nginx php5-fpm php5-mysqlnd php5-cli mysql-server supervisor
RUN sed -e 's/;daemonize = yes/daemonize = no/' -i /etc/php5/fpm/php-fpm.conf
RUN sed -e 's/;listen\.owner/listen.owner/' -i /etc/php5/fpm/pool.d/www.conf
RUN sed -e 's/;listen\.group/listen.group/' -i /etc/php5/fpm/pool.d/www.conf
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf
ADD vhost.conf /etc/nginx/sites-available/default
ADD supervisor.conf /etc/supervisor/conf.d/supervisor.conf
ADD init.sh /init.sh
EXPOSE 80 3306
VOLUME ["/srv"]
WORKDIR /srv
CMD ["/usr/bin/supervisord"]
我們通過擴展 debian:wheezy 這個基礎鏡像開始,然後通過一系列的sed命令來配置Nginx和PHP5-FPM。
RUN sed -e 's/;daemonize = yes/daemonize = no/' -i /etc/php5/fpm/php-fpm.conf
RUN sed -e 's/;listen\.owner/listen.owner/' -i /etc/php5/fpm/pool.d/www.conf
RUN sed -e 's/;listen\.group/listen.group/' -i /etc/php5/fpm/pool.d/www.conf
RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf
這里我們要做兩件事。 首先配置PHP5-FPM和Nginx讓他們在前台運行以便supervisord可以追蹤到他們。
然後,配置PHP5-FPM以指定的用戶運行Web-Server,並處理好文件許可權。
接下來需要安裝一組配置文件,首先是Nginx的虛擬主機配置文件vhost.conf:
server {
listen 80;
server_name _;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
root /srv/web;
index app_dev.php;
location / {
try_files $uri $uri/ /app_dev.php?$query_string;
}
location ~ [^/]\.php(/|$) {
fastcgi_pass unix:/var/run/php5-fpm.sock;
include fastcgi_params;
}
}
因為我們不需要域名,所以把server_name設成了_(有點像perl的$_佔位符變數), 並配置根目錄(document root)為/svr/web, 我們會把應用程序部署在/srv下,剩下的就是標準的Mginx + PHP5-FPM配置.
因為一個容器每次只能運行一個程序, 我們需要supervisord(或者任何別的進程管理器,不過我比較中意supervisord)。幸運的是, 這個進程管理器會產生我們需要的所有進程!下面是一小段supervisord的配置:
[supervisord]
nodaemon=true
[program:nginx]
command=/usr/sbin/nginx
[program:php5-fpm]
command=/usr/sbin/php5-fpm
[program:mysql]
command=/usr/bin/mysqld_safe
[program:init]
command=/init.sh
autorestart=false
redirect_stderr=true
redirect_stdout=/srv/app/logs/init.log
這里我們需要做的是定義所有的服務, 加上一個特殊的program:init進程,它不是一個實際的服務,而是一個獨創的運行啟動腳本的方式。
這個啟動腳本的問題在於,它通常需要先啟動某些服務。比如,你可能要初始化一些資料庫表,但前提是你得先把MySQL跑起來,一個可能的解決辦法是,在啟動腳本中啟動MySQL,然後初始化表,然後為了防止影響到supervisord的進程管理,需要停掉MySQL,最後再啟動supervisord。
這樣的腳本看起來類似下面這樣:
/etc/init.d/mysql start
app/console doctrine:schema:update --force
/etc/init.d/mysql stop
exec /usr/bin/supervisord
看起來丑爆了有木有,咱換種方式,讓supervisor來運行它並且永不重啟。
實際的init.sh腳本如下:
#!/bin/bash
RET=1
while [[ RET -ne 0 ]]; do
sleep 1;
mysql -e 'exit' > /dev/null 2>&1; RET=$?
done
DB_NAME=${DB_NAME:-symfony}
mysqladmin -u root create $DB_NAME
if [ -n "$INIT" ]; then
/srv/$INIT
fi
腳本先等待MySQL啟動,然後根據環境變數DB_NAME創建DB,默認為symfony, 然後在INIT環境變數中查找要運行的腳本,並嘗試運行它。本文的結尾有說明如何使用這些環境變數。
構建並運行鏡像
萬事俱備只欠東風。我們還要構建Symfony Docker鏡像, 使用docker build命令:
$ cd docker-symfony
$ docker build -t symfony .
現在,可以使用它來運行你的Symfony工程了:
$ cd SomeProject
$ docker run -i -t -P -v $PWD:/srv symfony
我們來看看這一連串的選項分別是幹嘛的:
-i 啟動交互(interactive)模式, 也就是說,STDIO(標准輸入輸出)連接到了你當前的終端上。當你要接收日誌或者給進程發送信號時,它很有用。
-t 為容器創建一個虛擬TTY, 它跟-i是好基友,通常一起使用。
-P 告訴Docker守護進程發布所有指定的埠, 本例中為80埠。
-v $PWD:/srv 把當前目錄掛載到容器的/srv目錄。掛載一個目錄使得目錄內容對目標掛載點可用。
現在你還記得之前提到的DB_NAME和INIT環境變數了吧,幹嘛用的呢:用於自定義你的環境。 基本上你可以通過 docker run的-e選項在容器中設置環境變數,啟動腳本會拿到環境變數,因此,如果你的DB名為some_project_dev, 你就可以這么運行容器:
$ docker run -i -t -P -v $PWD:/srv -e DB_NAME=some_project_dev symfony
INIT 環境變數就更強大了,它允許你啟動時運行指定的腳本。比如, 你有一個bin/setup腳本運行composer install命令並且設置資料庫schema:
#!/bin/bash
composer install
app/console doctrine:schema:update --force
用-e來運行它:
$ docker run -i -t -P \
-v $PWD:/srv \
-e DB_NAME=some_project_dev \
-e INIT=bin/setup
注意,-e選項可以在docer run中多次使用,看起來相當酷。另外,你的啟動腳本需要可執行許可權(chmod +x)。
現在我們通過curl發送請求到容器,來檢查一下是否所有的東西都像預期一樣工作。首先,我們需要取到Docker映射到容器的80埠的公共埠,用docker port命令:
$ docker port $(docker ps -aql 1) 80
0.0.0.0:49153
docker ps -aql 1 是個好用的命令,可以方便的檢索到最後一個容器的id, 在我們的例子中,Docker 把容器的80埠映射到了49153埠。我們 curl 一下看看。
$ curl http://localhost:49153
You are not allowed to access this file. Check app_dev.php for more information.
當我們不從localhost(譯者註:容器的localhost)訪問dev controller時,得到了Symfony的默認錯誤消息,這再正常不過了, 因為我們不是從容器內部發送 curl 請求的, 所以,可以安全的從前端控制器web/app_dev.php中移除這些行。
// This check prevents access to debug front controllers that are deployed by accident to proction servers.
// Feel free to remove this, extend it, or make something more sophisticated.
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1')) || php_sapi_name() === 'cli-server')
) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
這些行阻止了任何從localhost以外的地方訪問dev controller。
現在再curl的時候就可以正常工作了,或者用瀏覽器訪問 http://localhost:49153/:
很容易吧! 現在我們可以快速的啟動、更新環境了,但還是有很多地方需要改進。
C. 啟動mysql的docker鏡像,怎麼自動執行初始化sql腳本
在docker中有一個mysql服務,其數據文件是掛在在主機外面的文件,在docker中的root有訪問該數據文件的許可權,但是docker中mysql訪問數據文件的時候提示許可權不足,於是只有以root用戶來啟動mysql了。
數據初始化:
mysql_install_db --user=root --explicit_defaults_for_timestamp=111
初始化後以root用戶啟動
mysqld --user=root --explicit_defaults_for_timestamp=111
mysql啟動正常。
啟動方式主要有以下三種:
1、使用systemctl 啟動 systemctl start mysqld
2、使用腳本啟動 /etc/inint.d/mysqld start
3、使用safe_mysqld或mysqld --user=mysql啟動
關閉方式也有以下三種:
1、使用systemctl 關閉 systemctl stop mysqld
2、使用腳本關閉 /etc/inint.d/mysqld stop
3、mysqladmin shutdown
注意:使用safe_mysqld或mysqld --user=mysql啟動的服務,只能通過mysqladmin shutdown關閉,不能通過systemctl 或腳本關閉。
mysqladmin shutdown可關閉以上三種服務。腳本可關閉systemctl開啟的服務,同樣systemctl也可關閉腳本開
D. 如何用Dockerfile創建鏡像
Dockerfile的指令是忽略大小寫的,建議使用大寫,使用 # 作為注釋,每一行只支持一條指令,每條指令可以攜帶多個參數。
Dockerfile的指令根據作用可以分為兩種,構建指令和設置指令。構建指令用於構建image,其指定的操作不會在運行image的容器上執行;設置指令用於設置image的屬性,其指定的操作將在運行image的容器中執行。
(1)FROM(指定基礎image)
構建指令,必須指定且需要在Dockerfile其他指令的前面。後續的指令都依賴於該指令指定的image。FROM指令指定的基礎image可以是官方遠程倉庫中的,也可以位於本地倉庫。
該指令有兩種格式:
[plain] view plain
FROM <image>
指定基礎image為該image的最後修改的版本。或者:
[plain] view plain
FROM <image>:<tag>
指定基礎image為該image的一個tag版本。
(2)MAINTAINER(用來指定鏡像創建者信息)
構建指令,用於將image的製作者相關的信息寫入到image中。當我們對該image執行docker inspect命令時,輸出中有相應的欄位記錄該信息。
格式:
[plain] view plain
MAINTAINER <name>
(3)RUN(安裝軟體用)
構建指令,RUN可以運行任何被基礎image支持的命令。如基礎image選擇了ubuntu,那麼軟體管理部分只能使用ubuntu的命令。
該指令有兩種格式:
[plain] view plain
RUN <command> (the command is run in a shell - `/bin/sh -c`)
RUN ["executable", "param1", "param2" ... ] (exec form)
(4)CMD(設置container啟動時執行的操作)
設置指令,用於container啟動時指定的操作。該操作可以是執行自定義腳本,也可以是執行系統命令。該指令只能在文件中存在一次,如果有多個,則只執行最後一條。
該指令有三種格式:
[plain] view plain
CMD ["executable","param1","param2"] (like an exec, this is the preferred form)
CMD command param1 param2 (as a shell)
當Dockerfile指定了ENTRYPOINT,那麼使用下面的格式:
[plain] view plain
CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
ENTRYPOINT指定的是一個可執行的腳本或者程序的路徑,該指定的腳本或者程序將會以param1和param2作為參數執行。所以如果CMD指令使用上面的形式,那麼Dockerfile中必須要有配套的ENTRYPOINT。
(5)ENTRYPOINT(設置container啟動時執行的操作)
設置指令,指定容器啟動時執行的命令,可以多次設置,但是只有最後一個有效。
兩種格式:
[plain] view plain
ENTRYPOINT ["executable", "param1", "param2"] (like an exec, the preferred form)
ENTRYPOINT command param1 param2 (as a shell)
該指令的使用分為兩種情況,一種是獨自使用,另一種和CMD指令配合使用。
當獨自使用時,如果你還使用了CMD命令且CMD是一個完整的可執行的命令,那麼CMD指令和ENTRYPOINT會互相覆蓋只有最後一個CMD或者ENTRYPOINT有效。
[plain] view plain
# CMD指令將不會被執行,只有ENTRYPOINT指令被執行
CMD echo 「Hello, World!」
ENTRYPOINT ls -l
另一種用法和CMD指令配合使用來指定ENTRYPOINT的默認參數,這時CMD指令不是一個完整的可執行命令,僅僅是參數部分;ENTRYPOINT指令只能使用JSON方式指定執行命令,而不能指定參數。
E. 如何寫docker自動創建容器腳本
dockerfile 和docker-compose file
F. 如何調用docker裡面的腳本
在使用weave之前,你需要在所有宿主機上安裝Docker環境,參考這些教程,在Ubuntu或CentOS/Fedora發行版中安裝Docker。Docker環境部署完成後,使用下面的命令安裝weave:$wget/zettio/weave/releases/download/latest_release/weave$chmoda+xweave$sudocpweave/usr/local/bin注意你的PATH環境變數要包含/usr/local/bin這個路徑,請在/etc/profile文件中加入一行(LCTT譯註:要使環境變數生效,你需要執行這個命令:source/etc/profile):exportPATH="$PATH:/usr/local/bin"在每台宿主機上重復上面的操作。Weave在TCP和UDP上都使用6783埠,如果你的系統開啟了防火牆,請確保這兩個埠不會被防火牆擋住。在每台宿主機上啟動Weave路由器當你想要讓處於在不同宿主機上的容器能夠互相通信,第一步要做的就是在每台宿主機上啟動weave路由器。第一台宿主機,運行下面的命令,就會創建並開啟一個weave路由器容器(LCTT譯註:前面說過了,weave路由器也是一個容器):$sudoweavelaunch第一次運行這個命令的時候,它會下載一個weave鏡像,這會花一些時間。下載完成後就會自動運行這個鏡像。成功啟動後,終端會輸出這個weave路由器的ID號。下面的命令用於查看路由器狀態:$sudoweavestatus第一個weave路由器就緒了,目前為止整個peer對等網路中只有一個peer成員。你也可以使用docker的命令來查看weave路由器的狀態:$dockerps第二台宿主機部署步驟稍微有點不同,我們需要為這台宿主機的weave路由器指定第一台宿主機的IP地址,命令如下:$sudoweavelaunch當你查看路由器狀態,你會看到兩個peer成員:當前宿主機和第一個宿主機。當你開啟路由器,這個peer成員列表會更長。當你新開一個路由器時,要指定前一個宿主機的IP地址,請注意不是第一個宿主機的IP地址(LCTT譯註:鏈狀結構)。現在你已經有了一個weave網路了,它由位於不同宿主機的weave路由器組成。把不同宿主機上的容器互聯起來接下來要做的就是在不同宿主機上開啟Docker容器,並使用虛擬網路將它們互聯起來。假設我們創建一個私有網路10.0.0.0/24來互聯Docker容器,並為這些容器隨機分配IP地址。如果你想新建一個能加入weave網路的容器,你就需要使用weave命令來創建,而不是docker命令。原因是weave命令內部會調用docker命令來新建容器然後為它設置網路。下面的命令是在宿主機hostA上建立一個Ubuntu容器,然後將它放到10.0.0.0/24網路中,分配的IP地址為10.0.0.1:hostA:~$sudoweaverun10.0.0.1/24-t-iubuntu成功運行後,終端會顯示出容器的ID號。你可以使用這個ID來訪問這個容器:hostA:~$dockerattach在宿主機hostB上,也創建一個Ubuntu容器,IP地址為10.0.0.2:hostB:~$sudoweaverun10.0.0.2/24-t-iubuntu訪問下這個容器的控制台:hostB:~$dockerattach這兩個容器能夠互相ping通,你可以通過容器的控制台檢查一下。如果你檢查一下每個容器的網路配置,你會發現有一塊名為「ethwe」的網卡,你分配給容器的IP地址出現在它們那裡(比如這里分別是10.0.0.1和10.0.0.2)。Weave的其他高級用法weave提供了一些非常巧妙的特性,我在這里作下簡單的介紹。應用分離使用weave,你可以創建多個虛擬網路,並為每個網路設置不同的應用。比如你可以為一群容器創建10.0.0.0/24網路,為另一群容器創建10.10.0.0/24網路,weave會自動幫你維護這些網路,並將這兩個網路互相隔離。另外,你可以靈活地將一個容器從一個網路移到另一個網路而不需要重啟容器。舉個例子:首先開啟一個容器,運行在10.0.0.0/24網路上:$sudoweaverun10.0.0.2/24-t-iubuntu然後讓它脫離這個網路:$sudoweavedetach10.0.0.2/24最後將它加入到10.10.0.0/24網路中:$sudoweaveattach10.10.0.2/24現在這個容器可以與10.10.0.0/24網路上的其它容器進行通信了。這在當你創建一個容器而網路信息還不確定時就很有幫助了。將weave網路與宿主機網路整合起來有時候你想讓虛擬網路中的容器能訪問物理主機的網路。或者相反,宿主機需要訪問容器。為滿足這個功能,weave允許虛擬網路與宿主機網路整合。舉個例子,在宿主機hostA上一個容器運行在10.0.0.0/24中,運行使用下面的命令:hostA:~$sudoweaveexpose10.0.0.100/24這個命令把IP地址10.0.0.100分配給宿主機hostA,這樣一來宿主機hostA也連到了10.0.0.0/24網路上了。顯然,你在為宿主機選擇IP地址的時候,需要選一個沒有被其他容器使用的地址。現在hostA就可以訪問10.0.0.0/24上的所有容器了,不管這些容器是否位於hostA上。好巧妙的設定啊,32個贊!
G. DockerHub里的鏡像太多了,怎麼選擇
目前系統或基礎語言的鏡像選官方的就可以了,多數情況下以他們為 base image 做自己的鏡像。問題就是這些鏡像大多是國外的源下載依賴會很費勁,最好from 之後換一下源。
應用相關的鏡像,很多其實都不能很好滿足需求,都需要自己改,這方面也沒啥太好的辦法,看自己需求了,一般是看他們的 dockerfile 然後把啟動腳本改一下。
official 的鏡像很多並不好用,可以參考一下 tutum 或者 Sameer 感覺他們做的要實用一些
H. 如何避免Docker容器啟動腳本運行後自動退出
1.安裝Docker在開始前,我們首先得確保在Linux主機中已經安裝了Docker。這里,我運行的是CentOS7主機,我們將運行yum管理器和下面的命令來安裝Docker。#yuminstalldocker#systemctlrestartdocker.service2.創建Dockerfile現在
I. 如何使用Docker構建運行時間較長的腳本
問題
讓我們從這個我試圖解決的問題開始。我開發了一個會運行很長時間的構建腳本,這個腳本中包含了很多的步驟。
這個腳本會運行1-2個小時。
它會從網路下載比較大的文件(超過300M)。
後面的構建步驟依賴前期構建的庫。
但最最煩人的是,運行這個腳本真的需要花很長的時間。
文件系統是固有狀態
我們一般是通過一種有狀態的方式與文件系統進行交互的。我們可以添加、刪除或移動文件。我們可以修改文件的 許可權或者它的訪問時間。大部分獨立的操作都可以撤銷,例如將文件移動到其它地方後,你可以將文件恢復到原來的位置。但我們不會通過快照的方式來將它恢復到 原始狀態。這篇文章我將會介紹如何在耗時較長的腳本中充分利用快照這一特性。
使用聯合文件系統的快照
Docker使用的是聯合文件系統叫做AUFS(譯者註:簡單來說就是支持將不同目錄掛載到同一個虛擬文件系統下的文件系統)。聯合文件系統實現了Union mount。顧名思義,也就是說不同的文件系統的文件和目錄可以分層疊加在單個連貫文件系統之上。這是通過分層的方式完成的。如果一個文件出現在兩個文件系統,那最高層級的文件才會顯示(該文件其它版本也是存在於層級中的,不會改變,只是看不到的)。
在Docker中,每一個在Union mount轉哦給你的文件系統都被稱為layers(層)。使用這種技術可以輕松實現快照,每個快照都是所有層的一個Union mount。
生成腳本的快照
使用快照可以幫助構建一個長時運行的腳本。總的想法是,將一個大的腳本分解為許多小的腳本(我喜歡稱之為 scriptlets),並單獨運行這些小的腳本,腳本運行後為其文件系統打一個快照 (Docker會自動執行此操作)。如果你發現一個scriptlet運行失敗,你可以快速回退到上次的快照,然後再試一次。一旦你完成腳本的構建,並且 可以保證腳本能正常工作,那你就可以將它分配給其它主機。
回過頭來再對比下,如果你沒有使用快照功能了?當你辛辛苦苦等待了一個半小時後,腳本卻構建失敗了,我想除了少部分有耐心的人外,很多人是不想再來一次了,當然,你也會盡最大努力把系統恢復到失敗前的狀態,比如可以刪除一個目錄或運行make clean。
但是,我們可能沒有真正地理解我們正在構建的組件。它可能有復雜的Makefile,它會把把文件放到文件系統中我們不知道的地方,唯一真正確定的途徑是恢復到快照。
使用快照構建腳本的Docker
在本節中,我將介紹我是如何使用Docker實現GHC7.8.3 ARM交叉編譯器的構建腳本。Docker非常適合做這件事,但並非完美。我做了很多看起來沒用的或者不雅的事情,但都是必要的,這都是為了保證將開發腳本的總時間降到最低限度。構建腳本可以在這里找到。
用Dockerfile構建
Docker通過讀取Dockerfile來構建鏡像。Dockerfile會通過一些命令來具體指定應該執行哪些動作。具體使用說明可以參考這篇文章。在我的腳本中主要用到WORKDIR、ADD和RUN。ADD命令非常有用因為它可以讓你在運行之前將外部文件添加到當前Docker鏡像中然後轉換成鏡像的文件系統。你可以在這里看到很多scriptlets構成的構建腳本。
設計
1. 在RUN之前ADD scriptlets
如果你很早就將所有的scriptletsADD在Dockerfile,您可能會遇到以下問題:如果你的腳本構建失敗,你回去修改scriptlet並再次運行docker build。但是你發現,Docker開始在首次加入scriptlets的地方構建!這樣做會浪費了大量的時間並且違背了使用快照的目的。
出現這種情況的原因是由於Docker處理它的中間鏡像(快照)的方式。當Docker通過Dockerfile構建鏡像時,它會與中間鏡像比較當前命令是否一致。然而,在ADD命令的情況下被裝進鏡像的文件里的內容也會被檢查。如果相對於現有的中間鏡像,文件已經改變,那麼Docker也別無選擇,只能從這點開始建立一個新的鏡像。因為Docker不知道這些變化會不會影響到構建。
此外,使用RUN命令要注意,每次運行時它都會導致文件系統有不同的更改。在這種情況下,Docker會發現中間鏡像並使用它,但是這將是錯誤的。RUN命令每次運行時會造成文件系統相同的改變。舉個例子,我確保在我的scriptlets我總是下載了一個已知版本的文件與一個特定MD5校驗。
對Docker 構建緩存更詳細的解釋可以在這里找到。
2.不要使用ENV命令來設置環境變數,請使用scriptlet。
它似乎看起來很有誘惑力:使用ENV命令來設置所有構建腳本需要的環境變數。但是,它不支持變數替換的方式,例如 ENV BASE=$HOME/base 將設置BASE的值為$HOME/base著很可能不是你想要的。
相反,我用ADD命令添加一個名為set-env.sh文件。此文件會包含在後續的scriptlet中:
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source $THIS_DIR/set-env-1.sh
如果你沒有在第一時間獲取set-env.sh會怎麼樣呢?它很早就被加入Dockerfile並不意味著修改它將會使隨後的快照無效?
是的,這會有問題。在開發腳本時,我發現,我已經錯過了在set-env.sh添加一個有用的環境變數。解決方案是創建一個新的文件set-env-1.sh包含:
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source $THIS_DIR/set-env.sh
if ! [ -e "$CONFIG_SUB_SRC/config.sub" ] ; then
CONFIG_SUB_SRC=${CONFIG_SUB_SRC:-$NCURSES_SRC}
fi
然後,在所有後續的scriptlets文件中包含了此文件。現在,我已經完成了構建腳本,我可以回去解決這個問題了,但是,在某種意義上,它會破壞最初的目標。我將不得不從頭開始運行構建腳本看看這種變化是否能成功。
缺點
一個主要缺點是這種方法是,所構建的鏡像尺寸是大於它實際需求的尺寸。在我的情況下尤其如此,因為我在最後刪除了大量文件的。然而,這些文件都仍然存在於聯合掛載文件系統的底層文件系統內,所以整個鏡像是大於它實際需要的大小至少多餘的是刪除文件的大小。
然而,有一個變通。我沒有公布此鏡像到Docker Hub Registry。相反,我:
使用docker export導出內容為tar文件。
創建一個新的Dockerfile簡單地添加了這個tar文件的內容。
產生尺寸盡可能小的鏡像。
結論
這種方法的優點是雙重的:
它使開發時間降至最低,不再做那些已經構建成功的子組件。你可以專注於那些失敗的組件。
這非常便於維護構建腳本。構建可能會失敗,但只要你搞定Dockerfiel,至少你不必再從頭開始。
此外,正如我前面提到的Docker不僅使寫這些構建腳本更加容易,有了合適的工具同樣可以在任何提供快照的文件系統實現。