以 Docker Compose 建立 Node.js 全端開發環境 (一)— 配置Docker Compose

Toki Lee
11 min readAug 30, 2022

前陣子有段時間,我個人相當專注於 Monorepo 的相關技術,而近期專案上反而比較少使用到那些工具,不過這次的主題是建立一個以容器化為基礎的開發環境,並且依然會把不同 App 的 Source Code 放在同一個 Repo 下來開發,而就這點來看也還是有那麼些類似的感覺。

開始之前

本系列的文章會說明一個簡易的架構,並著重在展示一個專案開發環境建立的過程,所以有些套件、框架的細節並不會完整描述,那在這系列的文章中我們大致上會使用到的技術為:

  1. Docker & Docker Compose
  2. TypeScript
  3. Express & Prisma
  4. Create React App & React

如果以上所使用到的技術有任何不清楚的地方,我會建議可以參考不同的文章與相關的文件一併閱讀,尤其是 Docker 的部分建議先有些基礎的認識會比較好。

另外,我個人所使用的編輯器為 VSCode,有些設置與開發體驗會相依於編輯器本身,這點也可能要注意一下。

因為等等說明的過程可能會有偏差,這邊先在開頭附上 Github 的 Repo ,這是這章節結束後專案該長的樣子,可以參照看看:

那麼就讓我們從本機該有的東西開始吧。

Docker Compose

Docker Compose 的部分,我是按照目前 Docker 的官方文件進行安裝,而在這篇文章中使用的指令是主要是 docker compose,可能有些人安裝的版本是使用 docker-compose 這個指令,兩者基本上應該不會相差太多,但如果有發生問題,就可能要請讀者確認下目前的版本有沒有什麼限制。

系統方面,我目前是使用 Ubuntu 22.04 LTS 與 MacOS Monterey 進行開發,如果是 Linux 或 Ubuntu 的使用者,我想應該是沒有什麼藥特別注意的地方,但如果是 Window 的使用者,可能就不一定能適用這篇文章,因為目前我個人的開發並沒有在 Window 上進行過,可能會有許多沒有注意到的問題會發生。

MacOS 的使用者要開發前,請先打開 Docker Desktop 的介面,這邊有一個可能會顯著影響到速度的設定要調整:

請把上面的 Enable VirtioFS accelerated directory sharing 這個選項勾選並 Apply,因為這系列文章所描述的這個架構上,會需要把開發的專案 Bind 到容器中。而 node_modules 建構的過程在 MacOS 會相當緩慢(緩慢到讓人懷疑人生),但再開啟這項設定後,個人使用的專案實測速度上有提升了快三倍,並且無論是在 Intel 還是 Arm 版本的 MacOS 都有顯著改善,筆者個人是以 M1Pro 的 MBP 與 2015 的 MBP 進行測試過。

目錄結構

這次專案目錄的結構方面大致會像是這樣:

├── .vscode                   <--- vscode 的配置設定
├── apps <--- 所有專案 app 放置的目錄
│ ├── backend
│ │ ├── .dockerignore <--- 打包 Docker Image 時要忽略的內容
│ │ ├── docker <--- 打包 Docker Image 所需的內容
│ │ ├── package.json
│ │ ├── src
│ │ ├── .eslintignore <--- ESLint 要忽略的檔案
│ │ ├── .eslintrc.js <--- ESLint 的配置
│ │ ├── .gitignore
│ │ ├── .prettierrc <--- Prettier 的配置
│ │ └── tsconfig.json <--- TypeScript 的配置
│ └── frontend
│ .
│ .
│ .
├── .env <--- Docker Compose 的環境變數
├── .gitignore
├── docker-compose.yml <--- Docker Compose 的配置
└── scripts

當然這會視專案的需求有所不同,目前在這章節大概只需要建立 apps, docker-compose.yml 而已。

那在建構專案之前,讓我先說明兩個概念 - 命令式宣告式

命令式(Imperative)

假定現在你剛入職、得到了一個新的開發環境,為了要開始加入開發,你一定會需要建立你個人的工作環境,而大部分的工程師對此肯定也是駕輕就熟了,通常也會有自己配置環境的起手式(總之就是把 VSCode 載下來、擴充下載個遍,裝個 Node.js 還是 Python、搞定 SSH… 等等之類的事情)。

以安裝 Node.js 為例,我們可能會使用一些套件管理工具來安裝像是: apt-getyumnvm 等等,並且也可能會有些環境變數會需要設置,而這些過程在終端機上執行或整合成幾個 Shell Script 執行,一步一步來建立環境的方式,我先稱之為命令式(Imperative),當然這也是大家都了解也是最基本的方法。

宣告式(declaratively)

剛才提到的命令式,這樣的建立方式有著彈性,然而有時候也是種缺點,可能隨著時間過去後,同樣的建構流程無法再現、 registry 的位置壞了、東西找不到了,又或者這是團隊中的專案,而建立的方式相對複雜、又沒有統一的方案,那麼工程師每到新環境建立的時期就會需要耗費大量的時間來把環境建立出來。

