第一天學習 Docker 該知道的基礎知識
先聲明這是一個讀演算法的人玩 NAS 半路出家想說自己寫個 Docker 邊玩邊學的記錄。
前言
身為 NAS 用戶,Docker 最方便的就是怎麼搞都不會壞,不像傳統程式搞壞之後高機率有奇怪的殘留設定清不掉有夠搞人,有問題就是把 mount 的東西全部 rm -rf 就清潔溜溜。不過一開始單純作為使用者使用真的不好理解,尤其是 Docker 和虛擬機的差別。
這篇文章包含:
- Docker 介紹,說明和傳統虛擬機的差異
- 如何撰寫 Dockerfile 並構建自己的 Image
- 解決 Matplotlib 的中文字體 (CJK) 問題,解法同時適用於 Docker 以外的環境
- 如何撰寫 Docker for Github Actions 完成 Python CI
- 常用指令小抄
Docker 和虛擬機差異
或者說 Docker 到底如何比虛擬機還快,這是我一開始最疑惑的點。
簡單來說 Docker 就是一個超輕量化的虛擬機,和虛擬機最大差異在不用模擬硬體(所以你不會看到在 docker 上安裝驅動程式,但是 VMware/QEMU-KVM 要),並且共用宿主 kernel,這就是讓他輕量快速的原因。以宿主機配備 i5-7400 16G ram 而言,開一個虛擬機等同一台完整主機,記憶體分配宿主機 10G 虛擬機 6G 就差不多了,但是我在裡面架設的 ubuntu server 中開了十多個容器,加上 ubuntu 本身作業系統還吃不到 2G。
接著馬上介紹剛剛提到的新名詞「容器」。Docker 主要組成為
- image 鏡像
- container 容器
- volume 卷宗
- network 網路
這四項組合而成。Dockerfile 作為設計圖設計這個容器該用哪些 image,再用 volume 把內部儲存空間掛載到宿主機資料夾,network 設定網路,最後合起來是運行時的實體、消耗記憶體和 CPU 資源的叫做 container。上面那隻鯨魚是 Docker Hub,是類似 Github 的地方,把 build 好的鏡像讓大家使用。
Docker 的好處
講完原理那他好在哪呢?
-
快速方便的跨平台部署
最大優勢是「跨平台」特性。只要打包成 Docker 映像檔,在全平台都能一致運行。 -
輕量級的虛擬化
Docker 容器比傳統虛擬機輕量,因為它們共享主機的作業系統核心。啟動快,資源佔用少。 -
快速擴展和更新
需要擴展應用程式時,使用 Docker 可以輕鬆啟動多個相同容器來應對負載增加。更新應用時,只需更新映像檔並重新部署容器。
撰寫 Dockerfile
沒那麼難理解只是資源太分散,簡易版本是 python slim,還加上兩段構建版本,以及 --virtual
方式共三個版本:
- slim (basic)
- alpine with virtual
- alpine with multi-stage build
FROM python:3.10-slim
WORKDIR /app
COPY . /app
RUN apt-get update && \
apt-get install -y rsync fonts-noto-cjk && \
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
VOLUME ["/mnt/local_path", "/mnt/remote_path", "/app/data"]
ENTRYPOINT ["python", "-m", "p5d"]
FROM python:3.10-alpine
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY . .
RUN apk add --no-cache fontconfig libstdc++ rsync
RUN apk add --virtual .build-deps build-base curl \
&& mkdir -p /usr/share/fonts/opentype/noto \
&& curl -L -o /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
https://salsa.debian.org/fonts-team/fonts-noto-cjk/-/raw/debian/unstable/Sans/OTC/NotoSansCJK-Regular.ttc \
&& pip install --no-cache-dir -r requirements.txt \
&& apk del .build-deps \
&& apk del build-base
VOLUME ["/mnt/local_folder", "/mnt/remote_folder"]
ENTRYPOINT ["python", "-m", "p5d"]
# Build stage
FROM python:3.10-alpine AS builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY . .
RUN apk add --no-cache build-base curl
RUN mkdir -p /usr/share/fonts/opentype/noto
RUN curl -L -o /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc https://salsa.debian.org/fonts-team/fonts-noto-cjk/-/raw/debian/unstable/Sans/OTC/NotoSansCJK-Regular.ttc
RUN pip install --no-cache-dir -r requirements.txt
# Final stage
FROM python:3.10-alpine AS final
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /app /app
COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages
COPY --from=builder /usr/share/fonts/opentype/noto /usr/share/fonts/opentype/noto
RUN apk add --no-cache fontconfig libstdc++ rsync
VOLUME ["/mnt/local_folder", "/mnt/remote_folder"]
ENTRYPOINT ["python", "-m", "p5d"]
解釋 slim (basic) 版本的構建指令:
FROM python:3.10-slim
: 使用輕量級的 Python 基礎鏡像,體積較小但功能齊全WORKDIR /app
: 設定工作目錄,告訴 Docker 在容器內的/app
目錄中執行後續命令COPY . /app
: 將當前目錄的所有文件複製到容器內的工作目錄 (/app
)RUN apt-get update && apt-get install -y rsync fonts-noto-cjk
: 安裝必要的系統套件,包含中日韓字體支援RUN pip install --no-cache-dir -r requirements.txt
: 安裝 Python 依賴套件,不保留快取以減少鏡像大小VOLUME ["/mnt/local_path", "/mnt/remote_path", "/app/data"]
: 設定容器掛載點,方便與宿主機交換數據ENTRYPOINT ["python", "-m", "p5d"]
: 指定容器啟動時執行的預設指令
解釋 alpine with virtual 版本的構建指令:
FROM python:3.10-alpine
: 使用超輕量級的 Alpine Linux 減少鏡像體積ENV PYTHONDONTWRITEBYTECODE=1
: 設定 Python 不生成.pyc
檔案,減少鏡像體積ENV PYTHONUNBUFFERED=1
: 設定 Python 標準輸出不緩衝,便於即時查看日誌RUN apk add --no-cache fontconfig libstdc++ rsync
: 安裝必要的常駐套件RUN apk add --virtual .build-deps build-base curl
: 使用--virtual
標記創建臨時套件組,方便後續一併移除- 下載並安裝中日韓字體,完成構建後刪除臨時套件,有效減少最終鏡像體積
VOLUME
和ENTRYPOINT
與基本版本類似,提供掛載點和啟動指令
解釋 alpine with multi-stage build 版本的構建指令:
- 使用多階段構建策略,分為
builder
和final
兩個階段 - Builder 階段:
- 安裝構建必要的套件和依賴
- 下載字體資源
- 安裝 Python 依賴
- Final 階段:
- 只從 builder 階段複製必要的文件,包括應用代碼、Python 套件和字體
- 只安裝運行時必要的輕量級套件
- 最終鏡像不包含構建工具,體積更小且更安全
很簡單吧,另外每個命令至少都會使用一層或多層的映像檔,所以相似命令盡量用 && 合併。
比較不同構建方式
接下來比較不同構建方式的鏡像大小
- slim:原始版本,使用 python slim 並且用 apt-get 安裝字體包
- alpine:改成 python alpine,安裝必要編譯工具後沒刪除編譯工具(沒放在上面的 tab 中)
- alpine --virtual:使用 python alpine 安裝後刪除未來用不到的包
- alpine 多段構建:用 FROM 分隔不同階段,用於減少鏡像容量,例如 alpine 沒有編譯功能,編譯完成後直接複製到下一階段使用
容量分別是
方式 | 大小 |
---|---|
slim 安裝字體包 | 402MB |
alpine 沒刪除額外工具 | 462MB |
alpine --virtual | 245MB |
多階段構建減少容量 | 226MB |
可以看到什麼都不管,直接使用 slim 檔案來到誇張的 402MB,而 alpine 雖然本體小,但是加上必要的編譯工具容量反而變大。用另外兩種方式進一步優化之後可降低將近 50% 容量。進入容器內部觀察,發現 /usr/local/lib/python3.10 就佔據 190 MB,該資料夾中都是小檔案最大只有 1MB,相較本身電腦開發端的 .venv 資料夾約為 130MB 約多出 60MB。
本以為優化空間也差不多了,但是參考其他專案鏡像發現專業專案的 python 資料夾還更小所以一定有優化空間,目前已知最大問題是 matplotlib 和他需求的 numpy 兩個都是容量怪物。
構建和執行指令
構建名為 p5d 的 Docker 映像,使用當前目錄下的 Dockerfile。
docker build -t p5d .
執行 p5d,容器,掛載本地路徑與容器內路徑,並以互動模式運行
docker run --rm -v /Users/leo/Pictures/downloads拷貝3/:/mnt/local_path \
-v /Users/leo/Downloads/dst/:/mnt/remote_path \
-v ~/Downloads:/app/data \
-it p5d
在 docker 中運行 unittest 偵錯
docker run --rm -v /Users/leo/local_folder:/mnt/local_folder \
-v /Users/leo/remote_folder:/mnt/remote_folder \
-v ~/Downloads:/app/data \
p5d:test python -m unittest discover -s tests -p "*.py"