製作 Python Docker 映象的最佳實踐

東風微鳴發表於2022-12-15

概述

?️Reference:

製作容器映象的最佳實踐

這篇文章是關於製作 Python Docker 容器映象的最佳實踐。(2022 年 12 月更新)
最佳實踐的目的一方面是為了減小映象體積,提升 DevOps 效率,另一方面是為了提高安全性。希望對各位有所幫助。

通用 Docker 容器映象最佳實踐

這裡也再次羅列一下對 Python Docker 映象也適用的一些通用最佳實踐。

  • 使用 LABEL maintainer
  • 標記重要埠
  • 設定環境變數
  • 使用非 root 使用者執行容器程式
  • 使用 .dockerignore 排除無關檔案

Python 映象推薦設定的環境變數

Python 中推薦的常見環境變數如下:

# 設定環境變數
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
  1. ENV PYTHONDONTWRITEBYTECODE 1: 建議構建 Docker 映象時一直為 1, 防止 python 將 pyc 檔案寫入硬碟
  2. ENV PYTHONUNBUFFERED 1: 建議構建 Docker 映象時一直為 1, 防止 python 緩衝 (buffering) stdout 和 stderr, 以便更容易地進行容器日誌記錄
  3. ❌不再建議使用 ENV DEBUG 0 環境變數,沒必要。

使用非 root 使用者執行容器程式

出於安全考慮,推薦執行 Python 程式前,建立 非 root 使用者並切換到該使用者。

# 建立一個具有明確 UID 的非 root 使用者,並增加訪問 /app 資料夾的許可權。
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

使用 .dockerignore 排除無關檔案

需要排除的無關檔案一般如下:

**/__pycache__
**/*venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
*.db
.python-version
LICENSE
README.md

這裡選擇幾個說明下:

  1. **/__pycache__: python 快取目錄
  2. **/*venv: Python 虛擬環境目錄。很多 Python 開發習慣將虛擬環境目錄建立在專案下,一般命名為:.venvvenv
  3. **/.env: Python 環境變數檔案
  4. **/.git **/.gitignore: git 相關目錄和檔案
  5. **/.vscode: 編輯器、IDE 相關目錄
  6. **/charts: Helm Chart 相關檔案
  7. **/docker-compose*: docker compose 相關檔案
  8. *.db: 如果使用 sqllite 的相關資料庫檔案
  9. .python-version: pyenv 的 .python-version 檔案

不建議使用 Alpine 作為 Python 的基礎映象

為什麼呢?大多數 Linux 發行版使用 GNU 版本(glibc)的標準 C 庫,幾乎每個 C 程式都需要這個庫,包括 Python。但是 Alpine Linux 使用 musl, Alpine 禁用了 Linux wheel 支援。

理由如下:

  • 缺少大量依賴
    • CPython 語言執行時的相關依賴
    • openssl 相關依賴
    • libffi 相關依賴
    • gcc 相關依賴
    • 資料庫驅動相關依賴
    • pip 相關依賴
  • 構建可能更耗時
    • Alpine Linux 使用 musl,一些二進位制 wheel 是針對 glibc 編譯的,但是 Alpine 禁用了 Linux wheel 支援。現在大多數 Python 包都包括 PyPI 上的二進位制 wheel,大大加快了安裝時間。但是如果你使用 Alpine Linux,你可能需要編譯你使用的每個 Python 包中的所有 C 程式碼。
  • 基於 Alpine 構建的 Python 映象反而可能更大
    • 乍一聽似乎違反常識,但是仔細一想,因為上面羅列的原因,確實會導致映象更大的情況。

?️Reference:

Using Alpine can make Python Docker builds 50× slower (pythonspeed.com)

這裡以這個 Demo FastAPI Python 程式 為例,其基於 Alpine 的 Dockerfile 地址是這個:https://github.com/east4ming/fastapi-url-shortener/blob/main/Dockerfile.alpine

因為缺少很多依賴,所以在用 pip 安裝之前,就需要儘可能全地安裝相關依賴:

RUN set -eux \
    && apk add --no-cache --virtual .build-deps build-base \
    openssl-dev libffi-dev gcc musl-dev python3-dev \
    && pip install --upgrade pip setuptools wheel \
    && pip install --upgrade -r /app/requirements.txt \
    && rm -rf /root/.cache/pip

這裡也展示一下基於 Alpine 構建完成後的 映象未壓縮大小:

基於 Alpine 的 Python Demo 映象大小:472 MB

△ 基於 Alpine 的 Python Demo 映象大小:472 MB; 相比之下,基於 slim 的只有 189 MB

在上面程式碼的這一步,就佔用了太多空間:

?思考:

可能上面一段可以精簡,但是要判斷對於哪個 Python 專案,可以精簡哪些包,實在是太難了。

