[Docker Note] PyLadies TW 官方網站開發環境製作|開發筆記

Ichi Tsai
Ichi’s Dev Blog
Published in
11 min readNov 22, 2018

大家好,我是本次負責 UI 設計的美宣志工 Ichi,PyLadies 官網改版的第一階段在今年 9 月的週年慶上線了(撒花),集結了幾個月來小組線上討論、實體聚會以及上線前趕工的各方努力!志工群下一步招募了官網小組會接手繼續第二版之後的工作。

由於第一版的環境都是在同一台 server 進行,開權限讓前後端工程師登入工作。接下來團隊擴編,希望參與者都有本機的統一環境,於是有了這次的任務:將開發環境容器化。

目前初版環境已經出來了,本文目的是為了要記錄包 docker 的心路歷程及踩到的雷,會分成「成功的版本」、「嘗試過的方向」兩部分進行。在「嘗試過的方向」中,是幾條後來沒有實作到最後的路,不過還是記錄下來供以後參考。

成功實作的版本

官網環境 docker 架構圖

以 docker-compose 來串 microservice 們。具體的架構如上圖,我們有一台 production DB、一台 test DB,能用 phpmyadmin 登入,同時兩台 DB 也能讓 flask app 那台有 python 環境的容器 access,再來架一台 nginx 能支援靜動態網頁導流。

執行畫面

原本資料夾內沒有 data 及其子資料夾,在 repo 最外層執行 docker-compose up 會自動創建並對應容器內指定位置。然後至各服務指定 port 可以開啟對應服務:phpmyadmin 因為有設定 2 個 DB,這邊有選單可以選擇。然後可以在 nginx 的 5555 port 看到他和 55688 的 flask 有一樣的結果代表成功導流。

原本沒有 data 以及子資料夾, 執行後就會自動創建並且對應至指定路徑
phpmyadmin 畫面,因為有設定兩個 DB,可以選擇伺服器
flask 和 nginx 的 port 都有成功連通
靜態網頁

Docker Related Codes

docker-compose.yaml

包 docker 基本的流程與概念

  1. image 指定版號:不管是寫 Dockerfile 的 FROM 或是 docker-compose 裡的 image,最好指定版號(例如:nginx:1.14-alpine),才能真正做到統一環境。因為如果不指定版號則預設是 latest,build 的時候會去檢查最新版,導致 image 從頭重 build(因為是最底層 layer)。
  2. docker instance 沒事做就會 stop(掛掉):所以一開始常常會遇到,明明 image build 過了 docker run 卻沒有活著,馬上進入 docker ps -a 才能看到的墓地。所以在需要 debug 的時候,最後可以加入 CMD while :; do sleep 1 ; done 或是參考其他 Dockerfile 的 CMD 讓 docker instance 有事做(e.g., nginx -g 'daemon off;')。
    補充:如果像 nginx 就算跑起來也需要等待其他 container 起床才能有效運作,在 docker-compose 裡可以加個 restart: always 就會一直重啟直到可以吃到其他 host。這段如果目前還看不懂,後面的筆記會再解釋一次。
  3. DB 的資料如果已經存在則不會重新 init:使用 mariadb 的 image 有參數MYSQL_DATABASE可以直接自訂一個資料庫,但更改這邊的名字再重新啟動 instance 時,如果沒有把 mount 進去的資料夾刪掉,則視為已經存在資料,所以不會做新的 DB。
    其實這 behavior 滿正常,沒有人會希望資料隨服務重啟就不見,只是在做 docker 測試或開發的時候會鬼打牆,覺得設定的新密碼怎麼沒用、資料庫沒有生出來之類的。
  4. Dockerfile 通常在 repo 最外層:像是 python 的 requirements.txt 有指定套件版本,製作環境的時候就要先將它複製到 container 裡做 pip install -r requirements.txt ,所以在執行 docker build or docker-compose up 的資料夾要是能看得到 requirements.txt 的,因此建議 Dockerfile 要在 repo 最外層。
  5. virtual environment 與 docker:對 python 開發有點基礎的話會知道用 venv 來控制每個專案的套件定版,這件事情在轉移到 docker 上的時候,因為通常這份 docker image 就是給這個專案,所以不需要再做出虛擬環境。
  6. 開發版本 mount code 進去:如果去參考 production Dockerfile 的寫法,他們會將整個 repo copy 進去( COPY . . 前面的 . 是目前所在資料夾,後面的 . 是 container 內的工作資料夾),但因為我們現在就是要邊改動 code 然後邊在容器測試運行,所以除了 requirements.txt 這種檔案其他就是啟動 instance 的時候帶 -v 就可以了。(具體請參考 docker-compose)

