构建更好的Docker镜像的一些技巧

2023-11-02 18:32:27 浏览数 (1)

现在,使用Docker或更复杂的K8S来部署你的服务应该是主流的选择了. 而这个做法的前提是使用把你的程序用docker打包构建成Docker镜像.

在这篇文章中, 我总结了我在构建Docker镜像积累的一些好的实践. 供大家参考与借鉴.

  1. 使用国内源

虽然国内这个情况令我们程序员觉得困扰. 但在国内做开发, 使用国内源基本是每个程序员的必备技能. 从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依赖的下载速度

如果在国内不使用国内源, 这个镜像的构建时间久的令人难以接受.

  1. 使用官方的镜像做为基础镜像

构建Docker镜像,最开始需要做的是选择一个基础镜像, 比如Java语言,需要JRE或是JDK; Node服务需要一个node环境等.

千万不要自己基于Linux来构建这些语言环境. 而是选择Docker官方提供的基础镜像. Docker提供了非常多的基础镜像, 这些都是久经验证的非常可靠的基础镜像.

在Docker Hub网站上去搜索一下就能找到这些优秀的基础镜像. 凡是Docker提供的基础镜像, 都是非常好认的, 就直接一个名称,没有隶属名. 比如golang这样的就是官方镜像. 而google/cloud-sdk这样的就不是官方镜像,而是Google提供的.

在这里,需要对Java做特别的说明,虽然官方提供了一个OpenJDK的基础镜像,但这个基础镜像已经不再维护了. 我现在用的都是eclipse-temurin这个Java基础镜像, 这也是Docker官方提供的.

  1. 使用统一的定制基础镜像

有时候你可能需要在基础镜像基础之上,添加一些定制的能力或功能. 而这个又是一种通用的需求. 比如你的一个Java微服务,可能要构建几十个镜像,每个镜像都有这些定制的功能.

这时候,为了避免不同服务或不同团队使用不同的基础镜像, 在公司或团队级别,使用统一镜像是更好的做法.

代码语言:javascript复制
FROM my-company/base-java:17.0.3

比如,构建了一个基于eclipse-temurin的自己的定制基础镜像. 这样,所有需要构建的Java服务都使用这个定制基础镜像是最好的选择.

  1. 考虑更小的基础镜像

在合适的前提下, 你应该考虑使用更小的基础镜像.

比如,你在Linux系统上选择构建一个服务, 那其实你有很多基础Linux镜像可以选择,比如Debian, Ubuntu或是Alpine. 在合适的前提下, 选择Alpine是更好的.

但是, 需要注意的是, 除非它确实不影响你的服务,在其它要素不影响的前提下才这样做, 网上有一种docker镜像干啥都推荐用Alpine的主张,我并不赞同, 因为这里面有其它因素或影响. Alpine用的是***musl***而不是Linux主流的***libc***等,在选择时不能一概而论.

关于这个,我过往写过专门的文章,需要了解的可以参阅: 对Docker基础镜像的思考,该不该选择alpine

  1. 使用多平台构建

虽然服务器主流都是X64架构的, 但这并不是完全. ARM架构现在也越来越多的被使用,特别在国内, 统信主流是ARM而不是X64.

在构建你的镜像时,不要只考虑支持X64架构. 而应该考虑支持多平台, 构建一次,支持不同的架构是最佳实践. 事实上, 大多数编程语言都没有说只支持X64, 那你基于这些编程语言构建出来的东西,理论上也不会只依赖特定平台.

Docker的buildx是专门支持多平台的, 而在Docker Hub中,你只要稍等用心都会发现主流的镜像都是支持多平台的.

关于如何基于buildx构建多平台镜像,我写过专门的文章供参阅: Docker多平台镜像构建指引

  1. 利用多阶段构建

有时候,构建Docker镜像有一个很不好的问题,就是一些编译语言的依赖包下载. 比如Java中, 如果你不会多阶段构建,而又在镜像中编译项目的话,那每次都要下载maven或gradle中定义的那些依赖.

这个耗时非常久,而且浪费网络.

而针对这个困境, Docker特别提供了多阶段镜像. 多阶段构建大致就是指把一个Docker镜像构建分为多个阶段. 比如以上面的Java服务为便,利用多阶段构建你可以做成这样

  • 阶段一: 编译项目,这个过程会下载依赖
  • 阶段二: 构建真正的镜像

这样不同阶段的好处在于, 如果你的依赖定义文件没有发生变更的前提下, 阶段一的构建Docker会缓存,意味着下次在再构建时, 阶段一会直接跳过去,使用缓存.

这样就解决了前面的问题.

  1. 善用.dockerignore文件

如果你构建Docker镜像,都从来没有定义,甚至不知道.dockerignore的存在, 那就不应该了.

在构建Docker镜像的过程中, Docker会先将本地的一个目录加载到Context上下文中,你才能COPY等. 但是项目中的很多目录,比如java中的build目录, npm中的node_modules其实并不需要加载到Context中, 因为我们会在构建过程中重新编译生成这些目录或文件.

这时候,你可以利用.dockerignore文件来忽略这些目录. 它的使用方式与.gitignore大致类似.

不要觉得这个无所谓或多此一举, 如果你没有合理的设置, 它会影响镜像的构建时间,更不好的是极大的加大你镜像的大小.

  1. 不要使用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时可能会有权限上的问题. 这一点后续我有时间再单独聊一下.

  1. 扫描你的镜像

镜像做好后,花点时间来分析与扫描一下你的镜像,也是非常有必要的. 在安全上这一步不可少.

大多数人可能都没有这个意识, 但安全非常重要.

而Docker官方其实提供了工具,也就是Docker Scout,专门干这个的. 稍微花点时间学习研究下如何使用这个工具,再利用它来优化与加固你的镜像, 是非常好的做法.

最后

上面这些点就是我在构建镜像时,会特别注意的一些点, 相比过往,Dccker确实方便很多. 善用Docker, 能极大的简化我们服务的部署与运维.

0 人点赞