天天看點

通過 Docker 化一個部落格網站來開啟我們的 Docker 之旅

這篇文章包含 docker 的基本概念,以及如何通過建立一個定制的 dockerfile 來 docker 化dockerize一個應用。

docker 是一個過去兩年來從某個 idea 中孕育而生的有趣技術,公司組織們用它在世界上每個角落來部署應用。在今天的文章中,我将講述如何通過“docker 化dockerize”一個現有的應用,來開始我們的 docker 之旅。這裡提到的應用指的就是這個部落格!

<a target="_blank"></a>

當我們開始學習 docker 基本概念時,讓我們先去搞清楚什麼是 docker 以及它為什麼這麼流行。docker 是一個作業系統容器管理工具,它通過将應用打包在作業系統容器中,來友善我們管理和部署應用。

容器和虛拟機并不完全相似,它是另外一種提供作業系統虛拟化的方式。它和标準的虛拟機還是有所不同。

标準的虛拟機一般會包括一個完整的作業系統、作業系統軟體包、最後還有一至兩個應用。這都得益于為虛拟機提供硬體虛拟化的管理程式。這樣一來,一個單一的伺服器就可以将許多獨立的作業系統作為虛拟客戶機運作了。

容器和虛拟機很相似,它們都支援在單一的伺服器上運作多個操作環境,隻是,在容器中,這些環境并不是一個個完整的作業系統。容器一般隻包含必要的作業系統軟體包和一些應用。它們通常不會包含一個完整的作業系統或者硬體的虛拟化。這也意味着容器比傳統的虛拟機開銷更少。

容器和虛拟機常被誤認為是兩種對立的技術。虛拟機采用一個實體伺服器來提供全功能的操作環境,該環境會和其餘虛拟機一起共享這些實體資源。容器一般用來隔離一個單一主機上運作的應用程序,以保證隔離後的程序之間不能互相影響。事實上,容器和 bsd jails 以及 <code>chroot</code> 程序的相似度,超過了和完整虛拟機的相似度。

現在,我們應該知道 docker 是什麼了,然後,我們将從安裝 docker,并部署一個公開的預建構好的容器開始,學習 docker 是如何工作的。

預設情況下,docker 并不會自動被安裝在您的計算機中,是以,第一步就是安裝 docker 軟體包;我們的教學機器系統是 ubuntu 14.0.4,是以,我們将使用 apt 軟體包管理器,來執行安裝操作。

<code># apt-get install docker.io</code>

<code>reading package lists... done</code>

<code>building dependency tree</code>

<code>reading state information... done</code>

<code>the following extra packages will be installed:</code>

<code>aufs-tools cgroup-lite git git-man liberror-perl</code>

<code>suggested packages:</code>

<code>btrfs-tools debootstrap lxc rinse git-daemon-run git-daemon-sysvinit git-doc</code>

<code>git-el git-email git-gui gitk gitweb git-arch git-bzr git-cvs git-mediawiki</code>

<code>git-svn</code>

<code>the following new packages will be installed:</code>

<code>aufs-tools cgroup-lite docker.io git git-man liberror-perl</code>

<code>0 upgraded, 6 newly installed, 0 to remove and 0 not upgraded.</code>

<code>need to get 7,553 kb of archives.</code>

<code>after this operation, 46.6 mb of additional disk space will be used.</code>

<code>do you want to continue? [y/n] y</code>

為了檢查目前是否有容器運作,我們可以執行<code>docker</code>指令,加上<code>ps</code>選項

<code># docker ps</code>

<code>container id image command created status ports names</code>

<code>docker</code>指令中的<code>ps</code>功能類似于 linux 的<code>ps</code>指令。它将顯示可找到的 docker 容器及其狀态。由于我們并沒有啟動任何 docker 容器,是以指令沒有顯示任何正在運作的容器。

