mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-27 05:36:42 +08:00
1001 lines
67 KiB
HTML
1001 lines
67 KiB
HTML
<!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>"experimental": true</code> 配置,然后使用 <code>systemctl restart docker</code> 重启 dockerd 。</p>
|
||
<p>如果将 /etc/docker/daemon.json 文件中添加以下配置:</p>
|
||
<pre><code>{
|
||
"experimental": true,
|
||
"features": {
|
||
"buildkit": 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)
|
||
=> [internal] load git source https://github.com/tao12345666333/spring-boot-hello-world.git 6.4s
|
||
=> => # 已初始化空的 Git 仓库于 /var/lib/docker/overlay2/xieo69jwu3qd18uqmuwa6er9l/diff/
|
||
898cc478c6bbec5dab019a36fdfdd2dd172cee9erefs/heads/master
|
||
[+] Building 394.0s (12/12) FINISHED
|
||
=> [internal] load git source https://github.com/tao12345666333/spring-boot-hello-world.git 6.4s
|
||
=> [internal] load metadata for docker.io/library/openjdk:8-jre-alpine 3.6s
|
||
=> [internal] load metadata for docker.io/library/maven:3.6.1-jdk-8-alpine 3.3s
|
||
=> CACHED [stage-2 1/2] FROM docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d7effabda5b2fab6bba7beb9b297a4bfb6e5e2e1">[email protected]</a>:f362b165b870ef129cbe730f29065f 0.0s
|
||
=> => resolve docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6e5643041c0b430f021e07000b2e1d060f5c5b58">[email protected]</a>:f362b165b870ef129cbe730f29065ff37399c0aa8bcab 0.0s
|
||
=> [builder 1/6] FROM docker.io/library/maven:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="794a574f574854131d1254415418150910171c390a11184b4c4f">[email protected]</a>:16691dc7e18e5311ee7ae38b40dcf98e 14.3s
|
||
=> => resolve docker.io/library/maven:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9facb1a9b1aeb2f5fbf4b2a7b2fef3eff6f1fadfecf7feadaaa9">[email protected]</a>:16691dc7e18e5311ee7ae38b40dcf98ee1cfe4a48 0.0s
|
||
=> => sha256:e4ef40f7698347c89ee64b2e5c237d214cae777f33735c52039824eb44feb796 2.18MB / 2.18MB 2.7s
|
||
...
|
||
=> => extracting sha256:c2274a1a0e2786ee9101b08f76111f9ab8019e368dce1e325d3c284a0ca33397 0.7s
|
||
=> [builder 2/6] WORKDIR /app 0.3s
|
||
=> [builder 3/6] COPY pom.xml /app/ 0.0s
|
||
=> [builder 4/6] RUN mvn dependency:go-offline 352.4s
|
||
=> [builder 5/6] COPY src /app/src 0.1s
|
||
=> [builder 6/6] RUN mvn -e -B package 16.3s
|
||
=> [stage-2 2/2] COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / 0.1s
|
||
=> exporting to image 0.1s
|
||
=> => exporting layers 0.1s
|
||
=> => writing image sha256:82f3748307e8c43af8e28fc5c303b89973e22ba0d2e85c1b43648a5f0c332219 0.0s
|
||
=> => 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 [ "java", "-jar", "/gs-spring-boot-0.1.0.jar" ]
|
||
</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 ["java" "-jar" "/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
|
||
<missing> 2 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 18.2MB buildkit.dockerfile.v0
|
||
<missing> 2 weeks ago /bin/sh -c set -x && apk add --no-cache o… 79.4MB
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_VERSION=8u212 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B
|
||
<missing> 2 weeks ago /bin/sh -c { echo '#!/bin/sh'; echo 'set… 87B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
|
||
<missing> 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 [ "java", "-jar", "/gs-spring-boot-0.1.0.jar" ]
|
||
</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 ["java" "-jar" "/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
|
||
<missing> 292 years ago ENV CACHE_PASSWD=moelove 0B buildkit.dockerfile.v0
|
||
<missing> 2 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 18.2MB buildkit.dockerfile.v0
|
||
<missing> 2 weeks ago /bin/sh -c set -x && 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 ["java" "-jar" "/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
|
||
<missing> 2 minutes ago RUN /bin/sh -c ./fetch_remote_data.sh # buil… 19B buildkit.dockerfile.v0
|
||
<missing> 2 minutes ago COPY fetch_remote_data.sh . # buildkit 37B buildkit.dockerfile.v0
|
||
<missing> 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 && ssh-keyscan github.com >> ~/.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 protected]</a>:tao12345666333/moe.git \
|
||
&& cd moe \
|
||
&& 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
|
||
=> [internal] load build definition from Dockerfile 0.1s
|
||
=> => transferring dockerfile: 96B 0.0s
|
||
=> [internal] load .dockerignore 0.1s
|
||
=> => transferring context: 2B 0.0s
|
||
=> resolve image config for docker.io/docker/dockerfile:experimental 0.0s
|
||
=> CACHED docker-image://docker.io/docker/dockerfile:experimental 0.0s
|
||
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
|
||
=> [1/4] FROM docker.io/library/alpine 0.0s
|
||
=> CACHED [2/4] RUN apk add --no-cache git openssh-client 0.0s
|
||
=> CACHED [3/4] RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts 0.0s
|
||
=> CACHED [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="583f312c183f312c302d3a763b3735">[email protected]</a>:tao12345666333/moe.git 0.0s
|
||
=> exporting to image 0.0s
|
||
=> => exporting layers 0.0s
|
||
=> => writing image sha256:35d3ded5595a48de50054121feed13ebadf9b5e73b6cefeeba4215e1a20a20fd 0.0s
|
||
=> => 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 protected]</a>:tao1… 16.9kB buildkit.dockerfile.v0
|
||
<missing> 35 minutes ago RUN /bin/sh -c mkdir -p -m 0700 ~/.ssh && ss… 392B buildkit.dockerfile.v0
|
||
<missing> 36 minutes ago RUN /bin/sh -c apk add --no-cache git openss… 20.8MB buildkit.dockerfile.v0
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
|
||
<missing> 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
|
||
=> [internal] load .dockerignore 0.1s
|
||
=> => transferring context: 2B 0.0s
|
||
=> [internal] load build definition from Dockerfile 0.1s
|
||
=> => transferring dockerfile: 96B 0.0s
|
||
=> resolve image config for docker.io/docker/dockerfile:experimental 0.0s
|
||
=> CACHED docker-image://docker.io/docker/dockerfile:experimental 0.0s
|
||
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
|
||
=> CACHED [1/4] FROM docker.io/library/alpine 0.0s
|
||
=> [2/4] RUN apk add --no-cache git openssh-client 5.5s
|
||
=> [3/4] RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts 3.0s
|
||
=> ERROR [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0c6b65784c6b657864796e226f6361">[email protected]</a>:tao12345666333/moe.git 2.9s
|
||
------
|
||
> [4/4] RUN --mount=type=ssh,required git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="492e203d092e203d213c2b672a2624">[email protected]</a>:tao12345666333/moe.git && cd moe && 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 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 protected]</a>:tao12345666333/moe.git && cd moe && 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>{
|
||
"experimental": true,
|
||
"features": {
|
||
"buildkit": true
|
||
},
|
||
"builder": {
|
||
"gc": {
|
||
"enabled": true,
|
||
"defaultKeepStorage": "20GB"
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>以上配置中 experimental 表示是否开启实验性功能,features 中是选择开启 BuildKit 支持,builder 中的 gc 则表示控制垃圾回收的策略,上面配置的含义是:保留 20G 的缓存,超出则会进行清理。</p>
|
||
<h4>多实例 builder 管理</h4>
|
||
<p>我们知道 Docker CLI 是提供插件支持的,并且开发一个插件也并不难,不过这不是今天的重点,之后开 Chat 再聊。</p>
|
||
<p>Docker 19.03 会提供两个主要的插件 app 和 buildx;buildx 就是这一小节的主角。</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
|
||
=> [internal] booting buildkit 21.2s
|
||
=> => pulling image moby/buildkit:master 20.7s
|
||
=> => creating container buildx_buildkit_d18090 0.5s
|
||
=> => unpacking docker.io/library/openjdk:<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="fdc5d0978f98d09c918d949398bd8e959ccfc8cb">[email protected]</a>:f362b165b870ef129cbe730f29065ff37399c0aa8bc 2.2s
|
||
=> [builder 2/6] WORKDIR /app 0.0s
|
||
=> [builder 3/6] COPY pom.xml /app/ 0.1s
|
||
=> [builder 4/6] RUN mvn dependency:go-offline 596.4s
|
||
=> [builder 5/6] COPY src /app/src 0.2s
|
||
=> [builder 6/6] RUN mvn -e -B package 25.3s
|
||
=> [stage-2 2/2] COPY --from=builder /app/target/gs-spring-boot-0.1.0.jar / 0.2s
|
||
=> exporting to oci image format 2.3s
|
||
=> => exporting layers 1.3s
|
||
=> => exporting manifest sha256:f5af6ad923434c4d7d2d6f94f095ccacfe6983cec592de6b8a0a3af37206686a 0.0s
|
||
=> => exporting config sha256:644867602b8a4a5162dee8534378e3dab28807f593759c6b25bcf16492d807bc 0.0s
|
||
=> => sending tarball 0.9s
|
||
=> 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 "buildkitd" 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 [ "java", "-jar", "/gs-spring-boot-0.1.0.jar" ]
|
||
</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
|
||
...
|
||
=> exporting to image 0.0s
|
||
=> => exporting layers 0.0s
|
||
=> => writing image sha256:2d5ba7eb86d2ad5594f82a896637c91137d150dab61fe8dc3acbdfcd164f6686 0.0s
|
||
=> => 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
|
||
<missing> 292 years ago CMD ["java" "-jar" "/gs-spring-boot-0.1.0.ja… 0B buildkit.dockerfile.v0
|
||
<missing> About a minute ago RUN /bin/sh -c rm /tmp/gs-spring-boot-0.1.0.… 0B buildkit.dockerfile.v0
|
||
<missing> About a minute ago COPY /app/target/gs-spring-boot-0.1.0.jar /t… 0B buildkit.dockerfile.v0
|
||
<missing> 3 days ago COPY /app/target/gs-spring-boot-0.1.0.jar / … 0B buildkit.dockerfile.v0
|
||
<missing> 2 weeks ago /bin/sh -c set -x && apk add --no-cache o… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_VERSION=8u212 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B
|
||
<missing> 2 weeks ago /bin/sh -c { echo '#!/bin/sh'; echo 'set… 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
|
||
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
|
||
<missing> 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>
|