随着容器技术的普及,愈来愈多的应用被容器化。人们使用容器的频率愈来愈高,但经常忽略一个基本但又很是重要的问题 - 容器镜像的体积。本文将介绍精简容器镜像的必要性并以基于 spring boot 的 java 应用为例描述最小化容器镜像的经常使用技巧。java
精简容器镜像是很是必要的,下面分别从安全性和敏捷性两个角度进行阐释。node
安全性python
基于安全方面的考虑,将没必要要的组件从镜像中移除能够减小攻击面、下降安全风险。虽然 docker 支持用户经过 Seccomp 限制容器内能够执行操做或者使用 AppArmor 为容器配置安全策略,但它们的使用门槛较高,要求用户具有安全领域的专业素养。git
敏捷性github
精简的容器镜像能提升容器的部署速度。假设某一时刻访问流量激增,您须要经过增长容器副本数以应对突发压力。若是某些宿主机不包含目标镜像,须要先拉取镜像,而后启动容器,这时使用体积较小的镜像能加速这一过程、缩短扩容时间。另外,镜像体积越小,其构建速度也越快,同时还能减小存储和传输的成本。spring
将一个 java 应用容器化所需的步骤可概括以下:docker
本章所用的样例是一个基于 spring boot 的 java 应用 spring-boot-docker,所用的未经优化的 dockerfile 以下:shell
FROM maven:3.5-jdk-8 COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]
因为应用使用 maven 构建,dockerfile 中指定maven:3.5-jdk-8
做为基础镜像,该镜像的大小为 635MB。经过这种方式最终构建出的镜像很是大,达到了 719MB,这是由于一方面基础镜像自己就很大,另外一方面 maven 在构建过程当中会下载许多用于执行构建任务的 jar 包。缓存
多阶段构建安全
Java 程序的运行只依赖 JRE,并不须要 maven 或者 JDK 中众多用于编译、调试、运行的工具,所以一个明显的优化方法是将用于编译构建 java 源码的镜像和用于运行 java 应用的镜像分开。为了达到这一目的,在 docker 17.05 版本以前须要用户维护 2 个 dockerfile 文件,这无疑增长了构建的复杂性。好在自 17.05 开始,docker 引入了多阶段构建的概念,它容许用户在一个 dockerfile 中使用多个 From 语句。每一个 From 语句能够指定不一样的基础镜像并将开启一个全新的构建流程。您能够选择性地将前一阶段的构建产物复制到另外一个阶段,从而只将必要的内容保留在最终的镜像里。优化后的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM openjdk:8-jre ARG DEPENDENCY=/usr/src/app/target/dependency COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
该 dockerfile 选用maven:3.5-jdk-8
做为第一阶段的构建镜像,选用openjdk:8-jre
做为运行 java 应用的基础镜像而且只拷贝了第一阶段编译好的.claass
文件和依赖的第三方 jar 包到最终的镜像里。经过这种方式优化后的镜像大小为 459MB。
使用 distroless 做为基础镜像
虽然经过多阶段构建能减少最终生成的镜像的大小,但 459MB 的体积仍相对过大。经调查发现,这是由于使用的基础镜像openjdk:8-jre
体积过大,到达了 443MB,所以下一步的优化方向是减少基础镜像的体积。
Google 开源的项目 distroless 正是为了解决基础镜像体积过大这一问题。Distroless 镜像只包含应用程序及其运行时依赖项,不包含包管理器、shell 以及在标准 Linux 发行版中能够找到的任何其余程序。目前,distroless 为依赖 java、python、nodejs、dotnet 等环境的应用提供了基础镜像。
使用 distroless 的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM gcr.io/distroless/java ARG DEPENDENCY=/usr/src/app/target/dependency COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
该 dockerfile 和上一版的惟一区别在于将运行阶段依赖的基础镜像由openjdk:8-jre
(443 MB)替换成了gcr.io/distroless/java
(119 MB)。通过这一优化,最终镜像的大小为 135MB。
使用 distroless 的惟一不即是您没法 attach 到一个正在运行的容器上排查问题,由于镜像中不包含 shell。虽然 distroless 的 debug 镜像提供 busybox shell,但须要用户从新打包镜像、部署容器,对于那些已经基于非 debug 镜像部署的容器无济于事。 但从安全角度来看,没法 attach 容器并不彻底是坏事,由于攻击者没法经过 shell 进行攻击。
使用 alpine 做为基础镜像
若是您确实有 attach 容器的需求,又但愿最小化镜像的大小,能够选用 alpine 做为基础镜像。Alpine 镜像的特色是体积很是下,基础款镜像的体积仅 4 MB 左右。
使用 alpine 后的 dockerfile 以下:
FROM maven:3.5-jdk-8 AS build COPY src /usr/src/app/src COPY pom.xml /usr/src/app RUN mvn -f /usr/src/app/pom.xml clean package FROM openjdk:8-jre-alpine ARG DEPENDENCY=/usr/src/app/target/dependency COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]
这里并未直接继承基础款 alpine,而是选用从 alpine 构建出的包含 java 运行时的openjdk:8-jre-alpine
(83MB)做为基础镜像。使用该 dockerfile 构建出的镜像体积为 99.2MB,比基于 distroless 的还要小。
执行命令docker exec -ti <container_id> sh
能够成功 attach 到运行的容器中。
distroless vs alpine
既然 distroless 和 alpine 都能提供很是小的基础镜像,那么在生产环境中到底应该选择哪种呢?若是安全性是您的首要考虑因素,建议选用 distroless,由于它惟一可运行的二进制文件就是您打包的应用;若是您更关注镜像的体积,能够选用 alpine。
其余技巧
除了能够经过上述技巧精简镜像外,还有如下方式:
想了解更多优化 dockerfile 的小窍门可参考教程 Best practices for writing Dockerfiles。
本文为云栖社区原创内容,未经容许不得转载。