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

本文讲述了如何通过 Docker 的多阶段构建功能来大幅度减小镜像大小,适用于需要在 Dockerfile 中构建程式(如 javac),且需要另外安装编译工具链的镜像。(如 Java)

先来学习单词(本文全部采用中文词汇,如需查询外文文档可对照该词汇表。理论上个人不赞成翻译术语):

  • multi-stage 多阶段
  • build 构建
  • image 镜像
  • stage 阶段

再来看一下效果: 原 110M+,现 92M。

对比一下 Dockerfile

优化前 Dockerfile:

FROM openjdk:8u171-jdk-alpine3.8

ADD . /app
WORKDIR /app

RUN apk add maven \
  && mvn clean package \
  && apk del maven \
  && mv target/final.jar / \
  && cd / \
  && rm -rf /app \
  && rm -rf /root/.m2

ENTRYPOINT java -jar /final.jar

优化后 Dockerfile:

FROM openjdk:8u171-jdk-alpine3.8 as builder

ADD . /app
WORKDIR /app

RUN apk add maven \
  && mvn clean package \
  && apk del maven \
  && mv target/final.jar /

FROM openjdk:8u181-jre-alpine3.8 as environment
WORKDIR /
COPY --from=builder /final.jar .
ENTRYPOINT java -jar /final.jar

很明显,优化后的 Dockerfile 新增了 FROM AS 这个命令,并出现了两个 FROM。这就是多阶段构建。

了解一下多阶段构建

多阶段构建是 Docker 17.05 的新增功能,它可以在一个 Dockerfile 中使用多个 FROM 语句,以创建多个 Stages(阶段)。每个阶段间独立(来源请求),可以通过 COPY --from 来获取其它阶段的文件。我们来打个比方,把最终镜像比作一盘菜(炒青椒)。把原料青椒炒完后上桌。

# 对比清单
镜像 -> 一盘菜
第一个阶段 -> 炒
第二个阶段 -> 上桌

两个阶段的目标是做好(生成)最终的菜(镜像)。我们要做的是将第一个阶段「炒」出来的食物进行「上桌」。我们的目标是 做出菜,且 菜盘子(盛菜和中间产物)最轻。

可视化流程如下:

# 做菜流程
... 省略原料
原料 -> [第一个阶段——炒] # 此时盘子里有炒的工具、炒的结果和中间产物
# 这时候开启第二个阶段,只保留炒的结果,而不再需要其它。
-> 炒的结果 -> [开始上桌,只保留结果] # 把炒出来的青椒拿来(COPY --from),其它不要
-> 最终是一盘菜。

现在应该大致理解多阶段构建的流程了吧。我们把话筒交给 Java,看看在 Dockerfile 中使用编译工具构建一个 JAR,并只保留构建完的 JAR 和运行时交给 Image,其它则扔掉应该怎么做:

# 第一阶段——编译(炒)
FROM openjdk:8u171-jdk-alpine3.8 as builder # 自带编译工具

ADD . /app
WORKDIR /app

RUN ... 省略编译和清理工作...

# 现在,JAR 已经出炉。JDK 不再需要,所以不能留在镜像中。
# 所以我们开启第二阶段——运行(上桌),并扔掉第一阶段的所有文件(包括编译工具)
FROM openjdk:8u181-jre-alpine3.8 as environment # 只带运行时

# 目前,编译工具等上一阶段的东西已经被我们抛下。目前的镜像中只有运行时,我们需要把上一阶段(炒)的结果拿来,其它不要。
COPY --from=0 /final.jar .

# 好了,现在镜像只有必要的运行时和 JAR 了。
ENTRYPOINT java -jar /final.jar

如上就是多阶段构建的介绍。

使用多阶段构建

多阶段构建的核心命令是 FROM。FORM 对于身经百战的你来说已经不用多讲了。在多阶段构建中,每次 FROM 都会开启一个新的 Stage(阶段),可以看作一个新的 Image(不够准确、来源请求),与其它阶段隔离(甚至包括环境变量)。只有最后的 FROM 才会被纳入 Image 中。

我们来做一个最 simple 的多阶段构建例子:

# Stage 1
FROM alpine:3.8
WORKDIR /demo
RUN echo "Hello, stage 1" > /demo/hi-1.txt

# Stage 2
FROM alpine:3.8
WORKDIR /demo
RUN echo "Hello, stage 2" > /demo/hi-2.txt

可以自己构建一下这个 Dockerfile,然后 docker save <tag> > docker.tar 看看其中的内容。不出意外应该只有 /demo/hi-2.txt 和 Alpine。

在这个 Dockerfile 中,我们创建了两个阶段。第一个阶段创建 hi-1.txt,第二个阶段创建 hi-2.txt,且第二个阶段会被加入最终 Image,其它不会。

复制文件——阶段间的桥梁