我比較喜歡的 docker 特性之一就是 docker 部署預先建構好的容器的方式,就像<code>yum</code>和<code>apt-get</code>部署包一樣。為了更好地解釋,我們來部署一個運作着 nginx web 伺服器的預建構容器。我們可以繼續使用<code>docker</code>指令,這次選擇<code>run</code>選項。

<code># docker run -d nginx</code>

<code>unable to find image 'nginx' locally</code>

<code>pulling repository nginx</code>

<code>5c82215b03d1: download complete</code>

<code>e2a4fb18da48: download complete</code>

<code>58016a5acc80: download complete</code>

<code>657abfa43d82: download complete</code>

<code>dcb2fe003d16: download complete</code>

<code>c79a417d7c6f: download complete</code>

<code>abb90243122c: download complete</code>

<code>d6137c9e2964: download complete</code>

<code>85e566ddc7ef: download complete</code>

<code>69f100eb42b5: download complete</code>

<code>cd720b803060: download complete</code>

<code>7cc81e9a118a: download complete</code>

<code>docker</code>指令的<code>run</code>選項,用來通知 docker 去尋找一個指定的 docker 鏡像,然後啟動運作着該鏡像的容器。預設情況下,docker 容器運作在前台,這意味着當你運作<code>docker run</code>指令的時候,你的 shell 會被綁定到容器的控制台以及運作在容器中的程序。為了能在背景運作該 docker 容器,我們使用了<code>-d</code> (detach)标志。

再次運作<code>docker ps</code>指令,可以看到 nginx 容器正在運作。

<code>container id image command created status ports names</code>

<code>f6d31ab01fc9 nginx:latest nginx -g 'daemon off 4 seconds ago up 3 seconds 443/tcp, 80/tcp desperate_lalande</code>

從上面的輸出資訊中,我們可以看到正在運作的名為<code>desperate_lalande</code>的容器,它是由<code>nginx:latest image</code>(lctt 譯注: nginx 最新版本的鏡像)建構而來得。

鏡像是 docker 的核心特征之一,類似于虛拟機鏡像。和虛拟機鏡像一樣,docker 鏡像是一個被儲存并打包的容器。當然,docker 不隻是建立鏡像,它還可以通過 docker 倉庫釋出這些鏡像,docker 倉庫和軟體包倉庫的概念差不多,它讓 docker 能夠模仿<code>yum</code>部署軟體包的方式來部署鏡像。為了更好地了解這是怎麼工作的,我們來回顧<code>docker run</code>執行後的輸出。

我們可以看到第一條資訊是,docker 不能在本地找到名叫 nginx 的鏡像。這是因為當我們執行<code>docker run</code>指令時,告訴 docker 運作一個基于 nginx 鏡像的容器。既然 docker 要啟動一個基于特定鏡像的容器,那麼 docker 首先需要找到那個指定鏡像。在檢查遠端倉庫之前,docker 首先檢查本地是否存在指定名稱的本地鏡像。

因為系統是嶄新的,不存在 nginx 鏡像,docker 将選擇從 docker 倉庫下載下傳之。

和 github 一樣,在 docker hub 建立公共倉庫是免費的,私人倉庫就需要繳納費用了。當然,部署你自己的 docker 倉庫也是可以的,事實上隻需要簡單地運作<code>docker run registry</code>指令就行了。但在這篇文章中,我們的重點将不是講解如何部署一個定制的注冊服務。

在我們繼續建構定制容器之前,我們先清理一下 docker 環境,我們将關閉先前的容器,并移除它。

我們利用<code>docker</code>指令和<code>run</code>選項運作一個容器,是以,為了停止同一個容器,我們簡單地在執行<code>docker</code>指令時,使用<code>kill</code>選項,并指定容器名。

<code># docker kill desperate_lalande</code>

<code>desperate_lalande</code>

當我們再次執行<code>docker ps</code>,就不再有容器運作了

