learn.lianglianglee.com/文章/进阶:Dockerfile 高阶使用指南及镜像优化.md.html
2022-09-06 22:30:37 +08:00

1001 lines
67 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>进阶Dockerfile 高阶使用指南及镜像优化.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇)</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log)</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count()</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM)</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题</a>
</li>
<li>
<a class="current-tab" href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>进阶Dockerfile 高阶使用指南及镜像优化</h1>
<h3>Dockerfile 高阶使用及新特性解读</h3>
<p>通过之前的学习,我们已经知道 Dockerfile 是一种可用于镜像构建,具备特定语法的文本文件。而 Docker 自身在使用此文件进行构建镜像的过程中,遵循其固定的行为。</p>
<p>比如在上次 <a href="https://gitbook.cn/gitchat/activity/5cd527e864de19331ba79278">Chat</a> 提到的<strong>缓存</strong></p>
<p>Docker 构建系统中,默认情况下为了加快构建的速度,会将构建过程中的每层都进行缓存,我们建议在编写 Dockerfile 的时候,将更新最为频繁的步骤写到最后面,以避免因为该步骤的内容变更,进而导致后续步骤的缓存失效(缓存的控制是 Docker 固定的行为,我们在之后的 Chat 中会进一步深入内部进行分析)。</p>
<p>而同时,我们通过深入到 Docker 镜像内部发现了它内部的组织形式对于镜像而言它其实是使用配置元信息将对应内容的层layer组织起来的一个集合。</p>
<p>那么在使用 Dockerfile 构建镜像的时候,除了上次 <a href="https://gitbook.cn/gitchat/activity/5cd527e864de19331ba79278">Chat</a> 聊到的内容外,有哪些值得掌握的高级技巧呢? 我们来正式开始本次 Chat 。</p>
<h3>打开 BuildKit 支持</h3>
<p>在上次 <a href="https://gitbook.cn/gitchat/activity/5cd527e864de19331ba79278">Chat</a> 的最后,我们提到可以通过 BuildKit 以提高构建效率,这里我们来对它进行更加详细的解读和分析。</p>
<p>首先,我们知道 Docker 是一个典型的 C/S 架构模型,我们平时使用的 <code>docker</code> 命令,是它的 CLI 客户端,而它的服务端是 dockerd ,在 Linux 系统中,通常它是由 systemd 进行管理的,我们可以通过 <code>systemctl status docker</code> 查看当前 dockerd 的运行状态。</p>
<p>对于构建镜像而言,它同样是需要将待构建的内容(我们称之为 context发送给 dockerd并由 dockerd 的特定模块最终完成构建。</p>
<h4>builder</h4>
<p>这里我们需要引入一个概念 <strong>builder</strong> .</p>
<p>builder 就是上面提到的特定模块,也就是说构建内容 context 是由 Docker CLI 发送给 dockerd并最终由 builder 完成构建。</p>
<p><img src="assets/7141ec30-830d-11e9-8eb9-49b38b06f9d6.jpg" alt="png" /></p>
<p><code>docker</code> 的顶级命令中,我们可以看到有一个 <code>builder</code> 的命令组。它有一个子命令 <code>prune</code> 用于清理所有构建过程中的缓存。</p>
<p>以下是 Docker 18.09 的输出信息。</p>
<pre><code>/ # docker builder
Usage: docker builder COMMAND
Manage builds
Commands:
prune Remove build cache
Run 'docker builder COMMAND --help' for more information on a command.
</code></pre>
<p>而在 Docker 19.03 中,它新增了一个子命令:</p>
<pre><code>/ # docker builder
Usage: docker builder COMMAND
Manage builds
Commands:
build Build an image from a Dockerfile
prune Remove build cache
Run 'docker builder COMMAND --help' for more information on a command.
</code></pre>
<p>这里新增的这个 <code>build</code> 子命令,其实就是我们平时使用的 <code>docker build</code> 或者是 <code>docker image build</code>,现在将它放到 builder 的子目录下也是为了凸显 builder 的概念。</p>
<p>builder 其实很早就存在于 Docker 当中了,我们之前在使用或者说默认在使用的就是 builder 的 v1 版本(在 Docker 内部也将它的版本号定为 1但是由于它太久了有一些功能缺失和不足由此诞生了 builder 的 v2 版本,该项目被称之为 BuildKit 。</p>
<h4><a href="https://github.com/moby/buildkit">BuildKit</a></h4>
<p>BuildKit 的产生主要是由于 v1 版本的 builder 的性能,存储管理和扩展性方面都有不足(毕竟它已经产生了很久,而且近些年 Docker 火热,问题也就逐步暴露出来了), 所以它的重点也在于解决这些问题,关键的功能列在下面:</p>
<ul>
<li>支持自动化的垃圾回收</li>
<li>可扩展的构建格式</li>
<li>并发依赖解决</li>
<li>高效的缓存系统</li>
<li>插件化的架构</li>
</ul>
<p>这些功能我们暂且略过,先回到我们的主线上来。</p>
<p>BuildKit 在 Docker v18.06 版本之后可通过 <code>export DOCKER_BUILDKIT=1</code> 环境变量来设置是否启用。对于 Docker v18.06 需要将 dockerd 也以实现性模式运行。即,修改 /etc/docker/daemon.json 文件,增加 <code>&quot;experimental&quot;: true</code> 配置,然后使用 <code>systemctl restart docker</code> 重启 dockerd 。</p>
<p>如果将 /etc/docker/daemon.json 文件中添加以下配置:</p>
<pre><code>{
&quot;experimental&quot;: true,
&quot;features&quot;: {
&quot;buildkit&quot;: true
}
}
</code></pre>
<p>则会默认使用 BuildKit 进行构建,就不再需要指定环境了。</p>
<h4>小结</h4>
<ul>
<li>在上面的内容中,我们知道了 Docker 是 C/S 架构,而我们通常使用的 <code>docker</code> 命令便是它的 CLI 客户端,服务端是 dockerd 通常由 systemd 进行管理;</li>
<li>我们介绍了一个概念 builder它是 Docker 构建系统中的实际执行者;用于将构建的上下文 context 按照 Dockerfile 的描述最终生成 Docker 镜像image;</li>
<li>BuildKit 是 v2 版本的 builder </li>
<li>我们可以通过增加 <code>export DOCKER_BUILDKIT=1</code> 的环境变量,或是修改 dockerd 的配置文件来临时启用或者默认启用 BuildKit 作为 builder。</li>
</ul>
<p>我们来体验一下开启 BuildKit 的镜像构建:</p>
<pre><code>(MoeLove) ➜ ~ docker build -t local/spring-boot:buildkit https://github.com/tao12345666333/spring-boot-hello-world.git
[+] Building 0.2s (0/1)
[+] Building 0.6s (0/1)
...
[+] Building 6.4s (0/1)
=&gt; [internal] load git source https://github.com/tao12345666333/spring-boot-hello-world.git 6.4s
=&gt; =&gt; # 已初始化空的 Git 仓库于 /var/lib/docker/overlay2/xieo69jwu3qd18uqmuwa6er9l/diff/
898cc478c6bbec5dab019a36fdfdd2dd172cee9erefs/heads/master
[+] Building 394.0s (12/12) FINISHED
=&gt; [internal] load git source https://github.com/tao12345666333/spring-boot-hello-world.git 6.4s
=&gt; [internal] load metadata for docker.io/library/openjdk:8-jre-alpine 3.6s
=&gt; [internal] load metadata for docker.io/library/maven:3.6.1-jdk-8-alpine 3.3s
=&gt; CACHED [stage-2 1/2] FROM docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d7effabda5b2fab6bba7beb9b297a4bfb6e5e2e1">[email&#160;protected]</a>:f362b165b870ef129cbe730f29065f 0.0s
=&gt; =&gt; resolve docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6e5643041c0b430f021e07000b2e1d060f5c5b58">[email&#160;protected]</a>:f362b165b870ef129cbe730f29065ff37399c0aa8bcab 0.0s
=&gt; [builder 1/6] FROM docker.io/library/maven:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="794a574f574854131d1254415418150910171c390a11184b4c4f">[email&#160;protected]</a>:16691dc7e18e5311ee7ae38b40dcf98e 14.3s
=&gt; =&gt; resolve docker.io/library/maven:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9facb1a9b1aeb2f5fbf4b2a7b2fef3eff6f1fadfecf7feadaaa9">[email&#160;protected]</a>:16691dc7e18e5311ee7ae38b40dcf98ee1cfe4a48 0.0s
=&gt; =&gt; sha256:e4ef40f7698347c89ee64b2e5c237d214cae777f33735c52039824eb44feb796 2.18MB / 2.18MB 2.7s
...
=&gt; =&gt; extracting sha256:c2274a1a0e2786ee9101b08f76111f9ab8019e368dce1e325d3c284a0ca33397 0.7s
=&gt; [builder 2/6] WORKDIR /app 0.3s
=&gt; [builder 3/6] COPY pom.xml /app/ 0.0s
=&gt; [builder 4/6] RUN mvn dependency:go-offline 352.4s
=&gt; [builder 5/6] COPY src /app/src 0.1s
=&gt; [builder 6/6] RUN mvn -e -B package 16.3s
=&gt; [stage-2 2/2] COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / 0.1s
=&gt; exporting to image 0.1s
=&gt; =&gt; exporting layers 0.1s
=&gt; =&gt; writing image sha256:82f3748307e8c43af8e28fc5c303b89973e22ba0d2e85c1b43648a5f0c332219 0.0s
=&gt; =&gt; naming to docker.io/local/spring-boot:buildkit 0.0s
</code></pre>
<p>以上便是一个开启了 BuildKit 的镜像构建过程,可以看到与我们之前默认的 builder 的输出之类的都不一样,这里暂不展开了,我们开始下一步的学习。</p>
<h3>构建历史</h3>
<p>我们仍然使用上次 <a href="https://gitbook.cn/gitchat/activity/5cd527e864de19331ba79278">Chat</a> 中的<a href="https://github.com/tao12345666333/spring-boot-hello-world.git">例子</a>:一个 Spring Boot 的项目,同样的本次 Chat 中并不涉及 Spring Boot 的任何知识。只需要知道对于这个项目而言, 需要先安装依赖、构建,才能运行。</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ ls -l
总用量 20
-rw-rw-r--. 1 tao tao 0 5月 15 06:52 Dockerfile
drwxrwxr-x. 2 tao tao 4096 5月 15 06:54 docs
-rw-rw-r--. 1 tao tao 1992 5月 15 06:33 pom.xml
-rw-rw-r--. 1 tao tao 89 5月 15 06:50 README.md
drwxrwxr-x. 4 tao tao 4096 5月 15 06:33 src
drwxrwxr-x. 9 tao tao 4096 5月 15 06:52 target
</code></pre>
<p>我们来看下该项目的 Dockerfile 的内容:</p>
<pre><code>FROM maven:3.6.1-jdk-8-alpine AS builder
WORKDIR /app
COPY pom.xml /app/
RUN mvn dependency:go-offline
COPY src /app/src
RUN mvn -e -B package
FROM builder AS dev
RUN apk add --no-cache vim
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar /
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>我们以此 Dockerfile 来构建镜像,这里我增加了 <code>-q</code> 参数忽略掉默认的输出。</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) docker build -q -t local/spring-boot:1 .
sha256:01e4898d1141763400d39111609425ba6232b8bf42f46a6033fdb2b7306dc75b
</code></pre>
<p>可以看到镜像已经构建成功了,这里我们来介绍一个新的命令 <code>docker image history</code>,对新构建的镜像执行此命令:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) docker image history local/spring-boot:1
IMAGE CREATED CREATED BY SIZE COMMENT
01e4898d1141 292 years ago CMD [&quot;java&quot; &quot;-jar&quot; &quot;/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
&lt;missing&gt; 2 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 18.2MB buildkit.dockerfile.v0
&lt;missing&gt; 2 weeks ago /bin/sh -c set -x &amp;&amp; apk add --no-cache o… 79.4MB
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_VERSION=8u212 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c { echo '#!/bin/sh'; echo 'set… 87B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) CMD [&quot;/bin/sh&quot;] 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB
</code></pre>
<p>可以看到我们镜像的构建记录(以逆序排列),最上面的部分是我们多阶段构建中的。</p>
<pre><code>COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar /
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>这两步所对应的内容。</p>
<p>而下面的部分,则是我们的基础镜像 <code>openjdk:8-jre-alpine</code> 的构建记录。我们的操作基本都可以在 history 中看到。</p>
<h4>构建历史的不安全性</h4>
<p>假如,我们的项目在构建过程当中,需要连接远端的数据库获取对应的信息(比如:获取某个特定的配置),之后才可以进行构建,我们通常情况下会如何去做呢?</p>
<ul>
<li>将密码硬编码写入代码中,如果使用此方法,当密码变更的时候,便需要修改代码才能支持,并且镜像分发的时候,会造成信息泄漏,导致安全问题;</li>
<li>通过环境变量的方式构建,相对灵活,比较容易满足需求。</li>
</ul>
<p>这里我们对 Dockerfile 做一点小改变,比如:我们使用 ENV 将密码通过环境变量的方式注入到镜像中。</p>
<pre><code># 以下省略了基础镜像的构建记录
(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker build -q -t local/spring-boot:2 .
sha256:2f85141a35c386bbeac0ba77acd470025682bebc7da9eb204295ff8fafb6e0a8
(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker image history local/spring-boot:2
IMAGE CREATED CREATED BY SIZE COMMENT
2f85141a35c3 292 years ago CMD [&quot;java&quot; &quot;-jar&quot; &quot;/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
&lt;missing&gt; 292 years ago ENV CACHE_PASSWD=moelove 0B buildkit.dockerfile.v0
&lt;missing&gt; 2 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 18.2MB buildkit.dockerfile.v0
&lt;missing&gt; 2 weeks ago /bin/sh -c set -x &amp;&amp; apk add --no-cache o… 79.4MB
...
</code></pre>
<p>很明显,刚才增加的 ENV 可以直接通过 docker history/docker image history 看到。 <strong>不建议真的这样做</strong></p>
<p>由此,得出了我们的第一个结论,<strong>Docker 镜像的构建历史是不安全的,通过 ENV 设置的信息可在 history 中看到</strong></p>
<p>这也引出了我们的第一个问题:<strong>Docker 镜像的构建记录是可查看的,如何管理构建过程中需要的密码/密钥等敏感信息?</strong></p>
<h4>高阶特性:密码管理</h4>
<p>为了应对类似前面这样的问题,当开启 BuildKit 时我们可以使用高阶用法Dockerfile 的实验特性。</p>
<p>Dockerfile 的实验特性,通过给它的顶部添加 <code># syntax = docker/dockerfile:experimental</code> 来实现,这也是 BuildKit 扩展性的一种表现形式。</p>
<p>具体用法如下:</p>
<pre><code># syntax = docker/dockerfile:experimental
COPY fetch_remote_data.sh .
RUN --mount=type=secret,id=moelove,target=/cache_builder,required ./fetch_remote_data.sh
</code></pre>
<p>然后通过以下命令进行构建:</p>
<pre><code>docker build --secret id=moelove,src=./secret -t local/spring-boot:4 .
</code></pre>
<p>构建成功后,我们来看下 history 的记录:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker history local/spring-boot:4
IMAGE CREATED CREATED BY SIZE COMMENT
b5fcff644568 292 years ago CMD [&quot;java&quot; &quot;-jar&quot; &quot;/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
&lt;missing&gt; 2 minutes ago RUN /bin/sh -c ./fetch_remote_data.sh # buil… 19B buildkit.dockerfile.v0
&lt;missing&gt; 2 minutes ago COPY fetch_remote_data.sh . # buildkit 37B buildkit.dockerfile.v0
&lt;missing&gt; 2 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 18.2MB buildkit.dockerfile.v0
...
</code></pre>
<p>并没有在记录中看到我们的密码,同时,当我们用该镜像启动一个容器后会发现,刚才挂载进去的文件变成空的了。</p>
<h4>高阶特性:密钥管理</h4>
<p>另一种很常见的情况是,在构建过程中,可能需要 <code>git clone</code> 一个私有仓库,或者是 <code>ssh</code> 到某个远程主机上获取一些数据之类的操作。</p>
<p>对于这种情况,我们也可以使用高阶特性, (这里就不在上面例子的基础上来写了,写了一个新的 Dockerfile</p>
<pre><code># syntax = docker/dockerfile:experimental
FROM alpine
# 安装必要的包
RUN apk add --no-cache git openssh-client
# 创建必要的目录 .ssh 由于要使用 ssh 连接,所以需要使用 ssh-keyscan 先获取 public SSH host key
# 当然也可以给 .ssh/config 写配置文件来跳过验证,但容易带来安全问题,不推荐
RUN mkdir -p -m 0700 ~/.ssh &amp;&amp; ssh-keyscan github.com &gt;&gt; ~/.ssh/known_hosts
# clone 私有项目仓库,并创建分支
RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4f28263b0f28263b273a2d612c2022">[email&#160;protected]</a>:tao12345666333/moe.git \
&amp;&amp; cd moe \
&amp;&amp; git checkout -b release
</code></pre>
<p><strong>注意</strong> :使用此功能的时候,需要使用 <a href="https://linux.die.net/man/1/ssh-agent"><code>ssh-agent(1)</code></a> 进行认证代理,所以需要提前安装。</p>
<p>构建方式如下:</p>
<pre><code>(MoeLove) ➜ d eval $(ssh-agent)
Agent pid 28184
(MoeLove) ➜ d ssh-add ~/.ssh/id_rsa
Enter passphrase for /home/tao/.ssh/id_rsa:
Identity added: /home/tao/.ssh/id_rsa (/home/tao/.ssh/id_rsa)
(MoeLove) ➜ d docker build --ssh=default -t local/ssh .
[+] Building 0.5s (10/10) FINISHED
=&gt; [internal] load build definition from Dockerfile 0.1s
=&gt; =&gt; transferring dockerfile: 96B 0.0s
=&gt; [internal] load .dockerignore 0.1s
=&gt; =&gt; transferring context: 2B 0.0s
=&gt; resolve image config for docker.io/docker/dockerfile:experimental 0.0s
=&gt; CACHED docker-image://docker.io/docker/dockerfile:experimental 0.0s
=&gt; [internal] load metadata for docker.io/library/alpine:latest 0.0s
=&gt; [1/4] FROM docker.io/library/alpine 0.0s
=&gt; CACHED [2/4] RUN apk add --no-cache git openssh-client 0.0s
=&gt; CACHED [3/4] RUN mkdir -p -m 0700 ~/.ssh &amp;&amp; ssh-keyscan github.com &gt;&gt; ~/.ssh/known_hosts 0.0s
=&gt; CACHED [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="583f312c183f312c302d3a763b3735">[email&#160;protected]</a>:tao12345666333/moe.git 0.0s
=&gt; exporting to image 0.0s
=&gt; =&gt; exporting layers 0.0s
=&gt; =&gt; writing image sha256:35d3ded5595a48de50054121feed13ebadf9b5e73b6cefeeba4215e1a20a20fd 0.0s
=&gt; =&gt; naming to docker.io/local/ssh
</code></pre>
<p>我们使用该镜像启动一个容器:</p>
<pre><code>(MoeLove) ➜ d docker run --rm -it local/ssh
/ # du -sh moe/
108.0K moe/
/ # ls -al ~/.ssh/*
-rw-r--r-- 1 root root 788 May 30 06:35 /root/.ssh/known_hosts
</code></pre>
<p>可以看到,代码仓库已经成功的 clone 下来了。同时,在 <code>~/.ssh</code> 目录内也并没有保留任何我们公/私钥的信息。</p>
<pre><code>(MoeLove) ➜ d docker history local/ssh
IMAGE CREATED CREATED BY SIZE COMMENT
35d3ded5595a 35 minutes ago RUN /bin/sh -c git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fa9d938eba9d938e928f98d4999597">[email&#160;protected]</a>:tao1… 16.9kB buildkit.dockerfile.v0
&lt;missing&gt; 35 minutes ago RUN /bin/sh -c mkdir -p -m 0700 ~/.ssh &amp;&amp; ss… 392B buildkit.dockerfile.v0
&lt;missing&gt; 36 minutes ago RUN /bin/sh -c apk add --no-cache git openss… 20.8MB buildkit.dockerfile.v0
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) CMD [&quot;/bin/sh&quot;] 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB
</code></pre>
<p>镜像的 history 中也没有任何额外的敏感信息。</p>
<p>如果没有运行 <code>ssh-agent</code> 或者是密钥没有 ssh-add 添加进去, 你就会看到类似下面的问题:</p>
<pre><code>(MoeLove) ➜ d docker build --no-cache --ssh=default -t local/ssh .
[+] Building 11.9s (9/9) FINISHED
=&gt; [internal] load .dockerignore 0.1s
=&gt; =&gt; transferring context: 2B 0.0s
=&gt; [internal] load build definition from Dockerfile 0.1s
=&gt; =&gt; transferring dockerfile: 96B 0.0s
=&gt; resolve image config for docker.io/docker/dockerfile:experimental 0.0s
=&gt; CACHED docker-image://docker.io/docker/dockerfile:experimental 0.0s
=&gt; [internal] load metadata for docker.io/library/alpine:latest 0.0s
=&gt; CACHED [1/4] FROM docker.io/library/alpine 0.0s
=&gt; [2/4] RUN apk add --no-cache git openssh-client 5.5s
=&gt; [3/4] RUN mkdir -p -m 0700 ~/.ssh &amp;&amp; ssh-keyscan github.com &gt;&gt; ~/.ssh/known_hosts 3.0s
=&gt; ERROR [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0c6b65784c6b657864796e226f6361">[email&#160;protected]</a>:tao12345666333/moe.git 2.9s
------
&gt; [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="492e203d092e203d213c2b672a2624">[email&#160;protected]</a>:tao12345666333/moe.git &amp;&amp; cd moe &amp;&amp; git checkout -b release:
#9 0.691 Cloning into 'moe'...
#9 1.923 Warning: Permanently added the RSA host key for IP address '192.30.253.112' to the list of known hosts.
#9 2.842 <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="76111f0236111f021e03145815191b">[email&#160;protected]</a>: Permission denied (publickey).
#9 2.843 fatal: Could not read from remote repository.
#9 2.843
#9 2.843 Please make sure you have the correct access rights
#9 2.843 and the repository exists.
------
rpc error: code = Unknown desc = executor failed running [/bin/sh -c git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b7d0dec3f7d0dec3dfc2d599d4d8da">[email&#160;protected]</a>:tao12345666333/moe.git &amp;&amp; cd moe &amp;&amp; git checkout -b release]: exit code: 128
</code></pre>
<h4>小结</h4>
<p>在上面的内容中,我们学习到了通过 <code>docker image history</code> 可以查看镜像的构建历史,但构建历史是透明的,凡是可以拿到该镜像的人均可查看到其构建历史;所以它是不安全的。</p>
<p>尤其是当我们通过 ENV 或者 RUN 指令等,将密码/配置信息等传递进去,或者是将自己的私钥之类的文件拷贝到镜像中, <strong>这些操作都是不安全的,不应该这样使用</strong> ,在启用 BuildKit 之后,我们可以通过使用新的实验性语法做到更安全的操作。</p>
<p>实验性语法是在 Dockerfile 的头部增加了一个表示当前语法规则的 <code># syntax = docker/dockerfile:experimental</code> (事实上,我们将它称之为 frontend它其实是一个真实存在的 Docker 镜像,在构建过程中,会将它拉取下来使用,这里的详细内容我们可以之后对 frontend 详解的时候再进行讨论。</p>
<p>在 Dockerfile 中通过使用 <code>RUN --mount=type=ssh</code> 或是 <code>RUN --mount=type=secret</code> 的方式,配合 <code>docker build</code> 时,传递 <code>--ssh</code><code>--secret</code> 参数即可使用。可参考<a href="https://docs.docker.com/develop/develop-images/build_enhancements/">官方文档</a>了解更多。</p>
<p>这是一种<strong>推荐</strong><strong>安全</strong>的处理方式,虽然就结果而言这并不是唯一的解决方案,但我还是推荐你及时升级 <code>Docker</code> 并使用这种方式。</p>
<h3>Docker 19.03 构建系统解读</h3>
<p>Docker 19.03 在2019/05/30 发布了 beta5 版本正式版也将在不久之后会发布。相比其他版本而言19.03 在构建系统方面的变化是比较大的,虽然一些特性是 18.09 时就已经增加的。</p>
<h4>builder cache 管理</h4>
<p>在 18.09 之前,有一个命令 <code>docker system prune</code> 可以清除所有的停止状态的容器、所有未被使用的网络、所有 dangling 状态的镜像以及所有 dangling 状态的构建缓存。</p>
<p>但有时候你可能并不想把他们都删掉。在 18.09 版本中,新增了 <code>docker builder prune</code> 命令,该命令可以只删除所有的 BuildKit 的构建缓存。</p>
<p>同样的,之前 builder 产生的构建缓存是需要手动进行清理的,否则磁盘空间将会浪费很多。在 18.09 之后也为 BuildKit 增加了可配置的垃圾回收策略。</p>
<p>具体配置方式是Docker 19.03 中)在 /etc/docker/daemon.json 中写入以下内容:</p>
<pre><code>{
&quot;experimental&quot;: true,
&quot;features&quot;: {
&quot;buildkit&quot;: true
},
&quot;builder&quot;: {
&quot;gc&quot;: {
&quot;enabled&quot;: true,
&quot;defaultKeepStorage&quot;: &quot;20GB&quot;
}
}
}
</code></pre>
<p>以上配置中 experimental 表示是否开启实验性功能features 中是选择开启 BuildKit 支持builder 中的 gc 则表示控制垃圾回收的策略,上面配置的含义是:保留 20G 的缓存,超出则会进行清理。</p>
<h4>多实例 builder 管理</h4>
<p>我们知道 Docker CLI 是提供插件支持的,并且开发一个插件也并不难,不过这不是今天的重点,之后开 Chat 再聊。</p>
<p>Docker 19.03 会提供两个主要的插件 app 和 buildxbuildx 就是这一小节的主角。</p>
<p>如果你安装了 Docker 19.03 但你输入 <code>docker buildx</code> 发现报错时,那说明你的 Docker 还尚未安装 buildx可以使用下面的命令进行安装</p>
<pre><code>(MoeLove) ➜ export DOCKER_BUILDKIT=1
(MoeLove) ➜ docker build --platform=local -o . git://github.com/docker/buildx
(MoeLove) ➜ mkdir -p ~/.docker/cli-plugins/
(MoeLove) ➜ mv buildx ~/.docker/cli-plugins/docker-buildx
</code></pre>
<p>完成后,执行 <code>docker buildx</code> 就会看到以下内容的输出:</p>
<pre><code>(MoeLove) ➜ ~ docker buildx
Usage: docker buildx COMMAND
Build with BuildKit
Management Commands:
imagetools Commands to work on images in registry
Commands:
bake Build from a file
build Start a build
create Create a new builder instance
inspect Inspect current builder instance
ls List builder instances
rm Remove a builder instance
stop Stop builder instance
use Set the current builder instance
version Show buildx version information
Run 'docker buildx COMMAND --help' for more information on a command.
</code></pre>
<p>buildx 主要作用其实是为了扩展 BuildKit 的能力,包括多 builder 实例的管理;多 node 构建以支持扩平台构建等能力。</p>
<p>我们主要来看下如何使用它,深入的分析之后再进行讨论。</p>
<p>我们来演示多实例构建。首先需要创建一个 builder 实例。</p>
<pre><code>(MoeLove) ➜ docker buildx create --name d1809 172.17.0.3
d1809
(MoeLove) ➜ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
d1809 docker-container
d18090 tcp://172.17.0.3:2375 inactive
d1903 * docker-container
d19030 tcp://172.17.0.2:2375 running linux/amd64
default docker
default default running linux/amd64
</code></pre>
<p><code>docker buildx create</code> 通过 <code>--name</code> 来指定 builder 的名称,最后跟的是 host/IP 地址,默认使用 2375 端口。</p>
<p>如果要使用新创建的 builder 需要先通过 <code>docker buildx use</code> 命令来进行切换,当前在使用的 builder 通过 <code>ls</code> 命令的时候会带有一个 <code>*</code> 标记。当然你也可能注意到了它当前的状态是 inactive这是因为只有当它真正开始构建任务了或者是执行过构建任务了 agent 才会启动,将它注册回来。</p>
<pre><code>(MoeLove) ➜ ~ docker buildx use d1809
(MoeLove) ➜ ~ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
d1809 * docker-container
d18090 tcp://172.17.0.3:2375 inactive
d1903 docker-container
d19030 tcp://172.17.0.2:2375 running linux/amd64
default docker
default default running linux/amd64
</code></pre>
<p>接下来还是以前面的 Spring Boot 的项目为例进行构建:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) docker buildx build --load -t remote/spring-boot:1 .
[+] Building 31.1s (6/14)
[+] Building 686.6s (16/16) FINISHED
=&gt; [internal] booting buildkit 21.2s
=&gt; =&gt; pulling image moby/buildkit:master 20.7s
=&gt; =&gt; creating container buildx_buildkit_d18090 0.5s
=&gt; =&gt; unpacking docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fdc5d0978f98d09c918d949398bd8e959ccfc8cb">[email&#160;protected]</a>:f362b165b870ef129cbe730f29065ff37399c0aa8bc 2.2s
=&gt; [builder 2/6] WORKDIR /app 0.0s
=&gt; [builder 3/6] COPY pom.xml /app/ 0.1s
=&gt; [builder 4/6] RUN mvn dependency:go-offline 596.4s
=&gt; [builder 5/6] COPY src /app/src 0.2s
=&gt; [builder 6/6] RUN mvn -e -B package 25.3s
=&gt; [stage-2 2/2] COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / 0.2s
=&gt; exporting to oci image format 2.3s
=&gt; =&gt; exporting layers 1.3s
=&gt; =&gt; exporting manifest sha256:f5af6ad923434c4d7d2d6f94f095ccacfe6983cec592de6b8a0a3af37206686a 0.0s
=&gt; =&gt; exporting config sha256:644867602b8a4a5162dee8534378e3dab28807f593759c6b25bcf16492d807bc 0.0s
=&gt; =&gt; sending tarball 0.9s
=&gt; importing to docker 0.3s
(MoeLove) ➜ spring-boot-hello-world git:(master) docker image ls remote/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
remote/spring-boot 1 644867602b8a About a minute ago 103MB
</code></pre>
<p>镜像构建成功了。<strong>注意</strong> 这里给 <code>docker buildx build</code> 命令传递了 <code>--load</code> 参数,表示我们要将构建好的镜像加载到我们现在在用的 dockerd 当中。</p>
<p>此时再查看 builder 的状态:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS
d1809 * docker-container
d18090 tcp://172.17.0.3:2375 running linux/amd64
d1903 docker-container
d19030 tcp://172.17.0.2:2375 running linux/amd64
default docker
default default running linux/amd64
</code></pre>
<p>可以看到它状态已经上报回来了,处于了 running 状态了。</p>
<p>我们到这个 builder 实际对应的机器上查看该机器上容器的状态:</p>
<pre><code>/ # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ff4a9e18658e moby/buildkit:master &quot;buildkitd&quot; About an hour ago Up About an hour buildx_buildkit_d18090
</code></pre>
<p>可以看到实际上是在该机器的 Docker 中运行了一个 BuildKit 的后端容器,以此来进行构建相关的操作。</p>
<p>当然buildx 还有很多特性,比如可以构建多架构平台的镜像等。可以通过<a href="https://github.com/docker/buildx/blob/master/README.md.html">官方文档</a>对它进一步了解。</p>
<h4>小结</h4>
<p>通过这一小节,我们了解到在 Docker 19.03 版本中,我们可以通过 <code>docker builder prune</code> 清理构建缓存;并且可以通过给 /etc/docker/daemon.json 中写配置的方式来开启构建缓存的自动垃圾回收机制,以减轻磁盘压力。</p>
<p>buildx 是 Docker 的一个 CLI 插件,默认安装完 19.03 后将会同时安装它,当然你也可以手动进行安装。我们通过 buildx 可以进行多个 builder 实例的管理,通过这种方式,可以将很多机器组成一个集群来分担构建压力,或者是分担不同架构的构建任务等。</p>
<h3>发现并优化镜像大小 dive</h3>
<p>镜像的构建系统我们了解的差不多了,我们再聊聊如何发现,并优化镜像大小。这里分两个部分,其一是,发现;其二是,优化。</p>
<h4>发现</h4>
<p>首先推荐一个工具 <a href="https://github.com/wagoodman/dive">dive</a> ; 通过上次的 Chat 我们已经知道了镜像的组成和结构dive 是一个命令行工具,使用它可以浏览 Docker 镜像每层的内容,以此来发现我们镜像中是否有什么不需要的东西存在。</p>
<p>关于 dive 这里不做过多介绍了,该项目的文档中介绍还是比较详细的,我们可以用它来分析下刚才我们构建成功的镜像:</p>
<p><img src="assets/2f5601d0-830d-11e9-a8d7-c164a8393e9a.jpg" alt="png" /></p>
<p>第二种方法,则是比较一般的,通过之前介绍的 <code>docker image history</code> 来查看构建记录和每层的大小,以此来观察是否有非必要的操作之类的。</p>
<h4>优化</h4>
<p>我们对前面所举例中的 Spring Boot 项目的 Dockerfile 做点小改动:</p>
<pre><code>FROM maven:3.6.1-jdk-8-alpine AS builder
WORKDIR /app
COPY pom.xml /app/
RUN mvn dependency:go-offline
COPY src /app/src
RUN mvn -e -B package
FROM builder AS dev
RUN apk add --no-cache vim
FROM openjdk:8-jre-alpine
COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar /
# 增加两句完全没必要的操作,仅做演示
COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar /tmp/
RUN rm /tmp/gs-spring-boot-0.1.0.jar
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>给它增加了两句完全没有必要的操作,现在构建该镜像。</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker image ls remote/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
remote/spring-boot 2 11559170c3fd 7 minutes ago 121MB
remote/spring-boot 1 644867602b8a About an hour ago 103MB
</code></pre>
<p>可以看到使用上面修改后的 Dockerfile 构建的镜像比之前的镜像大了 18M我们之前也讲过了镜像是层的叠加后面操作删掉的文件并不会减少镜像的体积。</p>
<p><strong>那我们如何在不修改 Dockerfile 的情况下让镜像体积变小呢?答案就在构建系统上。</strong></p>
<p>我们可以通过给 <code>docker build</code> 传递 <code>--squash</code> 的参数,来将镜像的层进行合并。</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker build --squash -t remote/spring-boot:3 .
[+] Building 2.5s (16/16) FINISHED
...
=&gt; exporting to image 0.0s
=&gt; =&gt; exporting layers 0.0s
=&gt; =&gt; writing image sha256:2d5ba7eb86d2ad5594f82a896637c91137d150dab61fe8dc3acbdfcd164f6686 0.0s
=&gt; =&gt; naming to docker.io/remote/spring-boot:3 0.0s
</code></pre>
<p>查看构建好的镜像大小:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker image ls remote/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
remote/spring-boot 3 a2c1e139697b 5 seconds ago 103MB
remote/spring-boot 2 11559170c3fd 12 minutes ago 121MB
remote/spring-boot 1 644867602b8a About an hour ago 103MB
</code></pre>
<p>可以看到镜像的体积又恢复了正常,这表示我们对之前层的删除操作生效了。我们来看看构建历史:</p>
<pre><code>(MoeLove) ➜ spring-boot-hello-world git:(master) ✗ docker image history remote/spring-boot:3
IMAGE CREATED CREATED BY SIZE COMMENT
a2c1e139697b About a minute ago 103MB create new from sha256:2d5ba7eb86d2ad5594f82a896637c91137d150dab61fe8dc3acbdfcd164f6686
&lt;missing&gt; 292 years ago CMD [&quot;java&quot; &quot;-jar&quot; &quot;/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
&lt;missing&gt; About a minute ago RUN /bin/sh -c rm /tmp/gs-spring-boot-0.1.0.… 0B buildkit.dockerfile.v0
&lt;missing&gt; About a minute ago COPY /app/target/gs-spring-boot-0.1.0.jar /t… 0B buildkit.dockerfile.v0
&lt;missing&gt; 3 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 0B buildkit.dockerfile.v0
&lt;missing&gt; 2 weeks ago /bin/sh -c set -x &amp;&amp; apk add --no-cache o… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_VERSION=8u212 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c { echo '#!/bin/sh'; echo 'set… 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) CMD [&quot;/bin/sh&quot;] 0B
&lt;missing&gt; 2 weeks ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 0B
</code></pre>
<p>可以看到之前的每层大小都已经变成了 0这是因为把所有的层都合并到了最终的镜像上去了。</p>
<p><strong>特别注意:</strong> <code>--squash</code> 虽然在 1.13.0 版本中就已经加入了 Docker 中,但他至今仍然是实验形式;所以你需要按照我在本篇文章开始部分的介绍那样,打开实验性功能的支持。</p>
<p>但直接传递 <code>--squash</code> 的方式,相对来说足够的简单,也更安全。</p>
<h3>总结</h3>
<p>通过本次 Chat 我们学习到了关于 Docker builder 的概念,以及了解到了下一代版本的 BuildKit学习了 Docker 19.03 中多实例的构建,以及对构建缓存的垃圾回收配置等;学习了 Dockerfile 的高阶特性,并通过这些特性来管理密码和密钥等信息;学习了如何发现并优化镜像的体积。</p>
<p>以上内容中虽然没有具体到它们的全部功能,也没有深入到源码级的分析,但已经涵盖了 Docker 构建系统的最新特性,希望能对你有所帮助。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709980839e6a8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>