qinghuan
← 工程实践 / 全部笔记

单元 100 / 工程实践

P0 · 必会主线L2 · 进阶

Docker 交付入门:镜像、网络与数据卷分别负责什么

把已测试的 Java 应用做成镜像,再分清容器网络、端口和数据卷,确保部署的是同一份内容。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能构建一个 Java 服务镜像,并解释服务连接数据库及容器重建后的数据去向。

学习理由、准备要求与知识点

为什么学它

可复现的运行环境是交付基础,但镜像、容器、网络和数据卷各自承担不同职责。

先读这些内容

知识点清单

  • 镜像与容器
  • Dockerfile 层
  • Java 17 运行时
  • 非 root 用户
  • 容器 DNS
  • 端口发布
  • 持久卷
  • 镜像摘要与秘密管理

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

Docker 里先分清三样东西:镜像是可重复使用的运行文件,容器是按镜像启动的实例,数据卷让数据不只依赖容器自己的可写层。先按项目要求构建并测试 JAR,再制作只含必要运行文件的镜像。不要把本机所有目录、缓存、凭据和测试数据库不加选择地放入构建范围。

镜像标签以后可以指向新内容,摘要则用于识别确定的一份内容。需要重复部署同一版本时,记录提交、依赖和镜像摘要,再把同一验证结果推广到后续环境,并安排基础镜像更新。使用非 root 用户能减少默认权限,但写目录和挂载权限仍要正确配置,不能只加一条 USER 指令就认定全部设置已完成。

在普通 Compose 网络中,容器里的 localhost 通常指它自己的网络环境,不能用它代替另一个数据库容器的地址。同一网络的服务一般通过服务名和容器内部端口连接;从宿主机访问时,则使用发布到宿主机的端口。共享宿主网络等特殊模式有不同规则。EXPOSE 只是说明镜像打算使用哪些端口,不会自动开放端口。数据卷能让数据不随容器一起消失,也不等于已经做好备份,恢复还要另行验证。

文中这几个词,先认一认

镜像摘要
识别确定镜像内容的标识,用来确认部署的究竟是哪一份镜像。
挂载点
把外部存储接到容器中某个路径;接上后可能遮住镜像在该路径原有的文件。
容器可写层
某个容器实例自己产生的文件改动,其存活规则不同于独立数据卷。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

本题用路径组件前缀树解释嵌套挂载选择。假设/app挂载卷A,/app/data挂载卷B,访问/app/data/file应选择更深的卷B;访问/application不能因为字符串以/app开头而选卷A。路径匹配必须按组件边界,而非普通字符串前缀。

沿着路径一级级查找时,记下最近遇到的挂载位置。比如先遇到 /app 的卷 A,再遇到 /app/data 的卷 B,后续访问这个 data 目录里的文件就选择 B,因为它匹配得更具体。没有遇到挂载位置时返回 IMAGE,表示模型中的镜像或容器可写层。这个算法只解释怎样选择路径,不读取磁盘,也不模拟 Linux 真正的挂载、符号链接、叠加文件系统或不同卷驱动。

输入只接受规范绝对POSIX路径,拒绝空组件、点和点点,避免在教学模型中混入路径归一化与符号链接语义。真实挂载配置要结合宿主平台、容器内权限和传播选项阅读文档;Java算法解释路由选择,不声称Docker内部由Java实现。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart TD
  B["测试通过的JAR"] --> I["确定镜像制品"]
  I --> C["非root容器进程"]
  N["服务网络与端口"] --> C
  V["独立数据卷"] --> C
  V --> R["备份与恢复验证"]

按顺序看,每一步做了什么

  1. 锁定JDK和构建工具,生成已验证JAR。
  2. 减少构建上下文,选择并记录基础镜像版本或摘要。
  3. 明确容器用户、写目录、日志和启动信号。
  4. 按服务网络配置地址与端口,单独定义持久数据卷。
  5. 验证关键请求、重建行为和独立备份恢复。

03对照实现代码

OpenJDK 17 Path 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · Path.startsWith / relativize

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,不代表数据库或容器内核实现。完整程序见算法模块。

核心机制代码 · java
Path mount = fileSystem.getPath("/app/data");
Path target = fileSystem.getPath("/app/data/item.json"); // fileSystem必须对应所解释的平台路径语义。
if (target.startsWith(mount)) { // 按路径组件判断,不用普通字符串前缀。
    Path relative = mount.relativize(target); // 得到挂载内部相对位置。
    inspectLogicalMapping(relative); // 只解释路径映射,未实际访问或移动文件。
}