但是,此時,我們這是停止了容器;雖然它不再運作,但仍然存在。預設情況下,<code>docker ps</code>隻會顯示正在運作的容器,如果我們附加<code>-a</code> (all) 辨別,它會顯示所有運作和未運作的容器。

<code># docker ps -a</code>

<code>container id image command created status ports names</code>

<code>f6d31ab01fc9 5c82215b03d1 nginx -g 'daemon off 4 weeks ago exited (-1) about a minute ago desperate_lalande</code>

為了能完整地移除容器,我們在用<code>docker</code>指令時,附加<code>rm</code>選項。

<code># docker rm desperate_lalande</code>

雖然容器被移除了;但是我們仍擁有可用的nginx鏡像(lctt 譯注:鏡像緩存)。如果我們重新運作<code>docker run -d nginx</code>,docker 就無需再次拉取 nginx 鏡像即可啟動容器。這是因為我們本地系統中已經儲存了一個副本。

為了列出系統中所有的本地鏡像,我們運作<code>docker</code>指令,附加<code>images</code>選項。

<code># docker images</code>

<code>repository tag image id created virtual size</code>

<code>nginx latest 9fab4090484a 5 days ago 132.8 mb</code>

截至目前,我們已經使用了一些基礎的 docker 指令來啟動、停止和移除一個預建構好的普通鏡像。為了“docker 化(dockerize)”這篇部落格,我們需要建構我們自己的鏡像,也就是建立一個 dockerfile。

在大多數虛拟機環境中,如果你想建立一個機器鏡像,首先,你需要建立一個新的虛拟機、安裝作業系統、安裝應用,最後将其轉換為一個模闆或者鏡像。但在 docker 中,所有這些步驟都可以通過 dockerfile 實作全自動。dockerfile 是向 docker 提供建構指令去建構定制鏡像的方式。在這一章節,我們将編寫能用來部署這個部落格的定制 dockerfile。

我們開始建構 dockerfile 之前,第一步要搞明白,我們需要哪些東西來部署這個部落格。

<code># git clone https://github.com/madflojo/blog.git</code>

<code>cloning into 'blog'...</code>

<code>remote: counting objects: 622, done.</code>

<code>remote: total 622 (delta 0), reused 0 (delta 0), pack-reused 622</code>

<code>receiving objects: 100% (622/622), 14.80 mib | 1.06 mib/s, done.</code>

<code>resolving deltas: 100% (242/242), done.</code>

<code>checking connectivity... done.</code>

<code># cd blog/</code>

<code># vi dockerfile</code>

第一條 dockerfile 指令是<code>from</code>指令。這将指定一個現存的鏡像作為我們的基礎鏡像。這也從根本上給我們提供了繼承其他 docker 鏡像的途徑。在本例中,我們還是從剛剛我們使用的 nginx 開始,如果我們想從頭開始,我們可以通過指定<code>ubuntu:latest</code>來使用 ubuntu docker 鏡像。

<code>## dockerfile that generates an instance of http://bencane.com</code>

<code></code>

<code>from nginx:latest</code>

<code>maintainer benjamin cane &lt;[email protected]&gt;</code>

除了<code>from</code>指令,我還使用了<code>maintainer</code>,它用來顯示 dockerfile 的作者。

docker 支援使用<code>#</code>作為注釋,我将經常使用該文法,來解釋 dockerfile 的部分内容。

想要從 dockerfile 建構鏡像,我們隻需要在運作 <code>docker</code> 指令的時候,加上 <code>build</code> 選項。

<code># docker build -t blog /root/blog</code>

<code>sending build context to docker daemon 23.6 mb</code>

<code>sending build context to docker daemon</code>

<code>step 0 : from nginx:latest</code>

<code>---&gt; 9fab4090484a</code>

<code>step 1 : maintainer benjamin cane &lt;[email protected]&gt;</code>

<code>---&gt; running in c97f36450343</code>

<code>---&gt; 60a44f78d194</code>

