learn.lianglianglee.com/专栏/由浅入深吃透 Docker-完/22 多阶级构建:Docker 下如何实现镜像多阶级构建?.md.html
2022-05-11 18:57:05 +08:00

1411 lines
30 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>22 多阶级构建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="/专栏/由浅入深吃透 Docker-完/00 溯本求源,吃透 Docker.md.html">00 溯本求源,吃透 Docker.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/01 Docker 安装:入门案例带你了解容器技术原理.md.html">01 Docker 安装:入门案例带你了解容器技术原理.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/02 核心概念:镜像、容器、仓库,彻底掌握 Docker 架构核心设计理念.md.html">02 核心概念:镜像、容器、仓库,彻底掌握 Docker 架构核心设计理念.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/03 镜像使用Docker 环境下如何配置你的镜像?.md.html">03 镜像使用Docker 环境下如何配置你的镜像?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/04 容器操作:得心应手掌握 Docker 容器基本操作.md.html">04 容器操作:得心应手掌握 Docker 容器基本操作.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/05 仓库访问:怎样搭建属于你的私有仓库?.md.html">05 仓库访问:怎样搭建属于你的私有仓库?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/06 最佳实践:如何在生产中编写最优 Dockerfile.md.html">06 最佳实践:如何在生产中编写最优 Dockerfile.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/07 Docker 安全:基于内核的弱隔离系统如何保障安全性?.md.html">07 Docker 安全:基于内核的弱隔离系统如何保障安全性?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/08 容器监控:容器监控原理及 cAdvisor 的安装与使用.md.html">08 容器监控:容器监控原理及 cAdvisor 的安装与使用.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/09 资源隔离:为什么构建容器需要 Namespace .md.html">09 资源隔离:为什么构建容器需要 Namespace .md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/10 资源限制:如何通过 Cgroups 机制实现资源限制?.md.html">10 资源限制:如何通过 Cgroups 机制实现资源限制?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/11 组件组成:剖析 Docker 组件作用及其底层工作原理.md.html">11 组件组成:剖析 Docker 组件作用及其底层工作原理.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/12 网络模型:剖析 Docker 网络实现及 Libnetwork 底层原理.md.html">12 网络模型:剖析 Docker 网络实现及 Libnetwork 底层原理.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/13 数据存储:剖析 Docker 卷与持久化数据存储的底层原理.md.html">13 数据存储:剖析 Docker 卷与持久化数据存储的底层原理.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/14 文件存储驱动AUFS 文件系统原理及生产环境的最佳配置.md.html">14 文件存储驱动AUFS 文件系统原理及生产环境的最佳配置.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/15 文件存储驱动Devicemapper 文件系统原理及生产环境的最佳配置.md.html">15 文件存储驱动Devicemapper 文件系统原理及生产环境的最佳配置.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/16 文件存储驱动OverlayFS 文件系统原理及生产环境的最佳配置.md.html">16 文件存储驱动OverlayFS 文件系统原理及生产环境的最佳配置.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/17 原理实践:自己动手使用 Golang 开发 Docker.md.html">17 原理实践:自己动手使用 Golang 开发 Docker.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/18 原理实践:自己动手使用 Golang 开发 Docker.md.html">18 原理实践:自己动手使用 Golang 开发 Docker.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/19 如何使用 Docker Compose 解决开发环境的依赖?.md.html">19 如何使用 Docker Compose 解决开发环境的依赖?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/20 如何在生产环境中使用 Docker Swarm 调度容器?.md.html">20 如何在生产环境中使用 Docker Swarm 调度容器?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/21 如何使 Docker 和 Kubernetes 结合发挥容器的最大价值?.md.html">21 如何使 Docker 和 Kubernetes 结合发挥容器的最大价值?.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/由浅入深吃透 Docker-完/22 多阶级构建Docker 下如何实现镜像多阶级构建?.md.html">22 多阶级构建Docker 下如何实现镜像多阶级构建?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/23 DevOps容器化后如何通过 DevOps 提高协作效能?.md.html">23 DevOps容器化后如何通过 DevOps 提高协作效能?.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/24 CICD容器化后如何实现持续集成与交付.md.html">24 CICD容器化后如何实现持续集成与交付.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/25 CICD容器化后如何实现持续集成与交付.md.html">25 CICD容器化后如何实现持续集成与交付.md.html</a>
</li>
<li>
<a href="/专栏/由浅入深吃透 Docker-完/26 结束语 展望未来Docker 的称霸之路.md.html">26 结束语 展望未来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>22 多阶级构建Docker 下如何实现镜像多阶级构建?</h1>
<p>通过前面课程的学习,我们知道 Docker 镜像是分层的,并且每一层镜像都会额外占用存储空间,一个 Docker 镜像层数越多,这个镜像占用的存储空间则会越多。镜像构建最重要的一个原则就是要保持镜像体积尽可能小,要实现这个目标通常可以从两个方面入手:</p>
<ul>
<li>基础镜像体积应该尽量小;</li>
<li>尽量减少 Dockerfile 的行数,因为 Dockerfile 的每一条指令都会生成一个镜像层。</li>
</ul>
<p>在 Docker 的早期版本中,对于编译型语言(例如 C、Java、Go的镜像构建我们只能将应用的编译和运行环境的准备全部都放到一个 Dockerfile 中,这就导致我们构建出来的镜像体积很大,从而增加了镜像的存储和分发成本,这显然与我们的镜像构建原则不符。</p>
<p>为了减小镜像体积,我们需要借助一个额外的脚本,将镜像的编译过程和运行过程分开。</p>
<ul>
<li>编译阶段:负责将我们的代码编译成可执行对象。</li>
<li>运行时构建阶段:准备应用程序运行的依赖环境,然后将编译后的可执行对象拷贝到镜像中。</li>
</ul>
<p>我以 Go 语言开发的一个 HTTP 服务为例,代码如下:</p>
<pre><code>package main
import (
&quot;fmt&quot;
&quot;net/http&quot;
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, &quot;hello world!\n&quot;)
}
func main() {
http.HandleFunc(&quot;/&quot;, hello)
http.ListenAndServe(&quot;:8080&quot;, nil)
}
</code></pre>
<p>我将这个 Go 服务构建成镜像分为两个阶段:代码的编译阶段和镜像构建阶段。</p>
<p>我们构建镜像时,镜像中需要包含 Go 语言编译环境,应用的编译阶段我们可以使用 Dockerfile.build 文件来构建镜像。Dockerfile.build 的内容如下:</p>
<pre><code>FROM golang:1.13
WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
</code></pre>
<p>Dockerfile.build 可以帮助我们把代码编译成可以执行的二进制文件,我们使用以下 Dockerfile 构建一个运行环境:</p>
<pre><code>FROM alpine:latest
WORKDIR /root/
COPY http-server .
CMD [&quot;./http-server&quot;]
</code></pre>
<p>然后,我们将应用的编译和运行环境的准备步骤,都放到一个 build.sh 脚本文件中,内容如下:</p>
<pre><code>#!/bin/sh
echo Building http-server:build
docker build -t http-server:build . -f Dockerfile.build
docker create --name builder http-server:build
docker cp builder:/go/src/github.com/wilhelmguo/multi-stage-demo/http-server ./http-server
docker rm -f builder
echo Building http-server:latest
docker build -t http-server:latest .
rm ./http-server
</code></pre>
<p>下面,我带你来逐步分析下这个脚本。</p>
<p>第一步,声明 shell 文件,然后输出开始构建信息。</p>
<pre><code>#!/bin/sh
echo Building http-server:build
</code></pre>
<p>第二步,使用 Dockerfile.build 文件来构建一个临时镜像 http-server:build。</p>
<pre><code>docker build -t http-server:build . -f Dockerfile.build
</code></pre>
<p>第三步,使用 http-server:build 镜像创建一个名称为 builder 的容器,该容器包含编译后的 http-server 二进制文件。</p>
<pre><code>docker create --name builder http-server:build
</code></pre>
<p>第四步,使用<code>docker cp</code>命令从 builder 容器中拷贝 http-server 文件到当前构建目录下,并且删除名称为 builder 的临时容器。</p>
<pre><code>docker cp builder:/go/src/github.com/wilhelmguo/multi-stage-demo/http-server ./http-server
docker rm -f builder
</code></pre>
<p>第五步,输出开始构建镜像信息。</p>
<pre><code>echo Building http-server:latest
</code></pre>
<p>第六步,构建运行时镜像,然后删除临时文件 http-server。</p>
<pre><code>docker build -t http-server:latest .
rm ./http-server
</code></pre>
<p>我这里总结一下,我们是使用 Dockerfile.build 文件来编译应用程序,使用 Dockerfile 文件来构建应用的运行环境。然后我们通过创建一个临时容器,把编译后的 http-server 文件拷贝到当前构建目录中,然后再把这个文件拷贝到运行环境的镜像中,最后指定容器的启动命令为 http-server。</p>
<p>使用这种方式虽然可以实现分离镜像的编译和运行环境,但是我们需要额外引入一个 build.sh 脚本文件,而且构建过程中,还需要创建临时容器 builder 拷贝编译后的 http-server 文件,这使得整个构建过程比较复杂,并且整个构建过程也不够透明。</p>
<p>为了解决这种问题, Docker 在 17.05 推出了多阶段构建multistage-build的解决方案。</p>
<h3>使用多阶段构建</h3>
<p>Docker 允许我们在 Dockerfile 中使用多个 FROM 语句,而每个 FROM 语句都可以使用不同基础镜像。最终生成的镜像,是以最后一条 FROM 为准,所以我们可以在一个 Dockerfile 中声明多个 FROM然后选择性地将一个阶段生成的文件拷贝到另外一个阶段中从而实现最终的镜像只保留我们需要的环境和文件。多阶段构建的主要使用场景是<strong>分离编译环境和运行环境。</strong></p>
<p>接下来,我们使用多阶段构建的特性,将上述未使用多阶段构建的过程精简成如下 Dockerfile</p>
<pre><code>FROM golang:1.13
WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
FROM alpine:latest
WORKDIR /root/
COPY --from=0 /go/src/github.com/wilhelmguo/multi-stage-demo/http-server .
CMD [&quot;./http-server&quot;]
</code></pre>
<p>然后,我们将这个 Dockerfile 拆解成两步进行分析。</p>
<p>第一步,编译代码。</p>
<pre><code>FROM golang:1.13
WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
</code></pre>
<p>将代码拷贝到 golang:1.13 镜像(已经安装好了 go并且使用<code>go build</code>命令编译代码生成 http-server 文件。</p>
<p>第二步,构建运行时镜像。</p>
<pre><code>FROM alpine:latest
WORKDIR /root/
COPY --from=0 /go/src/github.com/wilhelmguo/multi-stage-demo/http-server .
CMD [&quot;./http-server&quot;]
</code></pre>
<p>使用第二个 FROM 命令表示镜像构建的第二阶段,使用 COPY 指令拷贝编译后的文件到 alpine 镜像中,--from=0 表示从第一阶段构建结果中拷贝文件到当前构建阶段。</p>
<p>最后,我们只需要使用以下命令,即可实现整个镜像的构建:</p>
<pre><code>docker build -t http-server:latest .
</code></pre>
<p>构建出来的镜像与未使用多阶段构建之前构建的镜像大小一致,为了验证这一结论,我们分别使用这两种方式来构建镜像,最后对比一下镜像构建的结果。</p>
<h3>镜像构建对比</h3>
<p>使用多阶段构建前后的代码我都已经放在了<a href="https://github.com/wilhelmguo/multi-stage-demo">Github</a>,你只需要克隆代码到本地即可。</p>
<pre><code>$ mkdir /go/src/github.com/wilhelmguo
$ cd /go/src/github.com/wilhelmguo
$ git clone https://github.com/wilhelmguo/multi-stage-demo.git
</code></pre>
<p>代码克隆完成后我们首先切换到without-multi-stage分支</p>
<pre><code>$ cd without-multi-stage
$ git checkout without-multi-stage
</code></pre>
<p>这个分支是未使用多阶段构建技术构建镜像的代码,我们可以通过执行 build.sh 文件构建镜像:</p>
<pre><code>$ chmod +x build.sh &amp;&amp; ./build.sh
Building http-server:build
Sending build context to Docker daemon 96.26kB
Step 1/4 : FROM golang:1.13
1.13: Pulling from library/golang
d6ff36c9ec48: Pull complete
c958d65b3090: Pull complete
edaf0a6b092f: Pull complete
80931cf68816: Pull complete
813643441356: Pull complete
799f41bb59c9: Pull complete
16b5038bccc8: Pull complete
Digest: sha256:8ebb6d5a48deef738381b56b1d4cd33d99a5d608e0d03c5fe8dfa3f68d41a1f8
Status: Downloaded newer image for golang:1.13
---&gt; d6f3656320fe
Step 2/4 : WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
---&gt; Running in fa3da5ffb0c0
Removing intermediate container fa3da5ffb0c0
---&gt; 97245cbb773f
Step 3/4 : COPY main.go .
---&gt; a021d2f2a5bb
Step 4/4 : RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
---&gt; Running in b5c36bb67b9c
Removing intermediate container b5c36bb67b9c
---&gt; 76c0c88a5cf7
Successfully built 76c0c88a5cf7
Successfully tagged http-server:build
4b0387b270bc4a4da570e1667fe6f9baac765f6b80c68f32007494c6255d9e5b
builder
Building http-server:latest
Sending build context to Docker daemon 7.496MB
Step 1/4 : FROM alpine:latest
latest: Pulling from library/alpine
df20fa9351a1: Already exists
Digest: sha256:185518070891758909c9f839cf4ca393ee977ac378609f700f60a771a2dfe321
Status: Downloaded newer image for alpine:latest
---&gt; a24bb4013296
Step 2/4 : WORKDIR /root/
---&gt; Running in 0b25ffe603b8
Removing intermediate container 0b25ffe603b8
---&gt; 80da40d3a0b4
Step 3/4 : COPY http-server .
---&gt; 3f2300210b7b
Step 4/4 : CMD [&quot;./http-server&quot;]
---&gt; Running in 045cea651dde
Removing intermediate container 045cea651dde
---&gt; 5c73883177e7
Successfully built 5c73883177e7
Successfully tagged http-server:latest
</code></pre>
<p>经过一段时间的等待,我们的镜像就构建完成了。
镜像构建完成后,我们使用<code>docker image ls</code>命令查看一下刚才构建的镜像大小:</p>
<pre><code>$ docker image ls http-server
REPOSITORY TAG IMAGE ID CREATED SIZE
http-server latest 5c73883177e7 3 minutes ago 13MB
http-server build 76c0c88a5cf7 3 minutes ago 819MB
</code></pre>
<p>可以看到http-server:latest 镜像只有 13M而我们的编译镜像 http-server:build 则为 819M虽然我们编写了很复杂的脚本 build.sh但是这个脚本确实帮助我们将镜像体积减小了很多。</p>
<p>下面,我们将代码切换到多阶段构建分支:</p>
<pre><code>$ git checkout with-multi-stage
Switched to branch 'with-multi-stage'
</code></pre>
<p>为了避免镜像名称重复,我们将多阶段构建的镜像命名为 http-server-with-multi-stage:latest ,并且禁用缓存,避免缓存干扰构建结果,构建命令如下:</p>
<pre><code>$ docker build --no-cache -t http-server-with-multi-stage:latest .
Sending build context to Docker daemon 96.77kB
Step 1/8 : FROM golang:1.13
---&gt; d6f3656320fe
Step 2/8 : WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
---&gt; Running in 640da7a92a62
Removing intermediate container 640da7a92a62
---&gt; 9c27b4606da0
Step 3/8 : COPY main.go .
---&gt; bd9ce4af24cb
Step 4/8 : RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
---&gt; Running in 6b441b4cc6b7
Removing intermediate container 6b441b4cc6b7
---&gt; 759acbf6c9a6
Step 5/8 : FROM alpine:latest
---&gt; a24bb4013296
Step 6/8 : WORKDIR /root/
---&gt; Running in c2aa2168acd8
Removing intermediate container c2aa2168acd8
---&gt; f026884acda6
Step 7/8 : COPY --from=0 /go/src/github.com/wilhelmguo/multi-stage-demo/http-server .
---&gt; 667503e6bc14
Step 8/8 : CMD [&quot;./http-server&quot;]
---&gt; Running in 15c4cc359144
Removing intermediate container 15c4cc359144
---&gt; b73cc4d99088
Successfully built b73cc4d99088
Successfully tagged http-server-with-multi-stage:latest
</code></pre>
<p>镜像构建完成后,我们同样使用<code>docker image ls</code>命令查看一下镜像构建结果:</p>
<pre><code>$ docker image ls http-server-with-multi-stage:latest
REPOSITORY TAG IMAGE ID CREATED SIZE
http-server-with-multi-stage latest b73cc4d99088 2 minutes ago 13MB
</code></pre>
<p>可以看到,使用多阶段构建的镜像大小与上一步构建的镜像大小一致,都为 13M。但是使用多阶段构建后却大大减少了我们的构建步骤使得构建过程更加清晰可读。</p>
<h3>多阶段构建的其他使用方式</h3>
<p>多阶段构建除了我们上面讲解的使用方式,还有更多其他的使用方式,这些使用方式,可以使得多阶段构建实现更多的功能。</p>
<h4>为构建阶段命名</h4>
<p>默认情况下,每一个构建阶段都没有被命名,你可以通过 FROM 指令出现的顺序来引用这些构建阶段,构建阶段的序号是从 0 开始的。然而,为了提高 Dockerfile 的可读性,我们需要为某些构建阶段起一个名称,这样即便后面我们对 Dockerfile 中的内容进程重新排序或者添加了新的构建阶段,其他构建过程中的 COPY 指令也不需要修改。</p>
<p>上面的 Dockerfile 我们可以优化成如下内容:</p>
<pre><code>FROM golang:1.13 AS builder
WORKDIR /go/src/github.com/wilhelmguo/multi-stage-demo/
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o http-server .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/github.com/wilhelmguo/multi-stage-demo/http-server .
CMD [&quot;./http-server&quot;]
</code></pre>
<p>我们在第一个构建阶段,使用 AS 指令将这个阶段命名为 builder。然后在第二个构建阶段使用 --from=builder 指令,即可从第一个构建阶段中拷贝文件,使得 Dockerfile 更加清晰可读。</p>
<h4>停止在特定的构建阶段</h4>
<p>有时候,我们的构建阶段非常复杂,我们想在代码编译阶段进行调试,但是多阶段构建默认构建 Dockerfile 的所有阶段,为了减少每次调试的构建时间,我们可以使用 target 参数来指定构建停止的阶段。</p>
<p>例如,我只想在编译阶段调试 Dockerfile 文件,可以使用如下命令:</p>
<pre><code>$ docker build --target builder -t http-server:latest .
</code></pre>
<p>在执行<code>docker build</code>命令时添加 target 参数,可以将构建阶段停止在指定阶段,从而方便我们调试代码编译过程。</p>
<h4>使用现有镜像作为构建阶段</h4>
<p>使用多阶段构建时,不仅可以从 Dockerfile 中已经定义的阶段中拷贝文件,还可以使用<code>COPY --from</code>指令从一个指定的镜像中拷贝文件,指定的镜像可以是本地已经存在的镜像,也可以是远程镜像仓库上的镜像。</p>
<p>例如,当我们想要拷贝 nginx 官方镜像的配置文件到我们自己的镜像中时,可以在 Dockerfile 中使用以下指令:</p>
<pre><code>COPY --from=nginx:latest /etc/nginx/nginx.conf /etc/local/nginx.conf
</code></pre>
<p>从现有镜像中拷贝文件还有一些其他的使用场景。例如,有些工具没有我们使用的操作系统的安装源,或者安装源太老,需要我们自己下载源码并编译这些工具,但是这些工具可能依赖的编译环境非常复杂,而网上又有别人已经编译好的镜像。这时我们就可以使用<code>COPY --from</code>指令从编译好的镜像中将工具拷贝到我们自己的镜像中,很方便地使用这些工具了。</p>
<h3>结语</h3>
<p>多阶段构建可以让我们通过一个 Dockerfile 很方便地构建出体积更小的镜像,并且我们只需要编写 Dockerfile 文件即可,无须借助外部脚本文件。这使得镜像构建过程更加简单透明,但要提醒一点:使用多阶段构建的唯一限制条件是我们使用的 Docker 版本必须高于 17.05 。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/由浅入深吃透 Docker-完/21 如何使 Docker 和 Kubernetes 结合发挥容器的最大价值?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/由浅入深吃透 Docker-完/23 DevOps容器化后如何通过 DevOps 提高协作效能?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997b6c8be53cfa","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>