制作 Python Docker 鏡像的最佳實(shí)踐

概述

???Reference:

制作容器鏡像的最佳實(shí)踐

這篇文章是關(guān)于制作 Python Docker 容器鏡像的最佳實(shí)踐绞绒。(2022 年 12 月更新)
最佳實(shí)踐的目的一方面是為了減小鏡像體積疤祭,提升 DevOps 效率疯特,另一方面是為了提高安全性键袱。希望對(duì)各位有所幫助格了。

通用 Docker 容器鏡像最佳實(shí)踐

這里也再次羅列一下對(duì) Python Docker 鏡像也適用的一些通用最佳實(shí)踐。

  • 使用 LABEL maintainer
  • 標(biāo)記重要端口
  • 設(shè)置環(huán)境變量
  • 使用非 root 用戶運(yùn)行容器進(jìn)程
  • 使用 .dockerignore 排除無關(guān)文件

Python 鏡像推薦設(shè)置的環(huán)境變量

Python 中推薦的常見環(huán)境變量如下:

# 設(shè)置環(huán)境變量
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
  1. ENV PYTHONDONTWRITEBYTECODE 1: 建議構(gòu)建 Docker 鏡像時(shí)一直為 1, 防止 python 將 pyc 文件寫入硬盤
  2. ENV PYTHONUNBUFFERED 1: 建議構(gòu)建 Docker 鏡像時(shí)一直為 1, 防止 python 緩沖 (buffering) stdout 和 stderr, 以便更容易地進(jìn)行容器日志記錄
  3. ?不再建議使用 ENV DEBUG 0 環(huán)境變量碗淌,沒必要馆截。

使用非 root 用戶運(yùn)行容器進(jìn)程

出于安全考慮,推薦運(yùn)行 Python 程序前硼砰,創(chuàng)建 非 root 用戶并切換到該用戶且蓬。

# 創(chuàng)建一個(gè)具有明確 UID 的非 root 用戶,并增加訪問 /app 文件夾的權(quán)限题翰。
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

使用 .dockerignore 排除無關(guān)文件

需要排除的無關(guān)文件一般如下:

**/__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

這里選擇幾個(gè)說明下:

  1. **/__pycache__: python 緩存目錄
  2. **/*venv: Python 虛擬環(huán)境目錄恶阴。很多 Python 開發(fā)習(xí)慣將虛擬環(huán)境目錄創(chuàng)建在項(xiàng)目下诈胜,一般命名為:.venvvenv
  3. **/.env: Python 環(huán)境變量文件
  4. **/.git **/.gitignore: git 相關(guān)目錄和文件
  5. **/.vscode: 編輯器、IDE 相關(guān)目錄
  6. **/charts: Helm Chart 相關(guān)文件
  7. **/docker-compose*: docker compose 相關(guān)文件
  8. *.db: 如果使用 sqllite 的相關(guān)數(shù)據(jù)庫文件
  9. .python-version: pyenv 的 .python-version 文件

不建議使用 Alpine 作為 Python 的基礎(chǔ)鏡像

為什么呢存淫?大多數(shù) Linux 發(fā)行版使用 GNU 版本(glibc)的標(biāo)準(zhǔn) C 庫耘斩,幾乎每個(gè) C 程序都需要這個(gè)庫沼填,包括 Python桅咆。但是 Alpine Linux 使用 musl, Alpine 禁用了 Linux wheel 支持。

理由如下:

  • 缺少大量依賴
    • CPython 語言運(yùn)行時(shí)的相關(guān)依賴
    • openssl 相關(guān)依賴
    • libffi 相關(guān)依賴
    • gcc 相關(guān)依賴
    • 數(shù)據(jù)庫驅(qū)動(dòng)相關(guān)依賴
    • pip 相關(guān)依賴
  • 構(gòu)建可能更耗時(shí)
    • Alpine Linux 使用 musl坞笙,一些二進(jìn)制 wheel 是針對(duì) glibc 編譯的岩饼,但是 Alpine 禁用了 Linux wheel 支持。現(xiàn)在大多數(shù) Python 包都包括 PyPI 上的二進(jìn)制 wheel薛夜,大大加快了安裝時(shí)間籍茧。但是如果你使用 Alpine Linux,你可能需要編譯你使用的每個(gè) Python 包中的所有 C 代碼梯澜。
  • 基于 Alpine 構(gòu)建的 Python 鏡像反而可能更大
    • 乍一聽似乎違反常識(shí)寞冯,但是仔細(xì)一想,因?yàn)樯厦媪_列的原因晚伙,確實(shí)會(huì)導(dǎo)致鏡像更大的情況吮龄。

???Reference:

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

這里以這個(gè) Demo FastAPI Python 程序 為例,其基于 Alpine 的 Dockerfile 地址是這個(gè):https://github.com/east4ming/fastapi-url-shortener/blob/main/Dockerfile.alpine

因?yàn)槿鄙俸芏嘁蕾嚺亓疲栽谟?pip 安裝之前漓帚,就需要盡可能全地安裝相關(guān)依賴:

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 構(gòu)建完成后的 鏡像未壓縮大小:

基于 Alpine 的 Python Demo 鏡像大形绱拧:472 MB

△ 基于 Alpine 的 Python Demo 鏡像大谐⒍丁:472 MB; 相比之下,基于 slim 的只有 189 MB

在上面代碼的這一步迅皇,就占用了太多空間:

??思考:

可能上面一段可以精簡(jiǎn)昧辽,但是要判斷對(duì)于哪個(gè) Python 項(xiàng)目,可以精簡(jiǎn)哪些包登颓,實(shí)在是太難了搅荞。

