详解六种减小Docker镜像大小的方法

我从2017年做Vulhub开始,一直在和一个麻烦的问题做斗争:在编写Dockerfile的时候, 如何减小 docker build 生成的镜像大小 ?这篇文章就给大家总结一下我自己使用过的六种减小镜像大小的方法。

1. 使用Alpine Linux

Alpine Linux是一个基于BusyBox和Musl Libc的Linux发行版,其最大的优势就是小。一个纯的基础Alpine Docker镜像在压缩后仅有2.67MB。

不少Docker官方镜像都有Alpine版本,比如PHP:

比较之下就可以发现,alpine版本镜像大小是普通版本的1/5左右。

但是在Docker Hub中,大部分镜像是没有Alpine版本的,比如Mysql和PHP-Apache,如果我们需要基于这些环境开发,就不得不自己编写Alpine版本,或者找一些第三方镜像。

另外,Alpine的另一个缺点是,其使用了Musl Libc作为传统的glibc的替代,编译软件的时候可能会遇到一些不可预知的问题,这一点会导致我们耗费不少不必要的时间。

2. 只安装最少的依赖

apt-get、yum、apk等软件包管理器是我们编译镜像时必然需要用到的工具,纯净的Docker基础镜像通常会缺少wget、curl、git、gcc等工具,需要我们手工来安装。

我们以apt为例,apt-get在安装软件的时候,可以指定一个选项: --no-install-recommends ,指定这个参数后,有一些非必须的依赖将不会被一起安装。比如,我们安装wget时,如果增加这个选项,待安装的包将从6个减少为3个:

这在一定程度上缩小了镜像的大小,但这样做带来的副作用就是,可能导致目标软件缺少一些功能。

比如,此时的wget将无法验证服务器证书的真伪,导致命令出错:

所以,我们一般的做法是,使用apt时尽量增加 --no-install-recommends ,等后面出现一些错误再及时纠正。像wget这种已知的问题,可以提前预判并进行处理:

apt-get install --no-install-recommends wget ca-certificates

3. 为apt擦屁股

某些工具只有编译阶段使用,我不希望它们占用我宝贵的镜像容量,就可以在镜像编译完成后,将这些中间依赖删掉。

