现在,使用Docker或更复杂的K8S来部署你的服务应该是主流的选择了. 而这个做法的前提是使用把你的程序用docker打包构建成Docker镜像.
在这篇文章中, 我总结了我在构建Docker镜像积累的一些好的实践. 供大家参考与借鉴.
- 使用国内源
虽然国内这个情况令我们程序员觉得困扰. 但在国内做开发, 使用国内源基本是每个程序员的必备技能. 从npm国内源, Java Maven仓库国内源, 想要更好更快的编译我们的程序, 不使用国内源是非常浪费时间的行为.
同样,构建Docker镜像时,同样会面临这个问题. 特别是你在构建镜像中, 需要安装Linux的一些服务或软件时, 使用默认的官方源,会显著的让构建时间变得很长.
因此,在国内构建Docker镜像,在Dockerfile文件中,主动加上国内源的设置吧.
以我的一个go服务的构建来说明:
代码语言:javascript复制FROM golang:alpine AS build
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN go env -w GOPROXY=https://goproxy.io,direct
RUN apk update && apk add --no-cache git gcc build-base linux-headers
在这个构建中,我设置了两个国内源:
- 针对alpine,设置了alpine国内源,加快alpine下安装软件的速度
- 针对go, 设置了go的国内代理源, 显著加快go依赖的下载速度
如果在国内不使用国内源, 这个镜像的构建时间久的令人难以接受.
- 使用官方的镜像做为基础镜像
构建Docker镜像,最开始需要做的是选择一个基础镜像, 比如Java语言,需要JRE或是JDK; Node服务需要一个node环境等.
千万不要自己基于Linux来构建这些语言环境. 而是选择Docker官方提供的基础镜像. Docker提供了非常多的基础镜像, 这些都是久经验证的非常可靠的基础镜像.
在Docker Hub网站上去搜索一下就能找到这些优秀的基础镜像. 凡是Docker提供的基础镜像, 都是非常好认的, 就直接一个名称,没有隶属名. 比如golang
这样的就是官方镜像. 而google/cloud-sdk
这样的就不是官方镜像,而是Google提供的.
在这里,需要对Java做特别的说明,虽然官方提供了一个OpenJDK
的基础镜像,但这个基础镜像已经不再维护了. 我现在用的都是eclipse-temurin
这个Java基础镜像, 这也是Docker官方提供的.
- 使用统一的定制基础镜像
有时候你可能需要在基础镜像基础之上,添加一些定制的能力或功能. 而这个又是一种通用的需求. 比如你的一个Java微服务,可能要构建几十个镜像,每个镜像都有这些定制的功能.
这时候,为了避免不同服务或不同团队使用不同的基础镜像, 在公司或团队级别,使用统一镜像是更好的做法.
代码语言:javascript复制FROM my-company/base-java:17.0.3
比如,构建了一个基于eclipse-temurin的自己的定制基础镜像. 这样,所有需要构建的Java服务都使用这个定制基础镜像是最好的选择.
- 考虑更小的基础镜像
在合适的前提下, 你应该考虑使用更小的基础镜像.
比如,你在Linux系统上选择构建一个服务, 那其实你有很多基础Linux镜像可以选择,比如Debian, Ubuntu或是Alpine. 在合适的前提下, 选择Alpine是更好的.
但是, 需要注意的是, 除非它确实不影响你的服务,在其它要素不影响的前提下才这样做, 网上有一种docker镜像干啥都推荐用Alpine的主张,我并不赞同, 因为这里面有其它因素或影响. Alpine用的是***musl***而不是Linux主流的***libc***等,在选择时不能一概而论.
关于这个,我过往写过专门的文章,需要了解的可以参阅: 对Docker基础镜像的思考,该不该选择alpine
- 使用多平台构建
虽然服务器主流都是X64架构的, 但这并不是完全. ARM架构现在也越来越多的被使用,特别在国内, 统信主流是ARM而不是X64.
在构建你的镜像时,不要只考虑支持X64架构. 而应该考虑支持多平台, 构建一次,支持不同的架构是最佳实践. 事实上, 大多数编程语言都没有说只支持X64, 那你基于这些编程语言构建出来的东西,理论上也不会只依赖特定平台.
Docker的buildx是专门支持多平台的, 而在Docker Hub中,你只要稍等用心都会发现主流的镜像都是支持多平台的.
关于如何基于buildx构建多平台镜像,我写过专门的文章供参阅: Docker多平台镜像构建指引
- 利用多阶段构建
有时候,构建Docker镜像有一个很不好的问题,就是一些编译语言的依赖包下载. 比如Java中, 如果你不会多阶段构建,而又在镜像中编译项目的话,那每次都要下载maven或gradle中定义的那些依赖.
这个耗时非常久,而且浪费网络.
而针对这个困境, Docker特别提供了多阶段镜像. 多阶段构建大致就是指把一个Docker镜像构建分为多个阶段. 比如以上面的Java服务为便,利用多阶段构建你可以做成这样
- 阶段一: 编译项目,这个过程会下载依赖
- 阶段二: 构建真正的镜像
这样不同阶段的好处在于, 如果你的依赖定义文件没有发生变更的前提下, 阶段一的构建Docker会缓存,意味着下次在再构建时, 阶段一会直接跳过去,使用缓存.
这样就解决了前面的问题.
- 善用.dockerignore文件
如果你构建Docker镜像,都从来没有定义,甚至不知道.dockerignore的存在, 那就不应该了.
在构建Docker镜像的过程中, Docker会先将本地的一个目录加载到Context上下文中,你才能COPY等. 但是项目中的很多目录,比如java中的build目录, npm中的node_modules其实并不需要加载到Context中, 因为我们会在构建过程中重新编译生成这些目录或文件.
这时候,你可以利用.dockerignore文件来忽略这些目录. 它的使用方式与.gitignore大致类似.
不要觉得这个无所谓或多此一举, 如果你没有合理的设置, 它会影响镜像的构建时间,更不好的是极大的加大你镜像的大小.
- 不要使用root用户
我见过很多程序员或运维人员, 一直使用root用户来部署或运维Linux系统. 这是非常不专业的做法.
这个行为在docker镜像中也是存在的, 很多人构建Docker镜像, 完全没有意识到Docker镜像中也存在用户的概念. 没有对这个做任何处理, 这意味着你就是使用Root用户在运行这个镜像服务.
从安全上来说,这是非常不妥当的.
代码语言:javascript复制FROM eclipse-temurin:17.0.6_10-jre
RUN useradd -ms /bin/bash lingen
USER lingen
定义一个用户是非常简单的, 如上代码所示. 只要这样, 这个镜像运行时, 就是以你定义的用户来运行.
当然,在一些复杂的镜像构建中,要考虑用户权限,及后续挂载Host Volume时可能会有权限上的问题. 这一点后续我有时间再单独聊一下.
- 扫描你的镜像
镜像做好后,花点时间来分析与扫描一下你的镜像,也是非常有必要的. 在安全上这一步不可少.
大多数人可能都没有这个意识, 但安全非常重要.
而Docker官方其实提供了工具,也就是Docker Scout,专门干这个的. 稍微花点时间学习研究下如何使用这个工具,再利用它来优化与加固你的镜像, 是非常好的做法.
最后
上面这些点就是我在构建镜像时,会特别注意的一些点, 相比过往,Dccker确实方便很多. 善用Docker, 能极大的简化我们服务的部署与运维.