Dockerfile 布局的良好实践
譯者前言
從本文可看出,作者?Steve 非常尊重長久以來屹立不倒的良好軟件工程實踐,這使得本文可以作為 Dockerfile 規范的參考。
本文原作者:Steve Mushero
原文鏈接:https://steve-mushero.medium.com/dockerfile-good-practices-5d677b9538a4
Docker 已經無處不在,寫 Dockerfile 的方式也五花八門,但很難找到一個可以作為范例的,大都較為簡單。少數寫的好的,既沒有解釋清楚好的原因,也沒有給出必要的規范或指引。
因此,本文旨在為復雜的 Dockerfile 編寫提供有價值的規范與指南。下面我將基于我的 DockerFile Annotated Example 而展開?(https://steve-mushero.medium.com/dockerfile-annotated-example-64a31ef3d144)。
1. General?Concepts
首先,介紹一下一般性規范,即長久以來軟件項目都應遵守的良好實踐,可直接應用于 Dockerfile。
這些規范在 Dockerfile 的場景下,作用發揮得更加明顯。這是因為 Dockerfile 是一種高度動態的文件,從項目伊始到進入維護階段很久,修改 Dockerfile 都非常正常。因此需要認真努力,來維持它持續的高質量。
2. Syntax?Block
The syntax block,如果要用,則必須放在第一行。雖然這讓漂亮的標題和注釋塊屈居第二,但是我們別無選擇。
很少有語言需要 syntax block,但使用 Docker 的各種實驗選項時,可能需要開啟它。下面是例子:
3. Title?&?Comment?Block
文件的正文,應開始于以下部分:
一個真正的標題?(title);
目的?(purpose);
所有者?(owner) ;
和其他文件頭部注釋應有的標準內容。
Dockerfile 通常放在頂層目錄,它需要獨立運行,因此在大型項目中,這里的標題和信息比一般的源文件重要得多。
還包括給后來人看的各種假設、issues、復雜性說明。例如所需的 Docker 版本,此文件如何與 Composer、Kubernetes 等系統交互。
你還可以指出此 Dockerfile 產生的容器,是如何與它所屬的更大系統交互的,以及任何與開發、測試、生產環境相關的要點,都應該有說明。
4. TODO
任何時候都應該有一個統一的 TODO 段落,雖然可以在文件中這寫一點,那寫一點,但通常也有一些更大或較為 meta 的 TODO。
5. Build-Time?Arguments
不建議使用,但如果你一定要用,可以加一個注釋掉的段落并附帶說明,便于之后及時了解它的用途。
6. FROM
FROM 語句是重中之重;
FROM “段落”是重中之重;
帶注釋的、帶歷史記錄的、帶現存問題記錄的 FROM “段落”是重中之重!
如果選擇此鏡像或版本有任何特殊原因,必須記錄下來。
下面的例子中,我們針對版本選擇的特殊原因做了記錄,這樣就避免了后續不知情的開發者隨意更改。
7. Global?Arguments
建議少用,如果你預感以后會用到,就事先給它準備一個注釋掉的段落,并附帶說明,這樣就限定了別人加 Global Arguments 的位置及用途(在本例中,它必須在 FROM 之后)。
# Global Args from Docker/BuildKit must be added here after FROMARG TARGETPLATFORM8. ONBUILD
不要用,但如果你覺得以后可能會用到,就事先給它準備一個注釋掉的段落,并附帶說明,原因同?Global?Arguments。
# ONBUILD used to run things in downstream builds # Such as single layer copies # Not used for now# ONBUILD9. Labels
Labels 很重要,且千奇百怪,它的應用場景也較為廣泛,包括 build、deployment、lifecycle management……等多種流程中。
本案例中,我們對其加以限定,只允許使用 OCI Labels,使其更通用和易于管理。
10. Base?Container?Info
總要有基礎鏡像,它通常已經預裝了一些程序,例如 Apache、PHP、Java 或其他容器。
你應當清楚地知道,你對基礎鏡像做了哪些假設、預期哪些文件處于哪些路徑、使用了哪些預設的環境變量。
把這些假設調查清楚,并在注釋中記錄(即使之后它們可能會發生變化)是好的實踐。這樣我們在修改此文件時,才能辨別自己修改的位置和內容是否正確。基礎鏡像越復雜,你越應該這么做,因為后續構建鏡像者很容易卡在這些問題上。
你也可以不使用基礎鏡像的預設,自己用到的部分,全都用自己提供的版本,以確保不被意外修改。但這有可能破壞原鏡像的完整性,因此不建議這么做。
11.?ENV?Variables
你可以在這里設置各種用戶、路徑等。
一定要將它們設置為 ENV,而不是在其他地方 hard-code,后者會使得 Dockerfile 變得難以修改。
雖然只要改變一個 ENV 值就會使緩存失效,但仍然建議在能用的地方盡可能使用 ENV,否則很容易出現難以排查的錯誤,尤其是隨著時間的推移,人和事發生變化時。
12. Install?&?Repo?Setup
Yum apt 只用來安裝你需要的東西,如果必要,可以構建 yum 或 apt 緩存。這個段落還可能包括安裝程序、其選項等的注釋,尤其是希望避免緩存、最小化空間時。
# apk supports --virtual to group & later remove packages # RUN apk add --no-cache --virtual .build-deps gcc # RUN apk del --no-cache .build-deps13. ENV?Install?Tools
將基本的 OS 工具放在一個像這樣的 ENV 變量中,會更容易管理,且未來修改時更加整潔。為支持 troubleshooting(問題排查)和部署,這個列表最開始會較長,后續隨著某些工具的去掉會逐漸縮短(當然,有必要的話該加就加)。
# Lists of tools - will shrink over time to reduce size # Alpha order, please # Telnet not available on alpine ENV INSTALL_TOOLS \bash \busybox-extras \curl \less?14. Install?Basic?Tools
使用變量安裝基本工具,這樣就再也不必修改實際安裝的那一行,就可以更容易地確保有正確的安裝程序選項等,而不必重復和編輯這些行:
# Update Repo Info & Install Basic PackagesRUN apk update --no-cache && \apk add --no-cache --clean-protected ${INSTALL_TOOLS}15. Install?Specialized?Packages
對某些特殊包,或不通過發行版的包管理器安裝的(例如 CentOS 上不通過 yum 安裝),要將其放到單獨段落,它們通常有特別的安裝順序、流程、選項。
單獨的段落,使得它們顯而易見,更容易管理。
# Install Specialized Packages # We need SQLite for Telescope & other usesENV EXTRA_PACKAGES sqlite3 RUN apk update --no-cache && \apk add --no-cache ${EXTRA_PACKAGES}16. Remove?Useless?Stuff
加一個段落,用于刪除用不到的軟件或程序,這會讓容器體積小,且從安全角度看,這會減小攻擊面。例如,許多基礎鏡像都包含 gcc,而在運行時你永遠都用不到 gcc,所以請把它去掉。
這假設你將進行多階段構建或使用 Squash 選項,這兩個選項都會進行最終復制和扁平化,以便只包括所有層中的活動文件。
# Stuff to remove for smaller size # Packages: Some images have dev stuf like gcc, g++, make, etc.ENV REMOVE_PACKAGES gcc RUN apk del $(REMOVE_PACKAGES)17. Section?Markers
段落標記是好實踐,這使得文件結構清晰,內容便于查找,并防止未來的修改被隨機添加到錯誤的地方。這是保持 Dockerfile 衛生的重要舉措。
##### End of OS Items #####18. Service?Items?Section
容器內的服務可以非常多樣化,從 Apache 或 Nginx 到大型代碼庫,再到像 MySQL 或 Elasticsearch 這樣的大型數據系統。它們都有自己的需求和復雜性,大多數都相當簡單,但部署過程一般都很復雜。通常情況下,最好使用它們的專用容器,但有時需要將它們包含在你的容器中,比如將 Apache 包含在 PHP Laravel 應用程序容器中。
在這種情況下,還有很多細節要處理。這些部分通常是前文講過的部分的迷你版,包括包列表、安裝文件和經常就地復制或編輯的配置文件。
這是針對 Apache 的,從一個 ENV 變量開始,包含我們需要安裝的包的列表,然后安裝它們。
##### Apache Items ###### Install Apache & PHP Modules # php7-apache2 installs much of PHP & Apache,ENV PHP_PACKAGES php7 php7-apache2 php7-json php7-phar php7-iconv \php7-openssl php7-curl php7-mbstring php7-fileinfo \php7-tokenizer php7-dom php7-session php7-pdo php7-pdo_sqlite \php7-xml php7-simplexml php7-xmlwriter php7-zipRUN apk update --no-cache && \apk add --no-cache --clean-protected ${PHP_PACKAGES}19. Specialized?Configurations
設置配置文件的方法有很多種,你應該將它們分開并清楚地記錄下來。
在本案例中,我們希望保留幾乎所有的默認值,所以與其復制文件,不如對配置文件做少量原地修改。
基本上,我們先設置 ENV,然后運行 sed 來實現修改。
請注意,在第一部分中,我們最開始用復制制品文件的方式,但后來轉移到使用基本鏡像中包含的文件的方式。
# Using default Alpine Apache configs and modifying from there # Then we override, which lets us use unmodified official filesENV APACHECONFFILE /etc/apache2/httpd.conf ENV APACHECONFDDIR /etc/apache2/conf.d ENV APACHEVHOSTCONFFILE ${APACHECONFDDIR}/default.conf ENV APACHESECURITYFILE ${APACHECONFDDIR}/security.conf# Copy over PHP file from PHP-Apache # Skipping as seems the Alpine version has one: php7-module.conf # COPY /deploy/apache/docker-php.conf ${APACHECONFDDIR}/docker-php.confRUN echo && \# Remove stuff we don't want nor need for security, etc.rm /etc/apache2/conf.d/userdir.conf && \rm /etc/apache2/conf.d/info.conf && \## Apache main config overrides#sed -ri -e 's/^#ServerName.*$/ServerName elkman/g' ${APACHECONFFILE} && \sed -ri -e 's/^ServerTokens.*$/ServerTokens Prod/g' ${APACHECONFFILE} && \sed -ri -e 's/^ServerSignature.*$/ServerSignature Off/g' ${APACHECONFFILE}?20. Other?Services
接下來是其他服務和配置,在本例中是 PHP。
基礎鏡像中已經安裝了 PHP,所以我們只需要處理配置,通過復制和原地修改,再加上清理基礎鏡像并刪除用不到的部分,以確保它都是清晰的。
##### PHP Items ###### PHP Configs - Complicated as there're two PHP on Alpine 7.3 # Some PHP containers use date-specific extension dir in php.ini # On Alpine, careful of which php is used for CLI # vs. mod_php to verify their paths - Very confusing# Disble default php so can't get confused on configs, modules, etc. # Then the one we want works fine in path RUN mv /usr/local/bin/php /usr/local/bin/php.bad # For Alphine 7.3 we use /usr/bin/php and /usr/etc/php ENV PHP_INI_DIR /etc/php7 ENV PHPEXTDIR "/usr/lib/php7/modules/" # Use the default prod configuration from php:7.4.4-apache (php.ini-development also exists) COPY deploy/php/php.ini-production $PHP_INI_DIR/php.ini # Copy overrides COPY deploy/php/php-override-prod.ini $PHP_INI_DIR/conf.d/ COPY deploy/php/php-sourceguardian.ini $PHP_INI_DIR/conf.d/ # Install composer & prestissimo for parallel downloads if needed RUN curl -sS https://getcomposer.org/installer | \php -- --install-dir=/usr/local/bin --filename=composer && \composer global require hirak/prestissimo --no-plugins --no-scripts21. Add?Your?Code
現在你已經安裝了服務,此時應該添加自己的代碼,這次從構建環境中拷貝。
你也可以從 git 倉庫 pull,以包(rpm 包?)的形式安裝等。但是我們的構建環境已經 pull 了所有的代碼、制品、構建腳本、Dockerfile 等,所以直接拷貝是最簡單的。
COPY 命令非常具體,且已經過大量測試。
還請注意,對于 .dockerignore 上的注釋、權限等,團隊需要保持充分且一致的理解。
.dockerignore 通常是很多很多個小時工作的結果,所以需要任何時候都需要大家清晰地理解它。
#### Add Code ##### Need to change WORKDIR as Apache default is /var/www/html WORKDIR ${MAINWORKDIR}# Copy files from VM # Copy App Directories - Not setting owners here, it's done later # Note will ignore the .dockerignore things, so tune that, too # Currently we depend on git to create/ignore all the dirs we need, especially in storage # We do this because later we want to git clone into container as part of build COPY app app COPY config config COPY resources resources COPY routes routes COPY bootstrap bootstrap COPY database database COPY storage storage COPY public public COPY tests tests # Copy Specific Files COPY artisan ./ COPY composer.json ./ COPY composer.lock ./ COPY package.json ./ COPY package-lock.json ./ COPY webpack.mix.js ./22. Building?&?Compiling?Things
寫了代碼后,通常需要 build 之后才能使用?——?對于 JavaScript 來說很常見,不過在我們的例子中,運行 PHP Composer 也是容器構建的一個步驟。
跟往常一樣,這里的文檔、目的、特殊問題(issues)要做到 crystal clear,因為這通常是幾天或幾周的工作和測試的結果。
在本例中,我們在容器構建過程中運行 PHP Composer 來獲取并設置所需的庫。
這很麻煩,而且我們還直接從外部 COPY 一份作為緩存,來提高性能,這是經過大量試驗并踩了很多坑的結果。
# Run Composer install # ENV COMPOSER_CACHE_DIR - Can set if needed, now using default # Cannot use RUN mount here as we need a cache dir, and mount only supports files (as far as I can tell) # Copy in composer cache, use and removeCOPY /composer-cache/files /root/.composer/cache/files # Note: Have to run 'composer dump-autoload' for some reason here; seems install not fully doing it RUN composer install --no-dev --classmap-authoritative --no-ansi \--no-scripts --no-interaction --no-suggest && \composer dump-autoload && \rm -rf /root/.composer/cache然后,跑 npm 以獲得 Vue.js 和所有必要的 Javascript 代碼。
# NPM Stuff & Webpack (part of dev script) # RUN npm install --no-optional # Moving to ci instead of install (ci uses lock file) RUN npm ci --no-optional RUN npm run prod在這個例子中,我們在尋求最優解的同時,先繞過問題。管理 JavaScript 的東西非常有挑戰性,所以我們保留 build 好的目錄,從而避免重新執行容易崩潰的 build 過程。
# Move public artifacts to doc root - do this after npm run # Get .htaccess, too # We missing anything in the standard html? # Not moving as better to point Doc Root to our public # RUN mv public/* html/ && mv public/.htaccess html/23. Data?&?Things
一旦所有的服務、代碼都準備好了,我們就開始準備數據。在本案例中,就是創建空的 sqllite 的 .db 文件(但表的創建和初始化不在此處,而是在更后面的步驟里)。
# Move DB file from source tree to writable storage area # For now, touch empty file - we initialize this DB later # Later we can copy a default DB if we wish # RUN mv database/db.sqlite storage/database/RUN touch storage/database/db.sqlite24. Environment?Setup
一旦所有的服務、代碼、數據都準備好了,我們就可以設置 .env 文件,這些文件在運行時會用到,在接下的一些 build 步驟中也會用到。
# .env File - Need to copy for productionCOPY .env.production .env# Copy dusk env for now for testingCOPY .env.dusk.testing .env.dusk.testing25. Setup?System
現在是時候對系統本身進行設置了。
對于 Laravel (PHP 框架)來說,這意味著運行一堆 Laravel 命令來設置 PHP 配置、秘鑰、初始化數據庫。
這部分會經常變更,所以好的注釋是很重要的。
# Setup configs & code; may later do as other user, fixed UID, etc. # Generate a new key each time (though we also need on install)RUN php artisan key:generate# Optimize & cache; do before we migrate or run other artisan jobsRUN php artisan optimize# Seed tables, Telescope, etc. data into DB # Run after keygen, before other artisan cmdsRUN php artisan migrate# Update DB version to app code version; this for container's initial DB onlyRUN php artisan elkman:update?26. Remove?Logs
移除上述所有步驟產生的日志,這樣鏡像更干凈,且體積更小。
謹記,一定要清除在構建過程中創建的任何日志(因為其中可能包含你不希望用戶看到的敏感信息)。
# Remove log file so we start clean (and with right log file owner)RUN rm -rf storage/logs/*27. File?Permissions
因為前面的各種 COPY 及命令執行,使得在 Docker 中設置文件權限這件事,很容易變成一團糟。對于 Laravel 這樣復雜的運行時環境,尤其如此。
所以一定要提前決定好用什么方法做,并且寫清楚注釋或文檔。我之所以這么說,是因為我們做了無數測試,對此深有體會。
下面的例子中,在同一個地方,一次性把權限設置都做完。這樣的好處是:容易查找、容易修改、容易加特例。
# Permissions carefully managed here # Set all directory permissions # Set global owner & read perms, then set for writable, exec, etc.ENV READPERM 440 ENV WRITEPERM 660 RUN chown -R ${MAINUSER}:${APACHEGROUP} ./ && \chmod -R ${READPERM} ./ && \chmod -R ${WRITEPERM} storage && \# Set all dirs to be executable so we can get into them# Do after any chmods abovefind ./ -type d -print0 | xargs -0 chmod ug+x28. Final?Purging
加一個最終的清理段落,降低鏡像大小。
### Data Purge # Need to purge & cleanup # rm composer & caches # rm npx & caches # rm any man pages, etc. # vendor cleanupRUN rm -rf /tmp/*# End of apk installs, we can clean # As apk cache clean seems uselessRUN rm -rf /var/cache/apk/*至此,一個相當好的 Dockerfile 就誕生了。
必 看
?加入我們?
崗位名稱:云操作系統研發工程師
工作職責:
1. 使用容器化技術解決大數據產品在私有化部署及 SaaS 場景下面臨的多種技術挑戰;
2. 開發基于 Kubernetes 的自動化部署及基礎應用平臺,提升產品的運維效率和穩定性。
崗位要求:
1. 計算機或相關專業畢業,本科及以上學歷;
2. 對操作系統、網絡等底層基礎知識有深入的理解;
3. 至少熟練掌握 C/C++/Java/Python/Go 中的一種編程語言,有良好的編碼習慣;
4. 熟悉 Kubernetes、Docker 原理及應用;
5. 熟練掌握 Linux Shell/Python/Go 語言開發者優先;
6. 熟悉 Hadoop 生態,有分布式系統開發經驗者優先;
7. 做事積極主動,責任心強,有快速學習能力。
這里有完全扁平的管理,這里有一群專注做事的伙伴,這里有開放的溝通文化,這里有輕松舒適的辦公環境,這里有我們,這里歡迎你!
掃碼二維碼投遞簡歷
???
【更多內容】
解讀四大應用場景,神策分析云之 LTV 分析模型搶先體驗
金融新基建系列報告:銀行業六大中期趨勢展望
神策數據王灼洲:如何進行有效的數據治理,提升數據價值?
▼?點擊“閱讀原文”,查看更多崗位
總結
以上是生活随笔為你收集整理的Dockerfile 布局的良好实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 神策合肥研发中心携手安徽开发者社区,深入
- 下一篇: 趁年轻,去硅谷!2021 “神策未来星”