用 Docker 建立多種資料庫的開發環境

前言

上一篇 用 Docker 建立不同 Angular CLI 版本的開發環境 我們利用 Docker 將 Angular CLI 封裝在容器(Container)內,其實筆者一開始是想透過 Docker 來執行資料庫,因為我們可能因應客戶環境不同系統後端所對應的資料庫也不同,因此常常需要安裝不同類型的資料庫以便系統開發,不過如果本機端安裝多套資料庫除了增加 CPU 效能及記憶體的負擔外,相容性也是個問題,以往比較快速的做法就是安裝虛擬機(Virtual Machine),針對每個專案來建置對應的開發環境,不過,在測試過程在 Windows 環境遇到一些問題,所以就先擱置了。

Docker for Ubuntu

查閱 Docker storage drivers 可以看到 Docker 所支援的都是 Linux。
img
看起來官方也是建議 Windows 與 Mac 系統僅是合作開發環境使用。
img
所以在 Linux 環境執行應該還是首選,可以避免掉一些問題發生,對於 Windows 環境的人,可以直些利用 VirtualBox 或是 VMware Workstation Player 來建置 Linux 的虛擬主機,筆者便是在 VM 上安裝 Ubuntu 17.10 來測試 Docker。
img
參考官方教學 Get Docker CE for Ubuntu,我們開啟終端機並依教學步驟安裝 Docker,首先先移除舊版 Docker,指令如下:
sudo apt-get remove docker docker-engine docker.io
因為全新的環境所以會出現找不到可移除的訊息。
img
接著先更新套件庫,指令如下:
sudo apt-get update
img
再來安裝 apt-transport-httpsca-certificatescurlsoftware-properties-common,指令如下:
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common -y
img
安裝官方 GPG 金鑰,指令如下:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
img

可以透過指令 sudo apt-key fingerprint 0EBFCD88 來驗證指紋金鑰是否為
9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88
img

透過下列指令將 Docker 安裝網址加入套件庫來源。
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
img

再次透過 sudo apt-get update 指令更新套件庫,並透過下列指令安裝 Docker 社群版。
sudo apt-get install docker-ce -y
img
我們可以透過 docker -v 來確認版本以及 Docker 是否可運行。
img

Volume

Docker 的映像檔(Image) 可以說是由**唯讀的容器(Container)**堆疊而成,既然是唯讀就代表無法去修改上層的容器內容,我們只能在當前容器可讀寫區域去建立新檔案來取代既有檔案,這個好處就是大家再引用映像檔時不用怕因為有人竄改而導致自己的系統出問題,所以在引用映像檔時最好加上版本號(Tag Name),當然相對的也會衍生出另一個問題,如果將資料庫封裝在容器來執行,這種會不斷修改檔案的程式不就很容易造成容器過度膨脹。

大部分在 Docker Hub 上 Tag Name 都會使用版本號來做區別,當然這並不是鐵則,創作者也可能會隨時修正一些 Bug 但是卻沿用既有的 Tag Name,不過這種善意的調整大部分都不會修改既有的運行模式,因此盡量使用官方釋出版本或是下載率或評鑑高的映像檔是最保險的作法,因為這些映像檔幾乎都會在 GitHub 上釋出 Dockerfile,我們可以檢視修改的差異,甚至可以抓取舊版來還原原本的映像檔。

Docker 提供另一種容器 Volume 來解決著個問題,我們可以把它視為獨立的可讀寫的容器,更重要的是 Volume 不會隨著容器的移除而跟著移除,我們在建立容器時可以搭配 --volume (縮寫:-v) 指令參數,總共可以帶入3個參數值(值之間使用 : 分隔),預設至少要有一個參數值,該值為容器內的資料夾或是檔案路徑,表示將該資料夾或是檔案移轉到 Volume 內以便修改。
例如我們建立一個名稱為 c1 的容器,載入 alpine:3.6 的映像檔,並將 alpine 內的 home 資料夾移轉到 Volume,指令如下:
docker run --name 'c1' -v '/home' -it -d alpine:3.6

因為我們並沒有要求容器啟動後要執行什麼程式,預設便會動關閉,所以我們加上 -it 參數來讓容器不要關閉,並讓我們可以連結通訊。