成功版本兩三事:

  1. docker-compose depends_on參考連結)container 可以寫 depends-on 來表示依賴關係,但是不代表會等到所依賴的 container 已經 ready 了才啟動現在這個。例如 app depends_on DB,但在 DB 啟動後 app 容器會馬上啟動,這時可能 DB 還沒 ready 所以有些環境設定連不到 DB port 就使設定失敗,有可能沒有成功啟動服務沒事做導致容器直接掛掉。
  2. healthcheck 只在 docker-compose v2.1:參考連結)在 v2.1 可以使用 healthcheck 並且依賴者可以等到 service_healthy 再開始啟動。(但version 3 沒有支援 depends_oncondition 語法:參考連結
  3. supervisord daemon :在 Dockerfile 的 CMD 寫 supervisord daemon 啟動,會發現他吃不到 docker-compose 給的 env,但是進 instance 內 debug 會發現 env 都在,所以猜測是生出 daemon 的 shell 與 docker-compose 生出來的不同。後來這邊改成在 Dockerfile 內 foreground 執行。
  4. link 與 docker-compose:參考連結)如果從 docker run 去設定容器間的連結,會看到 —-link 這個關鍵字,但因為在 docker-compose 中其實就已經會生成一個網路給 compose 內的容器們能用名字互通(service 的名字),所以不需要再寫 link
  5. flask —-host 0.0.0.0flask 如果是單機版只需要聽 localhost, 但這次實作將 flask 跟 nginx 切成不同容器,所以要改成聽 0.0.0.0 才會通。
  6. nginx 設定連到其他容器:前面有提到 docker-compose 內的容器用名字互通,具體在 nginx 的設定裡需要定義 upstream 再寫到 proxy_pass參考連結),注意這邊因為 appdocker-compose 內 service 的名稱,所以必須等到 app container ready 才能連到,在連到之前 nginx 要一直 restart 避免直接掛掉:
http{
upstream your-app {
server app:5000; # 注意是 container 裡的 port
}
server:{
proxy_pass http://your-app;
}
}

使用 docker-compose 的好處:

  1. 開發客製化容器的時候會開開關關:run docker run! Oh no, stop, stop, stop! 在過程中製造很多屍體,要一一清掉 or docker container prune ,有時候會突然忘記有沒有停掉而卡住。或是有不同的 docker image 要 buildrun,方向鍵一路向北容易手殘跑錯。這時使用 docker-compose 永遠只會用到 docker-compose up -d, docker-compose down, docker-compose build。(頂多想要 debug 進去容器是 docker-compose exec {service_name} bash
  2. 有意義的命名:剛剛說到的 docker stop 要去找 docker ps 裡的名稱或 id 做指定。沒有帶—- name 的話 docker run 會自動給個兩個 random 英文字的名字,沒有意義;但 compose 的預設命名會是有意義的,請看下圖 NAMES 那欄第一個名字以及後面的系列:
docker run 有隨機命名而 docker-compose 的預設名稱有意義

圖中指令多了一個 grc,grc 套件用來上色,不然有時候名字太長凸到換行很難對齊欄位就難過了。

嘗試過的方向

  1. 單機版一個 image 到底:因為原本的環境是單機版本,安裝說明也寫得很詳細,所以不知不覺就按著流程裝下來。裝到互動式選單的時候卡很久,去搜尋關鍵字發現很多人其實都做過這幾個組合的 docker-compose file 了,感覺可以無痛搞定,所以就跳了。
  2. 分成 DB、web 2 個 image:這也是一個方向,只是要注意如果有共同要安裝的東西(e.g., vim, curl, htop, networktools),最好獨立一個 image 出來,然後DB 跟 web 再在其上發展。不然前幾行很像在 duplicate code,而且因為是基本工具所以都在最底層動 script 造成 2 個 image 重 build 就很虧了。

其他 tip 補充

  • timezone 要先裝 tzdata, 16.04 沒有:參考連結
  • nginx config:官方說明會教你用 docker run 起一個 instance 然後 docker copy 拿出預設 config 檔來改,在官方 repo 也可以直接拿到檔案就是了。注意 http { include /etc/nginx/mime.types; } 不能刪掉,會造成 css 以 plantext 傳輸,render 不出好看的網頁。
  • 互動式選單解法,先寫好選項:phpmyadmin 會跳出對話式選單做選項安裝,這時可以先以 debconf-set-selections 寫好選項,具體作法請參考連結
  • add-apt-repository ppa:ondrej/phpC.UTF-8 的設定下爛掉:可以先安裝 locales 套件去生出 en_US.UTF-8
    apt-get install locales && locale-gen en_US.UTF-8 && LC_ALL=en_US.UTF-8 add-apt-repository -y ppa:ondrej/php
  • install software-properties-common before add-apt-repository不然會遇到 command not found
  • mysql 編碼使用 utf8mb4:參考連結)因為 unittest 的時候在 DB insert 中文字串有問題,所以發現資料庫編碼要改設定,具體作法請參考「成功版本」段落 — Docker Related Codes.
  • 如果需要不同的自訂 Dockerfile,可以指定檔名跟資料夾位置:參考連結)例如有兩個以上容器都需要客製化 image 則可以指定 build 的路徑
build:
context: ./dir
dockerfile: Dockerfile-alternate

Reference

--

--

Ichi Tsai
Ichi’s Dev Blog

A proactive and helpful individual who values integrity above all else. Have both backend engineering experience and project management skills.