Path接口由文件系统提供者定义路径语义,startsWith按路径元素比较,不能在Windows默认文件系统中直接假设所有Linux路径行为完全一致。

relativize 计算从一个路径到另一个路径的相对写法,前提是路径类型和文件系统关系符合要求。它不会确认文件存在,也不解析符号链接,所以算出相对路径不等于访问目标已经通过安全检查。

片段使用显式fileSystem说明平台前提;完整算法独立解析有限POSIX语法,不依赖本机磁盘路径,这样在Java17不同宿主上仍能验证相同样例。

Docker挂载与存储由容器运行时和内核实现,Java模型只说明最长组件前缀如何决定逻辑映射。备份、写权限和卷持久性必须使用真实环境证据验证。

代码先调用谁,再调用谁

  1. 确定路径所属文件系统
  2. 按组件检查挂载前缀
  3. 计算挂载内部相对位置
  4. 核对目标卷及权限
  5. 另行验证真实读写与恢复

04动手写一个例子

嵌套挂载路径的最长组件前缀

输入规范POSIX挂载路径到卷名映射,查询路径返回最深匹配卷;无匹配返回IMAGE。禁止重复挂载点与点点路径。纯内存Java17自测。

输入 1
/app=A,/app/data=B,查询/app/data/x
输出 1
B
输入 2
仅/app=A,查询/application/x
输出 2
IMAGE

每个路径组件成为前缀树的一条边,节点可保存挂载卷名。不同路径共享公共组件,因此无需对每次查询扫描所有挂载字符串。

查询逐组件向下走,沿途保存最后一个非空卷名。更深的挂载覆盖父路径的逻辑选择,未命中的后续组件不会改变已经找到的最深挂载。

根路径可以挂载,空挂载列表也合法。重复目标拒绝而不是默默覆盖配置,卷名只作为标识,不参与真实路径拼接或文件操作。

为什么这样写是对的遍历到第i个组件后,best是前i个组件路径上的最深挂载;树中每个完整挂载路径只有一个卷标记。

时间 建树O(C),查询O(Q)预期时间额外空间 O(C)索引空间