執行後可以發現會回傳一組雜湊碼(hash codes) 3372e927b683xxxxdaa5,此雜湊碼代表所建立容器的 Id。
我們可以使用 docker container ls 指令來檢視目前已經啟動的容器,即可發現剛剛所建立容器已經啟動(列表只會顯示 Container Id 前12碼)。
接著我們透過 docker volume ls 指令來檢視目前系統有哪些 Volume 被建立,可以發現目前由一組名稱為 bf0f77d36932xxxx29ab 的 Volume。
img
最後我們利用 docker container inspect -f '{ {json .Mounts } }' c1 指令來查看容器 c1 的構造資訊。

--format(縮寫 -f) 表示要輸入的內容格式,由於容器資訊很多,所以加上塞選條件讓它只顯示 .Mounts 的資訊(Mounts 儲存 Container 與 Volume 的聯結資訊),再加上 json 讓它輸出成 JSON 格式。

img
格式化後結果如下,Container c1 掛載了 Volume bf0f77d36932xxxx29ab,並取代既有的 home 資料夾。

我們也可以發現 Docker 會在 /var/lib/docker/volumes/ 目錄下以一組雜湊碼來建立一個內涵 _data 資料夾的資料夾,並當作 Volume 的存放位置,當我們沒有提供 Volume Name 時 Docker 便會將該雜湊碼當作 Volume 名稱。

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"Type":"volume",
"Name":"bf0f77d36932018bb09667867f1cfc755b61639ea5be001f3c975f38ed3c29ab",
"Source":"/var/lib/docker/volumes/bf0f77d36932018bb09667867f1cfc755b61639ea5be001f3c975f38ed3c29ab/_data",
"Destination":"/home",
"Driver":"local",
"Mode":"",
"RW":true,
"Propagation":""
}
]

嘗試連結到容器內,並在 home 資料夾內建立一個名為 abc 的資料夾,檔案管理工具開啟 Volume 資料夾,可以發現確實產生了一個 abc 資料夾,以此可確認容器的 home 資料夾確實被 Volume 取代。
img

接下來我們同樣再次建議一個容器,只是這次我們提供 Volume Name v2,加入位置是在容器路徑之前,並使用 : 區隔,指令如下:
docker run --name 'c2' -v 'v2':'/home' -it -d alpine:3.6
這一次可以發現 Volume Name 不再是雜湊碼,而是我們給予的名稱 v2
img
檢視容器 c2 資訊可以看到確實掛載 Volume ‘v2’。
img

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"Type":"volume",
"Name":"v2",
"Source":"/var/lib/docker/volumes/v2/_data",
"Destination":"/home",
"Driver":"local",
"Mode":"z",
"RW":true,
"Propagation":""
}
]

接著我們改用本機端的實體路徑(/home/jonny/Public/v3)來取代 Volume Name,指令如下:
docker run --name 'c3' -v '/home/jonny/Public/v3':'/home' -it -d alpine:3.6
可以發現這一次並沒有產生 Volume。
img
檢視容器 c3 資訊可以看到,Type 屬性不再是 volume 而是 bind,由此可知它是直接連結到本機資料夾。
img

1
2
3
4
5
6
7
8
9
10
[
{
"Type":"bind",
"Source":"/home/jonny/Public/v3",
"Destination":"/home",
"Mode":"",
"RW":true,
"Propagation":"rprivate"
}
]

筆者在 Windows 上便是在此出問題,當你建立資料庫的 Container 時,一般會將資料庫的資料儲存路徑給搬移出來方便,除了避免容器過度臃腫外也方便備份與救援,當我們透過建立 Volume 來存放時都是正常的,因為 Volume 其實是建立在 Hyper-V 內由 Docker 所產生的虛擬機內,但是當使用指定路徑(Windows 系統所在路徑)時就會出許多錯誤訊息(檔案建立失敗或是權限問題),不知是否是 Windows 是採用 NTFS 檔案系統之故。

一般我們透過 --volume (縮寫:-v) 參數來掛載 Volume,我們可利用第3個參數加入 ro (ReadyOnly 表示唯讀),讓所執行的 Container 只能讀取 Volume 的資料而沒有寫入的權限。
雖然官方建議改用 --mount 指令,不過因為相關文件不多,所以筆者先採用比較通用的舊版指令 -v 來執行。

建立資料庫:PostgreSQL

搜尋 Docker Hub,可以找到由官方釋除的 PostgreSQL 映像檔,執行下列指令:
docker run --name 'PostgreSQL' -v '/home/jonny/Public/pgdata':'/var/lib/postgresql/data' -e 'POSTGRES_PASSWORD=X4w5s6y4dKEG' -d -p 5432:5432 postgres:10.3-alpine