+ apk add --no-cache --virtual .build-deps build-base openssl-dev libffi-dev gcc musl-dev python3-dev
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
(1/28) Installing libgcc (12.2.1_git20220924-r4)
(2/28) Installing libstdc++ (12.2.1_git20220924-r4)
(3/28) Installing binutils (2.39-r2)
(4/28) Installing libmagic (5.43-r0)
(5/28) Installing file (5.43-r0)
(6/28) Installing libgomp (12.2.1_git20220924-r4)
(7/28) Installing libatomic (12.2.1_git20220924-r4)
(8/28) Installing gmp (6.2.1-r2)
(9/28) Installing isl25 (0.25-r0)
(10/28) Installing mpfr4 (4.1.0-r0)
(11/28) Installing mpc1 (1.2.1-r1)
(12/28) Installing gcc (12.2.1_git20220924-r4)
(13/28) Installing libstdc++-dev (12.2.1_git20220924-r4)
(14/28) Installing musl-dev (1.2.3-r4)
(15/28) Installing libc-dev (0.7.2-r3)
(16/28) Installing g++ (12.2.1_git20220924-r4)
(17/28) Installing make (4.3-r1)
(18/28) Installing fortify-headers (1.1-r1)
(19/28) Installing patch (2.7.6-r8)
(20/28) Installing build-base (0.5-r3)
(21/28) Installing pkgconf (1.9.3-r0)
(22/28) Installing openssl-dev (3.0.7-r0)
(23/28) Installing linux-headers (5.19.5-r0)
(24/28) Installing libffi-dev (3.4.4-r0)
(25/28) Installing mpdecimal (2.5.1-r1)
(26/28) Installing python3 (3.10.9-r1)
(27/28) Installing python3-dev (3.10.9-r1)
(28/28) Installing .build-deps (20221214.074929)
Executing busybox-1.35.0-r29.trigger
OK: 358 MiB in 65 packages
...

建議使用官方的 python slim 映象作為基礎映象

繼續上面,所以我是建議:使用官方的 python slim 映象作為基礎映象

映象庫是這個:https://hub.docker.com/_/python

並且使用 python:<version>-slim 作為基礎映象,能用 python:<version>-slim-bullseye 作為基礎映象更好(因為更新,相對就更安全一些).

這個映象不包含預設標籤中的常用包,只包含執行 python 所需的最小包。這個映象是基於 Debian 的。

使用官方 python slim 的理由還包括:

  • 穩定性
  • 安全升級更及時
  • 依賴更新更及時
  • 依賴更全
  • Python 版本升級更及時
  • 映象更小

?️Reference:

The best Docker base image for your Python application (Sep 2022) (pythonspeed.com)

一般情況下,Python 映象構建不需要使用"多階段構建"

一般情況下,Python 映象構建不需要使用"多階段構建".

理由如下:

  • Python 沒有像 Golang 一樣,可以把所有依賴打成一個單一的二進位制包
  • Python 也沒有像 Java 一樣,可以在 JDK 上構建,在 JRE 上執行
  • Python 複雜而散落的依賴關係,在"多階段構建"時會增加複雜度
  • ...

如果有一些特殊情況,可以嘗試使用"多階段構建"壓縮映象體積:

  • 構建階段需要安裝編譯器
  • Python 專案複雜,用到了其他語言程式碼(如 C/C++/Rust)

pip 小技巧

使用 pip 安裝依賴時,可以新增 --no-cache-dir 減少映象體積:

# 安裝 pip 依賴
COPY requirements.txt .
RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt

Python Dockerfile 最佳實踐樣例

最後, 就是基於以上最佳實踐的完整樣例, 也可以在這裡找到: https://github.com/east4ming/fastapi-url-shortener/blob/main/Dockerfile.slim

FROM python:3.10-slim

LABEL maintainer="cuikaidong@foxmail.com"

EXPOSE 8000

# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE=1

# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED=1

# Install pip requirements
COPY requirements.txt .
RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt

WORKDIR /app
COPY . /app

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

CMD ["uvicorn", "shortener_app.main:app", "--host", "0.0.0.0"]

總結

製作 Python Docker 容器映象的最佳實踐。最佳實踐的目的一方面是為了減小映象體積,提升 DevOps 效率,另一方面是為了提高安全性.

最佳實踐如下:

  • 推薦 2 個 Python 的環境變數
    • ENV PYTHONDONTWRITEBYTECODE 1
    • ENV PYTHONUNBUFFERED 1
  • 使用非 root 使用者執行容器程式
  • 使用 .dockerignore 排除無關檔案
  • 不建議使用 Alpine 作為 Python 的基礎映象
  • 建議使用官方的 python slim 映象作為基礎映象
  • 一般情況下, Python 映象構建不需要使用"多階段構建"
  • pip 小技巧: --no-cache-dir

希望對大家有所幫助.

最後也感嘆一下, 在雲原生時代, python 在分發這塊, 特別是映象構建這塊, 確實體驗、效率、映象大小等方面差 golang 太多了。???

?️參考文件

三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.

相關文章