Container 應用

到目前為止,已經說明了 docker run 幾個常用的選項和參數,也做了一些簡單的範例。今天將以情境的方式,介紹如何應用 docker run 指令完成任務。

資料庫 client 端

使用 container 連線資料庫,如:

docker run --rm -it percona mysql -h10.10.10.10 -umiles -pchou

曾遇過在連線資料庫的時候,遇到版本不相容的問題,所以有做過下面的測試:

# 換用其他 fork 的 client
docker run --rm -it mysql mysql -h10.10.10.10 -umiles -pchou
docker run --rm -it mariadb mysql -h10.10.10.10 -umiles -pchou

# 換用其他版本
docker run --rm -it percona:5.6 mysql -h10.10.10.10 -umiles -pchou
docker run --rm -it percona:5.7 mysql -h10.10.10.10 -umiles -pchou

最終成功地完成測試了。如果要在本機安裝多個版本會是困難的,相較使用具隔離性的 Docker container 測試起來就非常容易。

Redis 也可以用一樣的方法:

docker run --rm -it redis redis-cli -h10.10.10.10
docker run --rm -it redis:4 redis-cli -h10.10.10.10
docker run --rm -it redis:3 redis-cli -h10.10.10.10

資料庫 server 端

開發階段在測試的時候,最怕遇到多人共用資料庫的情境,因為會互相傷害影響測試結果。最好的方法還是人人自備資料庫,自己的資料自己建。

如果對機器管理不熟悉,就算照著網路教學自己建好資料庫,但遇到問題或開不起來,也只能重灌治萬病,這樣做的風險也不低。

Docker 在這種情境下,會是個非常好用的工具。container 砍掉重練的效果就等於重灌,而且常見的 image 幾乎都找得到,一個 docker pull 指令就搞定了,連學習安裝過程和踩雷的時間都可以省下來。配合 -p 選項把 port 開放出去,那就跟在本機安裝 server 幾乎完全一樣,非常方便。

docker run --rm -it -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pass mysql
docker run --rm -it -p 5432:5432 -e POSTGRES_PASSWORD=pass postgres
docker run --rm -it -p 6379:6379 redis
docker run --rm -it -p 11211:11211 memcached

指令借我用一下

小弟主要語言為 PHP,建置環境時,偶爾會需要用到 node.js。因為極少用到 node.js,所以不想額外安裝 nvm 等相關套件,但要用的當下又很麻煩,這有什麼方法能解決呢?

Docker 提供了滿滿的大平台,只要透過下面這個 docker run 指令,即可達成「不安裝工具還要能使用工具」的任性需求:

docker run --rm -it -v $PWD:/source -w /source node:14-alpine npm -v

來分解並複習一下這些選項與參數的用途:

  1. --rm 執行完後移除。當要使用功能的時候開 container,功能處理完後移除 container
  2. -it 通常需要跟 container 互動,因此會加這個選項
  3. -v $PWD:/source 把本機目錄綁定到 container,$PWD 為執行指令時的當下目錄,/source 則是 container 裡的絕對路徑
  4. -w /source 是進去 container 時,預設會在哪個路徑下執行指令
  5. node:14-alpine image 名稱,這裡用了 Alpine 輕薄短小版
  6. npm -v 在 container 執行的指令,可以視需求換成其他指令

步驟有點複雜,有個方法是將它拆解成「進 container」與「container 裡執行指令」兩個步驟執行。在不清楚 container 發生什麼事的時候,拆解指令是個非常好用的方法;相反地,了解 container 運作過程的話,合併指令則是方便又直接,大家可視情況運用。

# 先查看目前檔案列表
ls -l

# 使用 shell
docker run --rm -it -v $PWD:/source -w /source node:14-alpine sh

# 進入 container 執行指令
npm -v

# 進入做其他事看看
npm init

# 離開 container 看看,多了一個 packages.json 檔案
ls -l

上面這個範例可以看到,因為有做 bind mount,所以指令在 container 做的事情會同步回 host。簡單來說即:Host 沒有安裝指令也不打緊,用 Docker 啟動 container 幫忙執行後,再把結果傳回給 host。

接著拿上面產生的 packages.json 來做一個正式的範例:

# 確認內容
cat packages.json

# 安裝套件
docker run --rm -it -v $PWD:/source -w /source node:14-alpine npm install

# 確認產生的檔案
ls -l

# 再執行一次觀察差異
docker run --rm -it -v $PWD:/source -w /source node:14-alpine npm install

