不合理的要求是磨練:Docker 架站

前言

買一把菜、A一把蔥,在省錢至上的市場上可以說是屢見不鮮,但是買一把菜還要送一盤牛肉這種不合理的行徑,在近幾年來可以說這不是一種常態而是已經變成一種生態,但是對於軟體專案來說你可以放棄測試轉嫁給客戶,你可以減少非必需的文件,但是可以省的事情其實只佔小部分的比例,這些都還不夠客戶砍一次價,既然要決定要留在這種生態中,cost down 是必然會面對的問題,但是如何”不合理的”縮減成本但又不違背自己的良知,那就真的要傷透腦筋了。

測試環境

這次測試環境的設備如下:

  • 一台 PC:原本想在 Raspberry Pi 上測試,不過應該不會接觸到這麼自虐的客戶,所以就找一台低規的 mini pc 來測試,配備為 8GB 記憶體搭上無風扇的 Intel J1900 CPU 的弱雞電腦,第一刀最快的就是縮減硬體成本。
    img
  • 一個固定實體 IP:雖然動態 IP 也有解套方案,不過現在申請固定 IP 也很容易,所以就以固定 IP 來測試,這邊借測的 IP 為 118.163.1.106
  • **一個網域名稱(Domain Name)**:模擬多網站共用 IP,以及註冊 SSL 憑證需要,這邊借測的網域名稱為 beehouse.tw
  • DNS 代管:這邊使用中華電信的代管服務,因為 DNS 更新需要時間,所以我們先設定 將 beehouse.tw 指定到 118.163.1.106,並增加 site1 ~ site4 四組測試用的子網域,並都指向 beehouse.tw。  
    img
  • SSL 憑證:現在主流的瀏覽器都已將 HTTP 傳輸協定列為不安全,所以網站改走 HTTPS 已經變成必要的模式,也有不少廠商提供免費憑證申請,在這邊我們使用 Let’s Encrypt 所提供的憑證。

免錢的最貴

微軟的 Windows Server 與 SQL Server 可以說是很多公司都在使用的系統,但是要光是要”合法授權”,所需的費用就很容易超出客戶預算,用 Linux 取代 Windows Server 可以說是最簡單的解套方案,對於 Linux 達人來說以穩定度著稱的 CentOS 應該是最好的選擇,不過對於半路出家的筆者來說 Ubuntu 或是 Linux Mint 會是更好的選擇,因為有一群專家在持續維護,如果後續還需要花時間解決 Linux 系統產生的問題那就本末倒置了。

也可以參考 DistroWatch 網站,基本上瀏覽排名在前段班的 Linux 都是不錯的選擇。

安裝 Ubuntu

先從官方網站下載桌面版本(Desktop) 載映像檔並燒錄成光碟,目前的**長期支援版(LTS)**為 18.04.1,接下來便是開始安裝 Ubuntu 作業系統,系統安裝過程大概都一直下一步的點選下去即可,以下是筆者比較習慣的做法:

  • 安裝英文版,雖然可以選中文語系,但是為了避免使用者資料夾會變成中文檔名,我們可以先安裝英文版後續再變換語系。
    img

    在系統內可以透過 Language Support 來變更成中文版,
    img
    特別的是筆者在安裝繁體中文後只出現香港的版本,必須移除繁體中文語系再重新安裝一次之後才會出現臺灣的繁體版本。
    img
    將**漢語(臺灣)**拖曳至最上方,登出再重新登入便可切換語系。
    img
    重新登入後,選擇保留舊名稱可以避面後續路徑問題出現。
    img

  • 選擇最小安裝,避免安裝一些不需要的軟體。
    img

  • 選擇自動登入,站在資安立場這是非常不妥的行為,但是總是有客戶會擅自修改密碼卻又會忘記新密碼,讓系統自動登入可以減少他們修改密碼的機會。
    img

    進入系統後可以利用指令 sudo passwd root 來修改 root 帳號的密碼,當然這組密碼最好自己保留就好,這是搶救系統的最後手段。
    img

  • 執行軟體更新,Ubuntu 內建了視覺化的軟體更新程式,在主機有連網狀態下會自動檢測是否有新版程式需要更新。
    img

    當然我們也可以在終端機上透過指令手動更新:
    sudo apt update:更新套件清單。
    sudo apt dist-upgrade:依據套件清單更新套件,dist-upgradeupgrade 的差異請自己上網搜尋相關文章。
    img