+ 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 鏡像作為基礎(chǔ)鏡像

繼續(xù)上面,所以我是建議:使用官方的 python slim 鏡像作為基礎(chǔ)鏡像

鏡像庫是這個(gè):https://hub.docker.com/_/python

并且使用 python:<version>-slim 作為基礎(chǔ)鏡像挺据,能用 python:<version>-slim-bullseye 作為基礎(chǔ)鏡像更好(因?yàn)楦氯【撸鄬?duì)就更安全一些).

這個(gè)鏡像不包含默認(rèn)標(biāo)簽中的常用包,只包含運(yùn)行 python 所需的最小包扁耐。這個(gè)鏡像是基于 Debian 的暇检。

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

  • 穩(wěn)定性
  • 安全升級(jí)更及時(shí)
  • 依賴更新更及時(shí)
  • 依賴更全
  • Python 版本升級(jí)更及時(shí)
  • 鏡像更小

???Reference:

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

一般情況下,Python 鏡像構(gòu)建不需要使用"多階段構(gòu)建"

一般情況下婉称,Python 鏡像構(gòu)建不需要使用"多階段構(gòu)建".

理由如下:

  • Python 沒有像 Golang 一樣块仆,可以把所有依賴打成一個(gè)單一的二進(jìn)制包
  • Python 也沒有像 Java 一樣构蹬,可以在 JDK 上構(gòu)建,在 JRE 上運(yùn)行
  • Python 復(fù)雜而散落的依賴關(guān)系悔据,在"多階段構(gòu)建"時(shí)會(huì)增加復(fù)雜度
  • ...

如果有一些特殊情況庄敛,可以嘗試使用"多階段構(gòu)建"壓縮鏡像體積:

  • 構(gòu)建階段需要安裝編譯器
  • Python 項(xiàng)目復(fù)雜,用到了其他語言代碼(如 C/C++/Rust)

pip 小技巧

使用 pip 安裝依賴時(shí)科汗,可以添加 --no-cache-dir 減少鏡像體積:

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

Python Dockerfile 最佳實(shí)踐樣例

最后, 就是基于以上最佳實(shí)踐的完整樣例, 也可以在這里找到: 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"]

總結(jié)

制作 Python Docker 容器鏡像的最佳實(shí)踐藻烤。最佳實(shí)踐的目的一方面是為了減小鏡像體積,提升 DevOps 效率头滔,另一方面是為了提高安全性.

最佳實(shí)踐如下:

  • 推薦 2 個(gè) Python 的環(huán)境變量
    • ENV PYTHONDONTWRITEBYTECODE 1
    • ENV PYTHONUNBUFFERED 1
  • 使用非 root 用戶運(yùn)行容器進(jìn)程
  • 使用 .dockerignore 排除無關(guān)文件
  • 不建議使用 Alpine 作為 Python 的基礎(chǔ)鏡像
  • 建議使用官方的 python slim 鏡像作為基礎(chǔ)鏡像
  • 一般情況下, Python 鏡像構(gòu)建不需要使用"多階段構(gòu)建"
  • pip 小技巧: --no-cache-dir

希望對(duì)大家有所幫助.

最后也感嘆一下, 在云原生時(shí)代, python 在分發(fā)這塊, 特別是鏡像構(gòu)建這塊, 確實(shí)體驗(yàn)怖亭、效率、鏡像大小等方面差 golang 太多了坤检。??????

???參考文檔

三人行, 必有我?guī)? 知識(shí)共享, 天下為公. 本文由東風(fēng)微鳴技術(shù)博客 EWhisper.cn 編寫.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兴猩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子早歇,更是在濱河造成了極大的恐慌倾芝,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,029評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箭跳,死亡現(xiàn)場(chǎng)離奇詭異晨另,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衅码,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門拯刁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逝段,你說我怎么就攤上這事垛玻。” “怎么了奶躯?”我有些...
    開封第一講書人閱讀 157,570評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵帚桩,是天一觀的道長。 經(jīng)常有香客問我嘹黔,道長账嚎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,535評(píng)論 1 284
  • 正文 為了忘掉前任儡蔓,我火速辦了婚禮郭蕉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喂江。我一直安慰自己召锈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評(píng)論 6 386
  • 文/花漫 我一把揭開白布获询。 她就那樣靜靜地躺著涨岁,像睡著了一般拐袜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上梢薪,一...
    開封第一講書人閱讀 49,850評(píng)論 1 290
  • 那天蹬铺,我揣著相機(jī)與錄音,去河邊找鬼秉撇。 笑死甜攀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的畜疾。 我是一名探鬼主播赴邻,決...
    沈念sama閱讀 39,006評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼印衔,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼啡捶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奸焙,我...
    開封第一講書人閱讀 37,747評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤瞎暑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后与帆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體了赌,經(jīng)...
    沈念sama閱讀 44,207評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評(píng)論 2 327
  • 正文 我和宋清朗相戀三年玄糟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勿她。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,683評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阵翎,死狀恐怖逢并,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情郭卫,我是刑警寧澤砍聊,帶...
    沈念sama閱讀 34,342評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站贰军,受9級(jí)特大地震影響玻蝌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜词疼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評(píng)論 3 315
  • 文/蒙蒙 一俯树、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贰盗,春花似錦许饿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胸完。三九已至,卻和暖如春翘贮,著一層夾襖步出監(jiān)牢的瞬間赊窥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評(píng)論 1 266
  • 我被黑心中介騙來泰國打工狸页, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锨能,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,401評(píng)論 2 360
  • 正文 我出身青樓芍耘,卻偏偏與公主長得像址遇,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子斋竞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容