[WebConf 2023] 給前端的 Docker 入門課
在 WebConf 2023 分享了一場 Docker Workshop,之所以稱之為 workshop,原本是想以工作坊的形式,讓大家有機會在現場演練,但想像是美好的,現實總是殘酷的,也是我太愛跟台下的聽眾們聊天,以至於連原本規劃的內容都講不完,如果當時的聽眾,或是看到這篇文章的人,想要更多 Docker 的學習,歡迎向我許願,或許我們能找個機會再開一些分享。
2023/11/09 更新: 在 MWC#2023 分享了一場 90 分鐘的 Docker Workshop,內容跟 WebConf 這場差不多,但做了一些順序的討論,補充了 docker scout,整體應該比 WebConf 時更完整也更流暢,講義連結在此,可以自行下載: https://drive.google.com/file/d/1jRyeb6R_48PIvveNfok-6_CvQYCXwmcC/view?usp=sharing
(但這篇還是要幫我拍手喔 🤣)
自我介紹
前情提要
這次的內容是專門為了前端工程師設計的,但我脫離前端一陣子了,隱約從 AppWorks School 畢業的前端班學員中聽聞他們工作的情況,因此設計了這個 workshop,希望可以讓前端工程師們也能試試看 Docker。
如果你不是前端工程師,但從來沒用過 Docker,想試試看,那也歡迎參考看看。
這次的 Workshop 會做到什麼?做不到什麼?
希望能做到的
- 不害怕 Docker、願意開始使用
- 大概知道 Docker 是什麼
- 知道 Docker 有什麼好處
- 可以建立自己的 Docker Image
做不到的
- 教你怎麼安裝 Docker
- Best Practices
- Docker 原理與底層實作
- Kubernetes
簡單地說,就是這次先以「可以直接開始使用 Docker」為目標,至於「為什麼」或「這是什麼」,如果大家有興趣繼續往下探索,就看看以後還有沒有機會分享。或是關於 Docker 原理,也可以參考敝作:
開始前的準備
Prerequisite
在開始之前,請先安裝好 Docker 及註冊好 Docker Hub 的帳號。
- 先安裝好 Docker: 這部分可以參考 Docker 官網,官網已經有很好的指引了,如果不太知道要看官網的哪部分,可以參考這份說明:how-to-install-docker.md。
- 先註冊好 Docker Hub 帳號 https://hub.docker.com/
實驗素材
背景故事
假設,我是一個前端工程師,剛去一間公司上班,到公司後,後端工程師給我一個 Docker image ashleylai/webconf2023,跟我說只要跑起來就可以有後端串接了,但,我不會 Docker 啊?
BE:面試時,你不是說你會 git 跟基礎的 Linux 指令嗎?
我:是沒有錯,但這跟 Docker 的關係是?
BE:那讓我來跟你確認幾個指令
git pull
跟git push
你會嗎?ls
跟ls -a
是什麼意思?rm
是什麼意思?- log 跟 build 這兩個單字是什麼意思?
這些你都會的話,那你就會 Docker 喔!
我:???
Docker 基本元素
在開始前,先讓我們來看看 Docker 的基本元素,這邊先有個心理準備就好:
- Docker image 可以想成是一種「樣板」,我們可以用 image 這個樣板去「啟動」出很多個 container。如果有 OOP 的概念,這邊就有點類似可以用 class(類別)去建構出很多 instance(實例)。
- Container 就是我們的主角,先記得它是把我們想要執行的程式及其所有相關的套件、函式庫等所有資源包在一起運行。
- Docker Hub 是存放 image 的地方,是不是跟 GitHub 名稱很類似,總之就是某些東西的集散地。
- Dockerfile 可以用來「建立」image,後續我們會用到,用過才能有感覺!
Docker 指令格式
- docker:這就不用說了,我們現在就是要下 docker 的指令
- 對象:這裡的對象就是指你想要操作的對象,例如 Image 或是 Container。
- 指令:例如你是要啟動一個 container 或是列出你電腦裡的 images 等,這裡的指令大量借用了我們工程師過去的經驗,不會太記的,讓我們往下看下去!
- 參數:指令需要的參數,不同的指令需要不同的參數。
備註:這是新版的指令格式,現在開始學 Docker 的朋友,很推薦先試試看新版的格式,雖然比舊版的冗長,但比較有規律、比較好記跟學習。但你可能會在其他教學書籍或文章中看到舊版的指令格式,我後面會放一個對照表,也不會太難比對的。
範例:
# 列出目前在執行中的 container
docker container ls
# 列出所有 container
docker container ls -a
# 列出 image
docker image ls
ls
跟 ls -a
會的話,以上這些範例應該不難推測是什麼意思,這也是為什麼說,如果你會一點 Linux 基礎指令的話,Docker 指令就不會太難,它在設計上,都有盡量借用大家過去的經驗,沒有太多意外之處。
使用場景 1:使用後端給的 image
ashleylai/webconf2023 這個 image 裡是一個 Express web server,image 建立時已經設定好會在 port 3000 啟動這個 web server,因此我們在啟動 container 時,也需要做出相對應的配置。
專案原始碼: https://github.com/azole/docker-ironman-2022/tree/main/web-application/api
首先,我們要先把 image 從 Docker Hub 或是 registry 上 pull 下來,然後再用這個 image 來啟動 container。
取得 image
其實可以直接執行啟動 container 的指令,Docker 會先檢查你的電腦裡有沒有這個 image,沒有的話會自動去 pull,但這邊我們藉由 pull image 來學習一些觀念。
# 先把 image 從 DockerHub 或其他 registry 上 pull 下來
docker image pull ashleylai/webconf2023
# 列出 image 確認
docker image ls
docker image pull {image name}
image name 是 image 的名稱,完整的格式為 namespace/repository:tag
。namespace 是用來辨認個人或組織的,簡單地說,如果你是用 Docker Hub 的話,那就是你在 Docker Hub 上的帳號。
很多時候,你會看到教學範例並沒有加上 namespace,這是因為這時候使用的是預設的 namespace,通常就是很「大咖」、「知名」的組織的 image,例如 node, ubuntu, nginx 等。
repository 這是 image 儲存庫的名稱,tag 通常會被用來標註版本,如果沒有寫明,那預設就是 latest
。
如果不知道有什麼 image 可以用,可以到 Docker Hub 上去搜尋,也可以用指令 docker search {keyword}
來搜尋,例如 docker search node 那可以搜尋 nodejs 相關的 image。以我們的情境為例,後端已經提供了一個 image 名稱,那就直接 pull 即可。
docker image ls
其結果如下,你看到的顯示結果跟圖 5 可能略有不同,REPOSITORY 跟 TAG 前面介紹過的,就不多贅言。IMAGE ID 是這個 Docker image 的 ID,是用來唯一辨識 Docker image 的。SIZE 這個欄位要特別注意,這是這個 IMAGE 的大小,但不一定是真的佔用你硬碟的空間大小,原因是因為 Docker image 是一層一層疊起來的,而且會讓這些「層」被共用,想知道更多的話,可以參考我的書,或是將來有機會我們來開個進階課聊聊這部分。
啟動 container
docker container run \
-it \
-p 3001:3000 \
--rm \
ashleylai/webconf2023
這是啟動 container 的指令,格式一樣符合圖 3 的整理,這裡我們用到了幾個參數,這些參數都跟我們要怎麼使用這個 image 有關係:
-it
i
:-interactive
啟動互動模式,保持標準輸入的開放。t
:-tty
讓 Docker 分配一個虛擬終端機(pseudo-TTY),並且綁定到容器的標準輸出上。
加上 -it
的啟動方式,應該會讓你的終端機視窗被佔用,但好處是,可以比較方便地看到 container 中的 application 的 log 的顯示。
-p
--publish
通常格式為 -p local port#:container port#
,將 container 中的 port 3000 映射到 Host 中的 3001。container port# 在 image 建立的時候就會決定好了,所以要按照這個 image 的規定、不能修改。但 local port# 就可以自行決定了,
--rm
停止這個 container 時,就移除掉,通常我在實驗或練習的時候,習慣加上 --rm
,這樣就不用手動去移除停止後、不需要的 container。
這邊可以看到,image name 我用的是 ashleylai/webconf2023:arm,因為我的電腦是 M1,是 apple chip,因此我用符合這個晶片的版本,目前 Docker Hub 上的 image 是 intel chip 可以執行的,這也是 tag 的用途之一,可以用來標注版本。
測試 container
這個 Express web server 提供了兩個 API:
- GET /
- GET /conferences
都可以用瀏覽器試試看,記得 port number 要換成你啟動 container 指令中的 local port#。
如果可以成功看到圖 7 的結果,那表示已經啟動成功了,你就可以開始開發前端來串接這些 API 嚕~
使用場景 2:把自己的前端包成 Docker image 提供給其他人使用
為了設計這個實驗,開發了一個超級簡單的 react 前端,大家手邊如果有自己的專案,蠻鼓勵試試看直接用自己的專案來進行這個實驗。
前端原始碼: https://github.com/azole/docker-ironman-2022/tree/main/web-application/myweb
這邊要特別注意的是,由於前端是靜態資源,需要一個 web server 來接收 HTTP 請求,我這邊用的是 nginx。
開發 Dockerfile
當我們想要把自己的專案做成 image 給別人使用時,就必須要開發 Dockerfile,這是一個文字檔,我們可以在裡面記錄建置一個 image 需要的步驟,直接來看例子吧。
# build stage
FROM node:18-alpine as builder
# 建立工作目錄
WORKDIR /app
# 把 package.json 跟 package-lock.json 複製到 image 中
COPY package*.json ./
# 安裝相依套件
RUN npm ci && npm cache clean --force
# 把所有檔案複製到 image 中
COPY . .
# 執行 build
RUN npm run build
#################################
# production stage
FROM nginx:alpine
# 建立工作目錄
WORKDIR /usr/share/nginx/html
# 從 builder 階段裡的 /app/build 複製到目前位置(WORKDIR)
COPY --from=builder /app/build .
這就是一個 Dockerfile 的樣子,分為兩個階段:
- 第一個階段 build stage: 這是編譯階段,因為編譯需要 nodejs 環境,所以我從 nodejs:18 開始,並且把這個階段命名為 builder(這個你可以任意命名),會需要命名是因為後面的階段會用到,有命名的話比較方便。後續的指令前端工程師們應該覺得不陌生,就是用 npm 安裝這個專案需要的套件,執行
npm run build
去編譯 react 專案。 - 第二個階段 production stage: 這個階段就是要正式地開始把第一個階段編譯出來的檔案 serve 起來,這邊我是用 nginx 來接收 HTTP request,所以就直接用 nginx:alpine 這個 image,然後從第 builder 這個階段把編譯好的靜態檔案複製到這個階段即可。
提醒一下,不要嘗試一開始就憑空寫出 Dockerfile,可以先手動建立一個 container,並且在這個 container 裡面進行操作,讓這個 container 變成「你希望建立出來的 image,啟動出來的 container 的樣子」後,再把這些步驟寫進 Dockerfile 裡去,這樣成功的機率比較高,通常手動會失敗的,自動化也不太會成功。
備註:本文主要在讓大家可以立刻開始使用,所以僅介紹有用到的部分,其他還有興趣的話,可以參考 Dockerfile Tutorial(附錄版),之後也會再整理更完成的教學出來。
建立 Docker image
準備好 Dockerfile 之後,就可以透過指令來進行編譯:
docker image build -t ashleylai/myweb .
一樣是 docker 開頭,我們現在要操作的對象是 image,要進行的指令是 build
, -t
是用來指定 Docker image 名稱的,格式同前文介紹,如果你只是本地電腦使用,可以單純給 name:tag 即可,如果想要 push 到 Docker Hub 上去,記得要加上帳號,例如 ashleylai/myweb,tag 沒有設定的話,預設就是 latest。
注意一下指令的最後有一個「點」,這個點不能省略喔,一開始我學習的時候,以為這是英文的句點,自己為是地沒有複製這個點,結果就出錯了。這個部分還有其他用法,我們這邊「點」的意思就是當前位置,這是用來告訴 Docker 建立這個 image 需要的 context(上下文)可以在哪裡找到。
圖 9 就是編譯的過程(僅擷取部分):
建立完成後,可以用 docker image ls
確認看看是否存在。
測試 image
再分享給別人之前,記得可以用這個 image run
成 container 後測試驗證一下,以我們 build 出來的這個 image 來說,因為是 nginx,預設的 port number 是 80,因此啟動的指令如下:
docker container run \
-it \
-p 8081:80 \
--rm \
ashleylai/myweb
跟啟動後端 server 的指令是不是很像,差別只有在 -p
,我把 nginx 裡的 80 映射到我電腦的 8081,所以用瀏覽器測試時的網址會是 http://localhost:8081,記得後端 server 也要跑起來喔。測試結果如圖 10:
把 image 推送到 Docker Hub
既然從 Docker Hub 上下載 image 到我們的電腦用的是 docker image pull
指令,大家應該可以猜到,想要把 image 推送到 Docker Hub 上去用的是 push,不過,記得在 push 前要先登入你的 Docker Hub:
# 先登入 Docker Hub
docker login
# 把 image 推上 Docker Hub
docker image push ashleylai/myweb
使用場景 3:直接用 Docker 來當作開發環境
前面兩個場景都是要給別人用的,後端給前端、前端給後端之類的,現在我們來試試看,能不能在工程師的電腦裡只裝 Docker 就可以開始進行開發。
我們原本開發專案的工作流程會是:
- 開啟終端機後,將路徑切換至 react 專案
- 在終端機執行
npm start
以啟動開發模式(這邊可以換成你需要的指令)
如果想用 Docker 來進行上述動作,參考指令如下:
# 先將路徑切換至 react 專案
docker container run \
-it \
-p 3000:3000 \
-w=/app \
-v $(pwd):/app \
node:18 \
npm start
這個指令其實我們在前兩個場景用到的一樣,就是多了一個 -w
跟-v
。-w
負責指定這次啟動的 container 裡的工作目錄。而 -v
的用法跟 -p
有點像, -v $(pwd):/app
冒號前的 $(pwd)
指的是你電腦的當前目錄,冒號後的是 container 裡的,組合起來的意思就是「將當前目錄掛載(mount)至 container 裡的 /app」,這樣就可以讓你的電腦跟 container 裡共享同樣的檔案。
至於指令中的 node:18
跟 npm start
,你都可以置換成你需要的 nodejs 版本跟啟動指令,這樣就可以用 Docker container 裡 nodejs 的環境,而不需要在自己的電腦安裝 nodejs 了。
除了不需要在自己的電腦安裝環境之外,你應該也注意到,要換版本也是很容易的事,只需要換個 image 來用即可。
試試看以下步驟,可以讓你從頭建立一個 react 專案並且進行開發:
# 切換到你想要放置專案的路徑
# 用 Docker container 來建立 react 的新專案
docker container run \
-it \
--rm \
-w=/app \
-v $(pwd):/app \
node:18 \
npx create-react-app simple-react
# 用 Docker container 來啟動 react 專案的開發模式
docker container run \
-it \
--rm \
-p 3000:3000 \
-w=/app \
-v $(pwd)/simple-react:/app \
node:18 \
npm start
所以,Docker 是什麼?可以吃嗎?
本文一開始就讓大家先開始用用看,現在來解釋看看 Docker 是什麼。
Docker 是一種把你的專案、軟體,及其所有相依的套件、函式庫等「打包」在一起的工具,讓你可以很輕易地在任何地方執行它。
印象中,Docker 剛推出的時候,有一句行銷標語是「Build Once, Run anywhere」,時至今日,雖然不完全做到(例如不同晶片造成的影響),但也沒有相差太多。
Docker 透過 image 將你的專案、軟體需要的東西全打包在一起。啟動時,透過 Linux 的 namespace 技術,建立出一個隔離的環境,再透過 cgroups 來進行資源的限制。(這邊再講下去,我可以講上 6、7 個小時,有興趣歡迎找我討論唷。)
這邊也可以注意到,namespace 跟 cgroups 其實是 Linux 上的技術,那為什麼我們在 Mac 或 Windows 上可以用呢?以 Mac 上來說,安裝完 Docker Desktop 後,Docker 其實是在我們的 Mac 裡偷偷啟動了一台虛擬機(virtual machine),這台虛擬機上安裝了 Linux,之後我們啟動的 container 都是跑在這台虛擬機上的。透過這種偷天換日的方式,讓我們可以在非 Linux 的作業系統上使用 Docker,也是蠻有趣的。(Windows 我比較不熟悉,但應該類似。)
再另外提醒一件事,由於 Docker 當年太過成功,因此我們總習慣將 Docker 跟容器(container)畫上等號,但其實在 Docker 出現以前,就已經有了容器技術,只是當時應該是很難用,Docker 厲害的地方,就是將建立一個 container 這件事變得很簡單,讓大家都能很容易地去使用容器。
ps. 關於這點,我自己也有一點感觸,我自己的技術能力不是很厲害,但正是因為這樣,我好像比較能理解還不會某一個技術的人的感覺。91 哥也鼓勵我「能把知識講得很清楚好懂、引人入勝是一種稀缺能力」,想一想,跟 Docker 很像,我要多說服我自己,不要小看能把原有的、有門檻的東西變得簡單這件事!
Docker 的優點與缺點
Docker 有什麼好處
讓大家想想幾個情境:
Q. 當你有一台新電腦或是新的 server,你需要花多少時間把開發、執行環境安裝回來?
Q. 你有多常說「在我的電腦不能動」?
Q. 上版還好,你有退版的經驗嗎?覺得退版容易嗎?
Docker 的缺點
進入門檻,看起來好像很難、很遙遠 → 透過本文,你已經解決了,會 git 跟 linux 基礎指令,就會 Docker!
如果覺得指令不行,在 Mac 或 Windows 的話,也可以考慮使用 Docker Desktop,透過 Docker Desktop 提供的 UI 介面來控制容器也是可以的。
結語
Docker 絕對不是萬能的,但把它用在適合使用的地方,就能幫助你增加工作效率,甚至可以跟別人更好地合作。
先不要害怕它,無論如何,先用起來再說,用了就會遇到問題,遇到問題就去探索、查資料、找人討論(歡迎找我!),這樣就會愈來愈熟悉、愈來愈厲害。
在「會使用」之後,如果有興趣,可以藉由 Docker 去對 Linux 做更深地探索,也是很棒的,到時候記得要啟動一台 Linux (VM or AWS EC2 instance…等)來玩玩,會很有趣喔!
參考資料
遺珠之憾
這些是我覺得很可惜,沒有時間討論到的部分,有機會我們再找時間聊聊。
- Docker Compose
- Docker Network
- 底層原理
- Best practice
- Security
新舊指令對照
這邊提供新舊版的指令對照給大家參考:
# 註解是舊版指令
# docker pull ashleylai/webconf2023
docker image pull ashleylai/webconf2023
# docker push ashleylai/webconf2023
docker image push ashleylai/webconf2023
# docker rm {containerID or containerName}
docker container rm {containerID or containerName}
# docker rmi {imageID or imageName}
docker image rm {containerID or containerName}
# docker run -it --rm -p 3001:3000 ashleylai/webconf2023
docker container run -it --rm -p 3001:3000 ashleylai/webconf2023
# docker ps
docker container ls
# docker images
docker image ls
推薦閱讀
- Docker Container 基礎入門篇 1
- Docker Container 基礎入門篇 2
- Dockerfile Tutorial(附錄版)
- Docker實戰6堂課:56個實驗動手做,掌握Linux容器核心技術(軟精裝)
- Docker實戰6堂課:56個實驗動手做,掌握Linux容器核心技術(平裝)
透過參加鐵人賽與完成這本書,自己也學到了很多關於 Linux 的知識,設計了很多實驗去驗證自己的理解與觀察,或是從這些實驗的結果去找尋答案。
當初想要寫這樣的內容,是因為自己對 Docker 很有興趣,但市面上已經有很多很棒的 Docker 教學與實戰分享了,我問我自己,我還想讀些什麼呢?我的答案是,我會想要知道更底層的東西,想要知道這些好用的功能背後是怎麼做到的,我也想要更知道 Linux 一些,於是就給自己出了一個這樣的題目,過程中差點後悔,真的是有超過自己原本的能力,但還好沒有放棄。
所以,這的確不是一本初階的書籍,是我嘗試要讓自己更進階一點點的努力,想要跟大家一起變得再厲害一點點的心意,再請大家多多指教了。