学习如何基于预构建镜像创建您自己的容器镜像,这些预构建镜像已准备好为您提供帮助。该过程包括学习编写镜像的最佳实践、定义镜像元数据、测试镜像,以及使用自定义构建流程创建可用于 Alauda Container Platform Registry 的镜像。创建镜像后,您可以将其推送到 Alauda Container Platform Registry。
在为 Alauda Container Platform 创建容器镜像时,作为镜像作者需要考虑多项最佳实践,以确保镜像使用者获得良好体验。由于镜像旨在保持不可变并按原样使用,以下指南有助于确保您的镜像高度可用且易于在 Alauda Container Platform 上使用。
以下指南适用于一般容器镜像的创建,与镜像是否在 Alauda Container Platform 上使用无关。
复用镜像
尽可能基于合适的上游镜像使用 FROM 语句构建您的镜像。这确保当上游镜像更新时,您的镜像可以轻松获得安全修复,而无需您直接更新依赖项。
此外,在 FROM 指令中使用标签,例如 alpine:3.20
,可以让用户明确知道您的镜像基于哪个版本。使用非 latest 的标签可避免您的镜像受到上游镜像最新版本中可能引入的破坏性更改的影响。
保持标签内兼容性
为自己的镜像打标签时,尽量保持标签内的向后兼容性。例如,如果您提供了名为 image 的镜像,当前包含版本 1.0
,您可以提供标签 image:v1
。当您更新镜像时,只要保持与原镜像兼容,就可以继续使用 image:v1
标签,下游用户可以在不被破坏的情况下获取更新。
如果后续发布了不兼容的更新,则切换到新标签,例如 image:v2
。这样允许下游用户根据需要升级到新版本,而不会被不兼容的镜像意外破坏。使用 image:latest
的下游用户则承担了引入不兼容更改的风险。
避免多进程
不要在一个容器内启动多个服务,例如数据库和 SSHD
。这没有必要,因为容器轻量且可以轻松链接以编排多个进程。Alauda Container Platform 允许您通过将相关镜像分组到单个 pod 中,轻松实现共置和共管。
这种共置确保容器共享网络命名空间和存储以进行通信。更新也更不具破坏性,因为每个镜像可以更少频率且独立地更新。单进程的信号处理流程也更清晰,无需管理信号路由到派生进程。
在包装脚本中使用 exec
许多镜像使用包装脚本在启动软件进程前做一些设置。如果您的镜像使用此类脚本,脚本应使用 exec
,以便脚本进程被您的软件进程替换。如果不使用 exec
,容器运行时发送的信号会发送给包装脚本,而非您的软件进程,这不是您想要的。
例如,您有一个包装脚本启动某个服务器进程。您使用 docker run -i
启动容器,运行包装脚本,进而启动进程。如果您想用 CTRL+C
关闭容器,且包装脚本使用了 exec
启动服务器进程,docker
会将 SIGINT 发送给服务器进程,一切按预期工作。如果未使用 exec
,docker
会将 SIGINT 发送给包装脚本进程,您的服务器进程则继续运行。
另外,您的进程在容器中作为 PID 1
运行。这意味着如果主进程终止,整个容器会停止,您从 PID 1
进程启动的任何子进程也会被取消。
清理临时文件
删除构建过程中创建的所有临时文件,包括使用 ADD
命令添加的文件。例如,在执行 yum install
操作后运行 yum clean
命令。
您可以通过如下方式避免 yum
缓存进入镜像层:
注意,如果写成:
则第一次 yum
调用会在该层留下额外文件,这些文件在后续运行 yum clean
时无法被删除。虽然这些额外文件在最终镜像中不可见,但它们存在于底层镜像层中。
当前容器构建过程不允许后续层运行的命令缩小镜像因前层删除文件而占用的空间,但未来可能会改变。这意味着如果您在后续层执行 rm
命令,虽然文件被隐藏,但不会减少下载镜像的总体大小。因此,像 yum clean
示例一样,最好在创建文件的同一命令中删除它们,避免写入镜像层。
此外,在单个 RUN
语句中执行多个命令可以减少镜像层数,提升下载和解压速度。
按正确顺序放置指令
容器构建器从上到下读取 Dockerfile
并执行指令。每个成功执行的指令都会创建一个可在下一次构建相同或其他镜像时复用的层。将不常变更的指令放在 Dockerfile
顶部非常重要,这样下一次构建相同镜像时缓存不会因上层更改而失效,构建速度更快。
例如,如果您正在编写包含 ADD
命令安装您正在迭代的文件和 RUN
命令安装软件包的 Dockerfile
,最好将 ADD
命令放在最后:
这样每次编辑 myfile
并重新运行 docker build
时,系统会复用 yum
命令的缓存层,仅为 ADD
操作生成新层。
如果写成:
则每次更改 myfile
并重新构建时,ADD
操作会使 RUN
层缓存失效,导致 yum
操作也必须重新执行。
标记重要端口
EXPOSE
指令使容器中的端口对主机系统和其他容器可用。虽然可以通过 docker run
指定端口暴露,但在 Dockerfile
中使用 EXPOSE
指令明确声明软件运行所需端口,便于人和软件使用您的镜像:
docker ps
中显示,关联到由您的镜像创建的容器。docker inspect
返回的镜像元数据中。设置环境变量
使用 ENV
指令设置环境变量是良好实践。例如设置项目版本,方便用户无需查看 Dockerfile
即可获知版本。另一个例子是声明系统路径供其他进程使用,如 JAVA_HOME
。
避免默认密码
避免设置默认密码。许多人扩展镜像时忘记删除或更改默认密码,可能导致生产环境用户使用众所周知的密码,带来安全隐患。密码应通过环境变量配置。
如果确实设置了默认密码,确保容器启动时显示适当警告,告知用户默认密码的值及如何更改(例如设置哪个环境变量)。
避免 sshd
最好避免在镜像中运行 sshd
。您可以使用 docker exec
命令访问本地主机上运行的容器,或访问运行在 Alauda Container Platform 集群上的容器。安装和运行 sshd
会增加攻击面和安全补丁需求。
使用卷存储持久数据
镜像应使用卷来存储持久数据。这样,Alauda Container Platform 会将网络存储挂载到运行容器的节点上,容器迁移到新节点时,存储会重新挂载。通过卷存储所有持久数据,即使容器重启或迁移,内容也能保留。如果镜像将数据写入容器内任意位置,内容无法保留。
所有需要在容器销毁后仍保留的数据必须写入卷。容器引擎支持容器的 readonly
标志,可严格执行不向容器临时存储写入数据的良好实践。现在围绕该能力设计镜像,未来更易利用。
在 Dockerfile
中显式定义卷,方便镜像使用者了解运行镜像时必须定义的卷。
有关 Alauda Container Platform 中卷的更多信息,请参阅 Kubernetes 文档。
注意:
即使使用持久卷,您的镜像的每个实例都有自己的卷,实例间文件系统不共享。这意味着卷不能用于在集群中共享状态。
定义镜像元数据有助于 Alauda Container Platform 更好地使用您的容器镜像,为使用您的镜像的开发者创造更佳体验。例如,您可以添加元数据提供镜像的有用描述,或建议可能还需要的其他镜像。
本主题仅定义当前用例所需的元数据,未来可能添加更多元数据或用例。
您可以在 Dockerfile
中使用 LABEL
指令定义镜像元数据。标签类似于环境变量,是附加到镜像或容器的键值对。标签不同于环境变量的是,它们对运行中的应用不可见,也可用于快速查找镜像和容器。
有关 LABEL
指令的更多信息,请参阅 Docker 文档。
标签名称通常带有命名空间。命名空间根据将使用标签的项目设置。对于 Kubernetes,命名空间为 io.k8s。
有关格式的详细信息,请参阅 Docker 自定义元数据文档。