這個範例主要可以觀察到兩件事:

  1. 第一次執行 docker run 時,因為沒有 packages-lock.json,所以 npm 有產生這個檔案,ls -l 也有看到
  2. 第二次執行 docker run 時,已經有 packages-lock.json 了,所以 npm 做的事跟第一次不一樣

上面這個 docker run 指令即可安裝 packages.json 所需套件。

工程師可以再懶一點

每次打落落長的指令也很逼人,有個簡單的解決方案--設定 alias。

# 確認無法使用 npm
npm -v

# 設定 alias
alias npm="docker run -it --rm -v \$PWD:/source -w /source node:14-alpine npm"

# 確認可以透過 docker run 使用 npm
npm -v

設定好後,打 npm 就等於打了長長一串 docker run 指令了。到這裡讀者也能感受,最後用起來的感覺會跟平常使用 npm 一模一樣,幾乎可以取代安裝工具。

類似的概念可以做到非常多工具上,下面是目前實驗過可行的。

# 使用 Composer
alias composer="docker run -it --rm -v \$PWD:/source -w /source composer:1.10"

# 使用 npm
alias npm="docker run -it --rm -v \$PWD:/source -w /source node:14-alpine npm"

# 使用 Gradle
alias gradle="docker run -it --rm -v \$PWD:/source -w /source gradle:6.6 gradle"

# 使用 Maven
alias mvn="docker run -it --rm -v \$PWD:/source -w /source maven:3.6-alpine mvn"

# 使用 pip
alias pip="docker run -it --rm -v \$PWD:/source -w /source python:3.8-alpine pip"

# 使用 Go
alias go="docker run -it --rm -v \$PWD:/source -w /source golang:1.15-alpine go"

# 使用 Mix
alias mix="docker run -it --rm -v \$PWD:/source -w /source elixir:1.10-alpine mix"

最後要提醒一個重點:這個範例主要是想讓讀者知道 Docker 可以如何應用。實務上,跟安裝好工具的使用經驗還是有所差別的。

路徑差異

因工作目錄是設定 -w /source,也許會因專案設定不同而導致工具執行出錯;另外,工具如果跟某個絕對路徑有相依,如 ~/.npm,這也可能導致出錯。

Kernel 差異

Container Kernel 不同可能會導致無法預期的錯誤。

以 Mac + Docker Desktop for Mac 來說,實際執行 container 是使用 Linux kernel,因此若工具有產生 binary,通常會是 for Linux,這時回到 Mac 直接使用就會出錯。

另外,Container Kernel 不同,在做 bind mount 時會有效能問題,簡單來說就是會跑比較慢一點。

Docker 上跑就沒問題

若讀者有個好習慣是時常用 --rm 的話,那大部分的情況可以把這個標題大聲說出來:「Docker 上跑就沒問題」。

舉個例,laravel-bridge/scratch 套件需要在 PHP >= 7.1 版的環境裡執行單元測試,可以使用下面指令來測試套件在各個環境是否正常:

# 在 laravel-bridge/scratch 上跑測試
docker run -it --rm -v $PWD:/source -w /source php:7.1-alpine vendor/bin/phpunit
docker run -it --rm -v $PWD:/source -w /source php:7.2-alpine vendor/bin/phpunit
docker run -it --rm -v $PWD:/source -w /source php:7.3-alpine vendor/bin/phpunit
docker run -it --rm -v $PWD:/source -w /source php:7.4-alpine vendor/bin/phpunit

全部 pass,這樣就比較有信心跟其他開發者說,在 PHP 7.1 ~ 7.4 上都是沒問題的!

同樣地,我們也可以在 PHP 8.0 beta 上測試,來確保套件在即將發布的最新版上也是能正常運作的:

docker run -it --rm -v $PWD:/source -w /source php:8.0.0beta4-alpine vendor/bin/phpunit

指令補充說明

docker run

  • -w|--workdir 指定預設執行的路徑

今日自我回顧

今天比較像是如何使用 Docker 的小技巧,有些情境都是不想裝或不會裝,所以才會直接拿別人包好的 image 來用;有些情境則是確認使用官方 image 執行可以正常,則不管在哪個用一樣的方法執行也要正常。

  • 練習透過 Docker 建立資料庫,與連線資料庫
  • 練習拆解指令和合併指令,這在下一輪十天說明 build image 會重複使用這個技巧
  • 練習用 Docker 指令配合開發程式碼做測試

參考資料