用 Docker 建立不同 Angular CLI 版本的開發環境

前言

因為 Docker 熱潮越來越紅,那種炒熱的氛圍感覺好像如果不會就跟不會寫程式一樣,所以就花幾天的時間玩了一下,雖然不盡理解但是大概也能略懂大家在講什麼,最重要的就是了解自己程式是否能在容器(Container) 內執行,在這邊就不班門弄斧說明 Docker 原理,而著重在實際建置。

環境建置:Docker for Windows

Docker 在 Windows 上是透過 Hyper-V 建立一個 Linux 環境並在上方執行,因此需要 Windows 10 (1607) 專業版、企業版或教育版。

Windows 8 雖然也有 Hyper-V 不過筆者沒試過,Windows 7 則透過安裝 Docker Toolbox on Windows 來執行。

雖然 Docker 在安裝過程會協助安裝 Hyper-V,但是因為 Hyper-V 預設的虛擬硬碟檔案是存放在 C 槽下面,所以如果空間不足,或是希望存放位置要調整到別的地方,可以先自行手動安裝 Hyper-V 並修改存放路徑。
img
img

因為後續從 Docker Hub 下載的 **映像檔(Image)**,以及我們建置的 **容器(Container)**,都會存放在 Hyper-V 映像檔內,所以要考慮成長空間。

如果還未註冊帳號可以先連結到官方網站註冊一組帳號。
img
接著連到官方下載頁面 下載並安裝 Docker,安裝完成後啟動 Docker for Windows,第一次會在 Hyper-V 內建立執行環境,並要求登入 Docker 帳號。
img

因為 Hyper-V 與 VMware Workstation 只能擇一啟動,所以不便建置模擬環境來截圖說明整個安裝步驟。

比較特別的是在 Windows 上面記憶體的配置比例無法太高,筆者筆電是 8G 記憶體,記憶體若配置超過 3G,Docker 的映像檔就會啟動失敗,看來 Hyper-V 映像檔啟動時也會佔用不少記憶體。
img
img
如果是在 Linux 環境建置 Docker 就比較單純,雖然 Docker 常常被拿來跟 VM 比較,但是說穿了它就是一隻程式,所以在 Linux 上執行就不需要預先配置記憶體,需要多少就用多少,完全看主機上的記憶體還有多少可用。

過去有團隊在 Raspberry Pi 上執行 2499 個 Container,雖然跟正式專案會執行的 Container 有不少落差,但是足以看到 Docker 除了佔用資源少之外,藉由適度的配置可以讓每台伺服器發揮最大的效能,這點在寸土寸金的雲端上可以說是很重要的,參考連結:
一片Raspberry Pi能跑多少個Container?答案驚人
RASPBERRY PI DOCKERCON CHALLENGE: WE HAVE A WINNER!

使用 VS Code 管理 Docker

利用 VS Code 來管理 Dokcer 可以說是入門的好選擇,我們不需要先花時間看完所有指令參數就可以完成基本的操作。
開啟 VS Code,選擇擴充功能並安裝 Docker
img
重新啟動後就可以看到多了 Docker 的檢視區塊,我們可以直接在上面看到本機上的 Image、Container 以及上傳到 Docker Hub 的 Image。

docker images:列出本機上以下載的 Image,除了來自 Docker Hub,我們也可以自己將 Container 打包成 Image,在上傳到 Docker Hub。

img
我們可以透過 Image 的功能選單來執行或移除 Image,我們也可以從終端機顯示的對應指令來逐步學習。
Image 有點像 Linux 的 Live CD,是一個唯讀的映像檔,所以在建立 Image 就有點像是將 Container 燒錄在 CD-ROM 上,在執行時他會建立一個可讀寫的 Container 並將 Image(唯讀的 Container) 優先載入。
img
當然我們也可以透過 Container 的功能選單來做相關的操作,透過管理工具操作另一個好處就是不必輸入操作對象的 Id,有些指令(例如移除)需要輸入所要操作的 Image 或是 Container 的 Id,筆者遇到最大問題就是 Container 啟動失敗,從 Show Logs 就可以了解問題是出現在哪裡。
img

下載映像檔:docker pull