為什麼安裝桌面版本而不是伺服器版本,主要降低上手難度,畢竟視窗化的操作介面還是較容易,對於一般客戶來說感覺也會比較”有價值”,當然如果非常在意佔用資源問題的人,最後可以再將系統調整成文字模式(text mode)。

安裝 Docker

透過容器化的技術可以將每個系統隔離開來,避免互相干擾,因此我們需要安裝 Docker,後續要執行的系統也會打包成 Container 來運行,安裝方式可以參考官方說明 Get Docker CE for Ubuntu 或是參考 Docker for Ubuntu

因為是在單一主機上運行,所以我們在 Docker 環境中建立一個名稱為 bridge-network 的橋接網絡(bridge),指令如下:
sudo docker network create bridge bridge-network
sudo docker network create -d bridge bridge-network

img
感謝 Roy Hu 的糾正,漏掉了參數-d,要注意的是這邊的參數是 --driver 的縮寫,container 的操作參數 -d--detach 的縮寫,兩者代表意義不同。

安裝 Visual Studio Code

利用 VS Code 來協助我們管理 Container 是比較簡單又方便的方法,可以省去打指令的時間,雖然到官方下載頁面 下載 deb 檔來安裝是最簡單的,不過筆者比較建案參考 Visual Studio Code on Linux 說明手動安裝,這可以配合 apt 更新指令來更新。
我們透過下列指令來安裝 VS Code 的 GPG 金鑰:
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo install -o root -g root -m 644 microsoft.gpg /etc/apt/trusted.gpg.d/
接著將 VS Code 的遠端儲存庫添加到 apt 的來源清單內:
sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" > /etc/apt/sources.list.d/vscode.list'
安裝 apt-transport-https 套件:
sudo apt install apt-transport-https
更新套件清單:
sudo apt update
接下來就是安裝 VS Code:
sudo apt install code
最後開啟 VS Code 並安裝 Docker 擴充功能,詳細作法請參閱 使用 VS Code 管理 Docker

安裝資料庫:PostgreSQL

