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 的各种实验选项时,可能需要开启它。下面是例子:

# syntax = docker/dockerfile:experimental

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 “段落”是重中之重!

如果选择此镜像或版本有任何特殊原因,必须记录下来。

下面的例子中,我们针对版本选择的特殊原因做了记录,这样就避免了后续不知情的开发者随意更改。

# Based on official PHP container
#   Note below on assumptions from that base
# Use 7.3 for now as no mod_php yet via php7-apache2 on Alpine
 
FROM php:7.3.16-alpine

7. Global Arguments

建议少用,如果你预感以后会用到,就事先给它准备一个注释掉的段落,并附带说明,这样就限定了别人加 Global Arguments 的位置及用途(在本例中,它必须在 FROM 之后)。

# Global Args from Docker/BuildKit must be added here after FROM
 
ARG TARGETPLATFORM

8. ONBUILD

不要用,但如果你觉得以后可能会用到,就事先给它准备一个注释掉的段落,并附带说明,原因同 Global Arguments。

# ONBUILD used to run things in downstream builds
#   Such as single layer copies
#  Not used for now
 
# ONBUILD

9. Labels

Labels 很重要,且千奇百怪,它的应用场景也较为广泛,包括 build、deployment、lifecycle management……等多种流程中。

本案例中,我们对其加以限定,只允许使用 OCI Labels,使其更通用和易于管理。

# OCI Annotations from https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL org.opencontainers.image.maintainer="Steve.Mushero@ELKman.io"    
\      org.opencontainers.image.authors="Steve.Mushero@ELKman.io"       
\      org.opencontainers.image.title="ELKman"                      
 
#      org.opencontainers.image.revision="" FROM git
#      org.opencontainers.image.created="2020-05-01T01:01:01.01Z"

10. Base Container Info

总要有基础镜像,它通常已经预装了一些程序,例如 Apache、PHP、Java 或其他容器。

你应当清楚地知道,你对基础镜像做了哪些假设、预期哪些文件处于哪些路径、使用了哪些预设的环境变量。

把这些假设调查清楚,并在注释中记录(即使之后它们可能会发生变化)是好的实践。这样我们在修改此文件时,才能辨别自己修改的位置和内容是否正确。基础镜像越复杂,你越应该这么做,因为后续构建镜像者很容易卡在这些问题上。

你也可以不使用基础镜像的预设,自己用到的部分,全都用自己提供的版本,以确保不被意外修改。但这有可能破坏原镜像的完整性,因此不建议这么做。

# Offical PHP Apache container defaults & assumptions
#  From base Dockerfile:
#   User:  www-data
#   WORKDIR:  /var/www/html (Note we change this)
#   php.ini:  In /usr/local/etc/php (Note we update this via sed)
#   Apache Conf:  In /etc/apache2 (Note we update this via sed)
#   Packages:  LOTS of dev like gcc, g++, make, etc. probably remove

11. ENV Variables

你可以在这里设置各种用户、路径等。

一定要将它们设置为 ENV,而不是在其他地方 hard-code,后者会使得 Dockerfile 变得难以修改。

虽然只要改变一个 ENV 值就会使缓存失效,但仍然建议在能用的地方尽可能使用 ENV,否则很容易出现难以排查的错误,尤其是随着时间的推移,人和事发生变化时。

ENV MAINWORKDIR /var/www
ENV MAINUSER root
ENV MAINGROUP root
ENV APACHEUSER www-data
ENV APACHEGROUP www-data

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-deps

13. 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 Packages
 
RUN 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 uses
 
ENV 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-zip
 
RUN 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 files
 
ENV 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.conf
 
RUN 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-scripts

21. 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 remove
  
COPY /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.sqlite

24. Environment Setup

一旦所有的服务、代码、数据都准备好了,我们就可以设置 .env 文件,这些文件在运行时会用到,在接下的一些 build 步骤中也会用到。

# .env File - Need to copy for production
 
COPY .env.production .env
 
# Copy dusk env for now for testing
 
COPY .env.dusk.testing .env.dusk.testing

25. 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 jobs
 
RUN php artisan optimize
 
# Seed tables, Telescope, etc. data into DB
#   Run after keygen, before other artisan cmds
 
RUN php artisan migrate
 
# Update DB version to app code version; this for container's initial DB only
 
RUN 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 above
    find ./ -type d -print0 | xargs -0 chmod ug+x

28. Final Purging

加一个最终的清理段落,降低镜像大小。

### Data Purge
# Need to purge & cleanup
# rm composer & caches
# rm npx & caches
# rm any man pages, etc.
# vendor cleanup
 
RUN rm -rf /tmp/*
 
# End of apk installs, we can clean
#   As apk cache clean seems useless
 
RUN 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. 做事积极主动,责任心强,有快速学习能力。

这里有完全扁平的管理,这里有一群专注做事的伙伴,这里有开放的沟通文化,这里有轻松舒适的办公环境,这里有我们,这里欢迎你!

扫码二维码投递简历

✎✎✎

更多内容

▼ 点击“阅读原文”,查看更多岗位

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值