作者|Henrique Souza
译者|金灵杰
这是一篇关于如何 Docker 化任何应用程序的十步清单.
网上有很多关于如何将应用 Docker 化的教程, 为什么我还要再写一个呢?
我见过的大部分教程都是限定在某种特定技术(例如 Java 或者 Python), 可能无法满足读者的需求. 同时, 这些教程也没有说清楚关于 Dev 和 Ops 团队之间建立明确约定所涉及到的所有相关方面(这正是容器化的精髓所在).
我根据最近的经验总结了以下一些步骤. 它是一份细节清单, 包含了其他指南中忽略的内容.
声明: 这 不是一份新手指南 . 我建议读者先掌握一些如何设置和使用 docker 的基础知识, 并且创建和运行一些容器之后, 再来阅读.
让我们开始吧.
选择基础镜像
每种对应技术几乎都有自己的基础镜像, 例如:
- https://hub.docker.com/_/java/
- https://hub.docker.com/_/python/
- https://hub.docker.com/_/nginx/
如果不能直接使用这些镜像, 我们就需要从基础操作系统镜像开始安装所有的依赖.
外面有很多教程使用的都是 Ubuntu(例如 ubuntu:16.04)作为基础镜像, 这不能算有问题, 但是我建议优先考虑 Alpine 镜像:
https://hub.docker.com/_/alpine/
它是一个非常小的基础镜像(大约只有 5MB).
注意: 在基于 Alpine 的镜像中无法使用 "apt-get" 命令, Alpine 系统有自己的软件包仓库和包管理工具. 详细请参考:
- https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management
- https://pkgs.alpinelinux.org/packages
安装必要软件包
这个步骤通常比较琐碎, 有一些容易忽略的细节:
apt-get update 和 apt-get install 命令应该写在一行(如果使用 Alpine 则对应的是 apk 命令). 这不是常见的做法, 但是在 Dockerfile 中应该要这么做, 否则 "apt-get update" 命令产出的临时层可能会被缓存, 导致构建时没有更新包信息(参见 https://forums.docker.com/t/dockerfile-run-apt-get-install-all-packages-at-once-or-one-by-one/17191 这个讨论).
确认是否只安装了实际需要的软件(尤其是这个容器会在生产环境中运行). 我看见过有人在他们的镜像中安装了 vim 和其他开发工具.
如果有必要, 针对构建, 调试和开发环境创建不同的 Dockerfile. 这不仅仅关系到镜像大小, 还涉及到安全性, 可维护性等等.
添加自定义文件
一些优化 Dockerfile 的小提示:
理解 COPY 和 ADD 指令的区别, 详见: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy.
(尽可能)遵照文件系统惯例来存放文件: http://www.pathname.com/fhs/. 例如针对解释型应用程序(如 Python), 使用 /usr/src 目录.
检查添加文件的属性. 如果需要可执行权限, 没有必要在镜像上新建一个层(通过 "RUN chmod +x ..." 指令), 只需要在代码仓库的源文件上修正这些属性即可. 即使开发平台是 Windows, 也可以参照下文给文件增加可执行权限: https://stackoverflow.com/questions/21691202/how-to-create-file-execute-mode-permissions-in-git-on-windows.
定义容器运行时的用户权限
你要知道:
仅当应用程序需要访问用户或组数据 (/etc/passwd 或 /etc/group) 时, 才需要在容器启动时指定固定的用户 ID.
尽可能避免容器以 root 权限运行.
不幸的是, 不少热门应用程序镜像需要用特定的用户 id 来运行(例如 Elastic Search 需要 uid:gid = 1000:1000). 尽量不要在写出这样的镜像......
定义暴露的端口
这也是一个微不足道的小操作, 但是不要为了暴露特权端口 (例如 80) 而将容器以 root 权限运行. 如果有这样的需求, 可以让容器暴露一个非特权端口(例如 8080), 然后在启动时进行端口映射.
关于特权端口和非特权端口的不同: https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html.
定义入口点(entrypoint)
普通方式: 直接运行可执行文件.
更好的方式: 创建一个 "docker-entrypoint.sh" 脚本, 可以用来通过环境变量来配置容器的入口点(具体请参照下一节).
这是一个非常普遍的做法, 这里有一些例子:
https://github.com/elastic/elasticsearch-docker/tree/master/build/elasticsearch/binhttps://github.com/docker-library/postgres/tree/de8ba87d50de466a1e05e111927d2bc30c2db36d/10
定义一种配置方式
基本上每个应用程序都需要参数化. 基本上有两条路可以遵循:
使用应用程序特定的配置文件: 该方式需要通过文档说明配置文件的格式, 字段, 放置位置等等(当运行环境比较复杂, 例如应用程序跨越不同的技术, 则不太合适).
使用 (操作系统) 环境变量: 简单而有效.
如果读者认为这种方式不够现代, 记住这也是 12-factors 推荐的方式:
https://12factor.net/zh_cn/config
这并不意味着我们可以抛开所有的配置文件, 并对应用程序进行重构, 去除配置文件机制. 只需要通过 envsubst 命令来替换配置文件模板(这个流程需要在 docker-entrypoint.sh 文件中完成, 因为这需要在运行时完成).
例如:
https://docs.docker.com/samples/library/nginx/#using-environment-variables-in-nginx-configuration
这种方式可以将应用程序的配置文件封装在容器内部, 无须让使用者了解这些细节.
外部化数据
关于数据存储有一条黄金法则: 绝对不要将任何持久化数据保存到容器内 .
容器的文件系统被设计成临时和短暂的. 因此任何由应用程序生成的内容, 数据文件和处理结果都应该保存到 挂载的卷 或者 操作系统绑定挂载点上 (既将宿主机操作系统的目录挂载到容器中).
对于挂载卷我不太有经验, 因此我个人更倾向于将数据保存到 绑定挂载点 (bind mounts). 这些挂载点一般通过类似 Salt Stack 这样的配置管理工具仔细的在宿主机上创建.
这里说的 "仔细创建", 主要包括下面几个步骤:
在宿主机操作系统上创建非特权用户(和组).
所有需要绑定目录的所有者都是该用户.
根据使用场景给授权(仅针对这个特定的用户和组, 其他用户无权访问).
容器也以该用户运行.
此时容器就可以完全控制这些目录.
确保处理好日志
前面关于 "持久性数据" 没有一个明确的定义, 日志在这里就是灰色地带. 我们该如何处理它们呢?
如果这是一个新的应用程序, 并且希望它能够坚持 docker 约定, 就不应该将日志写入文件. 应用程序应该使用标准输出和标准错误输出日志. 和之前推荐使用环境变量一样, 这也是 12-factors 之一:
https://12factor.net/zh_cn/logs
Docker 会自动捕捉应用程序的标准输出, 并可以通过 "docker logs" 命令查看:
https://docs.docker.com/engine/reference/commandline/logs/
当然还有一些实际场景下会遇到问题. 例如运行一个简单的 nginx 容器, 至少会有两种不同的日志文件:
HTTP 访问日志(Access Logs)
错误日志(Error Logs)
对于这种日志按照特定结构输出的应用, 可能不太适合将它们的日志输出到标准输出. 这个例子中, 只需要按照前面一节中说的处理好持久化问题, 并确保正确配置文件的轮转.
轮转日志和其他仅追加文件
如果应用程序将日志写到文件, 或者会无限追加内容到文件, 就需要关注这些文件的轮转(rotation), 这对于防止服务器空间耗尽非常有用(尤其是 GDPR 和其他数据安全条例出来之后).
如果使用绑定挂载, 我们可以依靠宿主机的一些工具来实现文件轮转功能, 例如 logrotate(文档参见 https://linux.die.net/man/8/logrotate).
最近我找到的一个简单且完整的例子: https://www.aerospike.com/docs/operations/configure/log/logrotate.html
另外一个例子: https://www.digitalocean.com/community/tutorials/how-to-manage-logfiles-with-logrotate-on-ubuntu-16-04
来源: http://www.tuicool.com/articles/NrMVJbM