提到免費的資料庫,我想不少人使用 MySQL 或是 MariaDB,但是筆者比較喜歡使用 PostgreSQL,因為它內建視覺化管理工具 pgAdmin,對於習慣使用 SQL Server 或是 Oracle Database 的人來說上手難度應該較低,我們在終端機(也可以直接在 VS Code 內透過 ctrl + ``` 來開啟)執行下列指令來建立 PostgreSQL:

1
2
3
4
5
6
7
8
9
sudo docker run \
--name postgres \
-v $HOME/postgres/pgdata:/var/lib/postgresql/data \
-e 'POSTGRES_PASSWORD=vPxhP977DYEw5SvZ' \
-p 5432:5432 \
-d \
--restart unless-stopped \
--net bridge-network \
postgres:11-alpine

img

run:在建立容器後立即啟動。
--name postgres:容器名稱設定為 postgres
-v $HOME/postgres/pgdata:/var/lib/postgresql/data:將資料庫儲存路徑移至 $HOME/postgres/pgdata,當資料庫異常時可以較方便救援,Docker 會自動幫我們建立資料夾。
img
-e 'POSTGRES_PASSWORD=vPxhP977DYEw5SvZ':設定預設帳號(postgres)的密碼,目前設定為 vPxhP977DYEw5SvZ
-p 5432:5432:將容器的連接埠(後面) 5432 與本機連接埠(前面) 5432 串接,方便外部連線。
-d:讓容器再背景運行。
--restart unless-stopped:設定容器自動重新啟動,除非容器原本狀態為停止。
--net bridge-network:將容器的網路與剛所建立的橋接網路 bridge-network
postgres:11-alpine:指定容器要掛載的映像檔,目前是指用 PostgreSQL 11,alpine 是指 Alpine Linux,大部以此 OS 來建置的映像檔都會比較小。

接著我們安裝管理工具 pgAdmin,從第4版開始 pgAdmin 使用網頁來呈現操作介面,也因此很容易封裝成容器,所以官方也提供容器化的版本,安裝指令如下:

1
2
3
4
5
6
7
8
9
sudo docker run \
--name pgadmin \
-e 'PGADMIN_DEFAULT_EMAIL=jonnyhuang@outlook.com' \
-e 'PGADMIN_DEFAULT_PASSWORD=W7JzUjxEvT8CG5rC' \
-p 8081:80 \
-d \
--restart unless-stopped \
--network bridge-network \
dpage/pgadmin4

img

--name pgadmin:容器名稱設定為 pgadmin
-e 'PGADMIN_DEFAULT_EMAIL=jonnyhuang@outlook.com':設定預設帳號的 email。
-e 'PGADMIN_DEFAULT_PASSWORD=W7JzUjxEvT8CG5rC':設定預設帳號的密碼。
-p 8081:80:將容器的連接埠(後面) 80 與本機連接埠(前面) 8081 串接。
dpage/pgadmin4:指定容器要掛載的映像檔 pgadmin4,若沒有指定版本(tag)預設會使用 latest。

透過瀏覽器開啟 http://localhost:8081 ,便可出現登出畫面。
img
登入後便可建立資料庫連線,要注意的是主機名稱需填寫容器名稱
img
連線成功便可開始操作資料庫。
img

建立反向代理 (Reverse Proxy)

因為我們需要多網站共用一組 IP,所以必須建立一個反向代理服務來協助我們將不同的網域名稱指定到對應的網站服務,建立指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo docker run \
--name reverse-proxy \
-v $HOME/nginx/certs:/etc/nginx/certs:ro \
-v $HOME/nginx/vhost.d:/etc/nginx/vhost.d \
-v $HOME/nginx/html:/usr/share/nginx/html \
-v $HOME/nginx/conf.d:/etc/nginx/conf.d \
-v /var/run/docker.sock:/tmp/docker.sock:ro \
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true \
-p 80:80 \
-p 443:443 \
-d \
--restart unless-stopped \
--network bridge-network \
jwilder/nginx-proxy

img

--name reverse-proxy:容器名稱設定為 reverse-proxy
-v $HOME/nginx/certs:/etc/nginx/certs:ro:設定憑證存取路徑,讀取權限為唯讀(ro)。
-v $HOME/nginx/html:/usr/share/nginx/html:設定 nginx 靜態網頁存放路徑。
-v $HOME/nginx/conf.d:/etc/nginx/conf.d:設定 nginx 設定檔存放路徑。
-v /var/run/docker.sock:/tmp/docker.sock:ro:透過掛載 /var/run/docker.sock,讓容器可以與本機的 Docker daemon 溝通。
--label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true:設定標籤,後續說明。
-p 80:80-p 443:443:將容器的 80、443 連接埠與本機串接,這樣連接到本機的 HTTP、HTTPS 請求都會轉送到容器內。
jwilder/nginx-proxy:指定容器要掛載的映像檔,因為此映像檔已經設定好 nginx 的反向代理設定,讓我們可以省略設定步驟。

透過瀏覽器開啟 http://localhost/ 可以看到 nginx 已經可以運行。
img
因為我們已經將網域名稱 beehouse.tw 指定到本機的實體 IP 118.163.1.106,所改以網址 http://beehouse.tw/ 來連結,可以看到相同的頁面,表示外部裝置已經可以透過網域名稱連結到我們的測試主機,當然因為是使用 HTTP 協定,所以被瀏覽器標示為不安全。
img

做到這邊可以發現其實既使我們不了解 nginx,透過容器封裝技術可以很快速的架設起來。

訂閱 Let’s Encrypt 憑證

Let’s Encrypt 憑證雖然可以免費申請,但是它的有效期限只有 90 天,所以原則上我們要自己續訂,但是網路大神無所不在,我們可以利用別人建置好的憑證自動申請/續訂服務來省略申請流程,建立指令如下:

1
2
3
4
5
6
7
8
9
docker run \
--name letsencrypt \
--volumes-from reverse-proxy \
-v $HOME/nginx/certs:/etc/nginx/certs:rw \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-d \
--restart unless-stopped \
--network bridge-network \
jrcs/letsencrypt-nginx-proxy-companion

img

--name letsencrypt:容器名稱設定為 letsencrypt
--volumes-from reverse-proxy:設定讓此容器可以存取 reverse-proxy 容器的內容,這邊要注意是指定容器名稱。
-v $HOME/nginx/certs:/etc/nginx/certs:rw:設定憑證存取路徑,這邊具有寫入權限,因為我們必須提供憑證證書的存放位置。
jrcs/letsencrypt-nginx-proxy-companion:指定容器要掛載的映像檔。

還記得在 reverse-proxy 容器內設定了一組標籤 --label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true,此容器會透過 Docker 來搜尋具有此標前的容器,以便了解是哪個負責提供反向代理。

參考資料:
Running an NGINX Reverse Proxy with Docker and Let’s Encrypt on Google Compute Engine
nginx-proxy
docker -letsencrypt-nginx-proxy-companion

測試

接下來我們便可以開始佈署網站,重點是在建立容器時需要增加3個參數以提供申請 Let’s Encrypt 憑證所需資訊:

  • LETSENCRYPT_EMAIL:設定 Let’s Encrypt 憑證的電子信箱。
  • LETSENCRYPT_HOST:設定 Let’s Encrypt 憑證的網域名稱。
  • VIRTUAL_HOST:設定容器要接收的網域名稱請求,主要是讓 letsencrypt 知道我們所建立的容器服務是要接收來自哪個網域名稱的請求,所以設定會與 LETSENCRYPT_HOST 參數一致。

nginx

建立 nginx 網頁伺服器並指定給 site1.beehouse.tw,建立指令如下:

1
2
3
4
5
6
7
8
9
docker run \
--name site1 \
-e 'LETSENCRYPT_EMAIL=jonnyhuang@outlook.com' \
-e 'LETSENCRYPT_HOST=site1.beehouse.tw' \
-e 'VIRTUAL_HOST=site1.beehouse.tw' \
-d \
--restart unless-stopped \
--network bridge-network \
nginx:alpine

img
瀏覽 $HOME/nginx/certs/ 資料夾,可以發現 letsencrypt 容器服務自動幫我們申請憑證,並將相關的檔案下載回來。
img
瀏覽 https://site1.beehouse.tw/ 可以發現網站呈現 nginx 預設網頁,而且憑證也是有效的。
img
檢視憑證內容確實是由 Let’s Encrypt 所派發的憑證,有 90 天的有效期限。
img

有趣的是如果我們透過 HTTP 協定來開啟網站( http://site1.beehouse.tw/ ),瀏覽器會被自動轉換到 HTTPS 協定來瀏覽,也是就是說 reverse-proxy 非常貼心的自動幫我們切換成 HTTPS 協定。

Apache HTTP Server

同樣地也可以建置 Apache 網頁伺服器,我們將 site2.beehouse.tw 指定到此容器上,建立指令如下:

1
2
3
4
5
6
7
8
9
docker run \
--name site2 \
-e 'LETSENCRYPT_EMAIL=jonnyhuang@outlook.com' \
-e 'LETSENCRYPT_HOST=site2.beehouse.tw' \
-e 'VIRTUAL_HOST=site2.beehouse.tw' \
-d \
--restart unless-stopped \
--network bridge-network \
httpd:alpine

瀏覽 https://site2.beehouse.tw/ 也可以正常開啟。
img

GitLab

接下來就來建立比較有份量的網站,對於專案開發來說版控是必備工具,GitLab 非常佛心的提供打包好的容器讓我們可以快速架設私有版控系統,建立指令如下:

1
2
3
4
5
6
7
8
9
10
11
12
docker run --name gitlab \
-e 'LETSENCRYPT_EMAIL=jonnyhuang@outlook.com' \
-e 'LETSENCRYPT_HOST=site3.beehouse.tw' \
-e 'VIRTUAL_HOST=site3.beehouse.tw' \
--hostname beehouse.tw \
-v $HOME/gitlab/etc/gitlab:/etc/gitlab \
-v $HOME/gitlab/var/opt/gitlab:/var/opt/gitlab \
-v $HOME/gitlab/var/log/gitlab:/var/log/gitlab \
-d \
--restart unless-stopped \
--network bridge-network \
gitlab/gitlab-ce

img
不過 GitLab 需要建立資料庫來存放資料,所以容器啟動時會需要等待一段時間,我們可以查看 log 來了解目前狀態。
img
瀏覽 https://site3.beehouse.tw/ 看到 GitLab 確實正常執行。
img

因為 nginx 預設上傳檔案大小為 1MB,所以既使我們在 GitLab 內增大上傳檔案大小限制,當超過 1MB 的檔案經過 reverse-proxy 時就會被擋下來,所以我們可以在 nginx 內的 conf.d 資料夾修改設定,雖然也可以直接在 default.conf 修改設定,不過這邊我們以增加一個設定檔 custom_proxy_settings.conf 來修改,原因後續會說明,在檔案內設定 client_max_body_size 參數,改成允許的上傳大小即可。
img

Angular + .Net Core

最後一個我們就使用自己撰寫的網站,後端我們使用微軟的”真”跨平台方案 .Net Core 來建置 WebAPI,資料庫採用剛建立好的 PostgreSQL,前端則使用 Google 的開源技術 Angular
先將專案複製到伺服器主機上,開啟 appsettings.json 修改資料庫連線參數。
img
我們透過瀏覽器開啟資料庫管理工具 pgAdmin,先建立系統需要的資料庫 pps
img
利用管理工具的將測試環境的資料庫備份出來,再將它還原到當前資料庫。
img
接著我們編輯 Dockerfile,先透過具有 .Net Core SDK 的容器來編譯程式,這樣主機就不需要安裝 SDK,再將程式與 .NET Core runtime 容器打包在一起成為新的容器映像檔(Image)。
img
在終端機內切換到專案目錄下,透過指令 docker build -t spa-web . 來建置,Docker 會搜尋當前目錄下的 Dockerfile 來執行,最後會建立一個名稱(tag name)為 spa-web 的映像檔。
img
最後透過下列指令來建立容器:

1
2
3
4
5
6
7
8
9
10
docker run \
--name web \
-e 'LETSENCRYPT_EMAIL=jonnyhuang@outlook.com' \
-e 'LETSENCRYPT_HOST=site4.beehouse.tw' \
-e 'VIRTUAL_HOST=site4.beehouse.tw' \
-p 5000:5000 \
-d \
--restart unless-stopped \
--network bridge-network \
spa-web

img
瀏覽 https://site4.beehouse.tw/ 確實也可以正常執行,資料也有呈現,表示與資料庫連結正常。
img

在這邊要特別注意,因為在 Dockerfile 並未指定對外連接埠,所以我們在建立容器時要額外指定,當然能事先設定是最好的。
使用 --expose 只會將容器的連接埠暴露出來,而是用 -p 則會與本機連接埠綁定在一起。

效能

我們透過指令 docker stats 可以監看目前容器使用資料,我們可以看到就一般網站程式而言,其實需要硬體資源的時間只有使用者發出請求到資料處理並回復的這段期間,所以我們可以看到當前待機下使用資源並不多。
img

Docker 的優勢是可以共用硬體資源,當然這是一個雙面刃,這表示任何一個容器都有可能吃掉所有資源,可能造成其他容器服務無法運行,甚至整個系統陣亡,這在程式有 Bug 時最容易發生,因此最好適當的限制每個容器可用的資源,這邊不特別討論。

反向代理在做什麼?

開啟 $HOME/nginx/conf.d/default.conf 可以看到許多設定,因為用戶請求是透過反向代理轉送,所以它會幫我們加上相關的識別 header。
img
接著我們可以看到 site1.beehouse.tw 子網域的設定,最多的大概就是 SSL 憑證設定,如果是要筆者這種新手自己設定大概就直接放棄了,而且它預設已經支援 TLS 所有版本,包括最新的 1.3 版。
img
接著我們看到 HTTP 80 連接埠的設定,我們可以看到它會發出 HTTP 301 的轉址請求,將網址轉換到 HTTPS,這就是為什麼我們之前瀏覽時會被自動轉換的原因。
img
再 HTTPS 443 連接埠的設定上,除了 SSL 憑證設定外,我們還可以看到它也支援 HTTP/2 的 傳輸協定。
img
透過瀏覽器的開發者工具可以看到網頁確實是走 HTTP/2。
img
最後我們來看 location 節點,這邊是設定當街收到來自 site1.beehouse.tw 的請求時要轉送的位置,裡面的 site1.beehouse.tw 是對應 upstream 節點的名稱,因此最終會連結到 172.18.0.2:80,172.18.0.xxx 網段是我們上面建立 bridge-network 橋接網路的網段,任何容器有連結 Docker 都會配置一組 IP 給它。
img
所以我們也可在主機上透過此 IP 瀏覽容器的網站。
img

如果要建立多個容器來做負載平衡(Load Balance),可以將相關容器的 IP 都加到對應的 upstream 節點內即可。

我們可以看 site4 連結的是我們開啟的 5000 連接埠而不是 80,這也意味著 letsencrypt 在掃描時會以該容器提供的連接埠當作輸出。
img

比我們想得更聰明

當我們將 site1 ~ site3 的容器都給停止只保留 site4 時,我們可以看到 letsencrypt 自動幫我們移除相關憑證。
img
而且我們可以看到 default.conf 設定檔 site1 ~ site3 相關設定也一併被移除,也就是說 letsencrypt 並不是只有執行一次,而是每當容器有異動時都會立即掃瞄並修改設定,所以我們自己要客製化的設定最好另外建立設定檔存放,避免被影響。
img

整個實作過程其實可以發現我們只有設定少數參數,但是我們的主機已經是一台支援最新標準的網頁伺服器了,而且也已經做好相關優化了,這要是在 Windows Server 上透過 IIS 來建置網站,應該不是這麼簡單就可以完成的。

後記

就像報告班長電影裡的名言:”合理的要求是訓練,不合理的要求是磨練”,把花在抱怨一切的不合理的時間拿來思考可行方案,你會發覺另一個名言:”生命會自己找到出路”,過去所學的技術透過非正規的方法可以延伸出更多可能。

Cloudflare

筆者後續改用 Cloudflare 測試 DNS 託管,它提供每組帳號一個免費託管服務,而且更新速度很快,同時也有提供免費的 SSL 憑證以及一些基本的資安服務,這無疑就像多了一個硬體防火牆對網站也是多一層保護。

ufw

Ubuntu 內建了 ufw 防火牆,我們可以透過它在強化系統安全,筆者用到指令如下:

  • sudo apt install ufw:安裝,如果您的系統未安裝可以透過此指令來安裝。
  • sudo ufw status:查看防火牆狀態。
  • sudo ufw enable:開啟防火牆。
  • sudo ufw disable:關閉防火牆。
  • sudo ufw default allow:預設防火牆規則為允許。
  • sudo ufw default deny:預設防火牆規則為不允許。
  • sudo ufw allow ssh:允許 ssh 穿過防火牆。
  • sudo ufw allow in 80:允許 80 連接埠穿過防火牆。

參考資料:https://wiki.ubuntu-tw.org/index.php?title=Ufw

當然不想記指令可以安裝 GUI 工具 gufw,透過指令 sudo apt install gufw 來安裝,便可透過圖形操作來設定。
img

ClamAV

ClamAV 是一套開源的防毒軟體,很多 NAS 上都會預載這套軟體來掃毒,我們可以透過指令 sudo apt install clamav 來安裝,如果不想記指令的可以再安裝 GUI工具 ClamTk(sudo apt install clamtk),只是操作時要注意按鈕點選方式是連點2下(double click)。
img
預設 ClamAV 是被動掃描,也就是要我們自己手動來執行掃毒工作,我們可以再安裝 ClamAV-daemon(sudo apt install clamav-daemon),讓 ClamAV 常駐運行。

參考資料:https://help.ubuntu.com/community/ClamAV