我們知道 Angular-CLI 是透過 Node.js 來執行的,而 Node.js 則須執行在某個 OS 上。
OS => Node.js => Angular-CLI
因此正常情況下應該是先做出一個 OS 的 Image,我們可以直接從 Docker Hub 搜尋已經製作好的 Image,以 Ubuntu 為例,輸入關鍵字後便會列出許多熱心人士幫我們製作好的 Image,當然其中也包含官方釋出的版本。
img
點擊進去之後便可看見詳細說明,右邊也提示我們可以透過 docker pull ubuntu 指令來下載 Image。
img
完整指令是docker pull [OPTIONS] NAME[:TAG|@DIGEST],大部分我們都會加上 Tag 來抓取特定版本,Tag 一個識別名稱但是習慣上都會使用版本號來方便識別,如果沒有帶入 Tag 參數,預設會去尋找 Tag 為 latest 的 Image 來下載,一般建議加入指定的 Tag (版本)。
img
但是我們還可以發現同一個版本有多個 Tag Name,會讓人不知道要下載哪一個,其實將他們都下載之後就會發現他們的 Image Id 都一樣,也就是說其實任何一個 Tag Name 都可以使用,沒有差異,Docker 也不會重複下載同樣 Id 的 Image,不過 latest 這個 Tag Name 一般是指最新版本,所以有可能對應的 Image Id 會隨著新版本的發佈而變動。
img

站在巨人的肩膀上

其實 Docker Hub 已經有封裝好的 Node.js 映像檔我們可以直接利用而不需要從頭開始,我們一樣抓取官方製作的 Image,從說明頁面可以發現許多版本,最大的差異大概是底層所運行的 Linux 不同。
img
我們分別透過 docker pull node:9.8.0docker pull node:9.8.0-alpine 分別安裝標準版本(debian)以及比較熱門的 alpine 版本,可以發現容量大小差很多,因為 alpine 容量只有 3.97MB,應該算是在 Docker Hub 上最小的 Linux。
img

Dockerfile

我們除了可以在終端機上透過指令操作外還可以將指令打包成執行腳本 Dockerfile,Node.js 的說明頁面也提供了 Dockerfile 連結讓我們了解整個 Image 到底包含了什麼東西,例如我們開啟 9.8.0-alpine 版本的 Dockerfile,第一行 FROM alpine:3.6 便說明了這個 Image 又載入了 alpine:3.6 這個 Image,也由此可知此版本確實透過 alpine 來執行。

FROM:載入其他 Image,正常會放在 dockerfile 第一行。
我們可以透過 docker build 來執行 dockerfile 所有指令,藉此產生對應的 Image,可以藉由 --file(縮寫:-f)來指定要執行的 dockerfile,若沒有指定,預設會搜尋當前目錄下的 Dockerfile 來執行。

img
接著我們查看 9.8.0 所關聯的 Dockerfile,可以發現他總共包了5層 Image,在最後一層才發現它是以 Debian 為基底,這邊也說明了每個 Container 應該以單一任務為原則,不宜處理太多事情,這樣在維護上也比較容易,微服務似乎也是這個概念。
img

建立映像檔 (Image)

接著我們開始嘗試建立一個具有 Angular CLI 的 Image,首先我們建一個 Dockerfile 的檔案,並輸入下列內容。

1
2
3
4
5
6
7
FROM node:9.8.0-alpine

RUN chown -R node:node /usr/local/lib/node_modules \
&& chown -R node:node /usr/local/bin

USER node
RUN npm install -g @angular/cli@1.7.3

img
指令說明:
FROM node:9.8.0-alpine
表示我們會以 node:9.8.0-alpine 的 Image 當作基底。
RUN chown -R node:node /usr/local/lib/node_modules
&& chown -R node:node /usr/local/bin
因為筆者測試過程發現在安裝 Angular CLI 時會發生相關資料夾沒有寫入權限的問題,既使透過 root 權限,
img
img
加上觀察 node Image 的 Dockerfile 發現它會建立一組 node 的帳號與群組。
img
USER node

USER:指定要使用的帳號

RUN npm install -g @angular/cli@1.7.3
因此便切換成 node 帳號來安裝 Angular CLI。