如果阶段间完全隔离,那么多阶段就没有意义——上一个阶段的结果会被完全抛弃,并进入全新的下一阶段。

我们可以通过 COPY 命令来获取其它阶段的文件。在多阶段中使用 COPY 和普通应用完全一致,仅需要添加 –form ` 即可。那么,我们修正上一个例子,使最终镜像包含两个阶段的产物:

# Stage 1
FROM alpine:3.8
WORKDIR /demo
RUN echo "Hello, stage 1" > /demo/hi-1.txt

# Stage 2
FROM alpine:3.8
WORKDIR /demo
COPY --from=0 /demo/hi-1.txt /demo
RUN echo "Hello, stage 2" > /demo/hi-2.txt

重新构建并保存(Save),你会发现多了一层 Layer,其中包含 hi-1.txt。

阶段命名——快速识别

对于只有七秒记忆的我们来说,每次使用 stage index 并不是一件很妙的事情。这时候,可以通过阶段命名的方式给它们赋予名字,以方便识别。

为阶段添加名字很简单,只需要在 FROM 后加上 as <name> 即可。

现在,我们更新 Dockerfile,给予阶段名称并使用名称来 COPY。

# Stage 1, it's name is "build1"
FROM alpine:3.8 as build1
WORKDIR /demo
RUN echo "Hello, stage 1" > /demo/hi-1.txt

# Stage 2, it's name is "build2"
FROM alpine:3.8 as build2
WORKDIR /demo
# No longer use indexes
COPY --from=build1 /demo/hi-1.txt /demo
RUN echo "Hello, stage 2" > /demo/hi-2.txt

重新构建并保存,结果应该同上次相同。

仅构建部分阶段——轻松调试

Docker 还为我们提供了一个很方便的调试方式——仅构建部分阶段。它可以使构建停在某个阶段,并不构建后面的阶段。这可以方便我们调试;区分生产、开发和测试。

仍然沿用上次的 Dockerfile,但使用 --target <stage> 参数进行构建:

$ docker build --target build1 .

再次 Save,你会发现只有 build1 的内容。

总结

这就是多阶段构建的全部用法了。我们再回到开篇的两个 Dockerfile 对比,你能发现优化前的镜像胖在哪里了吗?

很显然,它包含了无用的 JDK,JDK 只在编译时起作用,编译完便无用了,只需要 JRE 即可。所以,利用多阶段构建可以隔离编译阶段和运行阶段,以达到镜像最优化。

参考文献

https://docs.docker.com/develop/develop-images/multistage-build/#name-your-build-stages

https://yeasy.gitbooks.io/docker_practice/image/multistage-builds.html

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Docker如何实现修改Docker0网桥默认网段

    1. 背景 Docker 服务启动后默认会创建一个 docker0 网桥(其上有一个 docker0 内部接口),它在内核层连通了其他的物理或虚拟网卡,这就将所有容器和本地主机都放到同一个物理网络. Docker 默认指定了 docker0 接口 的 IP 地址和子网掩码,让主机和容器之间可以通过网桥相互通信,它还给出了 MTU(接口允许接收的最大传输单元),通常是 1500 Bytes,或宿主主机网络路由上支持的默认值.这些值都可以在服务启动的时候进行配置. 2. 环境 [root@iZ2ze

  • 详解Docker Compose 中可用的环境变量问题

    Compose 的多个部分在某种情况下处理环境变量.本教程可以帮助你找到所需的信息. 1. 替换Compose文件中的环境变量 可以使用 shell 中的环境变量填充 Compose 文件中的值: web: image: "webapp:${TAG}" 更多信息请参考 Compose 文件手册中的 Variable substitution章节. 2. 设置容器中的环境变量 可以通过 environment 关键字设置服务容器中的环境变量,就跟使用 docker run -e VARI

  • docker私库Harbor的架构与组件说明

    这篇文章来了解一下harbor架构的组成和运行时各个组件的使用方式. 架构 容器信息 [root@liumiao harbor]# docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------------------------------ harbor-adminse

  • Docker-compose部署gitlab的方法步骤

    Docker-compose部署gitlab 1.安装Docker 安装必要工具 复制代码 代码如下: [root@vm_10_14_centos ~]# yum -y install yum-utils device-mapper-persistent-data lvm2 添加Docker-ce镜像源并安装 [root@vm_10_14_centos ~]# yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/li

  • Docker 搭建私有仓库(registry、harbor)

    为什么要弄私有仓库,大多是为了速度,我们再私有仓库中的push和pull的速度是特别快的. 利用registry快速搭建 https://hub.docker.com/_/registry/ Run a local registry: Quick Version $ docker run -d -p 5000:5000 --restart always --name registry registry:2 Now, use it from within Docker: $ docker pull

  • Docker部署Spring-boot项目的示例代码

    一.基础Spring-boot快速启动 1.1 快速启动 pom.xml加入如下依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <properties&g

  • Docker容器化部署尝试——多容器通信(node+mongoDB+nginx)

    原因是这样的 想要部署一个mocker平台,就在朋友的推荐下选择了 api-mocker 这个现成的项目 该项目分为服务端node.客户端vue.以及数据库mongoDB 在尝试直接部署的时候发现需要装一大堆的环境,node.mongo.nginx啊,特别的麻烦,之前简单的使用过docker,就在想能不能用docker免环境直接部署呢?于是就有了这次的尝试 多容器通信 该项目分为3个部分,于是就要建立3个容器(node.mongo.nginx) 那容器之间怎么实现通信呢? # 通过link指令建

  • linux使用docker-compose部署软件配置详解

    前言 本篇将分享一些 docker-compose 的配置,可参考其总结自己的一套基于docker的开发/生产环境配置.下面话不多说了,来一起看看详细的介绍吧 安装docker及docker-compose install docker curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun install docker-compose sudo curl -L https://github.com/docker/co

  • Docker实现同Ip网段联通的实现

    最近解决docker与宿主机同网段通信的问题,写此文章记录一下整个过程. 例如 宿主机A 和宿主机B是网络联通关系,在宿主机A上面创建了多个容器组成集群,但是我希望通过宿主机B也可以访问到宿主机A的容器,当然,你也可能会说,端口映射非常方便,如果我需要的端口比较多,或者着如果我临时需要增加某些端口,可能设置起来比较麻烦,那么如果我们将宿主机A里面的容器的IP与宿主机的IP在同一个网络,不就可以直接来进行互联互通了么. 1.安装Docker(Linux服务器) 安装 Docker yum inst

  • 详解基于Harbor搭建Docker私有镜像仓库

    什么是 Harbor? 第一次使用这个的时候是刚进公司处理的第一个任务的时候,发现 Harbor 就是一个用于存储和分发 Docker 镜像的企业级Registry 服务器. 网上找到一个 Harbor 的架构图: Harbor 是 VMware 公司开源的企业级 DockerRegistry 项目,项目地址为 https://github.com/vmware/harbor.其目标是帮助用户迅速搭建一个企业级的 Docker registry 服务.它以 Docker 公司开源的 regist

  • Docker-compose一键部署gitlab中文版的方法步骤

    一.gitlab介绍 gitlab官方地址:https://about.gitlab.com/ GitLab是利用 Ruby on Rails 一个开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目.它拥有与Github类似的功能,能够浏览源代码,管理缺陷和注释.可以管理团队对仓库的访问,它非常易于浏览提交过的版本并提供一个文件历史库.团队成员可以利用内置的简单聊天程序(Wall)进行交流.它还提供一个代码片段收集功能可以轻松实现代码复用,便于日后有需

  • Docker安装常用组件(mysql,redis)的方法

    docker安装mysql docker search mysql 搜索 docker pull mysql:5.6 下载 docker images |grep mysql 查看 docker run -p 3306:3306 --name mysql_docker -v $PWD/conf:/etc/mysql/conf.d -v $PWD/logs:/logs -v $PWD/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysq

  • Docker容器的创建、启动、和停止的方法

    1.容器是独立运行的一个或一组应用,及他们的运行环境.容器是Docker中的一个重要的概念. 2.docker容器的启动有三种方式 a.交互方式,基于镜像新建容器并启动 例如我们可以启动一个容器,打印出当前的日历表 [root@rocketmq-nameserver4 ~]# docker run my/python:v1 cal ##my/python:v1为镜像名和标签 我们还可以通过指定参数,启动一个bash交互终端. [root@rocketmq-nameserver4 ~]# dock

  • 基于Docker的Etcd分布式部署的方法步骤

    一 环境准备 1.1 基础环境 ntp配置:略 #建议配置ntp服务,保证时间一致性 etcd版本:v3.3.9 防火墙及SELinux:关闭防火墙和SELinux 名称 地址 主机名 备注 etcd1 172.24.8.71 etcd1.example.com 用于保存相关IP信息 docker01 172.24.8.72 docker01.example.com   docker02 172.24.8.73 docker02.example.com   # hostnamectl set-h

  • Docker能否成为下一个“Linux”?

    Linux操作系统在过去的20年里给数据中心带来了革命性的变化,今天它是应用程序托管平台无可争议的领导者.很难想象,将任何关键任务的生产工作负载部署到Linux以外的任何平台. 当Docker使Linux容器流行起来,这与几年前在打包.部署和托管应用程序方面开始的革命何其相似.此后,整个行业的容器使用呈指数级增长,并且与日俱增. 正如我们无法想象将任何关键应用程序部署到非Linux操作系统上一样,容器也是如此:任何应用程序都必须是容器,以满足未来Web规模的需求(即安全性.可伸缩性.平台无关性和

随机推荐