-v '/home/jonny/Public/pgdata':'/var/lib/postgresql/data':將容器內 PostgreSQL 資料庫儲存路徑移至外部。
-e 'POSTGRES_PASSWORD=X4w5s6y4dKEG':設定預設帳號 postgres 的密碼為 X4w5s6y4dKEG
-d:讓容器再背景運行。
-p 5432:5432:將容器的 5432 連接埠(port)與本機的 5432 連接埠綁定。

img
從檔案管理工具可以看到 PostgreSQL 相關資料確實被改寫到 /home/jonny/Public/pgdata 目錄下。
img

接著我們安裝 PostgreSQL 比較常用的管理工具 pgAdmin 4,從官方網址可以看到他們已經提供 Docker 版本。
img
開啟 Docker Hub 連結(dpage/pgadmin4),依文件說明加入對應的參數來執行,指令如下:
docker run --name 'pgAdmin4' -e 'PGADMIN_DEFAULT_EMAIL=denhuang@gmail.com' -e 'PGADMIN_DEFAULT_PASSWORD=X4w5s6y4dKEG' -d -p 8081:80 dpage/pgadmin4

-e 'PGADMIN_DEFAULT_EMAIL=denhuang@gmail.com'-e 'PGADMIN_DEFAULT_PASSWORD=X4w5s6y4dKEG':註冊一組 pgAdmin4 的管理帳號,此帳號是開啟 pgAdmin4 的帳號,並非是 PostgreSQL 資料庫的帳號。
-p 8081:80:將容器的 80 連接埠與本機的 8081 連接埠綁定。

img
目前架構如下:
img

瀏覽 http://localhost:8081/,出現登入畫面,這邊便是要輸入剛才註冊的帳號。
img
登入後便開始建立連線。
img
但是可以發現連結本機(localhost)的 PostgreSQL 資料庫會出現失敗。
img
這是因為 localhost 指的是 pgAdmin4 這個容器本身,而不是我們預想的 Ubuntu 本機。
img
改由 Ubuntu 本機的 IP (192.168.110.131) 就可以正常連線,這是因為我們在建立容器時已經將容器內部的連接埠與外部本機的連接埠綁定。
img
img
我們查詢容器 PostgreSQL 的網路設定,指令如下:
docker container inspect -f '{ {json .NetworkSettings.Networks } }' PostgreSQL
img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"bridge":{
"IPAMConfig":null,
"Links":null,
"Aliases":null,
"NetworkID":"0268147797e66c0f0a4b46fa7a427cf905187b70383b3b95d49ff3375f732b9d","EndpointID":"b712263bdd200999ccb6aabf08c86f8d3a90dfe2641a563d95628c9507099e23",
"Gateway":"172.17.0.1",
"IPAddress":"172.17.0.2",
"IPPrefixLen":16,
"IPv6Gateway":"",
"GlobalIPv6Address":"",
"GlobalIPv6PrefixLen":0,
"MacAddress":"02:42:ac:11:00:02",
"DriverOpts":null
}
}

可以看到容器的 IP 為 172.17.0.2,利用這組 IP 也可以連線。
img
這是因為 Docker 安裝時會建議一組 docker0 的橋接器,預設容器會透過它來通訊。
img
img

自訂橋接器:Bridge

既然預設已經有一個 docker0 的橋接器(bridge)了為什麼還要在自己建立一組,我們從官方網站可以查到自訂跟預設還是有差異,其中自訂的橋接器可以透過 Container Name 來彼此通訊。
img

參考資料:Differences between user-defined bridges and the default bridge

透過下列指令建立一組新的橋接器。
docker network create -d bridge bridge-network

-d--driver 的縮寫,表示驅動模式,目前我們是選用 Bridge Mode。
可以透過 docker network ls 查看 Docker 的網路清單。

img
接著我們先移除既有的容器 PostgreSQL、pgAdmin4,並再次重新建立,建立過程多加入 --network bridge-network 參數。

可以透過 VS Code 快速移除。
img

