如何 docker 化你的应用
作者 :Henrique Souza
原文 :how-to-dockerize-any-application具有一定基础的同学阅读起来风味更佳 ?
1. 挑选基础镜像
Docker 镜像仓库中有许多特定技术的基础镜像:
如果以上都运行不起来,那么你需要从基础系统镜像开始,自行安装所有东西。
大部分网上的教程都会以 Ubuntu 开始(比如 Ubuntu16.04),这也没什么错。
但我建议用 Alpine 镜像:
相比其他,Alpine 的镜像要小得多(可以小到5MB)。
注意:Alpine 不支持 apt-get
命令。Alpine 有自己的包仓库和包管理工具。详情可移步:
https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management
https://pkgs.alpinelinux.org/packages
# Alpine 镜像中使用 apk 来管理包
apk update
apk upgrade
apk search openssh
apk add openssh
apk del openssh
...
2. 安装必要的系统包
这通常是一些琐碎的东西,以致你可能忽略一些细节:
a)应该把 apt-get update 和 apt-get install 写在一行里(在 Alpine 中用 apk 也一样)。这不仅仅是一个惯例,而是应该这样做,否则 apt-get update
临时镜像(层)会被缓存起来,可能不会马上更新你接下来要用到的包信息。
https://forums.docker.com/t/dockerfile-run-apt-get-install-all-packages-at-once-or-one-by-one/17191
我们知道,Dockerfile 的构建过程是针对每一个指令构建一个容器,然后提交后形成一个新的镜像(层),然后运行新的镜像(层)产生一个容器,在容器上执行下一个指令,再提交生成一个新的镜像(层),而临时容器被销毁,直至得到最终的镜像。因为一个 RUN 指令会生成一个中间容器,所以分成多个 RUN 指令会导致生成多个中间容器,而增加清除时的开销,并且 apt 指令启动也是要耗时的。如果一个
apt-get
指令会频繁发生变化,那么把它分出来,单独执行就是可接受的,因为如没有设置清除临时镜像(层),那么前面的指令生成的镜像缓存就会得到复用,从而提高构建效率。
b)再次确认你 仅仅 安装了真正需要的东西(如果你要在生产环境使用 docker)。我见过人们把 vim 和 开发工具 都塞到了镜像里。
必要的话,针对构建/debug/开发
创建不同的 Dockerfile。这不仅是镜像大小的问题,还涉及安全、可维护性等等。
3. 自行添加的文件
有一些线索有助于改进你的 Dockerfiles:
a)理解 COPY 和 ADD 的区别:
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy
COPY
仅支持原样复制本地文件到容器中,而ADD
特殊一点,如果复制的是压缩文件,会自动解压,同时ADD
也可以获取 URL 指向的文件。
b)尽量遵循文件系统的常例来放置文件:
比如,对于解释性语言(Go,Python),就用 /usr/src 路径。
c)检查你要添加的文件的参数。如果只是要执行权限,那没必要在镜像上加一层(RUN chmod +x …)。仅仅需要在本地代码里改好初始参数就行。
就算你用的是 Windows 系统,也没有理由这样做,看这个?:
4. 设定运行用户
首先,休息一下,然后读读下面这篇精彩的文章:
https://medium.com/@mccode/understanding-how-uid-and-gid-work-in-docker-containers-c37a01d01cf
容器运行时,内部用户的 uid/gid 会映射到外部相同的 uid/gid 上,导致容器获得了宿主机上相同 uid/gid 用户的权限。
读完之后,你将理解到:
a)如果应用程序需要访问用户或组表(/etc/passwd 或 /etc/group),则只需要使用特定(固定ID)用户运行容器。
b)尽量避免用 root 来运行容器。
不幸的是,很多流行的应用要求你用特定的 id 组合来运行它们(比如,Elastic Search 要求 uid:gid = 1000:1000)。
骚年,别跟着这样做…
5. 设定对外端口
这是一个微不足道的问题,恳请你不要仅仅因为需要对外暴露一个特权级的小号端口(比如80),就以 root 来运行容器。你完全可以对外暴露一个非特权端口(比如 8080),然后在运行容器的时候把它映射到80端口就行。
这个区别由来已久:
https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html
小于1024的端口都是特殊的端口,不允许普通用户基于该端口跑服务。
6. 设定接入点
一般的方式:立即运行你的可执行文件。
更好的方式:创建一个 “docker-entrypoint.sh” 脚本,这样你可以钩住一些东西,像用环境变量的配置(更多的内容可以看下面):
这是一种非常通用的做法,一些例子如下:
https://github.com/elastic/elasticsearch-docker/tree/master/build/elasticsearch/bin
https://github.com/docker-library/postgres/tree/de8ba87d50de466a1e05e111927d2bc30c2db36d/10
7. 设置配置方法
每个应用都需要某种形式的参数化。你可以在以下两种中选:
1)应用特定配置文件:用这些配置文件记录格式、域、路径等等(当你的环境很复杂,各应用的技术栈差别很大的时候就不适合了)。
2)(操作系统)环境变量:简单而有效。
如果认为这些方法不现代或者是不受推崇,记住,这是 12因素的一部分。
这不是叫你扔掉你的配置文件,重构应用的配置机制。
只需要使用一个简单的 envsubst 命令替换掉配置模板(位于 docker-entrypoint.sh 中,因为是在运行时被执行)就行。
例子:
https://docs.docker.com/samples/library/nginx/#using-environment-variables-in-nginx-configuration
这就封装了应用的特定配置文件,将细节放到了容器中。
8. 外部化数据
记住黄金法则:不要在容器中存放持久化数据。
容器的文件系统通常都是临时的,存活时间有限。所以用户产生的内容、数据文件、程序输出都应该保存到 容器卷 或 绑定挂载 (就是将宿主机上的文件夹关联到容器中)中。
老实讲,对容器卷,我没有太多的使用经验,通常我采用 绑定挂载 方式来保存数据。绑定挂载中用到的文件路径是通过 配置管理工具 (如 Salt Stack)在前面经过 谨慎定义 后创建的。
提到 谨慎定义,我意思如下:
- 在基础系统上创建的用户和组为非特权的。
- 所有的绑定文件夹(-v)都以这个用户创建,并为其所有。
- 相应地给权限(只有这个用户和组有权限,其他用户没有)。
- 由这个用户来运行容器。
- 你能够完全掌控。
9. 确定能很好地处置日志
我意识到前面说的 “持久化数据” 远不是一个严瑾的定义,有些时候日志就这样掉进了灰色地带。你该如何处置它们?
如果你创建了一个应用,并且希望它严格遵守 docker 惯例,那么任何日志 文件 都不应该产生。应用应该把 stdout 和 stderr 作为 事件流 来输出。就像推荐你使用环境变量,这也来源于 12因素。
Docker 会截获所有你发到 stdout 的输出,然后可通过 “docker logs” 命令来访问:
https://docs.docker.com/engine/reference/commandline/logs/
尽管如此,还是有些实际情形处理起来会比较困难。假设你在运行 nginx 容器,会有两种类型的日志文件:
- HTTP Access Logs
- Error Logs
对于不同的结构,配置和现有的实现,在标准输出上管理它们可能并不是微不足道的。
在这里,就按上一节的描述来处置日志就行,另外确认你设置了日志轮替。
10. 轮替日志和其他追加型文件
如果你的应用可以无限地写日志或追加文件,那么你要操心一下文件轮替的问题。
注意设置数据的 存放 策略,这是避免服务器 磁盘空间 耗尽的关键(也是是否符合 GDPR 及其他数据规范的关键)。
如果使用绑定挂载,可以从基础操作系统获取一些帮助,用和本地轮替配置一样的工具,如 logrotate (操作手册)。
下面是我最近找到的一个简单但是完整的例子:
https://www.aerospike.com/docs/operations/configure/log/logrotate.html
另一个比较好的:
作者在文中提到的每个链接都值得你打开来读一读。