Python3 - Dockerfile 最佳实践
文章目錄
- 一、一般準則和建議
- 1.1 容器應該是短暫的
- 1.2 建立上下文
- 1.3 使用`.dockerignore`文件
- 1.4 使用多階段構建
- 1.5 避免安裝不必要的包
- 1.6 一個容器只專注做一件事情
- 1.7 最小化鏡像層數(shù)
- 1.8 對多行參數(shù)排序
- 1.9 構建緩存
- 二、Dockerfile 指令
- 2.1 FROM
- 2.2 LABEL
- 2.3 CMD
- 2.4 EXPOSE
- 2.5 ENV
- 2.6 ADD 和 COPY
- 2.7 ENTRYPOINT
- 2.8 VOLUME
- 2.9 USER
- 2.10 WORKDIR
- 2.11 ONBUILD
- 2.12 官方倉庫示例
Docker官方關于Dockerfile最佳實踐原文鏈接地址:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
Docker 可以通過從 Dockerfile 包含所有命令的文本文件中讀取指令自動構建鏡像,以便構建給定鏡像。
Dockerfiles 使用特定的格式并使用一組特定的指令。您可以在Dockerfile Reference頁面上了解基礎知識 。如果你是新手寫作Dockerfile,你應該從那里開始。
本文檔介紹了由 Docker,Inc. 和 Docker 社區(qū)推薦的用于構建高效鏡像的最佳實踐和方法。要查看許多實踐和建議,請查看Dockerfile for buildpack-deps。
一、一般準則和建議
1.1 容器應該是短暫的
通過 Dockerfile 構建的鏡像所啟動的容器應該盡可能短暫(生命周期短)。「短暫」意味著可以停止和銷毀容器,并且創(chuàng)建一個新容器并部署好所需的設置和配置工作量應該是極小的。我們可以查看下12 Factor(12要素)應用程序方法的進程部分,可以讓我們理解這種無狀態(tài)方式運行容器的動機。
1.2 建立上下文
當你發(fā)出一個docker build命令時,當前的工作目錄被稱為構建上下文。默認情況下,Dockerfile 就位于該路徑下,當然您也可以使用-f參數(shù)來指定不同的位置。無論 Dockerfile 在什么地方,當前目錄中的所有文件內容都將作為構建上下文發(fā)送到 Docker 守護進程中去。
下面是一個構建上下文的示例,為構建上下文創(chuàng)建一個目錄并 cd 放入其中。將“hello”寫入一個文本文件hello,然后并創(chuàng)建一個Dockerfile并運行cat。從構建上下文(.)中構建圖像:
mkdir myproject && cd myproject echo "hello" > hello echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile docker build -t helloapp:v1 .現(xiàn)在移動 Dockerfile 和 hello 到不同的目錄,并建立了圖像的第二個版本(不依賴于緩存中的最后一個版本)。使用-f指向 Dockerfile 并指定構建上下文的目錄:
mkdir -p dockerfiles context mv Dockerfile dockerfiles && mv hello context docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context在構建的時候包含不需要的文件會導致更大的構建上下文和更大的鏡像大小。這會增加構建時間,拉取和推送鏡像的時間以及容器的運行時間大小。要查看您的構建環(huán)境有多大,請在構建您的系統(tǒng)時查找這樣的消息
Dockerfile: Sending build context to Docker daemon 4.096kB Sending build context to Docker daemon 2.607kB1.3 使用.dockerignore文件
使用 Dockerfile 構建鏡像時最好是將 Dockerfile 放置在一個新建的空目錄下。然后將構建鏡像所需要的文件添加到該目錄中。為了提高構建鏡像的效率,你可以在目錄下新建一個.dockerignore文件來指定要忽略的文件和目錄。.dockerignore 文件的排除模式語法和 Git 的 .gitignore 文件相似。
1.4 使用多階段構建
在 Docker 17.05 以上版本中,你可以使用 多階段構建 來減少所構建鏡像的大小。上一節(jié)課我們已經(jīng)重點講解過了。
1.5 避免安裝不必要的包
為了降低復雜性、減少依賴、減小文件大小和構建時間,應該避免安裝額外的或者不必要的軟件包。例如,不要在數(shù)據(jù)庫鏡像中包含一個文本編輯器。
1.6 一個容器只專注做一件事情
應該保證在一個容器中只運行一個進程。將多個應用解耦到不同容器中,保證了容器的橫向擴展和復用。例如一個 web 應用程序可能包含三個獨立的容器:web應用、數(shù)據(jù)庫、緩存,每個容器都是獨立的鏡像,分開運行。但這并不是說一個容器就只跑一個進程,因為有的程序可能會自行產(chǎn)生其他進程,比如 Celery 就可以有很多個工作進程。雖然“每個容器跑一個進程”是一條很好的法則,但這并不是一條硬性的規(guī)定。我們主要是希望一個容器只關注意見事情,盡量保持干凈和模塊化。
如果容器互相依賴,你可以使用Docker 容器網(wǎng)絡來把這些容器連接起來,我們前面已經(jīng)跟大家講解過 Docker 的容器網(wǎng)絡模式了。
1.7 最小化鏡像層數(shù)
在 Docker 17.05 甚至更早 1.10之 前,盡量減少鏡像層數(shù)是非常重要的,不過現(xiàn)在的版本已經(jīng)有了一定的改善了:
- 在 1.10 以后,只有 RUN、COPY 和 ADD 指令會創(chuàng)建層,其他指令會創(chuàng)建臨時的中間鏡像,但是不會直接增加構建的鏡像大小了。
- 上節(jié)課我們也講解到了 17.05 版本以后增加了多階段構建的支持,允許我們把需要的數(shù)據(jù)直接復制到最終的鏡像中,這就允許我們在中間階段包含一些工具或者調試信息了,而且不會增加最終的鏡像大小。
當然減少RUN、COPY、ADD的指令仍然是很有必要的,但是我們也需要在 Dockerfile 可讀性(也包括長期的可維護性)和減少層數(shù)之間做一個平衡。
1.8 對多行參數(shù)排序
只要有可能,就將多行參數(shù)按字母順序排序(比如要安裝多個包時)。這可以幫助你避免重復包含同一個包,更新包列表時也更容易,也更容易閱讀和審查。建議在反斜杠符號 \ 之前添加一個空格,可以增加可讀性。 下面是來自 buildpack-deps 鏡像的例子:
RUN apt-get update && apt-get install -y \bzr \cvs \git \mercurial \subversion1.9 構建緩存
在鏡像的構建過程中,Docker 根據(jù) Dockerfile 指定的順序執(zhí)行每個指令。在執(zhí)行每條指令之前,Docker 都會在緩存中查找是否已經(jīng)存在可重用的鏡像,如果有就使用現(xiàn)存的鏡像,不再重復創(chuàng)建。當然如果你不想在構建過程中使用緩存,你可以在 docker build 命令中使用--no-cache=true選項。
如果你想在構建的過程中使用了緩存,那么了解什么時候可以什么時候無法找到匹配的鏡像就很重要了,Docker中緩存遵循的基本規(guī)則如下:
- 從一個基礎鏡像開始(FROM 指令指定),下一條指令將和該基礎鏡像的所有子鏡像進行匹配,檢查這些子鏡像被創(chuàng)建時使用的指令是否和被檢查的指令完全一樣。如果不是,則緩存失效。
- 在大多數(shù)情況下,只需要簡單地對比 Dockerfile 中的指令和子鏡像。然而,有些指令需要更多的檢查和解釋。
- 對于 ADD 和 COPY 指令,鏡像中對應文件的內容也會被檢查,每個文件都會計算出一個校驗值。這些文件的修改時間和最后訪問時間不會被納入校驗的范圍。在緩存的查找過程中,會將這些校驗和和已存在鏡像中的文件校驗值進行對比。如果文件有任何改變,比如內容和元數(shù)據(jù),則緩存失效。
- 除了 ADD 和 COPY 指令,緩存匹配過程不會查看臨時容器中的文件來決定緩存是否匹配。例如,當執(zhí)行完 RUN apt-get -y update 指令后,容器中一些文件被更新,但 Docker 不會檢查這些文件。這種情況下,只有指令字符串本身被用來匹配緩存。
- 一旦緩存失效,所有后續(xù)的 Dockerfile 指令都將產(chǎn)生新的鏡像,緩存不會被使用。
二、Dockerfile 指令
下面是一些常用的 Dockerfile 指令,我們也分別來總結下,根據(jù)上面的建議和下面這些指令的合理使用,可以幫助我們編寫高效且易維護的 Dockerfile 文件。
2.1 FROM
盡可能使用當前官方倉庫作為你構建鏡像的基礎。推薦使用Alpine鏡像,因為它被嚴格控制并保持最小尺寸(目前小于 5 MB),但它仍然是一個完整的發(fā)行版。
2.2 LABEL
你可以給鏡像添加標簽來幫助組織鏡像、記錄許可信息、輔助自動化構建等。每個標簽一行,由 LABEL 開頭加上一個或多個標簽對。
下面的示例展示了各種不同的可能格式。#開頭的行是注釋內容。
注意:如果你的字符串包含空格,那么它必須被引用或者空格必須被轉義。如果您的字符串包含內部引號字符("),則也可以將其轉義。
# Set one or more individual labels LABEL com.example.version="0.0.1-beta" LABEL vendor="ACME Incorporated" LABEL com.example.release-date="2022-04-13" LABEL com.example.version.is-production=""一個鏡像可以包含多個標簽,在 1.10 之前,建議將所有標簽合并為一條LABEL指令,以防止創(chuàng)建額外的層,但是現(xiàn)在這個不再是必須的了,以上內容也可以寫成下面這樣:
# Set multiple labels at once, using line-continuation characters to break long lines LABEL vendor=ACME\ Incorporated \com.example.is-production="" \com.example.version="0.0.1-beta" \com.example.release-date="2022-04-13"關于標簽可以接受的鍵值對,參考Understanding object labels。
為了保持 Dockerfile 文件的可讀性,以及可維護性,建議將長的或復雜的RUN指令用反斜杠\分割成多行。
RUN 指令最常見的用法是安裝包用的apt-get。因為RUN apt-get指令會安裝包,所以有幾個問題需要注意。
-
不要使用 RUN apt-get upgrade 或 dist-upgrade,如果基礎鏡像中的某個包過時了,你應該聯(lián)系它的維護者。如果你確定某個特定的包,比如 foo,需要升級,使用 apt-get install -y foo 就行,該指令會自動升級 foo 包。
-
永遠將 RUN apt-get update 和 apt-get install 組合成一條 RUN 聲明,例如:
RUN apt-get update && apt-get install -y \package-bar \package-baz \package-foo
將 apt-get update 放在一條單獨的 RUN 聲明中會導致緩存問題以及后續(xù)的 apt-get install 失敗。比如,假設你有一個 Dockerfile 文件:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl構建鏡像后,所有的層都在 Docker 的緩存中。假設你后來又修改了其中的 apt-get install 添加了一個包:
FROM ubuntu:14.04 RUN apt-get update RUN apt-get install -y curl nginxDocker 發(fā)現(xiàn)修改后的 RUN apt-get update 指令和之前的完全一樣。所以,apt-get update 不會執(zhí)行,而是使用之前的緩存鏡像。因為 apt-get update 沒有運行,后面的 apt-get install 可能安裝的是過時的 curl 和 nginx 版本。
使用RUN apt-get update && apt-get install -y可以確保你的Dockerfiles每次安裝的都是包的最新的版本,而且這個過程不需要進一步的編碼或額外干預。這項技術叫作cache busting(緩存破壞)。你也可以顯示指定一個包的版本號來達到 cache-busting,這就是所謂的固定版本,例如:
RUN apt-get update && apt-get install -y \package-bar \package-baz \package-foo=1.3.*固定版本會迫使構建過程檢索特定的版本,而不管緩存中有什么。這項技術也可以減少因所需包中未預料到的變化而導致的失敗。
下面是一個 RUN 指令的示例模板,展示了所有關于 apt-get 的建議。
RUN apt-get update && apt-get install -y \aufs-tools \automake \build-essential \curl \dpkg-sig \libcap-dev \libsqlite3-dev \mercurial \reprepro \ruby1.9.1 \ruby1.9.1-dev \s3cmd=1.1.* \&& rm -rf /var/lib/apt/lists/*其中 s3cmd 指令指定了一個版本號 1.1.*。如果之前的鏡像使用的是更舊的版本,指定新的版本會導致 apt-get udpate 緩存失效并確保安裝的是新版本。 另外,清理掉 apt 緩存 var/lib/apt/lists 可以減小鏡像大小。因為 RUN 指令的開頭為 apt-get udpate,包緩存總是會在 apt-get install 之前刷新。
注意:官方的 Debian 和 Ubuntu 鏡像會自動運行 apt-get clean,所以不需要顯式的調用 apt-get clean。
2.3 CMD
CMD指令用于執(zhí)行目標鏡像中包含的軟件和任何參數(shù)。CMD 幾乎都是以CMD ["executable", "param1", "param2"...]的形式使用。因此,如果創(chuàng)建鏡像的目的是為了部署某個服務(比如 Apache),你可能會執(zhí)行類似于CMD ["apache2", "-DFOREGROUND"]形式的命令。
多數(shù)情況下,CMD 都需要一個交互式的 shell (bash, Python, perl 等),例如 CMD [“perl”, “-de0”],或者 CMD [“PHP”, “-a”]。使用這種形式意味著,當你執(zhí)行類似docker run -it python時,你會進入一個準備好的 shell 中。
CMD 在極少的情況下才會以 CMD [“param”, “param”] 的形式與ENTRYPOINT協(xié)同使用,除非你和你的鏡像使用者都對 ENTRYPOINT 的工作方式十分熟悉。
2.4 EXPOSE
EXPOSE指令用于指定容器將要監(jiān)聽的端口。因此,你應該為你的應用程序使用常見的端口。
例如,提供 Apache web 服務的鏡像應該使用 EXPOSE 80,而提供 MongoDB 服務的鏡像使用 EXPOSE 27017。
對于外部訪問,用戶可以在執(zhí)行 docker run 時使用一個標志來指示如何將指定的端口映射到所選擇的端口。
2.5 ENV
為了方便新程序運行,你可以使用ENV來為容器中安裝的程序更新 PATH 環(huán)境變量。例如使用ENV PATH /usr/local/nginx/bin:$PATH來確保CMD ["nginx"]能正確運行。
ENV 指令也可用于為你想要容器化的服務提供必要的環(huán)境變量,比如 Postgres 需要的 PGDATA。 最后,ENV 也能用于設置常見的版本號,比如下面的示例:
ENV PG_MAJOR 9.3 ENV PG_VERSION 9.3.4 RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH類似于程序中的常量,這種方法可以讓你只需改變 ENV 指令來自動的改變容器中的軟件版本。
2.6 ADD 和 COPY
雖然ADD和COPY功能類似,但一般優(yōu)先使用 COPY。因為它比 ADD 更透明。COPY 只支持簡單將本地文件拷貝到容器中,而 ADD 有一些并不明顯的功能(比如本地 tar 提取和遠程 URL 支持)。因此,ADD的最佳用例是將本地 tar 文件自動提取到鏡像中,例如ADD rootfs.tar.xz。
如果你的 Dockerfile 有多個步驟需要使用上下文中不同的文件。單獨 COPY 每個文件,而不是一次性的 COPY 所有文件,這將保證每個步驟的構建緩存只在特定的文件變化時失效。例如:
COPY requirements.txt /tmp/ RUN pip install --requirement /tmp/requirements.txt COPY . /tmp/如果將COPY . /tmp/放置在 RUN 指令之前,只要 . 目錄中任何一個文件變化,都會導致后續(xù)指令的緩存失效。
為了讓鏡像盡量小,最好不要使用 ADD 指令從遠程 URL 獲取包,而是使用 curl 和 wget。這樣你可以在文件提取完之后刪掉不再需要的文件來避免在鏡像中額外添加一層。比如盡量避免下面的用法:
ADD http://example.com/big.tar.xz /usr/src/things/ RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things RUN make -C /usr/src/things all而是應該使用下面這種方法:
RUN mkdir -p /usr/src/things \&& curl -SL http://example.com/big.tar.xz \| tar -xJC /usr/src/things \&& make -C /usr/src/things all上面使用的管道操作,所以沒有中間文件需要刪除。 對于其他不需要 ADD 的自動提取功能的文件或目錄,你應該使用 COPY。
2.7 ENTRYPOINT
ENTRYPOINT的最佳用處是設置鏡像的主命令,允許將鏡像當成命令本身來運行(用 CMD 提供默認選項)。
例如,下面的示例鏡像提供了命令行工具 s3cmd:
ENTRYPOINT ["s3cmd"] CMD ["--help"]現(xiàn)在直接運行該鏡像創(chuàng)建的容器會顯示命令幫助:
docker run s3cmd或者提供正確的參數(shù)來執(zhí)行某個命令:
docker run s3cmd ls s3://mybucket這樣鏡像名可以當成命令行的參考。ENTRYPOINT 指令也可以結合一個輔助腳本使用,和前面命令行風格類似,即使啟動工具需要不止一個步驟。
例如,Postgres 官方鏡像使用下面的腳本作為 ENTRYPOINT:
#!/bin/bash set -e if [ "$1" = 'postgres' ]; thenchown -R postgres "$PGDATA"if [ -z "$(ls -A "$PGDATA")" ]; thengosu postgres initdbfiexec gosu postgres "$@"fi exec "$@"注意:該腳本使用了 Bash 的內置命令 exec,所以最后運行的進程就是容器的 PID 為 1 的進程。這樣,進程就可以接收到任何發(fā)送給容器的 Unix 信號了。
該輔助腳本被拷貝到容器,并在容器啟動時通過 ENTRYPOINT 執(zhí)行:
COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"]該腳本可以讓用戶用幾種不同的方式和 Postgres 交互。你可以很簡單地啟動 Postgres:
docker run postgres也可以執(zhí)行 Postgres 并傳遞參數(shù):
docker run postgres postgres --help最后,你還可以啟動另外一個完全不同的工具,比如 Bash:
docker run --rm -it postgres bash2.8 VOLUME
VOLUME指令用于暴露任何數(shù)據(jù)庫存儲文件,配置文件,或容器創(chuàng)建的文件和目錄。強烈建議使用 VOLUME來管理鏡像中的可變部分和用戶可以改變的部分。
2.9 USER
如果某個服務不需要特權執(zhí)行,建議使用 USER 指令切換到非 root 用戶。先在 Dockerfile 中使用類似 RUN groupadd -r postgres && useradd -r -g postgres postgres 的指令創(chuàng)建用戶和用戶組。
注意:在鏡像中,用戶和用戶組每次被分配的 UID/GID 都是不確定的,下次重新構建鏡像時被分配到的 UID/GID 可能會不一樣。如果要依賴確定的 UID/GID,你應該顯示的指定一個 UID/GID。
你應該避免使用 sudo,因為它不可預期的 TTY 和信號轉發(fā)行為可能造成的問題比它能解決的問題還多。如果你真的需要和 sudo 類似的功能(例如,以 root 權限初始化某個守護進程,以非 root 權限執(zhí)行它),你可以使用 gosu。
最后,為了減少層數(shù)和復雜度,避免頻繁地使用 USER 來回切換用戶。
2.10 WORKDIR
為了清晰性和可靠性,你應該總是在WORKDIR中使用絕對路徑。另外,你應該使用 WORKDIR 來替代類似于 RUN cd … && do-something 的指令,后者難以閱讀、排錯和維護。
2.11 ONBUILD
格式:ONBUILD <其它指令>。 ONBUILD是一個特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而這些指令,在當前鏡像構建時并不會被執(zhí)行。只有當以當前鏡像為基礎鏡像,去構建下一級鏡像的時候才會被執(zhí)行。Dockerfile 中的其它指令都是為了定制當前鏡像而準備的,唯有 ONBUILD 是為了幫助別人定制自己而準備的。
假設我們要制作 Node.js 所寫的應用的鏡像。我們都知道 Node.js 使用 npm 進行包管理,所有依賴、配置、啟動信息等會放到 package.json 文件里。在拿到程序代碼后,需要先進行 npm install 才可以獲得所有需要的依賴。然后就可以通過 npm start 來啟動應用。因此,一般來說會這樣寫 Dockerfile:
FROM node:slim RUN mkdir /app WORKDIR /app COPY ./package.json /app RUN [ "npm", "install" ] COPY . /app/ CMD [ "npm", "start" ]把這個 Dockerfile 放到 Node.js 項目的根目錄,構建好鏡像后,就可以直接拿來啟動容器運行。但是如果我們還有第二個 Node.js 項目也差不多呢?好吧,那就再把這個 Dockerfile 復制到第二個項目里。那如果有第三個項目呢?再復制么?文件的副本越多,版本控制就越困難,讓我們繼續(xù)看這樣的場景維護的問題:
如果第一個 Node.js 項目在開發(fā)過程中,發(fā)現(xiàn)這個 Dockerfile 里存在問題,比如敲錯字了、或者需要安裝額外的包,然后開發(fā)人員修復了這個 Dockerfile,再次構建,問題解決。第一個項目沒問題了,但是第二個項目呢?雖然最初 Dockerfile 是復制、粘貼自第一個項目的,但是并不會因為第一個項目修復了他們的 Dockerfile,而第二個項目的 Dockerfile 就會被自動修復。
那么我們可不可以做一個基礎鏡像,然后各個項目使用這個基礎鏡像呢?這樣基礎鏡像更新,各個項目不用同步 Dockerfile 的變化,重新構建后就繼承了基礎鏡像的更新?好吧,可以,讓我們看看這樣的結果。那么上面的這個 Dockerfile 就會變?yōu)?#xff1a;
FROM node:slim RUN mkdir /app WORKDIR /app CMD [ "npm", "start" ]這里我們把項目相關的構建指令拿出來,放到子項目里去。假設這個基礎鏡像的名字為 my-node 的話,各個項目內的自己的 Dockerfile 就變?yōu)?#xff1a;
FROM my-node COPY ./package.json /app RUN [ "npm", "install" ] COPY . /app/基礎鏡像變化后,各個項目都用這個 Dockerfile 重新構建鏡像,會繼承基礎鏡像的更新。
那么,問題解決了么?沒有。準確說,只解決了一半。如果這個 Dockerfile 里面有些東西需要調整呢?比如 npm install 都需要加一些參數(shù),那怎么辦?這一行 RUN 是不可能放入基礎鏡像的,因為涉及到了當前項目的 ./package.json,難道又要一個個修改么?所以說,這樣制作基礎鏡像,只解決了原來的 Dockerfile 的前4條指令的變化問題,而后面三條指令的變化則完全沒辦法處理。
ONBUILD 可以解決這個問題。讓我們用 ONBUILD 重新寫一下基礎鏡像的 Dockerfile:
FROM node:slim RUN mkdir /app WORKDIR /app ONBUILD COPY ./package.json /app ONBUILD RUN [ "npm", "install" ] ONBUILD COPY . /app/ CMD [ "npm", "start" ]這次我們回到原始的 Dockerfile,但是這次將項目相關的指令加上 ONBUILD,這樣在構建基礎鏡像的時候,這三行并不會被執(zhí)行。然后各個項目的 Dockerfile 就變成了簡單地:
FROM my-node是的,只有這么一行。當在各個項目目錄中,用這個只有一行的 Dockerfile 構建鏡像時,之前基礎鏡像的那三行 ONBUILD 就會開始執(zhí)行,成功的將當前項目的代碼復制進鏡像、并且針對本項目執(zhí)行 npm install,生成應用鏡像。
2.12 官方倉庫示例
這些官方倉庫的 Dockerfile 都是參考典范:https://github.com/docker-library/docs
好啦🌶🌶, Dockerfile的最佳使用就到這里, 喜歡就點個贊吧~ ???
總結
以上是生活随笔為你收集整理的Python3 - Dockerfile 最佳实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Busting Frame Bustin
- 下一篇: .net remoting和wcf自托管