learn.lianglianglee.com/文章/高效构建 Docker 镜像的最佳实践.md.html
2022-05-11 19:04:14 +08:00

1136 lines
66 KiB
HTML
Raw Permalink 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>高效构建 Docker 镜像的最佳实践.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 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</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>高效构建 Docker 镜像的最佳实践</h1>
<p>在真正实践之前,我们需要先搞明白几个问题:</p>
<ul>
<li>Docker 镜像是什么</li>
<li>Docker 镜像的作用</li>
<li>容器和镜像的区别及联系</li>
</ul>
<h3>Docker 镜像是什么</h3>
<p>这里,我们以一个 Debian 系统的镜像为例。通过 <code>docker run --it debian</code> 可以启动一个 <code>debian</code> 的容器,终端会有如下输出:</p>
<pre><code>/ # docker run -it debian
Unable to find image 'debian:latest' locally
latest: Pulling from library/debian
c5e155d5a1d1: Pull complete
Digest: sha256:75f7d0590b45561bfa443abad0b3e0f86e2811b1fc176f786cd30eb078d1846f
Status: Downloaded newer image for debian:latest
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4e3c21213a0e2a2d792a7e7b7928797e7f7a">[email&#160;protected]</a>:/# cat /etc/os-release
PRETTY_NAME=&quot;Debian GNU/Linux 9 (stretch)&quot;
NAME=&quot;Debian GNU/Linux&quot;
VERSION_ID=&quot;9&quot;
VERSION=&quot;9 (stretch)&quot;
ID=debian
HOME_URL=&quot;https://www.debian.org/&quot;
SUPPORT_URL=&quot;https://www.debian.org/support&quot;
BUG_REPORT_URL=&quot;https://bugs.debian.org/&quot;
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="75071a1a0135111642114540421342454441">[email&#160;protected]</a>:/#
</code></pre>
<p>看终端的日志,首先会查找本地是否有 <code>debian</code> 的镜像,如果没有则从镜像仓库(若不指定,默认是 docker.iopullpull 镜像成功后,再以此镜像来启动容器。</p>
<p>我们可以先退出此容器,来看看 Docker 镜像到底是什么。用 <code>docker image ls</code> 来查看已下载好的镜像:</p>
<pre><code>/ # docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
debian latest 8d31923452f8 4 days ago 101MB
</code></pre>
<p><code>docker image save</code> 命令将镜像保存成一个 tar 文件:</p>
<pre><code>/ # mkdir debian-image
/ # docker image save -o debian-image/debian.tar debian
/ # ls debian-image/
debian.tar
</code></pre>
<p>将镜像文件进行解压:</p>
<pre><code>/ # tar -C debian-image/ -xf debian-image/debian.tar
/ # tree -I debian.tar debian-image/
debian-image/
├── 8d31923452f8b79ae91b01568d28c90e7d667a9eaff9734c6faeb017b0efa8d0.json
├── b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3
│ ├── VERSION
│ ├── json
│ └── layer.tar
├── manifest.json
└── repositories
1 directory, 6 files
</code></pre>
<p>可以看到将镜像文件解压后,包含的内容主要是一些配置文件和 tar 包。</p>
<p>接下来我们来具体看看其中的内容,并通过这些内容来理解镜像的组成。</p>
<h3>manifest.json</h3>
<pre><code>/debian-image # cat manifest.json | jq
[
{
&quot;Config&quot;: &quot;8d31923452f8b79ae91b01568d28c90e7d667a9eaff9734c6faeb017b0efa8d0.json&quot;,
&quot;RepoTags&quot;: [
&quot;debian:latest&quot;
],
&quot;Layers&quot;: [
&quot;b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar&quot;
]
}
]
</code></pre>
<p><strong>注意:在实际存储时,是不包含空格的,这里为了便于展示所以使用了 jq 工具进行格式化。</strong></p>
<p><code>manifest.json</code> 包含了镜像的顶层配置,它是一系列配置按顺序组织而成的;以现在我们的 <code>debian</code> 镜像为例,它至包含了一组配置,这组配置中包含了 3 个主要的信息,我们由简到繁进行说明。</p>
<h3>RepoTags</h3>
<p><code>RepoTags</code> 表示镜像的名称和 tag ,这里简要的对此进行说明:<code>RepoTags</code> 其实分为两部分:</p>
<ul>
<li><code>Repo</code>: Docker 镜像可以存储在本地或者远端镜像仓库内Repo 其实就是镜像的名称。 Docker 默认提供了大量的官方镜像存储在 <a href="https://hub.docker.com/u/library">Docker Hub</a> 上,对于我们现在在用的这个 Docker 官方的 debian 镜像而言,完整的存储形式其实是 <code>docker.io/library/debian</code>,只不过 docker 自动帮我们省略掉了前缀。</li>
<li><code>Tag</code>: 我们可以通过 <code>repo:tag</code> 的方式来引用一个镜像,默认情况下,如果没有指定 tag (像我们上面操作的那样),则会 pull 下来最新的镜像latest</li>
</ul>
<h3>Config</h3>
<p><code>Config</code> 字段包含的内容是镜像的全局配置。我们来看看具体内容:</p>
<pre><code>/debian-image # cat 8d31923452f8b79ae91b01568d28c90e7d667a9eaff9734c6faeb017b0efa8d0.json | jq
{
&quot;architecture&quot;: &quot;amd64&quot;,
&quot;config&quot;: {
&quot;Hostname&quot;: &quot;&quot;,
&quot;Domainname&quot;: &quot;&quot;,
&quot;User&quot;: &quot;&quot;,
&quot;AttachStdin&quot;: false,
&quot;AttachStdout&quot;: false,
&quot;AttachStderr&quot;: false,
&quot;Tty&quot;: false,
&quot;OpenStdin&quot;: false,
&quot;StdinOnce&quot;: false,
&quot;Env&quot;: [
&quot;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;
],
&quot;Cmd&quot;: [
&quot;bash&quot;
],
&quot;ArgsEscaped&quot;: true,
&quot;Image&quot;: &quot;sha256:913ee5b96cb28ad1d43f11df4aeceee963fa07b53edccb3052a21939cae4a041&quot;,
&quot;Volumes&quot;: null,
&quot;WorkingDir&quot;: &quot;&quot;,
&quot;Entrypoint&quot;: null,
&quot;OnBuild&quot;: null,
&quot;Labels&quot;: null
},
&quot;container&quot;: &quot;e896ef8effcc3a696087509347ad072bf1ccfbd88e2e9e7a59d20293196bafa3&quot;,
&quot;container_config&quot;: {
&quot;Hostname&quot;: &quot;e896ef8effcc&quot;,
&quot;Domainname&quot;: &quot;&quot;,
&quot;User&quot;: &quot;&quot;,
&quot;AttachStdin&quot;: false,
&quot;AttachStdout&quot;: false,
&quot;AttachStderr&quot;: false,
&quot;Tty&quot;: false,
&quot;OpenStdin&quot;: false,
&quot;StdinOnce&quot;: false,
&quot;Env&quot;: [
&quot;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;
],
&quot;Cmd&quot;: [
&quot;/bin/sh&quot;,
&quot;-c&quot;,
&quot;#(nop) &quot;,
&quot;CMD [\&quot;bash\&quot;]&quot;
],
&quot;ArgsEscaped&quot;: true,
&quot;Image&quot;: &quot;sha256:913ee5b96cb28ad1d43f11df4aeceee963fa07b53edccb3052a21939cae4a041&quot;,
&quot;Volumes&quot;: null,
&quot;WorkingDir&quot;: &quot;&quot;,
&quot;Entrypoint&quot;: null,
&quot;OnBuild&quot;: null,
&quot;Labels&quot;: {}
},
&quot;created&quot;: &quot;2019-05-08T00:33:10.318602909Z&quot;,
&quot;docker_version&quot;: &quot;18.06.1-ce&quot;,
&quot;history&quot;: [
{
&quot;created&quot;: &quot;2019-05-08T00:33:09.837482517Z&quot;,
&quot;created_by&quot;: &quot;/bin/sh -c #(nop) ADD file:caf91edab64f988bc24766c58ee66c00311c7c921296b8e5b51d7023422a1485 in / &quot;
},
{
&quot;created&quot;: &quot;2019-05-08T00:33:10.318602909Z&quot;,
&quot;created_by&quot;: &quot;/bin/sh -c #(nop) CMD [\&quot;bash\&quot;]&quot;,
&quot;empty_layer&quot;: true
}
],
&quot;os&quot;: &quot;linux&quot;,
&quot;rootfs&quot;: {
&quot;type&quot;: &quot;layers&quot;,
&quot;diff_ids&quot;: [
&quot;sha256:f94641f1fe1f5c42c325652bf55f0513c881c86b620b912b15460e0bca07cc12&quot;
]
}
}
</code></pre>
<p>以上是配置文件的全部内容。其含义如下:</p>
<ul>
<li><code>architecture</code><code>os</code> : 表示架构及系统不再展开;</li>
<li><code>docker_version</code> : 构建镜像时所用 docker 的版本;</li>
<li><code>created</code>:镜像构建完成的时间;</li>
<li><code>history</code>: 镜像构建的历史记录,后面内容中再详细介绍;</li>
<li><code>rootfs</code>: 镜像的根文件系统;</li>
</ul>
<p>重点介绍下 <code>rootfs</code>:我们知道 <code>rootfs</code> 其实是指 <code>/</code> 下一系列文件目录的组织结构;虽然 Docker 容器与我们的主机(或者称之为宿主机)共享同一个 Linux 内核,但它也有自己完整的 <code>rootfs</code>; 我们继续回到一开始的实验环境中即我们刚才创建的这个容器内看看 <code>/</code> 下有什么:</p>
<pre><code>/# tree -L 1 /
/
|-- bin
|-- boot
|-- dev
|-- etc
|-- home
|-- lib
|-- lib64
|-- media
|-- mnt
|-- opt
|-- proc
|-- root
|-- run
|-- sbin
|-- srv
|-- sys
|-- tmp
|-- usr
`-- var
19 directories, 0 files
</code></pre>
<p>可以看到与我们正常 Linux 系统的 <code>/</code> 下目录相同。</p>
<p>回到这个例子当中,我们来看看这段配置的具体含义。由于一开始在 <code>manifest.json</code> 中已经定义了 layer 的内容,我们来看看该 layer 的 <code>sha256sum</code> 值:</p>
<pre><code>/debian-image # ls b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/
VERSION json layer.tar
/debian-image #
/debian-image # sha256sum b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar
f94641f1fe1f5c42c325652bf55f0513c881c86b620b912b15460e0bca07cc12 b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar
</code></pre>
<p>可以看到与配置文件中相符,表示 <code>b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar</code> 便是 debian 镜像的 <code>rootfs</code> 我们将它进行解压,看看它的内容。</p>
<pre><code>/debian-image # mkdir b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer
/debian-image # tar -C b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer -xf b50334e3be68f82
d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar
/debian-image # ls b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer
bin dev home lib64 mnt proc run srv tmp var
boot etc lib media opt root sbin sys usr
</code></pre>
<p>可以看到它的内容确实是 <code>rootfs</code> 应该有的内容。同时,上面操作中也包含了一个知识点:</p>
<p><strong>Docker 镜像相关的配置中,所用的 id 或者文件名/目录名大多是采用 sha256sum 计算得出的</strong></p>
<p>关于配置的部分我们先谈这些,我们继续看配置中尚未解释的 <code>Layers</code></p>
<h3>Layers</h3>
<p>其实根据前面的介绍我们已经大致看到Docker 镜像是分层的模式,将一系列层按顺序组织起来加上配置文件等共同构成完整的镜像。这样做的好处主要有:</p>
<ul>
<li>相同内容可以复用, 减轻存储负担;</li>
<li>可以比较容易的得到各层所做操作/操作后结果的记录;</li>
<li>后续操作不影响前一层的内容。</li>
</ul>
<p>通过 <code>manifest.json</code> 的内容,和前面对 <code>rootfs</code> 的解释,不难看出此镜像只包含了一层,即 <code>b50334e3be68f82d0b94bb7d3cfe1789119c040c6c159759f57a19ad34547af3/layer.tar</code></p>
<p>Docker 提供了一个命令可以更加直观的看到构建记录:</p>
<pre><code>/debian-image # docker image history debian
IMAGE CREATED CREATED BY SIZE COMMENT
8d31923452f8 4 days ago /bin/sh -c #(nop) CMD [&quot;bash&quot;] 0B
&lt;missing&gt; 4 days ago /bin/sh -c #(nop) ADD file:caf91edab64f988bc… 101MB
</code></pre>
<p>它的输出相比我们上面配置文件中的内容,多了一列 <code>SIZE</code>,表示该构建步骤所占空间大小。可以看到第二步(输出是逆序的) <code>/bin/sh -c #(nop) CMD [&quot;bash&quot;]</code> 所占空间为 0 。</p>
<p>我们首先分解这些步骤所表示的内容:</p>
<ul>
<li><code>/bin/sh -c #(nop) ADD file:caf91edab64f988bc…</code>: 使用 <code>ADD</code> 命令添加文件;</li>
<li><code>/bin/sh -c #(nop) CMD [&quot;bash&quot;]</code>:使用 <code>CMD</code> 配置默认执行的程序是 <code>bash</code></li>
</ul>
<p>从前面 <code>Config</code> 的配置中,我们也可以看到第二步其实是修改了 <code>Config</code> 的配置,所以占用空间为 0并没有使镜像变大。</p>
<p>从 Docker Hub 上我们也可以找到<a href="https://github.com/debuerreotype/docker-debian-artifacts/blob/fd138cb56a6a6a4fd9cb30c2acce9e8d9cccd28a/stretch/Dockerfile">此镜像的 <code>Dockerfile</code> 文件</a>,看下具体内容:</p>
<pre><code>FROM scratch
ADD rootfs.tar.xz /
CMD [&quot;bash&quot;]
</code></pre>
<p>步骤与我们上面提到的完全符合, 不再进行展开了。</p>
<p>以上便详细解释了 Docker 镜像是什么:它其实是一组按照规范进行组织的分层文件,各层互不影响,并且每层的操作都将记录在 <code>history</code> 中。</p>
<h3>Docker 镜像的作用</h3>
<p>从前面的讲述中,我们可以看到镜像中包含了一个完整的 <code>rootfs</code> ,在我们使用 <code>docker run</code> 命令时,便将指定镜像中的各层和配置组织起来共同启动一个新的容器;而在容器中,我们可以随意进行操作(包括读写)。</p>
<p>所以Docker 镜像的主要作用是:</p>
<ul>
<li>为启动容器提供必要的文件;</li>
<li>记录了各层的操作和配置等。</li>
</ul>
<h3>容器和镜像的区别及联系</h3>
<p>这里可以直接得出一个很直观的结论了。</p>
<p>镜像就是一系列文件和配置的组合,它是静态的,只读的,不可修改的; 而容器由镜像而来,但它是可操作的,是动态的,可修改的。</p>
<h3>Docker 镜像常规管理操作</h3>
<p>Docker 由于不断增加新功能,为了方便,在后续版本中便对命令进行了分组。对镜像相关的命令都放到了 <code>docker image</code> 组内:</p>
<pre><code>/ # docker image
Usage: docker image COMMAND
Manage images
Commands:
build Build an image from a Dockerfile
history Show the history of an image
import Import the contents from a tarball to create a filesystem image
inspect Display detailed information on one or more images
load Load an image from a tar archive or STDIN
ls List images
prune Remove unused images
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rm Remove one or more images
save Save one or more images to a tar archive (streamed to STDOUT by default)
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
Run 'docker image COMMAND --help' for more information on a command.
</code></pre>
<p>对于我们开始时对镜像进行分析的操作,我们可以直接通过 <code>docker inspect debian</code> 直接拿到它的配置信息。</p>
<p><code>pull</code>, <code>push</code>, <code>tag</code> 这三个子命令与和镜像仓库的交互比较相关,可以结合前面 <code>RepoTags</code> 理解。</p>
<p><code>save</code><code>load</code> 是将镜像保存到文件系统上及从文件系统中导入 Docker 中。</p>
<p><code>build</code> 命令会在接下来详细说明,剩余命令都比较简单直观了。</p>
<h3>如何构建 Docker 镜像</h3>
<p>前面详细讲述了 Docker 镜像是什么,以及简单介绍了常用的 Docker 镜像管理命令。那如何构建一个 Docker 镜像呢?通常情况下,有两种办法可以用于构建镜像(但并不只有这两种办法,以后再开 chat 来单独讲)。</p>
<h4>从容器创建</h4>
<p>还是以 debian 镜像为例,使用官方的 debian 镜像,启动一个容器:</p>
<pre><code>/ # docker run --rm -it debian
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e1938e8e95a18580828287d084d8d0d4d483">[email&#160;protected]</a>:/# toilet
bash: toilet: command not found
</code></pre>
<p>容器启动后,我们输入 <code>toilet</code> 来查看当前是否有 <code>toilet</code> 这个命令。 这是一个能将输入的字符串以更大的文本输出的命令行工具。</p>
<p>看上面的输入,当前的 PATH 中并没有该命令。我们使用 <code>apt</code> 进行安装。</p>
<pre><code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e795888893a78386848481d682ded6d2d285">[email&#160;protected]</a>:/# apt update
Get:1 http://security-cdn.debian.org/debian-security stretch/updates InRelease [94.3 kB]
Ign:2 http://cdn-fastly.deb.debian.org/debian stretch InRelease
Get:5 http://security-cdn.debian.org/debian-security stretch/updates/main amd64 Packages [489 kB]
Get:3 http://cdn-fastly.deb.debian.org/debian stretch-updates InRelease [91.0 kB]
Get:4 http://cdn-fastly.deb.debian.org/debian stretch Release [118 kB]
Get:6 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages [31.7 kB]
Get:7 http://cdn-fastly.deb.debian.org/debian stretch Release.gpg [2434 B]
Get:8 http://cdn-fastly.deb.debian.org/debian stretch/main amd64 Packages [7082 kB]
Fetched 7909 kB in 3s (2285 kB/s)
Reading package lists... Done
Building dependency tree
Reading state information... Done
All packages are up to date.
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="562439392216323735353067336f67636334">[email&#160;protected]</a>:/# apt install toilet -y
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
libcaca0 libslang2 toilet-fonts
Suggested packages:
figlet
The following NEW packages will be installed:
libcaca0 libslang2 toilet toilet-fonts
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
Need to get 1595 kB of archives.
...
Setting up toilet-fonts (0.3-1.1) ...
Setting up libslang2:amd64 (2.3.1-5) ...
Setting up libcaca0:amd64 (0.99.beta19-2+b2) ...
Setting up toilet (0.3-1.1) ...
update-alternatives: using /usr/bin/figlet-toilet to provide /usr/bin/figlet (figlet) in auto mode
Processing triggers for libc-bin (2.24-11+deb9u4) ...
</code></pre>
<p>可以看到,安装已经完成,我们在终端下输入 <code>toilet docker</code> 来查看下效果:</p>
<pre><code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d1a3bebea591b5b0b2b2b7e0b4e8e0e4e4b3">[email&#160;protected]</a>:/# toilet docker
# #
mmm# mmm mmm # m mmm m mm
#&quot; &quot;# #&quot; &quot;# #&quot; &quot; # m&quot; #&quot; # #&quot; &quot;
# # # # # #&quot;# #&quot;&quot;&quot;&quot; #
&quot;#m## &quot;#m#&quot; &quot;#mm&quot; # &quot;m &quot;#mm&quot; #
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1e6c71716a5e7a7f7d7d782f7b272f2b2b7c">[email&#160;protected]</a>:/#
</code></pre>
<p>该命令已经安装完成,并工作良好。现在我们使用当前容器来创建一个包含 <code>toilet</code> 命令的 Docker 镜像。</p>
<p>Docker 提供了一个命令 <code>docker container commit</code> 用于从容器创建一个镜像(当前也可以使用 <code>docker commit</code> )。</p>
<pre><code>/ # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
daccf1e9155b debian &quot;bash&quot; 19 minutes ago Up 19 minutes kind_lamport
/ # docker container commit -m &quot;install toilet&quot; daccf1e9155b local/debian:toilet
sha256:9ca72f9dcedbc7cf3f8643915184b6fbcba81fc207f1d45b0f872263ab9cc12c
/ # docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
local/debian toilet 9ca72f9dcedb 15 seconds ago 121MB
debian latest 8d31923452f8 5 days ago 101MB
</code></pre>
<p>直接将当前容器的 ID 传递给 <code>docker container commit</code> 作为参数,并提供一个新的镜像名称便可创建一个新的镜像(传递名称是为了方便使用,即使不传递名称也可以创建镜像)。</p>
<p>使用新的镜像来启动一个容器进行验证:</p>
<pre><code>/ # docker run --rm -it local/debian:toilet
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1765787863572e2e212f7125762f2f207126">[email&#160;protected]</a>:/# toilet debian
# # &quot;
mmm# mmm #mmm mmm mmm m mm
#&quot; &quot;# #&quot; # #&quot; &quot;# # &quot; # #&quot; #
# # #&quot;&quot;&quot;&quot; # # # m&quot;&quot;&quot;# # #
&quot;#m## &quot;#mm&quot; ##m#&quot; mm#mm &quot;mm&quot;# # #
</code></pre>
<p>可以看到 <code>toilet</code> 已经存在。从容器创建镜像的目的达成。</p>
<h4>从 Dockerfile 创建</h4>
<p>Docker 提供了一种可根据配置文件构建镜像的方式,该配置文件通常命名为 <code>Dockerfile</code>。我们将刚才创建镜像的过程以 Dockerfile 进行描述。</p>
<pre><code>/ # mkdir toilet
/ # cd toilet/
/toilet # vi Dockerfile
/toilet # cat Dockerfile
FROM debian
RUN apt update
RUN apt install -y toilet
</code></pre>
<p>Dockerfile 语法是固定的,但本篇不会对全部语法逐个解释,如有兴趣可查阅<a href="https://docs.docker.com/engine/reference/builder/#usage">官方文档</a>。接下来使用该 Dockerfile 构建镜像。</p>
<pre><code>/toilet # docker image build -t local/debian:toilet-using-dockerfile .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM debian
---&gt; 8d31923452f8
Step 2/3 : RUN apt update
---&gt; Running in 9ab1f635aa05
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
Get:1 http://security-cdn.debian.org/debian-security stretch/updates InRelease [94.3 kB]
Ign:2 http://cdn-fastly.deb.debian.org/debian stretch InRelease
...
Reading package lists...
Building dependency tree...
Reading state information...
All packages are up to date.
Removing intermediate container 9ab1f635aa05
---&gt; e6923f4d4e72
Step 3/3 : RUN apt install -y toilet
---&gt; Running in ac1eeb29c4da
...
Processing triggers for libc-bin (2.24-11+deb9u4) ...
Setting up toilet-fonts (0.3-1.1) ...
Setting up libslang2:amd64 (2.3.1-5) ...
Setting up libcaca0:amd64 (0.99.beta19-2+b2) ...
Setting up toilet (0.3-1.1) ...
update-alternatives: using /usr/bin/figlet-toilet to provide /usr/bin/figlet (figlet) in auto mode
Processing triggers for libc-bin (2.24-11+deb9u4) ...
Removing intermediate container ac1eeb29c4da
---&gt; 267c669f183f
Successfully built 267c669f183f
Successfully tagged local/debian:toilet-using-dockerfile
/toilet # docker image ls local/debian
REPOSITORY TAG IMAGE ID CREATED SIZE
local/debian toilet-using-dockerfile 267c669f183f 5 minutes ago 121MB
local/debian toilet 9ca72f9dcedb 26 minutes ago 121MB
</code></pre>
<p>使用 <code>-t</code> 参数来指定新生成镜像的名称,并且我们也可以看到该镜像已经构建成功。同样的使用该镜像创建容器进行测试:</p>
<pre><code>/toilet # docker run --rm -it local/debian:toilet-using-dockerfile
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="384a57574c785c0c5e0901095a005c0e0d0b">[email&#160;protected]</a>:/# toilet debian
# # &quot;
mmm# mmm #mmm mmm mmm m mm
#&quot; &quot;# #&quot; # #&quot; &quot;# # &quot; # #&quot; #
# # #&quot;&quot;&quot;&quot; # # # m&quot;&quot;&quot;# # #
&quot;#m## &quot;#mm&quot; ##m#&quot; mm#mm &quot;mm&quot;# # #
</code></pre>
<p>也都验证成功。如果你重复执行 <code>docker build</code> 命令的话,会看到有 <code>cache</code> 字样的输出,这是因为 Docker 为了提高构建镜像的效率,对已经构建过的每层进行了缓存,后面的内容会再讲到缓存相关的内容。</p>
<p>以上便是两种最常见构建容器镜像的方法了。其他办法之后开 chat 单独再聊。</p>
<h3>逐步分解构建 Docker 镜像的最佳实践</h3>
<h4>从容器构建 VS 从 Dockerfile 构建</h4>
<p>通过上面的介绍也可以看到,从容器构建很简单很直接,从 Dockerfile 构建则需要你描述出来每一步所做内容。</p>
<p>但是,如果对构建过程会有修改,或者是想要可维护,可记录,可追溯,那还是选择 Dockerfile 更为恰当。</p>
<h4>以一个 Spring Boot 的项目为例</h4>
<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>这里虽然以 Spring Boot 项目为例,但你如果对 Spring Boot 不熟悉的话也完全不影响后续内容,这里并不涉及 Spring Boot 的任何知识。你只需要知道对于这个项目而言,需要先装依赖,构建,才能运行。</p>
<p>那我们来看看一般情况下,对于这样的项目 <code>Dockerfile</code> 的内容是什么样的。</p>
<h4>利用缓存</h4>
<pre><code>FROM debian
COPY . /app
RUN apt update
RUN apt install -y openjdk-8-jdk
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/target/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>这是一种比较典型的,在本地先构建好之后,再复制到容器镜像中。注意,由于 <code>debian</code> 镜像默认没有 Java 环境,所以还需要有 <code>apt</code>/<code>apt-get</code> 来安装 Java 环境。</p>
<p>那这样的 <code>Dockerfile</code> 有问题吗?有。</p>
<p>前面我们提到了,如果你对同样内容的 <code>Dockerfile</code> 执行两次 <code>docker build</code> 命令的话,会看到有 <code>cache</code> 字样的输出,这是因为 Docker 的 build 系统内置了缓存的逻辑,在构建时,会检查当前要构建的内容是否已经被缓存,如果被缓存则直接使用,否则重新构建,并且后续的缓存也将失效。</p>
<p>对于一个正常的项目而言,源代码的更新是最为频繁的。所以看上面的 <code>Dockerfile</code> 你会发现 <code>COPY . /app</code> 这一行,很容易就会让缓存失效,从而导致后面的缓存也都失效。</p>
<p>对此 <code>Dockerfile</code> 进行改进:</p>
<pre><code>FROM debian
RUN apt update
RUN apt install -y openjdk-8-jdk
COPY . /app
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/target/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>第一个实践指南: <strong>为了更有效的利用构建缓存,将更新最频繁的步骤放在最后面</strong> 这样在之后的构建中,前三步都可以利用缓存。你可以运行多次 <code>docker build</code> 以进行验证。</p>
<h4>部分拷贝</h4>
<p>在项目变大,或者是项目中其他目录,比如 <code>docs</code> 目录内容很大时,根据前面对镜像相关的说明,直接使用 <code>COPY . /app</code> 会把所有内容拷贝至镜像中,导致镜像变大。</p>
<p>而对于我们要构建的镜像而言,那些文件是不必要的,所以我们可以将 <code>Dockerfile</code> 改成这样:</p>
<pre><code>FROM debian
RUN apt update
RUN apt install -y openjdk-8-jdk
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>第二个实践指南: <strong>避免将全部内容拷贝至镜像中, 至保留需要的内容即可</strong> 。当然除去修改 <code>Dockerfile</code> 文件外,也可以通过修改 <code>.dockerignore</code> 文件来完成类似的事情。</p>
<p><code>docker build</code> 的过程是先加载 <code>.dockerignore</code> 文件,然后才按照 <code>Dockerfile</code> 进行构建,<code>.dockerignore</code> 的用法与 <code>.gitignore</code> 类似,排除掉你不想要的文件即可。</p>
<h4>防止包缓存过期</h4>
<p>上面我们已经提到了, <code>docker build</code> 可以利用缓存,但你有没有考虑到,如果使用我们前面的 <code>Dockerfile</code>,当你机器上需要构建多个不同项目的镜像,或者是需要安装的依赖发生变化的时候,缓存可能就不是我们想要的了。</p>
<p>比如说,我想安装一个最新版的 <code>vim</code> 在镜像中,可以简单的修改第三行为 <code>RUN apt install -y openjdk-8-jdk vim</code> ,但由于 <code>RUN apt update</code> 是被缓存的,所以我无法安装到最新版本的 <code>vim</code></p>
<pre><code>FROM debian
RUN apt update &amp;&amp; apt install -y openjdk-8-jdk
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>第三个实践指南: <strong>将包管理器的缓存生成与安装包的命令写到一起可防止包缓存过期</strong></p>
<h4>谨慎使用包管理器</h4>
<p>了解 <code>apt</code>/<code>apt-get</code> 的朋友应该知道,在使用 <code>apt</code>/<code>apt-get</code> 安装包的时候,它会自动增加一些推荐安装的包,并且一同下载。但那些包对我们镜像中跑应用程序而言无关紧要。它有一个 <code>--no-install-recommends</code> 的选项可以避免安装那些推荐的包。</p>
<p>我们先来看下是否使用此选项的区别,我启动一个 <code>debian</code> 的容器进行测试:</p>
<pre><code><a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="790b16160d394c184b4a1c1b414c41484f4a">[email&#160;protected]</a>:/# apt install --no-install-recommends openjdk-8-jdk | grep 'additional disk space will be used'
...
After this operation, 344 MB of additional disk space will be used.
^C
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1664797962562377242573742e232e272025">[email&#160;protected]</a>:/# apt install openjdk-8-jdk | grep 'additional disk space will be used'
...
After this operation, 548 MB of additional disk space will be used.
^C
</code></pre>
<p>可以看到如果增加了 <code>--no-install-recommends</code> 选项的话,可以减少 200M 左右磁盘占用。</p>
<p>所以 <code>Dockerfile</code> 可以修改为:</p>
<pre><code>FROM debian
RUN apt update &amp;&amp; apt install -y --no-install-recommends openjdk-8-jdk
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>此时构建镜像,我们来与之前的镜像做下对比:</p>
<pre><code>(MoeLove) ➜ docker image ls local/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
local/spring-boot 4 716523c83a26 3 minutes ago 497MB
local/spring-boot 2 178dacdaf015 9 hours ago 600MB
</code></pre>
<p>可以很明显看到镜像明显变小了。</p>
<p>接下来还有个值得注意的地方。我们一开始执行了 <code>apt update</code> 这个命令,它主要是在缓存源信息。而对于我们构建所需镜像时,这没有必要。我们选择将这些缓存文件删掉。</p>
<p>启动一个新的容器验证下:</p>
<pre><code>(MoeLove) ➜ docker run --rm -it debian
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="90e2ffffe4d0f3f4a8a5a7f3a3f1f2a8a8a2">[email&#160;protected]</a>:/# apt -qq update
All packages are up to date.
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="7a0815150e3a191e424f4d19491b18424248">[email&#160;protected]</a>:/# du -sh /var/lib/apt/lists/
16M /var/lib/apt/lists/
<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1f6d70706b5f7c7b272a287c2c7e7d27272d">[email&#160;protected]</a>:/#
</code></pre>
<p>可以看到有 16M 左右的大小,我们修改 <code>Dockerfile</code> 增加删除操作:</p>
<pre><code>FROM debian
RUN apt update &amp;&amp; apt install -y --no-install-recommends openjdk-8-jdk \
&amp;&amp; rm -rf /var/lib/apt/lists/*
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>对比使用这个 <code>Dockerfile</code> 构建镜像的镜像大小</p>
<pre><code>(MoeLove) ➜ docker image ls local/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
local/spring-boot 4-2 ac272f3dcac2 24 seconds ago 481MB
local/spring-boot 4 716523c83a26 37 minutes ago 497MB
local/spring-boot 2 178dacdaf015 10 hours ago 600MB
</code></pre>
<p>可以看到小了 16M 左右。</p>
<p>第四个实践指南: <strong>谨慎使用包管理器,不安装非必要的包,注意清理包管理器缓存文件</strong></p>
<h4>选择合适的基础镜像</h4>
<p>Docker Hub 上提供了很多 <a href="https://hub.docker.com/search/?q=&amp;type=image&amp;image_filter=official">官方镜像</a> 这些镜像的构建基本上都经过了大量的优化,尽可能缩小镜像体积,减少镜像层数。</p>
<p>当我们构建镜像的时候不妨先查看官方镜像是否有满足需求的镜像可以作为基础镜像。Java 运行环境官方镜像是有提前提供的 <a href="https://hub.docker.com/_/openjdk">openjdk</a> 我们可以在 GitHub 上找到它构建镜像的 <a href="https://github.com/docker-library/openjdk/blob/b8ce9eff38451de3282b2eb2bcd8b520fb95e1ce/8/jdk/Dockerfile">Dockerfile</a> 可以看到其中的一些构建过程与我们前面所说的实践方式相符。</p>
<p>我们选择 Docker 官方 <code>openjdk</code> 镜像来作为基础镜像,<code>Dockerfile</code> 可以改写为:</p>
<pre><code>FROM openjdk:8-jdk-stretch
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p><code>openjdk</code> 有很多不同的 tag 比如 <code>8-jdk-stretch</code> <code>8-jre-stretch</code> 以及 <code>8-jre-alpine</code> 之类的,具体的可以在 <a href="https://hub.docker.com/_/openjdk?tab=tags">openjdk 的 tag 页面</a>查看。</p>
<p>我们其实只想要一个 Java 的运行环境,所以选择一个基于 <a href="https://alpinelinux.org/">Alpine Linux</a> 的超小的镜像 <code>openjdk:8-jre-alpine</code> 这样 <code>Dockerfile</code> 可以改写为:</p>
<pre><code>FROM openjdk:8-jre-alpine
COPY target/gs-spring-boot-0.1.0.jar /app/
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>分别用上面的 <code>Dockerfile</code> 构建镜像,可以看到镜像大小</p>
<pre><code>(MoeLove) ➜ docker image ls local/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
local/spring-boot 5-1 b423dfc8d995 23 minutes ago 103MB
local/spring-boot 5 7158d42a6a87 25 minutes ago 643MB
local/spring-boot 4-2 ac272f3dcac2 4 hours ago 481MB
local/spring-boot 4 716523c83a26 5 hours ago 497MB
local/spring-boot 2 178dacdaf015 14 hours ago 600MB
</code></pre>
<p>很明显,使用 <code>openjdk:8-jre-alpine</code> 后,镜像大小只有 103M 比之前的镜像小了很多。</p>
<p>第五个实践指南: <strong>尽可能选择官方镜像,看实际需求进行最终选择</strong> 这样说的原因,主要是因为 Alpine Linux 并非基于 glibc 的,而是基于 musl 的,如果是 Python 的项目,请实际测试下性能损失再决定是否选择 Alpine Linux <a href="http://moelove.info/docker-python-perf/">这里</a>是我做的一份关于 Python 各镜像主要的性能对比,有需要可以参考)</p>
<h4>保持构建环境一致</h4>
<p>在前面的实践中,我们都是先本地构建好之后,才 <code>COPY</code> 进去的,这容易导致不同用户构建出的镜像可能不同。所以我们将构建过程写入到 <code>Dockerfile</code>:</p>
<pre><code>FROM maven:3.6.1-jdk-8-alpine
WORKDIR /app
COPY pom.xml /app/
COPY src /app/src
RUN mvn -e -B package
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/target/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>这样所有人都可以使用相同的 <code>Dockerfile</code> 构建出相同的镜像了。</p>
<p>但我们也会发现一个问题,在 <code>mvn -e -B package</code> 这一步耗费的时间特别长,因为它需要先拉取依赖才能进行构建。而对于项目开发而言,代码变更比依赖变更更加频繁,为了能加快构建速度,有效的利用缓存,我们将解决依赖与构建分成两步。</p>
<pre><code>FROM maven:3.6.1-jdk-8-alpine
WORKDIR /app
COPY pom.xml /app/
RUN mvn dependency:go-offline
COPY src /app/src
RUN mvn -e -B package
CMD [ &quot;java&quot;, &quot;-jar&quot;, &quot;/app/target/gs-spring-boot-0.1.0.jar&quot; ]
</code></pre>
<p>这样, <strong>即使业务代码发生改变,也不需要重新解决依赖,可有效的利用了缓存,加快构建的速度</strong></p>
<p>当然,现在我们构建的镜像中,还是包含着项目的源代码,这其实并非我们所需要的。那么我们可以使用 <strong>多阶段构建</strong>来解决这个问题。<code>Dockerfile</code> 可以修改为:</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 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>当然,多阶段构建也并不只是为了缩小镜像体积;我们可以使用指定构建阶段,以满足多种不同的镜像需求。</p>
<p><code>Dockerfile</code> 可以修改为:</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>我们可以使用如下的命令来构建不同阶段的镜像;</p>
<pre><code># 构建用于开发的镜像
(MoeLove) ➜ docker build --target dev -t local/spring-boot:6-4-dev .
# 构建用于生产部署的镜像
(MoeLove) ➜ docker build -t local/spring-boot:6-4 .
</code></pre>
<p>我们来看看在这个过程中镜像大小的变化:</p>
<pre><code>(MoeLove) ➜ docker image ls local/spring-boot
REPOSITORY TAG IMAGE ID CREATED SIZE
local/spring-boot 6-4-dev f47a322c9de3 6 seconds ago 250MB
local/spring-boot 6-4 2ab6215ff05e 3 minutes ago 103MB
local/spring-boot 6-3 2ab6215ff05e 3 minutes ago 103MB
local/spring-boot 6-2 2b3d3f923e05 4 minutes ago 225MB
local/spring-boot 6 f96bea38825f 2 hours ago 188MB
</code></pre>
<h3>如何提升构建效率</h3>
<p>在构建 Docker 镜像的最佳实践部分中,我们提到了很多方法,比如利用缓存;减少安装依赖等,这些都可以提升构建效率。</p>
<p>我们还提到了多阶段构建,这是一种很方便而且很灵活的方式。但多阶段构建,在默认情况下是顺序构建;对于现在的新版本 18.09+ 以及即将发布的 19.03 版本,我们多了一种更有效的办法 <code>buildkit</code></p>
<p>通过添加环境变量 <code>DOCKER_BUILDKIT=1</code> 或者在 Docker 的配置文件 <code>/etc/docker/daemon.json</code> 中添加如下配置:</p>
<pre><code>&quot;features&quot;: {
&quot;buildkit&quot;: true
}
</code></pre>
<p>开启使用 <code>buildkit</code></p>
<p><code>buildkit</code> 在多阶段构建时,可进行并行构建,可大大提升了构建效率。</p>
<p>前面主要讲述的内容为Docker 镜像是什么,以及构建 Docker 镜像的最佳实践,最后简单提到了 <code>buildkit</code></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/领域驱动设计的菱形对称架构.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":"7099809659848b66","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>