容器建立指令如下:
docker run --name 'PostgreSQL' -v '/home/jonny/Public/pgdata':'/var/lib/postgresql/data' -e 'POSTGRES_PASSWORD=X4w5s6y4dKEG' -d -p 5432:5432 --network bridge-network postgres:10.3-alpine
docker run --name 'pgAdmin4' -e 'PGADMIN_DEFAULT_EMAIL=denhuang@gmail.com' -e 'PGADMIN_DEFAULT_PASSWORD=X4w5s6y4dKEG' -d -p 8081:80 --network bridge-network dpage/pgadmin4
img
這一次我們利用容器名稱來連接,這種連接模式在開發測試過程似乎會比較方便,因為就好像是透過主機名稱連結遠端主機上。
img

建立資料庫:MariaDB

參考 Docker Hub 上的 MariaDB 官方資訊,我們可以建立 MariaDB 的容器,網路一樣透過加入 --network bridge-network 參數來加入到我們自訂的橋接器上,指令如下:
docker run --name 'Mariadb' -v '/home/jonny/Public/mysql':'/var/lib/mysql' -e 'MYSQL_ROOT_PASSWORD=admin' -e 'MYSQL_USER=user' -e 'MYSQL_PASSWORD=X4w5s6y4dKEG' -d -p 3306:3306 --network bridge-network mariadb:10.3 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
管理介面則安裝最常用的 phpMyAdmin,從官方文件 得知我們可以設定預設連線主機,將 PMA_HOST 設定為資料庫的 Container Name Mariadb,連接埠則對應到 8082,完整指令如下:
docker run --name 'phpMyAdmin' -v '/home/jonny/Public/phpmyadmin/config.user.inc.php':'/etc/phpmyadmin/config.user.inc.php' -e 'PMA_HOST=Mariadb' -e 'PMA_PORTS=3306' -e 'PMA_USER=user' -e 'PMA_PASSWORD=X4w5s6y4dKEG' -p 8082:80 -d --network bridge-network phpmyadmin/phpmyadmin:4.7
img
開啟 http://localhost:8082/,便可開始操作資料庫。
img
目前架構如下:
img

建立資料庫:Microsofr SQL Server 2017

微軟在 SQL Server 2017 推出了 Linux 版,同時也在 Docker Hub(microsoft/mssql-server-linux) 上提供映像檔,比較特別的是我們須加入 ACCEPT_EULA=Y 參數代表我們已經同意授權模式,若不代 MSSQL_PID 參數預設就會安裝開發版(Developer),完整指令如下:
docker run --name 'MSSQL2017' -v '/home/jonny/Public/mssql':'/var/opt/mssql' -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=X4w5s6y4dKEG' -e 'MSSQL_PID=Developer' -p 1433:1433 -d --network bridge-network microsoft/mssql-server-linux:2017-latest
img
因為 MSSQL 管理工具 SQL Server Management Studio(SSMS) 只有 Windows 版本,所以改從遠端的電腦(IP:192.168.110.1)連線測試。
img

筆者特別先清除本機上的映像檔,整個安裝過程由網路下載映像檔到整個安裝設定完成不用到2分鐘,以往那種費時的安裝過程現在已經沒有了。
img

比較特別的是在建立資料庫時會出現錯誤,由錯誤訊息來看應該是與資料庫儲存路徑有關,大概是 SSMS 無法正確讀取 Linux 路徑,而又傳遞包含空值(null)儲存路徑的語法給資料庫,造成資料庫處理發生異常。
img
目前我們手動下語法來解決此問題,簡單的說就是不要指定儲存路徑讓資料庫使用預設路徑來建立,或者是傳遞正確的儲存路徑給資料庫。
img
目前架構如下:
img

後記

我們可以透過指令 docker stats 來查看目前容器使用資源的狀況,可以發現既使3個資料庫都開啟的情況下,待機下也才耗費1G多的記憶體,CPU的資源也使用非常少,對於開發環境其實幫助很大,尤其我們還關閉不需要使用的資料庫,隨時依開發需求只啟用必要的資料庫,這種秒開秒關的方式應該很適合有點惰性的我。
img
我們從 Docker Store 上其實也可看到企業版的 Oracle Database Enterprise EditionMySQL Server Enterprise Edition,在 DB-Engines 排名上比較常見的資料庫應該都可以找到 Docker 的映像檔。
img
雖然筆者習慣將 ContainerVolume 都給予名稱,但是若考量到系統佈署時就不見得很合適了,因為當主機上要執行的容器很多時可能會有名稱重複的問題,更不用說要叢集化管理多台主機時,給予名稱可能會增加平衡負載的困難,不過事事無絕對,如果事先討論制定好命名規則,對於中小型系統名稱可以更便於管理。