我们以apt为例,在使用完成后,我们需要做的事情有:

  • 删除那些 不需要 的依赖: apt-get pruge --autoremove ...
  • 删除本地的软件包列表: rm -rf /var/lib/apt/lists/*

这个过程中我们会遇到一个非常难解的问题,究竟哪些依赖是“不需要”的?

比如,在编译PHP时,我们可能会用到三个工具:wget、libxml、gcc。这三个工具,在编译PHP前都需要安装。但是在编译完成后,我们可以卸载wget和gcc,但不能卸载libxml。

原因是,libxml为PHP所依赖的一个动态链接库,如果我们将其卸载,将会出现找不到共享链接库的错误:

root@8eab53da8d5b:/# php -v
php: error while loading shared libraries: libxml2.so.2: cannot open shared object file: No such file or directory

那么,有没有一个比较方便的办法,我自动只找出那些不是“共享链接库”的依赖并删除他们呢?

当然有,比较简单的办法是,我们遍历刚编译的可执行文件,使用ldd命令列出其依赖的共享链接库文件名,并在源中搜索这个文件名对应的包名:

这些包就是PHP依赖的所有动态链接库,接着我们将这些包用 apt-mark 声明为“手工安装的包”,即可阻止 apt purge 的自动卸载。

然后,我们再自动卸载其余没有用到的包即可。完整shell脚本如下:

find /usr/local -type f -executable -exec ldd '{}' ';' \
 | awk '/=>/ { print $(NF-1) }' \
 | sort -u \
 | xargs -r dpkg-query --search \
 | cut -d: -f1 \
 | sort -u \
 | xargs -r apt-mark manual \
; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false;

4. 尽量将中间依赖的安装与卸载操作放在一个步骤中

docker镜像是一个由“层”来堆叠起来的“千层饼”,我们可以使用 docker history <image name> 这条命令来查看任意一个镜像是由哪些层组成的,以及每一层的大小:

对于Dockerfile来说,这些层的数据都将会被保存在镜像中,即使后一层删除了前一层内保存的文件。

比如,我们有如下Dockerfile:

FROM alpine:3.12
RUN truncate -s 50M /sample.dat
RUN rm -rf /sample.dat

我们可以试试看这个镜像编译出来有多大,58MB:

相比起来,正常的alpine:3.12只有5.57MB,说明即使我们已经删除了 /sample.dat 文件,在最后的镜像中也没有这个内容,但是它永远留在了镜像的history中。

所以,在删除上文说到的“中间依赖”时,我们需要将安装、使用、卸载三个部分写在一个步骤中,才能保证空间被释放。比如:

FROM debian:buster

RUN apt-get update \
 && apt-get install gcc \
 && gcc ... \
 && apt-get purge --autoremove gcc \
 && rm -rf /var/lib/apt/lists/*

5. 多阶段编译

在Docker 17.05版本以后,新引入了 multi-stage builds 这一概念,这将会极大地简化我们上述的所有操作。

简单来说,multi-stage builds支持我们将Docker镜像的编译分成多个“阶段”。比如常见的软件编译的情况,我们可以将编译阶段单独提出来,软件编译完成后直接将二进制文件拷贝到一个新的基础镜像中,这样做最大的好处就是,第二个镜像不再包含任何编译阶段使用的中间依赖,干干净净明明白白。

以最常见的Java项目为例,编译Jar包的时候,我们需要使用到JDK、Maven等工具,但在实际运行阶段,我们只需要JRE环境即可。简单比较下 maven:3-openjdk-8openjdk:8-jre 两个镜像的大小:

差别一倍有余。

以Vulhub中的Shiro 1.2.4环境为例,在其Dockerfile中可以看到两个 FROM 命令:

FROM maven:3-jdk-8 AS builder

LABEL MAINTAINER="phithon <root@leavesongs.com>"

COPY ./code/ /usr/src/

WORKDIR /usr/src

RUN cd /usr/src; \
 mvn -U clean package -Dmaven.test.skip=true

FROM openjdk:8u102-jre

LABEL MAINTAINER="phithon <root@leavesongs.com>"

COPY --from=builder /usr/src/target/shirodemo-1.0-SNAPSHOT.jar /shirodemo-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "/shirodemo-1.0-SNAPSHOT.jar"]

第一个 FROM 用来进入 maven:3-jdk-8 环境,使用maven对源码进行编译;第二个 FROM 进入较小的 openjdk:8u102-jre 环境,使用 COPY --from= 语法,从前一个阶段的编译结果中将jar文件复制到jre的环境中。

最后,在机器上将会留下两个镜像,一个是builder,一个是最终我们需要的那个shiro 1.2.4的环境,后者可以被其他任何用户独立使用,而前者可以直接删除。

对于使用者来说,我们无需再纠结编译软件时中间依赖如何删除才能让镜像比较小的问题,反正第一阶段使用的任何依赖多不会被遗留到正式的生产环境中。

但多阶段编译对于动态链接库的依赖仍然有上述的问题,如果我们拷贝编译成果时只拷贝了可执行文件,在新环境下运行仍然会出现找不到共享链接库的错误。所以个人觉得,多段式编译仅适合于Java、golang等能够跨平台或静态编译的语言,对于C、Python这些依赖较多的项目仍然不友好。

6. 使用slim版本的镜像

细心的同学可能注意过,Docker官方的Debian镜像有个slim版本,这个版本的大小比默认的版本要小一倍多:

slim的中文意思就是“苗条的”,顾名思义, debian:stretch-slim 确实苗条的多,原因是其删除了man文档等许多不会在容器里用到的文件。

有一些上层的镜像会基于slim版本的debian进行编写,比如python。如果我们开发python的项目,可以使用 python:slim 这个基础镜像。

总结一下,六种方法,互相不会影响,我们可以同时使用。但第5个,多阶段编译将会是以后的主流方式。

到此这篇关于详解六种减小Docker镜像大小的方法的文章就介绍到这了,更多相关减小Docker镜像大小内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 优化 Docker 镜像大小常见的方式

    平时我们构建的 Docker 镜像通常比较大,占用大量的磁盘空间,随着容器的大规模部署,同样也会浪费宝贵的带宽资源.本文将介绍几种常用的方法来优化 Docker 镜像大小,这里我们使用 Docker Hub 官方上的 Redis 镜像进行说明. 手动管理 我们能够直接想到的方法就是直接修改官方的 Redis 镜像 Dockerfile 文件,手动删除容器运行后不需要的组件,然后重新构建一个新镜像.这种方法理论上是可行的,但是容易出错,而且效果也不是特别明显.主要是不能和官方的镜像实时同步. 多阶

  • 使用Docker多阶段构建来减小镜像大小的方法

    本文讲述了如何通过 Docker 的多阶段构建功能来大幅度减小镜像大小,适用于需要在 Dockerfile 中构建程式(如 javac),且需要另外安装编译工具链的镜像.(如 Java) 先来学习单词(本文全部采用中文词汇,如需查询外文文档可对照该词汇表.理论上个人不赞成翻译术语): multi-stage 多阶段 build 构建 image 镜像 stage 阶段 再来看一下效果: 原 110M+,现 92M. 对比一下 Dockerfile 优化前 Dockerfile: FROM ope

  • 详解六种减小Docker镜像大小的方法

    我从2017年做Vulhub开始,一直在和一个麻烦的问题做斗争:在编写Dockerfile的时候, 如何减小 docker build 生成的镜像大小 ?这篇文章就给大家总结一下我自己使用过的六种减小镜像大小的方法. 1. 使用Alpine Linux Alpine Linux是一个基于BusyBox和Musl Libc的Linux发行版,其最大的优势就是小.一个纯的基础Alpine Docker镜像在压缩后仅有2.67MB. 不少Docker官方镜像都有Alpine版本,比如PHP: 比较之下

  • 详解复制备份docker容器数据的方法

    这里以jenkins容器为例,介绍三种方法. 方法一 将容器打包成镜像,这时数据就在镜像中了,im:1.0为容器名,可以随便取 docker commit <container id> im:1.0 重新运行镜像,-v将容器内tmp目录映射到宿主机上的tmp目录 docker run -itd -v /tmp:/tmp im:1.0 // 第一个tmp是宿主机目录,第二个是容器内目录 把 /var/jenkins_home的文件复制到tmp下,注意这里的container name是随机生成的

  • 详解制作各种docker镜像

    做了一个星期的镜像,收货颇多,现在整理记录下来,当做工作笔记吧.把常用的几个镜像的Dockerfile分享下. 制作基础docker镜像 第一步:设置Docker镜像源 复制代码 代码如下: yum install -y yum-priorities && rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm && rpm --import /etc/pki/rp

  • 详解SpringBoot构建Docker镜像的3种方式

    本文将介绍3种技术,通过 Maven 把 SpringBoot 应用构建成 Docker 镜像. (1)使用 spring-boot-maven-plugin 内置的 build-image. (2)使用 Google 的 jib-maven-plugin. (3)使用 dockerfle-maven-plugin. Spring Boot 应用 为了方便实践,需要一个 Spring Boot 项目. Demo 项目地址: https://github.com/davelms/medium-ar

  • 详解Mysql中日期比较大小的方法

    假如有个表product有个字段add_time,它的数据类型为datetime,有人可能会这样写sql: 代码如下 select * from product where add_time = '2013-01-12' 对于这种语句,如果你存储的格式是YY-mm-dd是这样的,那么OK,如果你存储的格式是:2013-01-12 23:23:56这种格式你就悲剧了,这是你就可以使用DATE()函数用来返回日期的部分,所以这条sql应该如下处理: 代码如下 select * from produc

  • 详解如何使用Docker部署Django+MySQL8开发环境

    前一段时间重装了系统,然后我还没有备份,导致电脑里的开发环境全都没有了. 一想到又要装 Python 环境,还要装数据库,然后安装过程中还可能报一堆错就头疼. 最近正在学习 Docker,这不正好解决了我当前的痛点了吗?而且,不止这次重装系统,以后再重装都不怕了,只要拿着 Dockerfile 和 docker-compose 文件,不管到什么环境,一条命令轻松跑起来. 之前部署 Python 开发环境,都是用的 virtualenv,或者是 Pipenv.这次使用 Docker 之后,对比下来

  • 详解如何用alpine镜像做一个最小的镜像并运行c++程序

    需求 工作中我们如果要制作镜像,一般都是直接pull官方镜像,比如我们要运行一个c++程序我们可能直接pull一个gcc,或者ubuntu镜像就可以了,但是存在一个问题,我们只是要运行一个c++程序却要运行一个ubuntu系统,这是非常消耗资源的,所以就去网上搜了搜发现早期的docker都是使用alpine镜像来做基础镜像,所以就用alpile镜像来制作镜像 dockerfile FROM alpine:3.7 MAINTAINER Rethink #更新Alpine的软件源为国内(清华大学)的

  • 详解SpringBoot项目docker环境运行时无限重启问题

    可能是我开始处理问题的思路不对,现在描述问题可能也有点乱,但是里面可能的处理方式希望能帮到遇到我这个坑的人 描述:springboot项目,docker镜像里面运行,看docker的日志,项目启动成功后,隔了一分钟左右他就自动重新启动,然后造成网站接口访问的时候nginx报502 gateway啥的,有两台服务器,一个是文件服务器,运行了很简单的上传下载文件的代码以及验证token,另一台运行了java应用,两台服务器都在一次更新项目的镜像,运行过后遇到了这个问题,很奇怪. 然后我将项目弄成ja

  • 详解Django+Vue+Docker搭建接口测试平台实战

    一. 开头说两句 大家好,我叫林宗霖,是一位测试工程师,也是全栈测开训练营中的一名学员. 在跟着训练营学习完Docker容器技术系列的课程后,理所应当需要通过实操来进行熟悉巩固.正好接口自动化测试平台需要迁移到新的测试服务器上,就想要体验一番Docker的"一次构建,处处运行".这篇文章简单介绍了下这次部署的过程,其中使用了Dockerfile定制镜像和Docker-Compose多容器编排. 二. 项目介绍 项目采用的是前后端分离技术来实现的,前端是Vue+ElementUI,后端是

  • 详解IOS UITableViewCell 的 imageView大小更改

    详解IOS UITableViewCell 的 imageView大小更改 实例代码: - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCell

随机推荐