<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Infrastructure on kenji.blog</title><link>http://kenji.blog/zh-cn/categories/infrastructure/</link><description>Recent content in Infrastructure on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 07:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/zh-cn/categories/infrastructure/index.xml" rel="self" type="application/rss+xml"/><item><title>Hyper-V vs WSL2：Windows上的虚拟化技术比较</title><link>http://kenji.blog/zh-cn/p/hyper-v-vs-wsl2-windows-virtualization/</link><pubDate>Sun, 13 Sep 2026 07:00:00 +0900</pubDate><guid>http://kenji.blog/zh-cn/p/hyper-v-vs-wsl2-windows-virtualization/</guid><description>&lt;img src="http://kenji.blog/p/hyper-v-vs-wsl2-windows-virtualization/img/eyecatch.jpg" alt="Featured image of post Hyper-V vs WSL2：Windows上的虚拟化技术比较" />&lt;h2 id="1-简介windows中虚拟化技术的演进">1. 简介：Windows中虚拟化技术的演进
&lt;/h2>&lt;p>Windows平台上的虚拟化技术在过去几十年里经历了戏剧性的演进。过去，第三方Type 2（宿主型）大管理程序（如VMware Workstation和VirtualBox）是主流，但自从Microsoft在Windows Server 2008中引入了“Hyper-V”以来，Type 1（裸机型）大管理程序（Hypervisor）也被整合到了桌面操作系统Windows 10/11中。&lt;/p>
&lt;p>而近年来，在开发者中最受关注的便是“WSL2 (Windows Subsystem for Linux 2)”。与依赖于系统调用转换（Translation）的WSL1不同，WSL2采用了应用Hyper-V技术的“轻量级实用工具虚拟机 (Lightweight Utility VM)”，实现了完全的Linux兼容性和飞跃性的性能提升。&lt;/p>
&lt;p>本文将针对这两种强大的虚拟化技术——全功能的“Hyper-V”与专为开发者体验优化的“WSL2”——它们的架构、性能（CPU、内存、磁盘I/O）、网络配置以及最佳用例，结合深层次的技术细节进行彻底的比较与解说。&lt;/p>
&lt;hr>
&lt;h2 id="2-大管理程序hypervisor的基础理论与架构比较">2. 大管理程序（Hypervisor）的基础理论与架构比较
&lt;/h2>&lt;p>在理解虚拟化技术时，不可或缺的是大管理程序（虚拟机监视器：VMM）的类型分类。&lt;/p>
&lt;h3 id="21-type-1-与-type-2-大管理程序的区别">2.1. Type 1 与 Type 2 大管理程序的区别
&lt;/h3>&lt;p>大管理程序是一个软件层，它抽象了对硬件的访问，使得多个操作系统（客户操作系统，Guest OS）能够在单一物理机上同时运行。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Type 1（裸机型）&lt;/strong>：直接在硬件上运行。不存在宿主操作系统（Host OS）的概念（严格来说可能会存在拥有特权的管理操作系统），开销极低，提供高阶的性能和安全性。示例：Hyper-V、VMware ESXi、Xen。&lt;/li>
&lt;li>&lt;strong>Type 2（宿主型）&lt;/strong>：作为宿主操作系统（如Windows或macOS）上的应用程序运行。由于所有的硬件访问都需要经过宿主操作系统，因此开销较大。示例：VMware Workstation、Oracle VirtualBox。&lt;/li>
&lt;/ul>
&lt;p>Windows的Hyper-V是纯粹的 &lt;strong>Type 1大管理程序&lt;/strong>。启用Hyper-V后，实际上用户平时操作的Windows操作系统本身，也开始运行在被称为“根分区 (Root Partition)”的特殊虚拟机之中。&lt;/p>
&lt;h3 id="22-hyper-v的架构细节">2.2. Hyper-V的架构细节
&lt;/h3>&lt;p>Hyper-V的架构采用了微内核设计，基于被称为分区（Partition）的逻辑隔离单位。&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;硬件 (CPU, RAM, 磁盘, NIC)&amp;#34;] --&amp;gt; B[&amp;#34;Windows大管理程序 (Ring -1)&amp;#34;]
B --&amp;gt; C[&amp;#34;根分区 (Windows OS)&amp;#34;]
B --&amp;gt; D[&amp;#34;子分区 1 (Windows VM)&amp;#34;]
B --&amp;gt; E[&amp;#34;子分区 2 (Linux VM)&amp;#34;]
C --&amp;gt; F[&amp;#34;VMBus (虚拟机总线)&amp;#34;]
D --&amp;gt; F
E --&amp;gt; F
C --&amp;gt; G[&amp;#34;VID (虚拟化基础架构驱动程序)&amp;#34;]
C --&amp;gt; H[&amp;#34;VMWP.exe (工作进程)&amp;#34;]
&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>Windows大管理程序 (Windows Hypervisor)&lt;/strong>：在CPU最高特权级别的状态下（Ring -1 或 VMX Root Mode）运行，仅负责内存分配和CPU调度。不包含设备驱动程序。&lt;/li>
&lt;li>&lt;strong>根分区 (Root Partition)&lt;/strong>：运行宿主Windows操作系统的分区。拥有所有的设备驱动程序，并直接控制硬件。此外，还提供子分区的管理功能（如WMI提供程序和VMWP.exe等）。&lt;/li>
&lt;li>&lt;strong>子分区 (Child Partition)&lt;/strong>：运行客户操作系统的分区。不允许直接访问硬件，而是通过被称为“VMBus”的逻辑内存共享总线，将I/O请求发送到根分区（合成I/O，Synthetic I/O）。&lt;/li>
&lt;/ul>
&lt;h3 id="23-wsl2与轻量级实用工具虚拟机的机制">2.3. WSL2与轻量级实用工具虚拟机的机制
&lt;/h3>&lt;p>WSL2利用了与Hyper-V相同的Type 1大管理程序的底层技术，但它使用的是不同于全功能Hyper-V虚拟机的、被称为“虚拟机平台 (Virtual Machine Platform: VMP)”的子集功能。&lt;/p>
&lt;p>WSL2中采用的“轻量级实用工具虚拟机 (Lightweight Utility VM)”，完全排除了传统虚拟机所拥有的旧式硬件模拟（如虚拟BIOS和虚拟主板等）。&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;Windows宿主OS (用户空间)&amp;#34;]
B[&amp;#34;NTFS文件系统&amp;#34;]
C[&amp;#34;9P协议服务器 (Plan 9)&amp;#34;]
D[&amp;#34;轻量级实用工具虚拟机 (Linux内核)&amp;#34;]
E[&amp;#34;ext4.vhdx (虚拟磁盘)&amp;#34;]
F[&amp;#34;Linux用户空间 (WSL2发行版)&amp;#34;]
A --&amp;gt; C
C --&amp;gt;| 跨OS文件共享 | D
D --&amp;gt; E
D --&amp;gt; F
&lt;/pre>
&lt;p>WSL2最大的特点是&lt;strong>启动速度快&lt;/strong>和&lt;strong>与宿主操作系统的无缝集成&lt;/strong>。Linux内核在不到几秒的时间内启动，并通过Plan 9的 &lt;code>9P&lt;/code> 网络文件系统协议访问Windows端的文件系统（NTFS）。&lt;/p>
&lt;hr>
&lt;h2 id="3-性能的彻底分析计算资源与io">3. 性能的彻底分析：计算资源与I/O
&lt;/h2>&lt;p>虚拟机的性能表现为CPU、内存以及磁盘I/O等各个组件中开销的总和。&lt;/p>
&lt;h3 id="31-cpu与上下文切换开销">3.1. CPU与上下文切换开销
&lt;/h3>&lt;p>Hyper-V和WSL2都使用了硬件辅助虚拟化（Intel VT-x / AMD-V）。CPU指令基本上以原生速度执行，但在执行特权指令或进行I/O处理时，会发生被称为“VM Exit”的中断，并进行上下文切换至大管理程序。&lt;/p>
&lt;p>此时的CPU开销 $T_{overhead}$ 可以用以下的数学模型来表示：&lt;/p>
$$ T_{overhead} = \sum_{i=1}^{N} (t_{vm\_exit} + t_{hypercall\_process} + t_{vm\_entry}) $$&lt;p>这里：&lt;/p>
&lt;ul>
&lt;li>$N$：单位时间内VM Exit发生的次数&lt;/li>
&lt;li>$t_{vm\_exit}$：从客户机转移到大管理程序的时间&lt;/li>
&lt;li>$t_{hypercall\_process}$：通过VMBus的I/O处理或中断处理的时间&lt;/li>
&lt;li>$t_{vm\_entry}$：从大管理程序返回客户机的时间&lt;/li>
&lt;/ul>
&lt;p>由于WSL2没有旧式硬件模拟，因此 $t_{hypercall\_process}$ 被优化得极其微小。因此，在纯粹的CPU运算（例如内核编译或机器学习模型推理）中，与裸机环境相比，性能下降可以控制在百分之几以内。&lt;/p>
&lt;h3 id="32-内存分配机制">3.2. 内存分配机制
&lt;/h3>&lt;p>在内存管理方法上，两者有着明确的设计理念差异。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hyper-V (动态内存)&lt;/strong>：根分区会根据客户虚拟机的内存需求动态地分配和回收内存。然而，在客户操作系统中作为页面缓存分配的内存，除非系统资源紧张，否则往往很难被释放。&lt;/li>
&lt;li>&lt;strong>WSL2 (动态内存回收)&lt;/strong>：WSL2拥有独特的机制，会定期将Linux虚拟机内不再需要的内存（包括缓存）返还（Reclaim）给Windows宿主机。早期的WSL2存在Linux页面缓存耗尽Windows内存的问题（Vmmem进程膨胀），但现在已经通过内核补丁得到了改善。&lt;/li>
&lt;/ul>
&lt;h3 id="33-磁盘io的特性vhdx-vs-ext4vhdx">3.3. 磁盘I/O的特性（VHDX vs ext4.vhdx）
&lt;/h3>&lt;p>在虚拟机性能中最容易成为瓶颈的是磁盘I/O。&lt;/p>
&lt;p>I/O的延迟 $L_{total}$ 按以下方式计算：&lt;/p>
$$ L_{total} = L_{guest\_fs} + L_{vmbus} + L_{host\_fs} + L_{physical\_disk} $$&lt;p>&lt;strong>在Hyper-V的情况下&lt;/strong>：
一般的Hyper-V客户机使用 &lt;code>VHDX&lt;/code> 格式的虚拟磁盘。客户操作系统内文件系统（ext4或NTFS）发出的I/O请求，穿过VMBus的块设备存储驱动程序（storvsc），在Windows端的NTFS上作为对VHDX文件的访问进行处理。&lt;/p>
&lt;p>&lt;strong>在WSL2的情况下&lt;/strong>：
WSL2的Linux发行版在专属的 &lt;code>ext4.vhdx&lt;/code> 文件内构建的原生ext4文件系统上运行。Linux内的文件操作（例如 &lt;code>~&lt;/code> 目录内）能够发挥与上述Hyper-V同等的原生性能。
但是，&lt;strong>当从WSL2的Linux端访问Windows端的文件（如 &lt;code>/mnt/c/&lt;/code> 等）时&lt;/strong>，或者反过来时，处理过程有很大的不同。这种跨操作系统的访问使用了 &lt;code>9P (Plan 9 File System Protocol)&lt;/code>。&lt;/p>
$$ L_{cross\_os} = L_{9p\_client} + L_{socket\_transfer} + L_{9p\_server} + L_{ntfs} $$&lt;p>这种通过9P协议的访问有着巨大的序列化处理开销。在大量读写小文件的场景下（例如：位于Windows端目录的Node.js项目执行 &lt;code>npm install&lt;/code> 或是Git操作时），性能会显著下降（有时延迟超过10倍）。
因此，&lt;strong>使用WSL2时，务必将项目文件放在Linux的原生文件系统（&lt;code>~/&lt;/code> 目录下）是铁则&lt;/strong>。&lt;/p>
&lt;hr>
&lt;h2 id="4-网络结构nat默认交换机桥接">4. 网络结构：NAT、默认交换机、桥接
&lt;/h2>&lt;p>网络功能的灵活性是Hyper-V和WSL2的巨大区别之一。&lt;/p>
&lt;h3 id="41-wsl2的网络-基于nat">4.1. WSL2的网络 (基于NAT)
&lt;/h3>&lt;p>WSL2的网络默认使用的是基于Hyper-V虚拟交换机技术的“NAT（网络地址转换）”配置。
Linux虚拟机将被自动分配一个与Windows宿主机不同的私有IP地址（例如：&lt;code>172.20.x.x&lt;/code>）。系统内置了一种机制，使得可以从Windows宿主机通过 &lt;code>localhost&lt;/code> 转发到在WSL2内启动的服务（端口），开发者在无需关注网络细节的情况下即可测试Web服务器等。&lt;/p>
&lt;p>近年来，WSL2的预览版中引入了一种名为“镜像模式 (Mirrored mode)”的新网络模式。由此提升了对IPv6的支持及VPN连接的兼容性（可通过 &lt;code>.wslconfig&lt;/code> 进行配置）。&lt;/p>
&lt;h3 id="42-hyper-v的虚拟交换机-virtual-switch">4.2. Hyper-V的虚拟交换机 (Virtual Switch)
&lt;/h3>&lt;p>Hyper-V可以进行企业级的高级网络构建。通过“虚拟交换机管理器”，主要提供以下三种模式：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>外部 (External)&lt;/strong>：将宿主机的物理网卡（NIC）绑定到虚拟交换机，让客户虚拟机直接加入到物理网络中（桥接连接）。虚拟机从DHCP服务器获取与物理网络相同子网的IP地址。&lt;/li>
&lt;li>&lt;strong>内部 (Internal)&lt;/strong>：仅允许宿主操作系统与虚拟机之间，以及虚拟机相互之间的通信。无法直接连接外部网络。&lt;/li>
&lt;li>&lt;strong>专用 (Private)&lt;/strong>：仅允许虚拟机相互之间的通信，甚至阻断与宿主操作系统的通信。用于构建隔离的验证环境。&lt;/li>
&lt;/ol>
&lt;h3 id="43-通过powershell构建高级的hyper-v网络">4.3. 通过PowerShell构建高级的Hyper-V网络
&lt;/h3>&lt;p>在开发或测试环境中，如果希望为虚拟机建立自定义的NAT网络，可以使用PowerShell进行更详细的控制。以下是一个创建内部虚拟交换机并在其上配置NAT以向虚拟机提供互联网访问的脚本示例。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 1. 创建内部虚拟交换机&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$SwitchName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;HyperV-NatSwitch&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-VMSwitch&lt;/span> &lt;span class="n">-SwitchName&lt;/span> &lt;span class="nv">$SwitchName&lt;/span> &lt;span class="n">-SwitchType&lt;/span> &lt;span class="n">Internal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 2. 在宿主机端的虚拟网卡上配置IP地址（作为网关的IP）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$GatewayIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;192.168.100.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NetPrefix&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mf">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$InterfaceAlias&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;vEthernet (&lt;/span>&lt;span class="nv">$SwitchName&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-NetIPAddress&lt;/span> &lt;span class="n">-IPAddress&lt;/span> &lt;span class="nv">$GatewayIP&lt;/span> &lt;span class="n">-PrefixLength&lt;/span> &lt;span class="nv">$NetPrefix&lt;/span> &lt;span class="n">-InterfaceAlias&lt;/span> &lt;span class="nv">$InterfaceAlias&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 3. 配置NAT网络&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NatName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;HyperV-NatNetwork&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NatSubnet&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;192.168.100.0/24&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-NetNat&lt;/span> &lt;span class="n">-Name&lt;/span> &lt;span class="nv">$NatName&lt;/span> &lt;span class="n">-InternalIPInterfaceAddressPrefix&lt;/span> &lt;span class="nv">$NatSubnet&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 用于验证的命令&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Get-NetNat&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>通过该配置，手动将指定的Hyper-V客户机IP设置为 &lt;code>192.168.100.x&lt;/code> 并将网关设为 &lt;code>192.168.100.1&lt;/code>，便能构建一个可以通过宿主机与外部通信的专有NAT网段。&lt;/p>
&lt;hr>
&lt;h2 id="5-用例与实用的选择指南">5. 用例与实用的选择指南
&lt;/h2>&lt;p>基于目前介绍的架构与性能差异，我们将明确在何种情况下应该采用哪种技术。&lt;/p>
&lt;h3 id="51-应该选择wsl2的场景">5.1. 应该选择WSL2的场景
&lt;/h3>&lt;p>WSL2专为“提升开发者的生产力”而设计。最适合以下用途：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Web开发及云原生开发&lt;/strong>：使用Docker Desktop（WSL2后端）或Podman的容器开发。&lt;/li>
&lt;li>&lt;strong>使用Linux专用工具&lt;/strong>：日常使用bash、grep、awk、sed，或者是面向Linux的GCC、Clang编译器时。&lt;/li>
&lt;li>&lt;strong>GUI应用程序 (WSLg)&lt;/strong>：希望在Windows桌面上无缝运行Linux的X11/Wayland应用程序时。&lt;/li>
&lt;li>&lt;strong>机器学习与AI开发&lt;/strong>：利用GPU直通功能（NVIDIA CUDA on WSL）进行TensorFlow或PyTorch的高速训练。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>注意事项&lt;/strong>：在需要精细定制内核，或是构建严重依赖systemd的复杂服务时（目前支持systemd，但默认被禁用或有一定限制），可能会受到限制。&lt;/p>
&lt;h3 id="52-应该选择hyper-v的场景">5.2. 应该选择Hyper-V的场景
&lt;/h3>&lt;p>Hyper-V的目的是为了实现“基础设施的虚拟化和完全隔离”。在以下用途中是必不可少的：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>运行Windows虚拟机&lt;/strong>：将不同版本的Windows（如Windows Server或旧版本的Windows 10等）作为测试环境运行时。&lt;/li>
&lt;li>&lt;strong>嵌套虚拟化&lt;/strong>：希望在虚拟机中进一步运行虚拟机（如Hyper-V或KVM）时。对基础设施工程师的验证环境至关重要。&lt;/li>
&lt;li>&lt;strong>高级网络需求&lt;/strong>：需要外部桥接连接（加入同一LAN）、VLAN标记、多网卡分配等，需严格控制网络配置的场景。&lt;/li>
&lt;li>&lt;strong>快照（检查点）&lt;/strong>：保存虚拟机特定时间点状态并在任何时候即刻回滚的功能。对于破坏性软件测试或恶意软件分析非常有用。&lt;/li>
&lt;li>&lt;strong>固定的资源分配&lt;/strong>：需要严格固定CPU核心数量和内存容量，并将对宿主机操作系统的影响降至最低时。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="6-基于数学模型的io吞吐量分析-附录">6. 基于数学模型的I/O吞吐量分析 (附录)
&lt;/h2>&lt;p>作为系统工程师，在评估两者的I/O性能极限时，从理论上把握吞吐量 $S$ 与块大小 $B$ 之间的关系是非常重要的。&lt;/p>
&lt;p>数据传输的吞吐量 $S$ 是单位时间内的传输数据量，可建立如下模型：&lt;/p>
$$ S(B) = \frac{B}{L_{setup} + \frac{B}{R_{max}}} $$&lt;ul>
&lt;li>$B$：块大小 (Bytes)&lt;/li>
&lt;li>$L_{setup}$：I/O请求的设置以及伴随上下文切换的固定延迟&lt;/li>
&lt;li>$R_{max}$：复制或设备传输时硬件的最大带宽&lt;/li>
&lt;/ul>
&lt;p>在WSL2中，通过9P协议访问文件时，该 $L_{setup}$ 会变得非常大（由于套接字通信和协议的序列化/反序列化）。因此，当块大小 $B$ 较小（大量读写几KB大小的小文件）时，分母中 $L_{setup}$ 的影响会占据主导，吞吐量 $S$ 会急剧下降。
相反，在Hyper-V中经由VMBus访问VHDX文件时，由于 $L_{setup}$ 已被优化到了接近硬件中断的级别，即使是小规模块也能维持较高的IOPS。&lt;/p>
&lt;p>这一数学现实，构成了“在WSL2中绝不能把项目文件放在Windows端”这一最佳实践的逻辑基础。&lt;/p>
&lt;hr>
&lt;h2 id="7-总结共存的两种虚拟化技术">7. 总结：共存的两种虚拟化技术
&lt;/h2>&lt;p>Hyper-V与WSL2并不存在谁优谁劣，而是**“目的截然不同的两种解决方案”**。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>WSL2&lt;/strong> 打破了Windows操作系统的外壳，是将Linux生态系统无缝、高速地送到Windows用户手中的“最佳集成工具”。说它是开发者的终极CLI环境也不为过。&lt;/li>
&lt;li>&lt;strong>Hyper-V&lt;/strong> 则是将企业级数据中心积累的强大隔离性与管理能力带入桌面的“正规大管理程序”。在网络构建、Windows操作系统的测试以及基础设施环境的模拟方面，它是无出其右的选择。&lt;/li>
&lt;/ul>
&lt;p>在现代的Windows环境中，这两种技术并非势均力敌地相互竞争，而是能够在同一个虚拟机平台上优美地共存。根据用途因地制宜地灵活使用，Windows将会成为世界上最强大、最灵活的工程工作站。&lt;/p></description></item><item><title>使用 Docker 构建可重现的本地开发环境的步骤</title><link>http://kenji.blog/zh-cn/p/docker-reproducible-local-dev-environment/</link><pubDate>Sun, 13 Sep 2026 01:00:00 +0900</pubDate><guid>http://kenji.blog/zh-cn/p/docker-reproducible-local-dev-environment/</guid><description>&lt;img src="http://kenji.blog/p/docker-reproducible-local-dev-environment/img/eyecatch.jpg" alt="Featured image of post 使用 Docker 构建可重现的本地开发环境的步骤" />&lt;h2 id="1-引言摆脱在我的机器上能运行的困境">1. 引言：摆脱“在我的机器上能运行”的困境
&lt;/h2>&lt;p>在软件开发现场，由于开发者之间的环境差异而导致的“在我的机器上能运行（It works on my machine）”问题，长期以来一直是许多项目浪费时间的因素。操作系统的差异、安装的语言版本、库的依赖关系、全局安装的工具冲突等，本地环境始终面临着“状态的不确定性”。&lt;/p>
&lt;p>从根本上解决这些挑战的是以 &lt;strong>Docker&lt;/strong> 为首的容器技术，以及 &lt;strong>基础设施即代码 (IaC)&lt;/strong> 的范式。通过将本地开发环境容器化，实现了操作系统级别的隔离，并可以将环境本身与代码库一起进行版本控制。&lt;/p>
&lt;p>本文将深入讲解如何充分利用 Docker、Docker Compose 和 VSCode DevContainers，构建一个**“无论谁在何时、哪台机器上启动，状态都分毫不差的可重现的本地开发环境”**的步骤，并从数学角度深入探讨其背后的深层技术机制。&lt;/p>
&lt;hr>
&lt;h2 id="2-基础设施即代码-iac-与容器技术的契合度">2. 基础设施即代码 (IaC) 与容器技术的契合度
&lt;/h2>&lt;h3 id="iac-的原则及其在本地环境中的应用">IaC 的原则及其在本地环境中的应用
&lt;/h3>&lt;p>基础设施即代码 (IaC) 是一种通过机器可读的定义文件，而非手动流程来管理和配置基础架构的方法。IaC 的核心原则包含以下要素：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>声明式方法 (Declarative Approach)&lt;/strong>：定义“最终应该是什么状态”，而不是“如何改变状态”。&lt;/li>
&lt;li>&lt;strong>幂等性 (Idempotency)&lt;/strong>：无论执行多少次脚本，始终保证相同的结果（状态）。&lt;/li>
&lt;li>&lt;strong>版本控制 (Version Control)&lt;/strong>：基础设施的状态作为代码保存在 Git 等 VCS 中，从而可以追踪更改历史并进行同行评审。&lt;/li>
&lt;/ol>
&lt;p>在本地开发环境中实践 IaC，意味着使用 &lt;code>Dockerfile&lt;/code>、&lt;code>docker-compose.yml&lt;/code> 和 &lt;code>devcontainer.json&lt;/code> 将开发环境的“理想状态”代码化。这样，新加入团队的成员也可以通过克隆存储库并运行一条命令，立即开始开发，实现丝滑的入职体验。&lt;/p>
&lt;h3 id="支撑容器技术的内核功能">支撑容器技术的内核功能
&lt;/h3>&lt;p>容器技术与虚拟机（VM）等基于管理程序的虚拟化不同，它是一种轻量级的虚拟化技术，在共享宿主机操作系统内核的同时，隔离进程（Isolation）。为了实现这一点，主要使用了 Linux 内核的以下功能：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Namespaces（命名空间）&lt;/strong>：为每个进程提供独立的系统资源视图（PID、网络、挂载点、用户等）。&lt;/li>
&lt;li>&lt;strong>Cgroups (Control Groups, 控制组)&lt;/strong>：限制和分配进程可以使用的物理资源（CPU、内存、磁盘 I/O 等）。&lt;/li>
&lt;li>&lt;strong>UnionFS (联合文件系统)&lt;/strong>：一种透明地叠加多个目录树（层）并将它们显示为一个文件系统的技术。Docker 的镜像层依赖于这项技术。&lt;/li>
&lt;/ul>
&lt;p>让我们考虑一下资源限制的数学模型。假设宿主机的总内存容量为 $M_{\text{total}}$，在宿主机上运行的 $n$ 个容器的内存限制为 $m_i$。考虑到操作系统和其他进程消耗的基础内存 $M_{\text{os}}$，系统稳定运行的必要条件可以用以下不等式表示：&lt;/p>
$$ \sum_{i=1}^{n} m_i \le M_{\text{total}} - M_{\text{os}} $$&lt;p>通过使用 Cgroups 严格定义每个容器的 $m_i$，即使特定容器发生内存泄漏，也能防止 OOM (Out Of Memory) Killer 导致其他容器或整个宿主系统崩溃。&lt;/p>
&lt;hr>
&lt;h2 id="3-高效的-dockerfile-设计掌握多阶段构建">3. 高效的 Dockerfile 设计：掌握多阶段构建
&lt;/h2>&lt;p>实现可重现环境的第一步是设计定义应用程序运行环境的 &lt;code>Dockerfile&lt;/code>。在这里，我们将以 Python (FastAPI) 为例，讲解利用&lt;strong>多阶段构建&lt;/strong>的，安全且轻量级的 Dockerfile 最佳实践。&lt;/p>
&lt;p>多阶段构建是一种在一个 &lt;code>Dockerfile&lt;/code> 中使用多个 &lt;code>FROM&lt;/code> 指令的方法，将构建环境（包含编译器和开发工具的繁重环境）与执行环境（仅包含所需工件的轻量级环境）分离。&lt;/p>
&lt;h3 id="实用的-python-fastapi-dockerfile">实用的 Python FastAPI Dockerfile
&lt;/h3>&lt;p>以下代码是结合 Poetry 依赖项管理和多阶段构建的高级 &lt;code>Dockerfile&lt;/code> 示例。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Stage 1: Builder (构建环境)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> python:3.11-slim AS builder&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 设置所需的环境变量&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PYTHONUNBUFFERED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PYTHONDONTWRITEBYTECODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>.6.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_HOME&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/poetry&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_VIRTUALENVS_IN_PROJECT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_NO_INTERACTION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 安装依赖包&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt-get update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt-get install -y --no-install-recommends &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl build-essential &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -sSL https://install.python-poetry.org &lt;span class="p">|&lt;/span> python3 - &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> apt-get clean &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> rm -rf /var/lib/apt/lists/*&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$POETRY_HOME&lt;/span>&lt;span class="s2">/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 复制并安装依赖关系文件&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install --no-root --only main&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Stage 2: Runtime (运行环境)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> python:3.11-slim AS runtime&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PYTHONUNBUFFERED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PYTHONDONTWRITEBYTECODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/app/.venv/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 创建一个最小的非特权用户&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> groupadd -r appuser &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> useradd -r -g appuser appuser&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 仅从 builder 复制虚拟环境（依赖项）&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>builder --chown&lt;span class="o">=&lt;/span>appuser:appuser /app/.venv /app/.venv&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 复制应用程序代码&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --chown&lt;span class="o">=&lt;/span>appuser:appuser ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 切换到非特权用户&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> appuser&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 容器启动时的默认命令&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;uvicorn&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;src.main:app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--port&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="多阶段构建对镜像大小的数学评估">多阶段构建对镜像大小的数学评估
&lt;/h3>&lt;p>假设单阶段构建时的镜像大小为 $S_{\text{single}}$，应用多阶段构建后的镜像大小为 $S_{\text{multi}}$。大小缩减率 $R$ 的计算公式如下：&lt;/p>
$$ R = \left( 1 - \frac{S_{\text{multi}}}{S_{\text{single}}} \right) \times 100 \ (\%) $$&lt;p>例如，假设 $S_{\text{single}}$ 包含操作系统的基础镜像（约 110MB）、开发包（如 gcc 约 150MB）、Poetry 本身（约 40MB）、项目的依赖库（约 80MB）和源代码（约 5MB），总计 385MB。
另一方面，在 $S_{\text{multi}}$ 中，仅将依赖库（80MB）和源代码（5MB）复制到基础镜像（110MB）中，因此总计为 195MB。&lt;/p>
$$ R = \left( 1 - \frac{195}{385} \right) \times 100 \approx 49.35\% $$&lt;p>通过引入多阶段构建，镜像大小可减少约一半。镜像大小的减小直接关系到从注册表提取（Pull）时间的缩短、磁盘空间的节省，以及通过减少攻击面（Attack Surface）来提高安全性。&lt;/p>
&lt;hr>
&lt;h2 id="4-使用-docker-compose-编排多个容器">4. 使用 Docker Compose 编排多个容器
&lt;/h2>&lt;p>在现代 Web 应用程序开发中，由 Web 服务器、数据库和缓存服务器等多个组件协同工作的微服务架构非常普遍。为了在本地环境中集中管理这些组件，我们使用 &lt;code>docker-compose.yml&lt;/code>。&lt;/p>
&lt;p>这次，我们将在本地构建一个“Web (FastAPI)”、“数据库 (PostgreSQL)”和“缓存 (Redis)”的三层架构系统。&lt;/p>
&lt;h3 id="架构图-mermaid">架构图 (Mermaid)
&lt;/h3>&lt;p>下图是表示本地机器中各容器、网络和卷之间关系的块图。&lt;/p>
&lt;pre class="mermaid">
graph TD
User[&amp;#34;宿主机 (浏览器/curl)&amp;#34;] --&amp;gt;|Localhost:8000| Web[&amp;#34;FastAPI Web 容器&amp;#34;]
subgraph &amp;#34;Docker 桥接网络 (app-network)&amp;#34;
Web --&amp;gt;|Port 5432| DB[&amp;#34;PostgreSQL 容器&amp;#34;]
Web --&amp;gt;|Port 6379| Redis[&amp;#34;Redis 容器&amp;#34;]
end
DB --&amp;gt; Volume1[&amp;#34;命名卷 (postgres_data)&amp;#34;]
Redis --&amp;gt; Volume2[&amp;#34;命名卷 (redis_data)&amp;#34;]
HostDir[&amp;#34;宿主机源代码 (./src)&amp;#34;] -.-&amp;gt;|Bind Mount| Web
&lt;/pre>
&lt;h3 id="docker-composeyml-的实现与详细讲解">docker-compose.yml 的实现与详细讲解
&lt;/h3>&lt;p>以下是足以承受实际环境构建的强大 &lt;code>docker-compose.yml&lt;/code> 示例。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">runtime&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_web&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8000:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./src:/app/src:ro &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 以只读方式挂载宿主机代码（用于热重载）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">REDIS_URL=redis://redis:6379/0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env_file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">.env&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_healthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_started&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;uvicorn&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;src.main:app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--port&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--reload&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:15-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;5432:5432&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${POSTGRES_PASSWORD}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_DB&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${POSTGRES_DB}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">postgres_data:/var/lib/postgresql/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthcheck&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;CMD-SHELL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pg_isready -U postgres -d ${POSTGRES_DB}&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redis:7-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_redis&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;6379:6379&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">redis_data:/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;redis-server&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--appendonly&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;yes&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">postgres_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="卷-volumes-与数据持久化">卷 (Volumes) 与数据持久化
&lt;/h3>&lt;p>容器原则上是“无状态的（Stateless）”且“短暂的（Ephemeral）”。一旦销毁容器，其内部的数据也会随之消失。为了保留数据库的数据或缓存，需要将宿主机的文件系统区域挂载到容器中。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Bind Mount (绑定挂载)&lt;/strong>：对应上述 &lt;code>web&lt;/code> 服务中的 &lt;code>./src:/app/src:ro&lt;/code>。将宿主机的特定目录直接映射到容器内。用于将本地的代码编辑立即反映到容器中（热重载）。从安全角度出发，最佳实践是添加 &lt;code>:ro&lt;/code> (Read-Only) 选项，以防止容器端修改宿主机的源代码。&lt;/li>
&lt;li>&lt;strong>Named Volume (命名卷)&lt;/strong>：对应 &lt;code>postgres_data&lt;/code> 和 &lt;code>redis_data&lt;/code>。是由 Docker 内部（如 &lt;code>/var/lib/docker/volumes/&lt;/code>）管理的区域，比绑定挂载具有更好的 I/O 性能，并且能吸收不同操作系统间文件系统的差异。数据库的持久化务必使用此方式。&lt;/li>
&lt;/ul>
&lt;h3 id="网络-networking-与服务发现">网络 (Networking) 与服务发现
&lt;/h3>&lt;p>Docker Compose 默认会为每个项目创建一个专有的桥接网络，即上述的 &lt;code>app-network&lt;/code>。
属于同一网络的容器之间，可以使用“服务名（如 &lt;code>db&lt;/code>, &lt;code>redis&lt;/code>）”作为主机名进行名称解析（DNS 解析），而不是使用 IP 地址。
例如，可以从 Web 容器通过 &lt;code>postgresql://postgres:password@db:5432/mydb&lt;/code> 这样的 URL 访问数据库。由此，无论是在本地环境还是生产环境，都能通过环境变量透明地切换连接目标。&lt;/p>
&lt;h3 id="运行状况检查与启动顺序控制">运行状况检查与启动顺序控制
&lt;/h3>&lt;p>&lt;code>depends_on&lt;/code> 指令用于控制容器的启动顺序，但仅指定 &lt;code>depends_on&lt;/code> 会在“DB 容器启动”的阶段就启动 Web 容器。实际上，DB 的初始化过程（PostgreSQL 进程启动和表准备）需要几秒钟的时间，因此 Web 容器发起的 DB 连接可能会报错。
为了防止这种情况发生，可以定义 &lt;code>healthcheck&lt;/code> 并指定 &lt;code>condition: service_healthy&lt;/code>，这样就能确认“DB 已处于可以接受连接请求的状态”后，再启动 Web 容器。&lt;/p>
&lt;hr>
&lt;h2 id="5-环境变量管理与安全性-env">5. 环境变量管理与安全性 (.env)
&lt;/h2>&lt;p>绝对应该避免将数据库密码或 API 密钥等敏感信息硬编码到 &lt;code>docker-compose.yml&lt;/code> 中，这是一种反模式。相反，我们应该使用环境变量文件 &lt;code>.env&lt;/code> 注入这些值。&lt;/p>
&lt;p>在项目根目录下创建一个 &lt;code>.env&lt;/code> 文件。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># .env 文件 (请将其添加到 .gitignore 中以排除在 Git 管理之外)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">POSTGRES_PASSWORD&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">supersecretpassword&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">POSTGRES_DB&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">devdb&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">API_SECRET_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">dev_secret_key_12345&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Docker Compose 默认会读取执行目录下的 &lt;code>.env&lt;/code> 文件，并展开 YAML 文件中的 &lt;code>${VAR_NAME}&lt;/code> 占位符。通过这种方法，无需修改基础架构代码，即可安全地管理本地、临时和生产等不同环境的配置值。&lt;/p>
&lt;hr>
&lt;h2 id="6-vscode-devcontainers-带来的极致开发体验">6. VSCode DevContainers 带来的极致开发体验
&lt;/h2>&lt;p>到目前为止，我们已经使用 Docker 构建了一个强大的后端环境。然而，我们还可以更进一步。通过使用 &lt;strong>VSCode DevContainers (Remote - Containers)&lt;/strong> 功能，可以将编辑器（VSCode）本身的后端运行在容器内部。&lt;/p>
&lt;p>这样一来，本地机器上甚至不需要安装 Python 或 Node.js，从 Linter（flake8/eslint）、格式化工具（black/prettier）到 IDE 扩展，所有的内容都可以在代码库中定义并由整个团队共享。&lt;/p>
&lt;h3 id="devcontainerjson-配置">devcontainer.json 配置
&lt;/h3>&lt;p>在项目根目录下创建一个 &lt;code>.devcontainer&lt;/code> 目录，并在其中放置配置文件。&lt;/p>
&lt;p>&lt;code>.devcontainer/devcontainer.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Python FastAPI Dev Environment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dockerComposeFile&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;../docker-compose.yml&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;web&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;workspaceFolder&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;customizations&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vscode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;settings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;python.defaultInterpreterPath&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/app/.venv/bin/python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;python.formatting.provider&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;black&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;editor.formatOnSave&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;extensions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.vscode-pylance&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.black-formatter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tamasfe.even-better-toml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;forwardPorts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8000&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5432&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6379&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;remoteUser&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;appuser&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;postCreateCommand&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;poetry install&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>将该文件包含在存储库中，当在 VSCode 中打开该项目时，系统会提示“在容器中重新打开（Reopen in Container）”，只需点击一下，就会启动所有必需的容器，安装扩展程序，并立即准备好进行编码。这真是一种如同魔法般的体验。&lt;/p>
&lt;hr>
&lt;h2 id="7-请求处理序列与性能建模">7. 请求处理序列与性能建模
&lt;/h2>&lt;p>我们通过序列图来确认在构建的本地开发环境中的 Web 应用程序请求处理的生命周期，并探讨其性能的数学模型。&lt;/p>
&lt;h3 id="序列图请求流程">序列图（请求流程）
&lt;/h3>&lt;pre class="mermaid">
sequenceDiagram
participant Client as &amp;#34;浏览器 / VSCode&amp;#34;
participant Web as &amp;#34;FastAPI (Web)&amp;#34;
participant Redis as &amp;#34;Redis 缓存&amp;#34;
participant DB as &amp;#34;PostgreSQL&amp;#34;
Client-&amp;gt;&amp;gt;Web: &amp;#34;GET /api/users/123&amp;#34;
activate Web
Web-&amp;gt;&amp;gt;Redis: &amp;#34;检查缓存 user:123&amp;#34;
activate Redis
alt &amp;#34;Cache Hit (缓存命中/有数据)&amp;#34;
Redis--&amp;gt;&amp;gt;Web: &amp;#34;返回缓存的用户数据&amp;#34;
Web--&amp;gt;&amp;gt;Client: &amp;#34;200 OK (快速响应)&amp;#34;
else &amp;#34;Cache Miss (缓存未命中/无数据)&amp;#34;
Redis--&amp;gt;&amp;gt;Web: &amp;#34;Null (未找到)&amp;#34;
deactivate Redis
Web-&amp;gt;&amp;gt;DB: &amp;#34;SELECT * FROM users WHERE id = 123&amp;#34;
activate DB
DB--&amp;gt;&amp;gt;Web: &amp;#34;返回数据库行&amp;#34;
deactivate DB
Web-&amp;gt;&amp;gt;Redis: &amp;#34;设置 user:123 数据 (TTL: 60s)&amp;#34;
activate Redis
Redis--&amp;gt;&amp;gt;Web: &amp;#34;OK&amp;#34;
deactivate Redis
Web--&amp;gt;&amp;gt;Client: &amp;#34;200 OK (标准响应)&amp;#34;
end
deactivate Web
&lt;/pre>
&lt;h3 id="处理延迟-latency-的数学模型">处理延迟 (Latency) 的数学模型
&lt;/h3>&lt;p>在上述系统中，我们对平均请求处理时间 $T_{\text{total}}$ 进行数学建模。
将各处理步骤的延迟定义如下：&lt;/p>
&lt;ul>
&lt;li>$T_{\text{net}}$: 客户端与 Web 容器之间的网络延迟&lt;/li>
&lt;li>$T_{\text{app}}$: 应用程序端纯粹的处理时间（如序列化等）&lt;/li>
&lt;li>$T_{\text{cache}}$: 在 Redis 中读取和写入所花费的时间&lt;/li>
&lt;li>$T_{\text{db}}$: 执行 PostgreSQL 查询所花费的时间&lt;/li>
&lt;li>$p_{\text{miss}}$: 缓存未命中率（$0 \le p_{\text{miss}} \le 1$）&lt;/li>
&lt;/ul>
&lt;p>此时，平均响应时间可以通过以下期望值计算公式表示：&lt;/p>
$$ T_{\text{total}} = T_{\text{net}} + T_{\text{app}} + T_{\text{cache}} + p_{\text{miss}} \times (T_{\text{db}} + T_{\text{cache\_write}}) $$&lt;p>在本地开发环境（Docker 内部）中，$T_{\text{net}}$ 几乎接近于 0。但是，值得注意的是&lt;strong>绑定挂载时的 I/O 性能&lt;/strong>。特别是在 Windows/macOS 上使用 Docker Desktop 时，由于宿主机操作系统和 VM（容器）之间的文件共享开销，$T_{\text{app}}$（代码加载时间等）容易变得过高。为了消除这个性能瓶颈，强烈建议利用前面提到的 DevContainers 将整个源代码放在命名卷内，或者采用在 WSL2（Windows Subsystem for Linux 2）原生环境中运行 Docker 引擎的架构。&lt;/p>
&lt;hr>
&lt;h2 id="8-docker-构建的性能优化层缓存策略">8. Docker 构建的性能优化：层缓存策略
&lt;/h2>&lt;p>在编写 Dockerfile 时，是否理解“层缓存（Layer Cache）”机制将大幅改变构建时间。
Docker 会在遇到 Dockerfile 的每个指令（如 &lt;code>FROM&lt;/code>, &lt;code>RUN&lt;/code>, &lt;code>COPY&lt;/code> 等）时创建文件系统差异（层），并将其作为缓存保留下来。在重新构建时，会重用未发生变更的层的缓存。&lt;/p>
&lt;p>一个重要的原则是：&lt;strong>“按更改频率从低到高的顺序编写”&lt;/strong>。&lt;/p>
&lt;p>考虑对代码更改影响构建时间的建模。设总构建时间为 $T_{\text{build}}$，每一步的执行时间为 $T_{\text{layer}_i}$，是否命中缓存为布尔值 $c_i \in \{0, 1\}$（命中缓存时为 1）。&lt;/p>
$$ T_{\text{build}} = T_{\text{init}} + \sum_{i=1}^{n} (1 - c_i) \times T_{\text{layer}_i} $$&lt;p>一旦在第 $k$ 层发生缓存未命中（$c_k = 0$），则其后所有层 $j > k$ 的缓存都将失效（$c_j = 0$）。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 反面教材 (先复制了源代码)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>在上面的例子中，仅仅修改了一行代码，最初的 &lt;code>COPY&lt;/code> 就会导致缓存未命中，进而使得极其耗时的 &lt;code>RUN poetry install&lt;/code> 每次都会被执行。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 好的示例 (先解析并安装依赖项)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>这样编写的话，即使更改了源代码，&lt;code>poetry install&lt;/code> 这一层的缓存 ($c_i = 1$) 依然有效，构建时间就能从几分钟大幅缩短到几秒钟。&lt;/p>
&lt;hr>
&lt;h2 id="9-故障排除与技巧">9. 故障排除与技巧
&lt;/h2>&lt;p>以下列出在运行本地环境时常遇到的问题及解决方案。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>端口冲突错误&lt;/strong>
如果出现类似 &lt;code>Bind for 0.0.0.0:8000 failed: port is already allocated&lt;/code> 的错误，说明本地机器上有其他进程正在使用该端口。可以修改宿主机一侧的端口号，如 &lt;code>ports: - &amp;quot;8080:8000&amp;quot;&lt;/code>，即可解决。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>磁盘空间耗尽&lt;/strong>
长期使用 Docker 时，可能会累积未使用的镜像或卷（Dangling Images / Volumes），从而占用几十 GB 的磁盘空间。建议定期运行以下命令清理系统：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker system prune -a --volumes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>文件权限问题&lt;/strong>
在 Linux 环境下使用绑定挂载时，如果在容器内创建的文件，所有者可能会变成 &lt;code>root&lt;/code>，导致在宿主机端无法编辑。可以通过在 Dockerfile 中创建一个非特权用户，并使其 UID/GID 与宿主机操作系统上自己的 UID/GID（例如：1000:1000）一致来解决该问题。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="10-结语可重现性带来的开发速度提升">10. 结语：可重现性带来的开发速度提升
&lt;/h2>&lt;p>通过结合 Docker、Docker Compose 和 VSCode DevContainers，您可以打造出一个“无论谁启动环境，状态都能完全一致”的稳固的本地开发环境。&lt;/p>
&lt;p>将 IaC 的范式引入本地环境，不仅仅是缩短了初始设置时间。它消除了在更改基础架构配置时的不安，简化了新技术栈的实验，并顺利过渡到 CI/CD 管道，这极大地提升了整个开发周期的速度和质量。&lt;/p>
&lt;p>请务必运用本文讲解的最佳实践——如通过多阶段构建优化镜像大小、使用运行状况检查控制依赖关系、以及编写意识到层缓存的 Dockerfile，为您的项目带来极致的开发体验（DX: Developer Experience）。&lt;/p></description></item></channel></rss>