接著我們透過 docker build -t ng-cli:1.7.3 . 指令來建立 Image。

--tag:縮寫 -t,表示此 Image 的 Tag Name,此範例名稱為 ng-cli:1.7.3
最後的 . 表示當前目錄。

img

仔細觀察可以發現 Docker 在執行 dockerfile 指令時,每一行指令都會建立一個臨時的 Container 來儲存。

最後建置完成可以發現 Image 列表多了一個 ng-cli:1.7.3 的 Image。
img

建立容器 (Container)

Container 可以說是 Docker 提供給我們的沙箱執行環境,藉由這種隔離模式可以讓我們在裡面的執行程式避免受到其他程式干擾,上一節我們已經建立了一個具有 Angular CLI 的 Image,理論上透過 Docker 放到 Container 內執行我們就可以得到一個開發環境,但是我們會遇到幾個問題,第一如果程式碼也放在 Container 內那別人要維護會變得很困難,第二是它無法執行GUI程式,所以無法執行 VS Code,除非你可以接受使用 vim 來撰寫程式。
Docker 提供一個參數 --volume (縮寫:-v) 讓我們可以指定 Container 內的特定路徑與外部掛載在一起。

接著我們透過下來指令來建立並執行 Container:
docker run --name angular_173 -v D:\demo-docker:"/home" -it -d -p 4200:4200 ng-cli:1.7.3
指令說明:
docker run
若只是單純建立 Container 則可以透過 docker create 來執行,docker run 除了會建立之外還會直接啟動 Container。
--name angular_173
將 Container 名稱設定為 angular_173
-v D:\demo-docker:"/home"
將 Windows 上的資料夾 D:\demo-docker 與 Container 內的資料夾 home 掛載在一起。

在 Windows 上,我們需要先設定 Shared Drives 才有辦法讓 Container 與 Windows 串接。
img

-it
--interactive(縮寫:-i,保持STDIN打開) 與 --tty(縮寫:-t,分配TTY) 的簡寫,藉此可以讓運行 Shell 的終端指令。
-d
讓 Container 在後台運行,--detach 的縮寫。
-p 4200:4200
將 Container 的4200連結埠(第二個參數)發佈到外部的4200連結埠(第一個參數),--publish 的縮寫。
ng-cli:1.7.3
最後指定要啟動的 Image。
img

如果希望停止 Container 時便自動移除可以加上 -rm 參數,Container 的建立速度很快,所以也可以在要使用時再建立。
完整參數說明請參閱:docker run

接著我們透過 Container 的 Attach Shell 功能來與 Container 通訊。
img
嘗試在 Container 的 home 資料夾內建立一個 abc 資料夾,開啟檔案總管可以發現 D:\demo-docker 確實也出現了一個 abc 資料夾,因此可以確認路徑掛載成功。
img

建立 Angular 專案

我們在 Container 內透過 Angular CLI 來建立一個名為 ng5 的 Angular 專案,執行方法就跟本機操作一樣,指令如下:
ng new ng5 --router --style scss --service-worker
img

接著我們在本機上透過 VS Code 來開啟,
img
img
當然我們也可以透過 CLI 來建立 Component。
img
img

重點在於在 Container 內執行 Angular CLI 指令,在本機上撰寫程式碼。

執行 Angular 專案

Container 內透 ng serve 來執行專案,可以發現與本機端模式一致。
img
接著我們在本機端開啟 http://localhost:4200/,可以發現網頁無法啟用。
img
其實這是因為 ng serve 預設只能從本機(localhost)開啟,Angular CLI 的本機是在 Container,由外部的瀏覽器開啟等同於遠端連結,因此我們改用 ng serve -H 0.0.0.0 指令來執行,允許外部主機可以連結到 Container 的 ng serve。
img
再透過瀏覽器開啟就發現網頁已經可以正常顯示了。
img

當然我們也可以直接將參數加在專案內的 package.json,並透過 npm start 來執行。
img

後記

依照上面操作,我們可以建立不同 Angular CLI 版本的 Image 以及 Container,並依專案需要執行對應版本的 Container,這樣就可以在同一台電腦上開發不同版本的專案。