C为所有挂载路径总字符数,Q为查询字符数;HashMap子节点查找平均常数,分割与校验扫描字符,查询还产生O(Q)临时组件。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static class Node {Map<String,Node> children=new HashMap<>();String volume;}
    static String[] parts(String path){
        if(path==null||!path.startsWith("/")||path.indexOf(92)>=0)throw new IllegalArgumentException("path");
        if(path.equals("/"))return new String[0];String[] p=path.substring(1).split("/",-1);
        for(String s:p)if(s.isEmpty()||s.equals(".")||s.equals("..")||s.indexOf(0)>=0)throw new IllegalArgumentException("component");return p; // 有限规范语法,不模拟符号链接。
    }
    static Node mounts(String[][] mappings){
        Node root=new Node();for(String[] pair:mappings){if(pair.length!=2||pair[1]==null||pair[1].isBlank())throw new IllegalArgumentException("mapping");Node n=root;
            for(String p:parts(pair[0]))n=n.children.computeIfAbsent(p,k->new Node());
            if(n.volume!=null)throw new IllegalArgumentException("duplicate");n.volume=pair[1]; // 同一挂载目标只接受一个定义。
        }return root;
    }
    static String resolve(Node root,String path){
        String[] p=parts(path);Node n=root;String best=n.volume==null?"IMAGE":n.volume;
        for(String part:p){n=n.children.get(part);if(n==null)break;if(n.volume!=null)best=n.volume;} // 记住最后一个完整组件前缀。
        return best;
    }
    static void eq(String e,String a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        Node n=mounts(new String[][]{{"/app","A"},{"/app/data","B"}});
        eq("B",resolve(n,"/app/data/x"));eq("A",resolve(n,"/app/log"));eq("IMAGE",resolve(n,"/application/x"));
        eq("B",resolve(n,"/app/data"));eq("IMAGE",resolve(mounts(new String[][]{}),"/"));
        eq("ROOT",resolve(mounts(new String[][]{{"/","ROOT"}}),"/any"));
        try{resolve(n,"/app/../secret");throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{mounts(new String[][]{{"/a","A"},{"/a","B"}});throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 8 tests");
    }
}

这些特殊情况也要试一试

  • 路径组件边界不能用字符串前缀代替。
  • 模型拒绝非规范路径,不解析符号链接。
  • 挂载遮蔽文件不意味着镜像原文件被删除。
进阶选读 · 后续再读:版本化挂载配置与多副本

场景服务滚动升级时旧新实例使用不同卷和目录布局。

做法为配置建立不可变版本,在启动前检查路径冲突和读写权限;需要共享数据时明确并发访问语义、锁与备份一致性。

复杂度前缀树查询仍按路径长度,但真实存储性能取决于卷驱动、文件系统和并发模式,不能由模型复杂度推断。

好处与代价共享卷简化部分数据访问却扩大故障影响;对象存储或数据库更适合许多跨副本共享状态。

05项目里怎么用

Dockerfile优先使用清楚的构建阶段与最小运行环境,应用以非root用户运行,JAR与配置采用明确路径。启动采用可正确传递信号的形式,日志输出到标准输出或约定采集路径,敏感配置在运行时通过受控机制提供,避免烘焙进镜像层。

网络检查从调用者位置出发:容器访问数据库通常使用服务名和数据库容器端口,宿主访问应用使用发布端口。不要把宿主端口误填进内部连接串,也不要认为EXPOSE自动完成映射;就绪检查需覆盖依赖和实际请求路径。

数据卷与备份是两套工作,删容器、删卷和恢复数据库也不是同一个动作。备份要考虑数据是否在一致状态、保留多久和恢复多久,并实际在隔离环境恢复核对。挂载到已有内容的目录会把原内容遮住;首次创建与使用已有卷的行为,应按对应 Docker 文档确认。

容易踩的坑

  • 容器内localhost被写成另一个服务地址。
  • EXPOSE被误当作发布主机端口。
  • 卷存在就宣称数据已经备份。
  • 挂载覆盖目录后误以为镜像文件消失。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
固定摘要制品便于复现和追踪需主动维护更新受控发布推广
命名数据卷独立容器生命周期仍需备份和权限管理数据库持久数据
无状态应用容器扩缩与替换更直接状态需外部服务承载多数后端API服务

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01镜像与容器区别是什么?

展开参考答案与推导

镜像是可复制内容与运行配置,容器是具体进程实例及其运行状态。

02挂载为何遮住镜像文件?

展开参考答案与推导

该路径映射到外部存储后访问看到挂载内容,原镜像层文件并未因此被删除。

03卷与备份为何不同?

展开参考答案与推导

卷保存当前数据,备份保留可独立恢复的历史副本,需要恢复验证。

换个情况,还会处理吗

01本机能连接数据库,容器不能,先查什么?

展开参考答案与推导

检查容器所用主机名、网络、容器端口与凭据,不把本机localhost配置直接搬入。

02更换镜像后配置仍旧,可能原因?

展开参考答案与推导

旧卷或绑定挂载遮住了镜像中的新文件,应检查挂载映射与初始化策略。

03应用收到停止信号仍有丢请求,检查什么?

展开参考答案与推导

检查入口流量摘除、信号传递、优雅停机时限和后台任务收尾,不能只看进程退出码。

写代码试一试

01为什么/app不匹配/application?

输入 / 条件
两个不同首组件
输出 / 预期
IMAGE

提示 按组件匹配。

展开参考答案与推导

树边分别为app与application,字符串公共前缀没有路径包含含义。

02嵌套挂载选谁?

输入 / 条件
/app与/app/data
输出 / 预期
更深/app/data

提示 最后匹配标记。

展开参考答案与推导

遍历到data后更新best,后续文件名不改变已选卷。

03为什么不自动处理点点?

输入 / 条件
/app/../x
输出 / 预期
拒绝

提示 限定模型范围。

展开参考答案与推导

真实路径还涉及符号链接与平台语义,本题采用规范路径避免虚假安全结论。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能构建一个 Java 服务镜像,并解释服务连接数据库及容器重建后的数据去向。

本篇重点加练

数据卷决定运行状态的归属,摘要与秘密管理决定制品是否可追溯且可安全分发。

  • 持久卷
  • 镜像摘要与秘密管理

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    同一镜像启动两个容器,其中一个修改了运行时文件。画出镜像层和容器写入层关系,说明重新创建容器时需要核实的数据来源。

    提交内容
    层级示意与文件清单
    怎样算完成
    • 区分镜像内容与运行数据
    • 不把容器删除等同于镜像删除

    练到的知识点镜像与容器 · Dockerfile 层

  2. 作业 02设计评审

    Java 17 应用以非 root 用户运行,需要读取配置并写临时文件。评审基础镜像、工作目录及文件权限的运行契约。

    提交内容
    Dockerfile 评审单
    怎样算完成
    • 注明运行时版本
    • 只给必要目录写权限

    练到的知识点Java 17 运行时 · 非 root 用户

  3. 作业 03代码推演

    镜像内含默认配置,运行时在其父目录挂载空卷。分析应用实际读取路径的来源,列出判断默认配置是否可见的观测。

    提交内容
    镜像与挂载路径图
    怎样算完成
    • 区分宿主、镜像和卷中的文件
    • 不凭镜像构建成功推断运行文件存在

    练到的知识点持久卷

  4. 作业 04原理问答

    Dockerfile 某层复制了虚构秘密,后续层又删除该文件。评审仅检查最终运行容器文件列表能够证明什么。

    提交内容
    制品内容审查范围
    怎样算完成
    • 只使用占位秘密
    • 区分运行视图与分发材料

    练到的知识点镜像摘要与秘密管理

综合应用

  1. 作业 05排错实验

    用两个本地容器提供应用与数据库桩,分别从宿主机和应用容器发请求,记录主机名、端口及解析结果。

    提交内容
    网络图与命令记录
    怎样算完成
    • 区分容器端口和宿主发布端口
    • 仅发布回环实验端口

    练到的知识点容器 DNS · 端口发布

  2. 作业 06代码推演

    部署配置挂载数据卷,镜像用标签选择,密码误写进构建参数。逐项审查重建、追溯与秘密暴露的边界,不使用真实秘密。

    提交内容
    交付清单与风险说明
    怎样算完成
    • 区分持久化与备份
    • 记录标签和摘要用途

    练到的知识点持久卷 · 镜像摘要与秘密管理

  3. 作业 07编程实践

    为本地 Java 小程序构建镜像,运行后写入测试卷,重建容器再核对数据;输出版本、用户、端口和制品摘要。

    提交内容
    Dockerfile、运行命令与核对报告
    怎样算完成
    • 所有资源明确命名并可清理
    • 不把保留卷等同于完成恢复演练

    练到的知识点镜像与容器 · Dockerfile 层 · Java 17 运行时 · 非 root 用户 · 容器 DNS · 端口发布 · 持久卷 · 镜像摘要与秘密管理

  4. 作业 08设计评审

    同一个镜像摘要在两台机器运行却表现不同,二者挂载内容和启动环境变量不同。制定可复现实验所需的完整运行声明。

    提交内容
    运行身份与数据清单
    怎样算完成
    • 制品、配置和持久状态分别记录
    • 敏感值只记录安全引用

    练到的知识点持久卷 · 镜像摘要与秘密管理

  5. 作业 09编程实践

    在本地构建两个包含虚构配置的微型镜像,分别使用空卷与预置卷运行,输出文件来源和镜像摘要;检查制品材料中的占位秘密痕迹。

    提交内容
    Dockerfile、运行记录与审查报告
    怎样算完成
    • 不使用真实凭据
    • 容器与卷限定独立实验名称

    练到的知识点持久卷 · 镜像摘要与秘密管理

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    新镜像需要迁移数据目录,旧镜像仍可能回滚。制定挂载路径与权限变更的兼容检查,说明镜像回退无法自动恢复哪些状态。

    提交内容
    迁移及回退评审
    怎样算完成
    • 包含文件属主与格式版本
    • 验收覆盖真实持久数据样本

    练到的知识点Dockerfile 层 · Java 17 运行时 · 非 root 用户 · 端口发布 · 持久卷 · 镜像摘要与秘密管理

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    构造应用镜像完全相同、制品摘要校验也通过,但恢复后业务行为不同的案例,明确差异来自哪里。

    提交内容
    可复现反例与缺失证据
    怎样算完成
    • 不得修改镜像内容
    • 包含卷或配置的版本信息

    练到的知识点持久卷 · 镜像摘要与秘密管理

  2. 作业 12思维拓展 · 条件变化

    迁移到离线交付环境,镜像可搬运但密钥不得随镜像复制,数据卷又需从旧机器恢复。重新定义交付包与验收清单。

    提交内容
    离线交付契约
    怎样算完成
    • 区分可分发制品与运行秘密
    • 包含数据兼容及来源校验

    练到的知识点持久卷 · 镜像摘要与秘密管理

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