Dockerfile构建镜像最佳实践

  在前文Dockefile及命令详解中咱们已经学习了如何经过Dockerfile构建镜像以及命令的详细说明,可是在生产环境或项目使用时如何构建出一个尽量小的镜像是一个必需要学会的要点,本文将带领你们讨论如何精简镜像以及精简镜像带来的好处。在学习本文前建议你们看下Docker核心技术原理Docker容器和镜像的区别文章中关于镜像的分层等知识有基础的了解。php

1、为何要精简镜像

  Docker镜像由不少镜像层(Layers)组成(最多127层),镜像层依赖于一系列的底层技术,好比文件系统(filesystems)、写时复制(copy-on-write)、联合挂载(union mounts)等技术,总的来讲,Dockerfile中的每条指令都会建立一个镜像层,继而会增长总体镜像的尺寸。html

  下面是精简Docker镜像尺寸的好处:java

  一、减小构建时间linux

  二、减小磁盘使用量nginx

  三、减小下载时间c++

  四、由于包含文件少,攻击面减少,提升了安全性git

  五、提升部署速度golang

2、如何精简镜像

2.1 优化基础镜像

  优化基础镜像的方法就是选用合适的更小的基础镜像,经常使用的 Linux 系统镜像通常有 Ubuntu、CentOs、Alpine,其中Alpine更推荐使用。大小对好比下:redis

lynzabo@ubuntu ~/s> docker images
REPOSITORY         TAG             IMAGE ID            CREATED             SIZE
ubuntu             latest        74f8760a2a8b        8 days ago          82.4MB
alpine             latest        11cd0b38bc3c        2 weeks ago         4.41MB
centos               7           49f7960eb7e4        7 weeks ago         200MB
debian             latest        3bbb526d2608        8 days ago          101MB
lynzabo@ubuntu ~/s>

  Alpine是一个高度精简又包含了基本工具的轻量级Linux发行版,基础镜像只有4.41M,各开发语言和框架都有基于Alpine制做的基础镜像,强烈推荐使用它。Alpine镜像各个语言和框架支持状况,能够参考《优化Docker镜像、加速应用部署,教你6个小窍门》spring

  查看上面的镜像尺寸对比结果,你会发现最小的镜像也有4.41M,那么有办法构建更小的镜像吗?答案是确定的,例如 gcr.io/google_containers/pause-amd64:3.1 镜像仅有742KB。为何这个镜像能这么小?在为你们解密以前,再推荐两个基础镜像:

一、scratch镜像

  scratch是一个空镜像,只能用于构建其余镜像,好比你要运行一个包含全部依赖的二进制文件,如Golang程序,能够直接使用scratch做为基础镜像。如今给你们展现一下上文提到的Google pause镜像Dockerfile:

FROM scratch
ARG ARCH
ADD bin/pause-${ARCH} /pause
ENTRYPOINT ["/pause"]

  Google pause镜像使用了scratch做为基础镜像,这个镜像自己是不占空间的,使用它构建的镜像大小几乎和二进制文件自己同样大,因此镜像很是小。固然在咱们的Golang程序中也会使用。对于一些Golang/C程序,可能会依赖一些动态库,你可使用自动提取动态库工具,好比ldd、linuxdeployqt等提取全部动态库,而后将二进制文件和依赖动态库一块儿打包到镜像中。

二、busybox镜像

  scratch是个空镜像,若是但愿镜像里能够包含一些经常使用的Linux工具,busybox镜像是个不错选择,镜像自己只有1.16M,很是便于构建小镜像。

2.2 串联 Dockerfile 指令 

  你们在定义Dockerfile时,若是太多的使用RUN指令,常常会致使镜像有特别多的层,镜像很臃肿,并且甚至会碰到超出最大层数(127层)限制的问题,遵循 Dockerfile 最佳实践,咱们应该把多个命令串联合并为一个 RUN(经过运算符&&/ 来实现),每个 RUN 要精心设计,确保安装构建最后进行清理,这样才能够下降镜像体积,以及最大化的利用构建缓存。

  下面是一个优化前Dockerfile:

FROM ubuntu
ENV VER     3.0.0  
ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz  
# ==> Install curl and helper tools...
RUN apt-get update  
RUN apt-get install -y  curl make gcc  
# ==> Download, compile, and install...
RUN curl -L $TARBALL | tar zxv  
WORKDIR  redis-$VER  
RUN make  
RUN make install  
#...
# ==> Clean up...
WORKDIR /  
RUN apt-get remove -y --auto-remove curl make gcc  
RUN apt-get clean  
RUN rm -rf /var/lib/apt/lists/*  /redis-$VER  
#...
CMD ["redis-server"]

  构建镜像,名称叫 test/test:0.1

  咱们对Dockerfile作优化,优化后Dockerfile:

FROM ubuntu
ENV VER     3.0.0  
ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz

RUN echo "==> Install curl and helper tools..."  && \  
    apt-get update                      && \
    apt-get install -y  curl make gcc   && \
    echo "==> Download, compile, and install..."  && \
    curl -L $TARBALL | tar zxv  && \
    cd redis-$VER               && \
    make                        && \
    make install                && \
    echo "==> Clean up..."  && \
    apt-get remove -y --auto-remove curl make gcc  && \
    apt-get clean                                  && \
    rm -rf /var/lib/apt/lists/*  /redis-$VER
#...
CMD ["redis-server"] 

  构建镜像,名称叫 test/test:0.2

  对比两个镜像大小:

root@k8s-master:/tmp/iops# docker images
REPOSITORY       TAG           IMAGE ID            CREATED             SIZE
test/test        0.2         58468c0222ed        2 minutes ago       98.1MB
test/test        0.1         e496cf7243f2        6 minutes ago       307MB
root@k8s-master:/tmp/iops#

  能够看到,将多条RUN命令串联起来构建的镜像大小是每条命令分别RUN的三分之一。

  提示:为了应对镜像中存在太多镜像层,Docker 1.13版本之后,提供了一个压扁镜像功能,即将 Dockerfile 中全部的操做压缩为一层。这个特性还处于实验阶段,Docker默认没有开启,若是要开启,须要在启动Docker时添加-experimental 选项,并在Docker build 构建镜像时候添加 --squash 。咱们不推荐使用这个办法,请在撰写 Dockerfile 时遵循最佳实践编写,不要试图用这种办法去压缩镜像。

2.3 多阶段构建

使用多阶段构建

  Dockerfile中每条指令都会为镜像增长一个镜像层,而且你须要在移动到下一个镜像层以前清理不须要的组件。实际上,有一个Dockerfile用于开发(其中包含构建应用程序所需的全部内容)以及一个用于生产的瘦客户端,它只包含你的应用程序以及运行它所需的内容。这被称为“建造者模式”。Docker 17.05.0-ce版本之后支持多阶段构建。使用多阶段构建,你能够在Dockerfile中使用多个FROM语句,每条FROM指令可使用不一样的基础镜像,这样您能够选择性地将服务组件从一个阶段COPY到另外一个阶段,在最终镜像中只保留须要的内容。
  下面是一个使用COPY --from 和 FROM ... AS ... 的Dockerfile:

# Compile
FROM golang:1.9.0
WORKDIR /go/src/v9.git...com/.../k8s-monitor
COPY . .
WORKDIR /go/src/v9.git...com/.../k8s-monitor
RUN make build
RUN mv k8s-monitor /root

# Package
# Use scratch image
FROM scratch
WORKDIR /root/
COPY --from=0 /root .
EXPOSE 8080
CMD ["/root/k8s-monitor"]

  构建镜像,你会发现生成的镜像只有上面COPY 指令指定的内容,镜像大小只有2M。这样在之前使用两个Dockerfile(一个Dockerfile用于开发和一个用于生产的瘦客户端),如今使用多阶段构建就能够搞定。  

  它是如何工做的?第二个FROM指令以alpine:latest image为基础开始一个新的构建阶段。COPY –from = 0行仅将前一阶段的构建文件复制到此新阶段。Go SDK和任何中间层都被遗忘,而不是保存在最终image中。

为多阶段构建命名

  默认状况下,阶段未命名,您能够经过整数来引用它们,从第0个FROM指令开始。 可是,您能够经过向FROM指令添加as NAME来命名您的阶段。此示例经过命名阶段并使用COPY指令中的名称来改进前一个示例。这意味着即便稍后从新排序Dockerfile中的指令,COPY也不会中断。

# Compile
FROM golang:1.9.0 AS builder
WORKDIR /go/src/v9.git...com/.../k8s-monitor
COPY . .
WORKDIR /go/src/v9.git...com/.../k8s-monitor
RUN make build
RUN mv k8s-monitor /root

# Package
# Use scratch image
FROM scratch
WORKDIR /root/
COPY --from=builder /root .
EXPOSE 8080
CMD ["/root/k8s-monitor"]

停在特定构建阶段

  构建映像时,不必定须要构建整个Dockerfile每一个阶段。您能够指定目标构建阶段。如下命令假定您使用的是之前的Dockerfile,但在名为builder的阶段中止:

$ docker build --target builder -t alexellis2/href-counter:latest .

  使用此功能可能的一些很是适合的场景是:

    • 调试特定的构建阶段

    • 在debug阶段,启用全部调试或工具,而在production阶段尽可能精简

    • 在testing阶段,您的应用程序将填充测试数据,但在production阶段则使用生产数据

使用外部镜像做为stage

  使用多阶段构建时,您不只能够从Dockerfile中建立的镜像中进行复制。您还可使用COPY –from指令从单独的image中复制,使用本地image名称,本地或Docker注册表中可用的标记或标记ID。若有必要,Docker会提取image并从那里开始复制。语法是:

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

 3、其余优化镜像构建方法

3.1利用分层机制,减少镜像传输大小

  Docker在build镜像的时候,若是某个命令相关的内容没有变化,会使用上一次缓存(cache)的文件层,在构建业务镜像的时候能够注意下面两点:

  • 不变或者变化不多的体积较大的依赖库和常常修改的自有代码分开;

  • 由于cache缓存在运行Docker build命令的本地机器上,建议固定使用某台机器来进行Docker build,以便利用cache。

  下面是构建Spring Boot应用镜像的例子,用来讲明如何分层。其余类型的应用,好比Java WAR包,Nodejs的npm 模块等,能够采起相似的方式。

一、在Dockerfile所在目录,解压缩maven生成的jar包

$ unzip <path-to-app-jar>.jar -d app

二、Dockerfile 咱们把应用的内容分红4个部分COPY到镜像里面:其中前面3个基本不变,第4个是常常变化的自有代码。最后一行是解压缩后,启动spring boot应用的方式。

FROM openjdk:8-jre-alpine
LABEL maintainer "opl-xws@xiaomi.com"
COPY app/BOOT-INF/lib/ /app/BOOT-INF/lib/
COPY app/org /app/org
COPY app/META-INF /app/META-INF
COPY app/BOOT-INF/classes /app/BOOT-INF/classes
EXPOSE 8080
CMD ["/usr/bin/java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher"]

这样在构建镜像时候可大大提升构建速度。

3.2RUN命令中执行apt、apk或者yum类工具技巧

  若是在RUN命令中执行apt、apk或者yum类工具,能够借助这些工具提供的一些小技巧来减小镜像层数量及镜像大小。举几个例子:

  (1)在执行apt-get install -y 时增长选项— no-install-recommends ,能够不用安装建议性(非必须)的依赖,也能够在执行apk add 时添加选项--no-cache 达到一样效果;

  (2)执行yum install -y 时候, 能够同时安装多个工具,好比yum install -y gcc gcc-c++ make ...。将全部yum install 任务放在一条RUN命令上执行,从而减小镜像层的数量;

  (3)组件的安装和清理要串联在一条指令里面,如 apk --update add php7 && rm -rf /var/cache/apk/* ,由于Dockerfile的每条指令都会产生一个文件层,若是将apk add ... rm -rf ... 命令分开,清理没法减少apk命令产生的文件层的大小。 Ubuntu或Debian可使用 rm -rf /**var**/lib/apt/lists/* 清理镜像中缓存文件;CentOS等系统使用yum clean all 命令清理。

3.3压缩镜像

  Docker 自带的一些命令还能协助压缩镜像,好比 export 和 import

$ docker run -d test/test:0.2
$ docker export 747dc0e72d13 | docker import - test/test:0.3

  使用这种方式须要先将容器运行起来,并且这个过程当中会丢失镜像原有的一些信息,好比:导出端口,环境变量,默认指令。查看这两个镜像history信息,以下,能够看到test/test:0.3 丢失了全部的镜像层信息:

root@k8s-master:/tmp/iops# docker history test/test:0.3
IMAGE               CREATED             CREATED BY          SIZE                COMMENT
6fb3f00b7a72        15 seconds ago                          84.7MB              Imported from -
root@k8s-master:/tmp/iops# docker history test/test:0.2
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
58468c0222ed        2 hours ago         /bin/sh -c #(nop)  CMD ["redis-server"]         0B       
1af7ffe3d163        2 hours ago         /bin/sh -c echo "==> Install curl and helper…   15.7MB   
8bac6e733d54        2 hours ago         /bin/sh -c #(nop)  ENV TARBALL=http://downlo…   0B       
793282f3ef7a        2 hours ago         /bin/sh -c #(nop)  ENV VER=3.0.0                0B       
74f8760a2a8b        8 days ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B       
<missing>           8 days ago          /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           8 days ago          /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$…   2.76kB
<missing>           8 days ago          /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           8 days ago          /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B    
<missing>           8 days ago          /bin/sh -c #(nop) ADD file:5fabb77ea8d61e02d…   82.4MB   
root@k8s-master:/tmp/iops#

  社区里还有不少压缩工具,好比Docker-squash ,用起来更简单方便,而且不会丢失原有镜像的自带信息,你们有兴趣能够试试。

3.4明确指定镜像版本,管理更方便

  为了让版本管理起来更方便,应用部署速度更快,在建立镜像的过程当中,建议工程师们明确指定包含版本或者其余辅助信息的tag若是不指定镜像tag,默认会使用latest。每次启动应用实例时,都须要去镜像仓库检查镜像是否更新。这种方式不利于版本管理,对应用启动速度也有必定影响。

4、总结

   使用小容器镜像的性能和安全优点不言而喻,使用小的基础镜像和builder pattern能够更容易地构建小镜像,而且还有许多其余技术可用于单个技术栈和编程语言,以最小化容器体积。 不管你作什么,你均可以确信你保持容器镜像最小化的努力是值得的!Docker镜像的精简手段和精简效果值得深刻探讨和实践,但愿本文能为你们带来帮助。

参考文献:

  1.https://mp.weixin.qq.com/s/T1Rp8x-WWzG9iXqXFp3ADw

  2.https://blog.csdn.net/a1010256340/article/details/80092038

  3.https://www.docker.com

  4.https://wilhelmguo.tk/blog/post/william/Docker构建之多阶段构建

  5.https://v.qq.com/x/page/t0752jh1emh.html

 

                                --- END ---