所以這裡要提到的另一種方法,我這邊稱之為宣告式(declaratively),在這種方式下,我們將使用 Dockerfiledocker-compose.yml 來建立好統一的環境,一但在 Image 打包完成後環境所需的內容就會包含在裡面了,並且藉由 docker-compose.yml 的配置我們可以將專案中服務啟動的資訊紀錄在配置檔中,這樣也能減少更多啟動專案時認知上的負荷。

宣告式的優點就如同我們會把開發常用的指令寫成腳本放在 Repo 中一樣,藉由 Docker Compose 的配置檔,我們甚至將建構環境與啟動服務的細節如同 Source Code 一般記錄在版本管理工具中,這樣任何人在開專案時只要 pull 下相關的 Repo 或 Image 就能獲得同樣的開發環境與體驗,也減少更多工程師交接上困難。

而在有了這樣的認識後,就能開始來建構我們的專案了。

建立專案

在建立專案的部分,我很猶豫要先有 App 的 Source Code 再開始,還是直接從 Docker Compose 開始配置,畢竟先有點畫面可能感覺會比較實在點。

但最終來說我認為這個開發的方式是要強調以 Docker Compose 包辦環境的特點,這樣工程師除了 docker 以外基本上不需要在開發機上安裝任何的 Library,所以這邊還是先從 Docker Compose 為環境的角度開始吧。

首先要先確保 apps/backendapps/frontend 的目錄有先建立過:

mkdir apps apps/frontend apps/backend
確保你的專案中確實有這兩個目錄

因會之後我們會將這兩個目錄 Bind 到容器中,如果目錄不存在, docker 會幫我們建立目錄,但所有 docker 幫我們建立的目錄將會是 root 的權限,這可能會對我們的開發造成問題。

後端

讓我們先從後端的開發環境開始建立,首先先在專案中加入 docker-compose.yml 並加入以下內容:

docker-compose.yml

建立好 Docker Compose 的配置檔後,就先執行一次 docker compose up 吧,等待 Image pull 完並跑到最後 log 應該會停在以下的畫面:

在配置檔中 command 的部分使用了 tail -f ,這是因為我們還沒有把任何的服務建立起來,為了讓我們可以進入容器中配置專案內容,我們先以這個方式使 container hang 住,並繼續下一步。

接下來我們開啟另個終端機並使用 docker compose exec backend bash的指令進到 backend 的容器中,如果成功了我們會看到以下的畫面:

因為我們在 Docker Compose 的配置檔中了 working_dir ,所以我們一進來就會在 /srv/app 的目錄下,又因為這個路徑 bind 到本機的 apps/backend 目錄,我們接下來在容器中這個目錄下的行為將會反映在本機 apps/backend 的目錄之中,而反過來說也是成立。

接下來你可以在容器中自行使用 yarn inityarn add 之類的指令逐步建構後端的專案,或使用編輯器加入程式碼,而這邊我就直接附上檔案:

apps/backend/package.json
apps/backend/tsconfig.json
apps/backend/src/server.ts

再加完檔案後,我們在容器內執行 yarn && yarn dev 來將 Express 跑起來:

結果畫面

到達這邊就代表成功了,那下一步再把剛才執行的指令紀錄到 docker-compose.yml 之中:

docker-compose.yml

修改完後,我們回到先前 hang 住的終端機,使用 ctrl + c 把他停止下來:

然後我們再重新執行一次 docker compose up,由於剛剛我們更動了 docker-compose.yml 的內容,所以重新跑起的 container 將會重新建立,並且改成新設置好的內容:

啟動容器後已經會自動啟動 Express 了

在這邊順便提到一件事情吧,啟動容器的部分,我建議使用 docker compose up -d 讓他跑在背景,然後以 docker compose logs -f --tail=100 來看噴出的 log 。

這樣我們就不需要一直保持著一個終端機在那邊了,而當想要終止啟動的容器時,則可以使用 docker compose stopdocker compose down 來關閉,而這兩者是有差異的,stop 只會將容器停止,但 down 還會將容器移除並移除建立的 Network,這方面可以請讀者自行了解一下相關的文件。

前端部分

接下來前端的部分就相對簡單了,因為大致的過程與後端雷同,所以一些部分就不再贅述,現在就讓我們加入前端的部分在 docker-compse.yml 吧:

docker-compse.yml

啟動後進到前端的容器中執行 yarn create react-app --template typescript .,這樣我們就會以 CRA 幫我們建立一個基礎的 React 專案,而內容如下:

接下來我們調整下 package.jsonname 與 dev 的 script:

package.json

接下來與建立後端專案的流程相同,如果 dev 的指令跑起來一切順利,那我們就可以接著在修改前端部分的 docker-compose.yml

docker-compose.yml

如果一切設置正確,重新啟動後你會看到以下的畫面:

前端容器成功跑起

總結

在這個章節,我們一步一步的從 Docker Compose 的配置開始,最後走到前後端專案的建立,我認為在建構的過程可能會稍加繁瑣,但一旦專案的架構完成了之後,未來使用者只要 pull 專案下來並且執行 docker compose up 就能幾乎在無痛的過程快速的啟動專案並進行開發。

--

--

Toki Lee

沒有技術上不可行,只是時間上做不到⋯