[CI/CD Note] — 透過 Docker 快速建立及部署環境
Introduction & 前言
Docker 可以取代傳統建置 虛擬機(VM) 需要耗費大量的時間、人力成本;也因為傳統 VM 需要佔用較多空間,最後往往導致環境衝突,或是環境髒亂的後果,就可以透過 Docker 去解決。
在這邊我們要解決的事情很簡單,就是『讓我們要執行專案的環境,在我的電腦可以跑,也要讓它在任何地方都可以跑,而且只需要下幾行簡單指令就可以跑,不需要繁瑣的安裝流程』。
Docker 提供的解法是,以應用程式為核心虛擬化,取代傳統需要 Guest OS 的虛擬化技術
詳細可參考:Docker vs Virtual Machines (VMs) : A Practical Guide to Docker Containers and VMs
Summary & 摘要
這邊將會記錄簡單的起手式及進階一點點的 Docker 玩法。
(沒錯就是一點點)
本篇文章預設學習前的基本條件需求
- 會使用終端機
- 知道 GitHub 是什麼有大概的概念(不知道麼操作沒關係)
這邊可以簡單分為兩個小節:
- 基本認識(適合完全不曉得怎麼操作 Docker 的新手)
- 進階用法(適合知道怎麼從 Dockerfile → Image → Container 的初心者)
基本認識
認識 Docker 之前需要先知道三寶
- 映像檔(Image)
- 容器(Container)
- 倉庫(Registry)、(Repository)
Docker 最基本就是基於 映像檔(Image) 組成的,可以想像成每一個 Image 都是一片光碟,光碟裡面已經有我們設定好的安裝參數(環境),例如我們會把 MySQL 服務包裝成一片 光碟(Image) 、前端需要的 Node 環境包裝成一片 光碟(Image)…
當我們把每一個 Image 寫好之後,透過指令或是 GUI 在把每一片光碟放進 光碟機(Container) 去執行,這時候就很好理解,當同事想要跟我們一樣的服務,就把 光碟(Image) 拿去放盡 光碟機(Container) 安裝環境,當他們不想要這個服務就把 光碟(Image) 從 光碟機(Container) 退出來。
透過這個方法我們可以快速的安裝解除服務,也不會污染全域環境,另外也可以區隔每一個 光碟機(Container) 各自在負責什麼事情。
我們可以把 光碟(Image) 想像成 酒精(Alcohol 120%) 的映像檔,當然我們不必真的把 光碟(Image) 燒出來拿給其他人去安裝; Docker 提供了一項類似 GitHub 的服務,稱為 Docker Hub ,我們可以透過 docker push 或 docker pull 去達成遠端推拉 **光碟(Image)**。
事前準備
開始之前我們必須先安裝 Docker,透過官方文件,選擇符合你作業系統的安裝方式,然後安裝。
與一般的專案事前準備不同,我們不需要東安裝西安裝,只需要把 Docker 安裝起來即可。接著鍵入下列指令到終端機。
1 | $ docker version |
出現上圖代表安裝成功了。
偉大始於一顆小螺絲 - 映像檔
學習任何東西都需要從最基礎開始,而在 Hello World 的世界裡依然如此,對於 Docker 我們需要從 映像檔(Image) 開始。
指令 V.S 圖形化介面
雖然官方有提供 Docker Desktop 的 GUI 介面,可以快速的建立及啟用 Docker,但一些基本指令還是必須認識一下的,畢竟真的要把專案推上 VM 總是要執行 Command Line 的對吧?
要寫出屬於我們自己的 映像檔(Image) 的話,我們需要先寫 Dockerfile 接著去執行它。這支檔案也會詳細敘述我們需要用到的環境,還有安裝等等的指令。
在我們的專案建立一個 Dockerfile,然後鍵入下列內容,這邊我們將用一個前端 React 專案來示範。
這邊有幾個基本的指令
- FROM - 從 Docker Hub 的哪個地方抓取 映像檔(Image) 回來安裝。
- WORKDIR - 將會指定在容器啟動時我們會待在容器的哪個地方。
- ENV - 將會定義再 build 執行時的環境變量。
- RUN - 執行後方指令,像是會在容器上執行 npm install。
- COPY - 複製 ${目前本機目標位置的檔案} 到 ${容器目標位置}。
- EXPOSE - 輸出的 Port。
- CMD - 預設執行 Container 時會執行的指令。
更多指令可參考 Dockerfile Document
關於 CMD 的差異可參考 了解 Docker 如何啟動 process
要讓你的 光碟(Image) 放進什麼資料取決於你,在其他使用這個 光碟(Image) 的人把它放進 光碟機(Container) 後就會安裝你由 Dockerfile 組成的內容。
Image 裡面除了可以寫自己的設定檔之外,也可以在自己的設定檔內,加上別人寫好的設定檔,然後包裝在一起,作為一個新的設定檔。
透過這個方法我可以包裝我想要的環境,例如我可以直接抓到 Node 的環境,接著我的光碟就有 Node 的環境,加上自己的客製化(像上圖的例子),我們可以讓前端專案順利透過 Node 環境安裝;同理我也可以輕鬆的把 MySQL 加上 phpMyAdmin 的環境裝進 Image 裡。
完成 Dockerfile 後,讓我們實際來用指令跑跑看吧。如果你有安裝 Docker Desktop 的話請打開它,然後鍵入下列指令:
1 | $ docker images |
這時候應該會是空的,我們將建置出我們第一個 Image,繼續鍵入下列指令到終端機:
1 | $ docker build -t docker-react:dev -f Dockerfile . --no-cache |
正常我們要建置 Image 的話,起手式都是 docker build,後面可帶的參數可參考 官方指令文件,但上方已經是可以符合普遍情況的簡單指令了。
- -t - 這邊將會給你的 Image 取一個名稱,後面的 react-docker 就是該 Image 的名稱;後面帶了一個冒號(:dev),意思是給你的 Image 加上版號,也可以給他 react-docker:v1.0,如果你有同一個 Dockerfile 但是想要有一點小客製化,建置出不同的 Image 就可以依照版號去建置。
- -f - 這邊是你要啟動的 Dockerfile 名稱,但通常我們都會命名為 Dockerfile,後面的空白在一個點點是路徑,如果你已經在該專案路徑就只要一個黑點即可,如果你的 Dockerfile 在上一層且名稱叫做 myDockerfile,也許你就會輸入 **-f myDockerfile ..**。
- –no-cache - 最後面加上的 no cache 是避免在 Build Docker image 時被 cache 住,造成沒有 build 到修改過的 Dockerfile。
接著在終端機再輸入一次 docker Images,就會在列表看到你的 Image 名稱、版號、建立時間…等等的資訊。
如果你不想要某個 Image 也可以執行指令刪除,將你的 Image 名稱帶入下列指令:
1 | $ docker rmi ${image name} |
沒有彼此無法偉大 - 容器
如果 Image 沒有 Container,它便無法被運作;同理,如果 Container 沒有 Image,他便無用武之地。
容器就是我們的光碟機,如何讓他運行呢?讓我們在終端機鍵入下列指令:
1 | $ docker run --name docker-react-container -p 3005:3000 docker-react:dev |
正常我們要執行 Image 的話,起手式都是 docker run,後面可帶的參數可參考 官方指令文件,但上方已經是可以符合普遍情況的簡單指令了。
- —name - 這邊將會給你的 container 取一個名稱,後面的 docker-react-container 就是該 container 的名稱。
- -p 是你要把當下啟動該 container 環境的哪個 Port 連結到當下啟動該 container 時該環境的 Port,之後在啟動該 container 的環境輸入 http://localhost:3005 就可以連到這個 container 的環境(可能有些模糊,等等上圖好理解)。
- 最後加上的 react-docker:dev 就是該 Image 的名稱加版號,在你建置 Image 時就已經給取好名稱了。
出現上圖就可以打開你的 http://localhost:3005 了,查看一下就會發現頁面出現前端專案啦!
執行成功後你應該會馬上浮現兩個問題,第一個是 背景執行,第二個是 熱更新,就讓我們來學學怎麼透過參數簡單的解決這兩個問題吧。
背景執行
這個問題比較簡單執行,你會發現透過上面指令執行 Image 的 container,如果終端機一關掉,整個 container 就被關閉了,這邊我們對原本啟動的指令動點手腳。
1 | $ docker run --name docker-react-container -d -p 3005:3000 docker-react:dev |
仔細瞧瞧,好像一樣又有點不同,沒錯!就只是加上一個 -d,就可以讓這個 container 在背景執行了。
熱更新
在啟動後我們開始修改我們的程式碼,這時候你可能會想,那我是不是改一行程式碼,我就要重新在 build 一個 Image,然後在 run 它呢?那豈不是累死了。
所以這邊我們會先介紹一個參數利器 volumes,如果透過 GUI 介面你也可以清楚發現這個參數設定。
所以他到底拿來幹嘛的呢?簡單說就是讓你的 container 的其中一個資料夾跟當下啟動該 container 的環境下其中一個資料夾產生連結。
先說說 當下啟動該 container 的環境 是什麼意思(與 當下啟動該 Image 的環境 同理),如果你是在本機跑 docker run
那你 當下啟動該 xxx 的環境 指的就是你本機,如果你把程式碼推上 **虛擬機(Linode、GCP、AWS)**,在該虛擬機裡面跑 docker run
指的 當下啟動該 xxx 的環境 就是該台虛擬機。
正所謂水能載舟亦能覆舟,因為 container 一停止,裡面的環境及資料都沒了,如果我們是建置出 MySQL 加上 phpMyAdmin 的環境,一但資料不見,肯定是初四了阿伯!
所以我們通常如果有需要保存資料,會將 container 裡某一個資料夾與當下啟動環境的某一個資料夾產生連結,就算 container 被停止了,我們一樣可以保存資料。
因為連結一但產生,是會隨時更新的,以在本機啟動 container 為例,如果在 container 裡面使用設定時與本地綁定 Volume 的資料夾新增了檔案 A,會同時本地產生連結的資料夾也新增檔案 A。相反的在本地綁定 Volume 的資料夾新增檔案 B,也會同時在 container 裡面綁定的資料夾產生檔案 B,如下圖。
透過這個特性,我們可以把本地的整個專案資料夾與 container 綁定,使用 docker stop docker-react-container
停止整個 container ,然後使用 docker rm docker-react-container
刪除原本的 container ,然後透過下面的指令在啟動一個新的 container:
1 | $ docker run --name docker-react-container -d -p 3005:3000 -v ${pwd}:/app docker-react:dev |
- -v - 透過這個參數就是把 ${指定當下啟動環境的資料夾路徑} 與指定的 container 的資料夾路徑 產生綁定,你可以先透過終端機輸入
pwd
查看當下環境,這邊我的路徑正處於前端專案資料夾裡,我把該前端專案與 container 裡面的 app(註1) 資料夾產生綁定。
註1:啟動前端專案後,專案位置會位於該 container 的 app 資料夾底下。
透過上面的指令,既可以在背景執行,又可以達到熱更新,這樣也不會污染到我們當下的環境,因為我們不需要這些環境(Node.js),的時候,只要把該 container 刪除掉,就可以了。
如果你有同事想啟動你的專案,但又不想在他們電腦安裝 Node.js 的話,之後把整個專案包含 Dockerfile 傳給同事即可。
現在可以使用下列其中一個指令到終端機:
1 | $ docker ps / docker container ls / docker ps --all / docker container ls --all |
最後嘗試看看修改後,畫面會不會更動吧!
最後如果你不想要某個 Image 或 container 也可以執行指令刪除,將你的 Image/container 名稱帶入下列指令(記得要先停止運行該 container 才可以刪除):
1 | $ docker rmi ${image name} // 刪除 image |
前人種樹後人乘涼
當我們把 Docker 的 Image 建置出來後,我們可以把這個 Image 分享給其他人,不需要透過壓縮檔傳送方式,也不需要透過雲端空間,Docker 很貼心提供了 Dockerhub 的地方,讓我們可以分享及使用他人的 Image,這個空間類似我們常用的 Github。
關於 Docker 的 倉庫(Registry) 可以分為 公開 及 私人,兩種都可以自己架設,如果嫌麻煩,Docker 有提供 Dockerhub,如果是自己公司的專案,不方便公開 Image 的話可以自己架設一個。
自行架設可參考 [Docker]自行架設私有的Docker Registry
這邊我們要把建置好的前端專案 Image 推倒 dockerhub 上。
申請 dockerhub 帳號密碼
首先需要上 dockerhub 官網 申請帳號密碼。
登入
接著我們需要登入我們申請好的帳號密碼,鍵入下列指令到終端機:
1 | $ docker login |
接著按照指示輸入帳號密碼即可,如果是透過 GUI,也可以點擊 Icon 圖示登入,這邊使用 Mac 示範:
新增 Repository
和 Github 一樣,想上傳你的 程式碼(Image),就必須要先建立一個 Repository,到 dockerhub 官網 登入帳號密碼後,應該會看到目前是空的,點擊藍色按鈕(Create Repository)新增 Repository 。
接著輸入相關資訊,請先設定為公開的 Repository。
建立完成後會看到目前 Repository 裡面都是空的,代表建立成功了。
旁邊依然很貼心提醒您相關指令
上版號 & 上傳 Image
仔細看旁邊會發現 docker 很貼心的提醒你必須要上 tag 版號。
接著在終端機上鍵入 docker images
查看我們前端專案的 Image 的 ID。
再鍵入下列指令:
1 | $ docker tag ${IMAGE ID} ${docker 帳號}/${docker repository 名稱} |
再輸入 docker images
你會發現多了一筆資料,因為我們沒有給版號,所以會出現 TAG 是 latest 的樣子,代表最新版本。
如果想加入版號可以稍微修改上面的指令(記得 ${} 要拿掉,裡面換上你該填入的資訊):
1 | $ docker tag ${IMAGE ID} ${docker 帳號}/${docker repository 名稱}:${版號} |
最後一步就是推 Image 上去了,跟 Github 一樣,使用 push 推上 dockerhub:
1 | $ docker push ${docker 帳號}/${docker repository 名稱}:${版號} |
讓我們再回去 dockerhub 瞧瞧,會發現現在已經把 Image 推上去囉!
因為 dockerhub 只是單純拿來存放 Image 的地方,不像 Github 會把整個專案上傳,所以要怎麼啟動,或是專案怎麼擺放,通常大家都會另外再把相關程式碼或是專案整包在傳道 Github,最後寫在 dockerhub 的 Readme。
雖然目前我們沒有把專案丟到 Github,但可以讓我們來寫啟動指令,讓其他人知道大概蓋怎麼執行。
嘗試下載
推上去之後我們就可以來試試看下載了,先把原本的 Image 清除:
1 | $ docker rmi ${docker 帳號}/${docker repository 名稱} |
然後下載我們推上 dockerhub 的 Image:
1 | $ docker pull ${docker 帳號}/${docker repository 名稱}:${版號} |
最後啟動它:
1 | $ docker run --name ${container name} -d -p 3005:3000 ${docker 帳號}/${docker repository 名稱}:${版號} |
大功告成!
嘗試搜尋
當然我們也可以搜尋其他人的 Image,鍵入下列指令到終端機:
1 | $ docker search -f=stars=100 ${image}:${版號} |
- -f - 這邊後面可以階篩選條件,詳細可參考 官方文件,**-f** 是 —filter 的縮寫
大膽假設
這邊筆者大膽想了一個做法,雖然還沒有實踐,但理論上應該是可以達到下圖這個目的,如果你辦到了,歡迎下面留言告訴筆者。
在某個 Container 裡面起 Nginx,透過掛載不同的 Port,然後把 Nginx 的 Root 指向前端專案與本地綁定的 Volume 資料夾內,達到一件部署的可能性。
尚待實驗成功
進階用法
會了基本的用法,大概大家這時候都會開始往進階的地方探索,小小的功能可能已經無法滿足你的需求。
舉例來說,你或許會覺得前端我要起一個專案,後端我要起一個專案,資料庫我要起一個專案,這樣不是太累了嗎?而且我到底該怎麼讓前端與後端溝通,後端能連接到資料庫呢?
其實原本的指令參數有提供方法,但指令肯定沒有直接寫成設定檔來得容易,所以……
這時候救星就出現了,Docker Compose!
就像 Logo 一樣,藍色的箱子就是你的 Container,章魚是 docker compose,他可以將不同的 Container 組合成一個 Container。
基本安裝
首先要安裝 docker compose,依照 官方安裝文件 一步一步安裝,接著鍵入下列指令:
1 | $ docker-compose version |
順利的話會看見 docker-compose version 1.29.2, build 5becea4c
這行文字。
關於網路(Network)
再說怎麼讓 Container 互相溝通前必須提到 docker 的網路,但是這邊不會深入討論。
正常我們透過 run 不同的 Image,會得到不同的 Container,最後會像下圖一樣。
仔細看會發現 Container 不能直接性的溝通,但如果我們使用 docker-compose 建置的話,Container 都會預設使用一個叫做 Bridge Network 的網路橋,所以互相的專案得以溝通。
上方的圖如果不是透過 docker-compose 建置的話,是必須要自己手動把 Container 連結在一起,詳細可參考 使用 Network 連結 container。
如果你是透過 docker-compose 建置的話,預設 docker 除了把你設定檔內使用到的 dockerfile 建置個別的 Container 之外,也會一併的把他們組起來。
這時候你可以把一些 Image 透過 docker-compose 組合起來,也可以在另外單獨起別的 Image,類似下圖。
這邊會發現網路如果不同,就無法互相溝通,所以設定檔怎麼寫就很重要!
關於網路這邊筆者大力推薦可以參考 [Docker] Bridge Network 簡介,作者說的很詳細,而且有圖解大推!
快速上手
前面說了很多終於要來寫我們的設定檔,我們可以簡單理解要使用 docker-compose,我們必須要有 docker-compose.yml 這支檔案,而這支檔案可以把不同的 Image 或是 Dockerfile 整合進來。
這邊使用 mariadb + adminer + react + nginx 組成的 Container 作為範例。
其實基本上配置都和 Dockerfile 差異不大,相關設定也都能爬到文,這邊就不贅述。可以簡單理解為,我在一個目錄底下,去把各個 Dockerfile 引進來使用(React、Nginx),甚至有使用到 Dockerhub 上面的 Image(mariadb、adminer)。
類似組積木的方式,就像 Logo 一樣,一隻鯨魚承載著不同的貨櫃。
寫好設定檔後就可以啟動了(請記得要位於設定檔的目錄底下):
1 | $ docker-compose up -d |
啟動結束還是可以透過 docker ps --all
查看是否正常啟動了,如果想暫停可以鍵入下列指令:
1 | $ docker-compose stop |
但有時候我們會想連 Image 一併刪除,可能因為 Dockerfile 沒寫好,這時候就可以使用下列指令:
1 | $ docker-compose down --rmi all |
課後複習
整個 Docker 流程大致可以分為以下的流程(當然我們省略了很多小細節)。
- 透過 公開 或 私人 倉庫,拉取 Image 下來使用,配上自己寫的 Image。
- 使用 docker-compose 把各個 Image 組合成 Container。
- 把自己寫好的設定檔組成新的 Image,創建好 Repository 之後,推上公開或私有倉庫。
- 可以選配 CI/CD,達到一鍵部署的效果。
Conclusion & 結論
其實一直想研究 Docker 很久了,有陣子都在亂點技能樹,但對於自動部署的領域,筆者自己認為不管哪一端都必須要了解,畢竟工程師就是喜歡越懶越好。
這次的 Docker 入門班雖然很淺很淺談,但我相信會熟能生巧的。
趁著這次工作研究 Docker,又讓我學到了一課,慢慢進步的感覺,很棒!
參考網站
官方文件 - StoryBook