<code>removing intermediate container c97f36450343</code>

<code>successfully built 60a44f78d194</code>

上面的例子,我們使用了<code>-t</code> (tag)辨別給鏡像添加“blog”的标簽。實質上我們就是在給鏡像命名,如果我們不指定标簽,就隻能通過 docker 配置設定的 image id 來通路鏡像了。本例中,從 docker 建構成功的資訊可以看出,image id值為 <code>60a44f78d194</code>。

除了<code>-t</code>辨別外,我還指定了目錄<code>/root/blog</code>。該目錄被稱作“建構目錄”,它将包含 dockerfile,以及其它需要建構該容器的檔案。

現在我們建構成功了,下面我們開始定制該鏡像。

用來生成 html 頁面的靜态站點生成器是用 python 語言編寫的,是以,在 dockerfile 中需要做的第一件定制任務是安裝 python。我們将使用 apt 軟體包管理器來安裝 python 軟體包,這意味着在 dockerfile 中我們要指定運作<code>apt-get update</code>和<code>apt-get install python-dev</code>;為了完成這一點,我們可以使用<code>run</code>指令。

<code>## install python and pip</code>

<code>run apt-get update</code>

<code>run apt-get install -y python-dev python-pip</code>

如上所示,我們隻是簡單地告知 docker 建構鏡像的時候,要去執行指定的<code>apt-get</code>指令。比較有趣的是,這些指令隻會在該容器的上下文中執行。這意味着,即使在容器中安裝了<code>python-dev</code>和<code>python-pip</code>,但主機本身并沒有安裝這些。說的更簡單點,<code>pip</code>指令将隻在容器中執行,出了容器,<code>pip</code>指令不存在。

還有一點比較重要的是,docker 建構過程中不接受使用者輸入。這說明任何被<code>run</code>指令執行的指令必須在沒有使用者輸入的時候完成。由于很多應用在安裝的過程中需要使用者的輸入資訊,是以這增加了一點難度。不過我們例子中,<code>run</code>指令執行的指令都不需要使用者輸入。

python 安裝完畢後,我們現在需要安裝 python 子產品。如果在 docker 外做這些事,我們通常使用<code>pip</code>指令,然後參考我的部落格 git 倉庫中名叫<code>requirements.txt</code>的檔案。在之前的步驟中,我們已經使用<code>git</code>指令成功地将 github 倉庫“克隆”到了<code>/root/blog</code>目錄;這個目錄碰巧也是我們建立<code>dockerfile</code>的目錄。這很重要,因為這意味着 docker 在建構過程中可以通路這個 git 倉庫中的内容。

當我們執行建構後,docker 将建構的上下文環境設定為指定的“建構目錄”。這意味着目錄中的所有檔案都可以在建構過程中被使用,目錄之外的檔案(建構環境之外)是不能通路的。

為了能安裝所需的 python 子產品,我們需要将<code>requirements.txt</code>從建構目錄拷貝到容器中。我們可以在<code>dockerfile</code>中使用<code>copy</code>指令完成這一需求。

<code>## create a directory for required files</code>

<code>run mkdir -p /build/</code>

<code>## add requirements file and run pip</code>

<code>copy requirements.txt /build/</code>

<code>run pip install -r /build/requirements.txt</code>

在<code>dockerfile</code>中,我們增加了3條指令。第一條指令使用<code>run</code>在容器中建立了<code>/build/</code>目錄。該目錄用來拷貝生成靜态 html 頁面所需的一切應用檔案。第二條指令是<code>copy</code>指令,它将<code>requirements.txt</code>從“建構目錄”(<code>/root/blog</code>)拷貝到容器中的<code>/build/</code>目錄。第三條使用<code>run</code>指令來執行<code>pip</code>指令;安裝<code>requirements.txt</code>檔案中指定的所有子產品。

