读完这篇,你能做什么
能构建一个 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["备份与恢复验证"]按顺序看,每一步做了什么
- 锁定JDK和构建工具,生成已验证JAR。
- 减少构建上下文,选择并记录基础镜像版本或摘要。
- 明确容器用户、写目录、日志和启动信号。
- 按服务网络配置地址与端口,单独定义持久数据卷。
- 验证关键请求、重建行为和独立备份恢复。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · Path.startsWith / relativize
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,不代表数据库或容器内核实现。完整程序见算法模块。
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模型只说明最长组件前缀如何决定逻辑映射。备份、写权限和卷持久性必须使用真实环境证据验证。
代码先调用谁,再调用谁
- 确定路径所属文件系统
- 按组件检查挂载前缀
- 计算挂载内部相对位置
- 核对目标卷及权限
- 另行验证真实读写与恢复
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个组件路径上的最深挂载;树中每个完整挂载路径只有一个卷标记。
C为所有挂载路径总字符数,Q为查询字符数;HashMap子节点查找平均常数,分割与校验扫描字符,查询还产生O(Q)临时组件。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
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 服务镜像,并解释服务连接数据库及容器重建后的数据去向。
本篇重点加练
数据卷决定运行状态的归属,摘要与秘密管理决定制品是否可追溯且可安全分发。
- 持久卷
- 镜像摘要与秘密管理
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
同一镜像启动两个容器,其中一个修改了运行时文件。画出镜像层和容器写入层关系,说明重新创建容器时需要核实的数据来源。
- 提交内容
- 层级示意与文件清单
- 怎样算完成
- 区分镜像内容与运行数据
- 不把容器删除等同于镜像删除
练到的知识点镜像与容器 · Dockerfile 层
- 作业 02设计评审
Java 17 应用以非 root 用户运行,需要读取配置并写临时文件。评审基础镜像、工作目录及文件权限的运行契约。
- 提交内容
- Dockerfile 评审单
- 怎样算完成
- 注明运行时版本
- 只给必要目录写权限
练到的知识点Java 17 运行时 · 非 root 用户
- 作业 03代码推演
镜像内含默认配置,运行时在其父目录挂载空卷。分析应用实际读取路径的来源,列出判断默认配置是否可见的观测。
- 提交内容
- 镜像与挂载路径图
- 怎样算完成
- 区分宿主、镜像和卷中的文件
- 不凭镜像构建成功推断运行文件存在
练到的知识点持久卷
- 作业 04原理问答
Dockerfile 某层复制了虚构秘密,后续层又删除该文件。评审仅检查最终运行容器文件列表能够证明什么。
- 提交内容
- 制品内容审查范围
- 怎样算完成
- 只使用占位秘密
- 区分运行视图与分发材料
练到的知识点镜像摘要与秘密管理
综合应用
- 作业 05排错实验
用两个本地容器提供应用与数据库桩,分别从宿主机和应用容器发请求,记录主机名、端口及解析结果。
- 提交内容
- 网络图与命令记录
- 怎样算完成
- 区分容器端口和宿主发布端口
- 仅发布回环实验端口
练到的知识点容器 DNS · 端口发布
- 作业 06代码推演
部署配置挂载数据卷,镜像用标签选择,密码误写进构建参数。逐项审查重建、追溯与秘密暴露的边界,不使用真实秘密。
- 提交内容
- 交付清单与风险说明
- 怎样算完成
- 区分持久化与备份
- 记录标签和摘要用途
练到的知识点持久卷 · 镜像摘要与秘密管理
- 作业 07编程实践
为本地 Java 小程序构建镜像,运行后写入测试卷,重建容器再核对数据;输出版本、用户、端口和制品摘要。
- 提交内容
- Dockerfile、运行命令与核对报告
- 怎样算完成
- 所有资源明确命名并可清理
- 不把保留卷等同于完成恢复演练
练到的知识点镜像与容器 · Dockerfile 层 · Java 17 运行时 · 非 root 用户 · 容器 DNS · 端口发布 · 持久卷 · 镜像摘要与秘密管理
- 作业 08设计评审
同一个镜像摘要在两台机器运行却表现不同,二者挂载内容和启动环境变量不同。制定可复现实验所需的完整运行声明。
- 提交内容
- 运行身份与数据清单
- 怎样算完成
- 制品、配置和持久状态分别记录
- 敏感值只记录安全引用
练到的知识点持久卷 · 镜像摘要与秘密管理
- 作业 09编程实践
在本地构建两个包含虚构配置的微型镜像,分别使用空卷与预置卷运行,输出文件来源和镜像摘要;检查制品材料中的占位秘密痕迹。
- 提交内容
- Dockerfile、运行记录与审查报告
- 怎样算完成
- 不使用真实凭据
- 容器与卷限定独立实验名称
练到的知识点持久卷 · 镜像摘要与秘密管理
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
新镜像需要迁移数据目录,旧镜像仍可能回滚。制定挂载路径与权限变更的兼容检查,说明镜像回退无法自动恢复哪些状态。
- 提交内容
- 迁移及回退评审
- 怎样算完成
- 包含文件属主与格式版本
- 验收覆盖真实持久数据样本
练到的知识点Dockerfile 层 · Java 17 运行时 · 非 root 用户 · 端口发布 · 持久卷 · 镜像摘要与秘密管理
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
构造应用镜像完全相同、制品摘要校验也通过,但恢复后业务行为不同的案例,明确差异来自哪里。
- 提交内容
- 可复现反例与缺失证据
- 怎样算完成
- 不得修改镜像内容
- 包含卷或配置的版本信息
练到的知识点持久卷 · 镜像摘要与秘密管理
- 作业 12思维拓展 · 条件变化
迁移到离线交付环境,镜像可搬运但密钥不得随镜像复制,数据卷又需从旧机器恢复。重新定义交付包与验收清单。
- 提交内容
- 离线交付契约
- 怎样算完成
- 区分可分发制品与运行秘密
- 包含数据兼容及来源校验
练到的知识点持久卷 · 镜像摘要与秘密管理
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读实践指南英文
Docker:Build best practices (新标签页)↗
阅读小型可信基础镜像、非 root 用户与构建缓存,编写可重复的应用镜像。
- 实践教程英文
Docker Compose:Application model (新标签页)↗
阅读 services、networks 与 volumes,区分容器网络和宿主机访问入口。
- 深入参考文档英文
Docker:Volumes (新标签页)↗
阅读数据卷生命周期和挂载行为,为数据库备份与重建容器保留数据。
- 实践实践指南英文
Eclipse Adoptium:Container Images (新标签页)↗
核对 Temurin 官方容器镜像与运行时版本,选择并记录实际构建使用的基础镜像。
- 实践实践指南中文
容器运行之后:学习部署、扩容和更新应用 (新标签页)↗
先做创建集群、部署应用和查看实例,再选扩容。小练习:在本地练习集群把一个实例改成两个,观察状态并记录它们怎样被同一服务访问;完成后按教程清理练习资源。 文档公开;需要自行准备 Minikube 等学习环境,不保证在线沙箱可用。部分示例限定处理器架构或使用 POSIX shell,Windows 学习者需核对环境。
- 深入参考文档英文
OpenJDK 17 Path 源码 (新标签页)↗
按指定版本定位
Path.startsWith/ relativize;对照源码分析理解实际调用边界。 - 深入参考文档英文
Docker Compose 网络 (新标签页)↗
阅读服务名发现、容器端口与主机发布端口的区别,对照连接串配置。