當建構定制鏡像時,<code>copy</code>是條重要的指令。如果在 dockerfile 中不指定拷貝檔案,docker 鏡像将不會包含requirements.txt 這個檔案。在 docker 容器中,所有東西都是隔離的,除非在 dockerfile 中指定執行,否則容器中不會包括所需的依賴。

現在,我們讓 docker 執行了一些定制任務,現在我們嘗試另一次 blog 鏡像的建構。

<code>sending build context to docker daemon 19.52 mb</code>

<code>---&gt; using cache</code>

<code>---&gt; 8e0f1899d1eb</code>

<code>step 2 : run apt-get update</code>

<code>---&gt; 78b36ef1a1a2</code>

<code>step 3 : run apt-get install -y python-dev python-pip</code>

<code>---&gt; ef4f9382658a</code>

<code>step 4 : run mkdir -p /build/</code>

<code>---&gt; running in bde05cf1e8fe</code>

<code>---&gt; f4b66e09fa61</code>

<code>removing intermediate container bde05cf1e8fe</code>

<code>step 5 : copy requirements.txt /build/</code>

<code>---&gt; cef11c3fb97c</code>

<code>removing intermediate container 9aa8ff43f4b0</code>

<code>step 6 : run pip install -r /build/requirements.txt</code>

<code>---&gt; running in c50b15ddd8b1</code>

<code>downloading/unpacking jinja2 (from -r /build/requirements.txt (line 1))</code>

<code>downloading/unpacking pyyaml (from -r /build/requirements.txt (line 2))</code>

<code>&lt;truncated to reduce noise&gt;</code>

<code>successfully installed jinja2 pyyaml mistune markdown markupsafe</code>

<code>cleaning up...</code>

<code>---&gt; abab55c20962</code>

<code>removing intermediate container c50b15ddd8b1</code>

<code>successfully built abab55c20962</code>

上述輸出所示,我們可以看到建構成功了,我們還可以看到另外一個有趣的資訊<code>---&gt; using cache</code>。這條資訊告訴我們,docker 在建構該鏡像時使用了它的建構緩存。

當 docker 建構鏡像時,它不僅僅建構一個單獨的鏡像;事實上,在建構過程中,它會建構許多鏡像。從上面的輸出資訊可以看出,在每一“步”執行後,docker 都在建立新的鏡像。

上面片段的最後一行可以看出,docker 在告訴我們它在建立一個新鏡像,因為它列印了image id :<code>cef11c3fb97c</code>。這種方式有用之處在于,docker能在随後建構這個 blog 鏡像時将這些鏡像作為緩存使用。這很有用處,因為這樣, docker 就能加速同一個容器中新建構任務的建構流程。從上面的例子中,我們可以看出,docker 沒有重新安裝<code>python-dev</code>和<code>python-pip</code>包,docker 則使用了緩存鏡像。但是由于 docker 并沒有找到執行<code>mkdir</code>指令的建構緩存,随後的步驟就被一一執行了。

docker 建構緩存一定程度上是福音,但有時也是噩夢。這是因為決定使用緩存或者重新運作指令的因素很少。比如,如果<code>requirements.txt</code>檔案發生了修改,docker 會在建構時檢測到該變化,然後 docker 會重新執行該執行那個點往後的所有指令。這得益于 docker 能檢視<code>requirements.txt</code>的檔案内容。但是,<code>apt-get</code>指令的執行就是另一回事了。如果提供 python 軟體包的 apt 倉庫包含了一個更新的 python-pip 包;docker 不會檢測到這個變化,轉而去使用建構緩存。這會導緻之前舊版本的包将被安裝。雖然對<code>python-pip</code>來說,這不是主要的問題,但對使用了存在某個緻命攻擊缺陷的軟體包緩存來說,這是個大問題。

出于這個原因,抛棄 docker 緩存,定期地重新建構鏡像是有好處的。這時,當我們執行 docker 建構時,我簡單地指定<code>--no-cache=true</code>即可。

python 軟體包和子產品安裝後,接下來我們将拷貝需要用到的應用檔案,然後運作<code>hamerkop</code>應用。我們隻需要使用更多的<code>copy</code> 和 <code>run</code>指令就可完成。

<code>## add blog code nd required files</code>

<code>copy static /build/static</code>

<code>copy templates /build/templates</code>

<code>copy hamerkop /build/</code>

<code>copy config.yml /build/</code>

<code>copy articles /build/articles</code>

<code>## run generator</code>

<code>run /build/hamerkop -c /build/config.yml</code>

現在我們已經寫出了剩餘的建構指令,我們再次運作另一次建構,并確定鏡像建構成功。

<code># docker build -t blog /root/blog/</code>

<code>step 7 : copy static /build/static</code>

<code>---&gt; 15cb91531038</code>

<code>removing intermediate container d478b42b7906</code>

<code>step 8 : copy templates /build/templates</code>

<code>---&gt; ecded5d1a52e</code>

<code>removing intermediate container ac2390607e9f</code>

<code>step 9 : copy hamerkop /build/</code>

<code>---&gt; 59efd1ca1771</code>

<code>removing intermediate container b5fbf7e817b7</code>

<code>step 10 : copy config.yml /build/</code>

<code>---&gt; bfa3db6c05b7</code>

<code>removing intermediate container 1aebef300933</code>

<code>step 11 : copy articles /build/articles</code>

<code>---&gt; 6b61cc9dde27</code>

<code>removing intermediate container be78d0eb1213</code>

<code>step 12 : run /build/hamerkop -c /build/config.yml</code>

<code>---&gt; running in fbc0b5e574c5</code>

<code>successfully created file /usr/share/nginx/html//2011/06/25/checking-the-number-of-lwp-threads-in-linux</code>

<code>successfully created file /usr/share/nginx/html//2011/06/checking-the-number-of-lwp-threads-in-linux</code>

<code>successfully created file /usr/share/nginx/html//archive.html</code>

<code>successfully created file /usr/share/nginx/html//sitemap.xml</code>

<code>---&gt; 3b25263113e1</code>

<code>removing intermediate container fbc0b5e574c5</code>

<code>successfully built 3b25263113e1</code>

成功的一次建構後,我們現在就可以通過運作<code>docker</code>指令和<code>run</code>選項來運作我們定制的容器,和之前我們啟動 nginx 容器一樣。

<code># docker run -d -p 80:80 --name=blog blog</code>

<code>5f6c7a2217dcdc0da8af05225c4d1294e3e6bb28a41ea898a1c63fb821989ba1</code>

我們這次又使用了<code>-d</code> (detach)辨別來讓docker在背景運作。但是,我們也可以看到兩個新辨別。第一個新辨別是<code>--name</code>,這用來給容器指定一個使用者名稱。之前的例子,我們沒有指定名稱,因為 docker 随機幫我們生成了一個。第二個新辨別是<code>-p</code>,這個辨別允許使用者從主機映射一個端口到容器中的一個端口。

之前我們使用的基礎 nginx 鏡像配置設定了80端口給 http 服務。預設情況下,容器内的端口通道并沒有綁定到主機系統。為了讓外部系統能通路容器内部端口,我們必須使用<code>-p</code>辨別将主機端口映射到容器内部端口。上面的指令,我們通過<code>-p 80:80</code>文法将主機80端口映射到容器内部的80端口。

經過上面的指令,我們的容器看起來成功啟動了,我們可以通過執行<code>docker ps</code>核實。

<code>container id image command created status ports names</code>

<code>d264c7ef92bd blog:latest nginx -g 'daemon off 3 seconds ago up 3 seconds 443/tcp, 0.0.0.0:80-&gt;80/tcp blog</code>

原文釋出時間為:2016-06-08

本文來自雲栖社群合作夥伴“linux中國”

繼續閱讀