想象你在云上开了一家蛋糕店。最早的时候,每来一位客人,你都要从和面开始,现烤一个完整蛋糕——又慢又累,每个蛋糕的尺寸和味道还不稳定。后来你学聪明了:把蛋糕胚分层做好,统一冷冻在冷柜里,这就是镜像。客人下单时,从冷柜取一份胚子,解冻铺上客人要的水果,几分钟就端上桌,这就是容器。客人吃完收走盘子,冷柜里的胚子还原封不动,下一单再取一份。Docker 的整个设计哲学,就是“冷冻蛋糕胚”和“现场解冻上桌”这套组合拳。读完这篇文章,你会彻底搞懂镜像的分层结构、容器的生命周期、Dockerfile 的写法,以及如何把镜像做得又小又安全。

岗位关联

Docker 是云原生时代的“集装箱”,AI 模型的依赖环境、推理服务、微服务部署都要靠它。搞懂镜像与容器,是你从“本地跑通代码”迈向“规模化部署”的必经之路。

目录
  1. 可视化演示
  2. 镜像分层原理
  3. Dockerfile编写
  4. 容器生命周期
  5. 常用命令实战
  6. 构建优化秘诀
  7. 安全加固实践
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Docker镜像分层存储架构、容器生命周期状态流转、以及多阶段构建优化与数据卷持久化。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+captions.layers+'

镜像分层原理

蛋糕店中央厨房里,一块冷冻芒果慕斯胚并不是一坨面团压出来的,而是一层一层叠起来的:最底下是戚风蛋糕底,抹一层奶油,铺一层芒果粒,最后淋上镜面果胶。每一层都在不同工位批量做好,再按固定顺序叠放。这样做最大的好处是:不同款式的蛋糕可以共用同一款戚风底,中央厨房不用为每款蛋糕重新烤底。

Docker 镜像的结构和这块蛋糕胚一模一样。一个镜像并不是磁盘上的一个大文件,而是由一层层只读文件系统叠加而成。Dockerfile 里的每一条指令,都会在原有层之上生成一个新的层。ubuntu:22.04 是最底层的蛋糕底,RUN apt-get install python3 是抹上的奶油层,COPY app.py 是铺上的芒果粒。每一层构建完成后就不可变,只能在其之上加新层,不能回头修改旧层。

这种分层设计带来了两个巨大好处。第一是层复用:十几个不同镜像都用 ubuntu 做底时,宿主机只需要存一份 ubuntu 层,剩下的各自存各自的增量层,磁盘占用急剧下降。第二是容器轻量:容器启动时,Docker 并不会复制整个镜像,而是在镜像层顶上叠加一个可写层,容器内所有的写操作都发生在这一层——就像客人吃蛋糕时,只是在蛋糕胚上临时加了颗草莓,胚子本身没有动。

回到蛋糕店:冷柜里一摞摞蛋糕胚,不同款式共用同一款戚风底,谁需要谁取。客人吃的“胚子加临时加料”,加料只在最顶部那一层,底下的胚子永远不会被碰坏。理解了镜像分层的“冷冻胚子思维”,后面所有容器操作都顺理成章了。

镜像=L1∪L2∪⋯∪Ln(Li 为只读层)\displaystyle \text{镜像} = L_1 \cup L_2 \cup \cdots \cup L_n \quad (L_i \text{ 为只读层})
这个公式说明镜像就是一堆只读层用“叠加”的方式拼在一起,每层对应 Dockerfile 里的一条指令。
  1. 镜像符号代表最终构建完成的只读镜像,类比冷冻柜里一份完整的蛋糕胚,谁取走解冻都一样。
    镜像\text{镜像}
  2. 并集运算表示各层按照先后顺序叠放,后加的层覆盖先加的层,文件冲突时上层优先。
    ∪\cup
  3. L₁ 到 Lₙ每一层对应 Dockerfile 里一条指令的产物,n 是总层数。
    L1,L2,⋯ ,LnL_1, L_2, \cdots, L_n
  4. 只读注释等式右侧的说明,强调每一层构建后都不可修改,保证层可以被任意容器共享。
    Li 为只读层L_i \text{ 为只读层}

Dockerfile编写

蛋糕店的招牌背后,一定有一本写满配方的菜谱:选什么胚子、加多少奶油、烤多久、如何摆盘。按照菜谱操作,一百次做出来味道都一样。店员换了一茬又一茬,蛋糕品质却始终稳定——菜谱就是这家店的“不可变资产”。

Dockerfile 就是镜像的菜谱。它用文本形式逐行声明“如何从一个基础镜像构建出最终镜像”。最常见的四个指令是:FROM 指定基础镜像,相当于“选胚子”;RUN 执行命令安装依赖、创建目录,相当于“加料”;COPY 把本地的代码和配置文件拷进镜像,相当于“把准备好的配料摆上去”;CMD 规定容器启动时执行什么进程,相当于“上桌流程”。每一条指令执行完,都会在文件系统上产生增量,这个增量被打包成一个新层。

把 Dockerfile 写好,镜像的构建就变成了一条自动化流水线。你不需要记住每一步手动操作,只需要维护这份菜谱。而且由于层是增量的,构建时可以逐层比对缓存:只要某层的指令和上下文没变,构建系统就直接复用旧层,跳过执行,这就是后面要讲的构建缓存。菜谱写得越清晰,层的划分越合理,构建和复用就越高效。

菜谱里改一行字,做出来的蛋糕就不一样;Dockerfile 里改一条指令,镜像就多出一层或变了一层。正因如此,Dockerfile 在团队里要像菜谱一样被认真 review 和版本管理——它是可复现部署的根基。

Si+1=Si+Δi\displaystyle S_{i+1} = S_i + \Delta_i
这个公式表示:每执行一条 Dockerfile 指令,文件系统就从旧状态 Sᵢ 变成新状态 Sᵢ₊₁,Δᵢ 就是这条指令带来的全部改动。
  1. Sᵢ 与 Sᵢ₊₁分别代表指令执行前后的文件系统快照,相当于蛋糕胚在第 i 步之前和之后的模样。
    Si,Si+1S_i, S_{i+1}
  2. 增量 Δᵢ当前这条指令对文件系统的所有改动之和,比如 RUN 安装的包、COPY 进来的文件。
    Δi\Delta_i
  3. 整体等式新状态等于旧状态叠加增量,Docker 把这份增量固化成一个新的只读层。
    Si+1=Si+ΔiS_{i+1} = S_i + \Delta_i

容器生命周期

一块蛋糕从客人下单到收盘倒垃圾,有一个完整的一生:下单(订单生成)、后厨开工(制作中)、暂停等客人(暂缓)、端上桌被吃完(结束)、收盘倒掉(清理)。蛋糕店老板必须清楚每一份蛋糕处于哪个阶段,后厨才不会乱成一团。

容器也有这样一套生命周期状态。docker create 基于镜像生成一个容器实例,此时容器处于 created 状态,进程还没启动,就像订单刚生成、后厨还没动手。docker start 启动容器,进程运行起来,进入 running 状态。docker pause 可以冻结容器内所有进程(发 SIGSTOP 信号),对应 paused 状态,类似后厨接到电话让蛋糕先别做。docker stop 优雅停止容器(发 SIGTERM,超时后 SIGKILL),进程退出,容器进入 stopped 状态——注意此时容器的文件系统还在磁盘上,随时可以 start 重新启动。最后 docker rm 删除容器,把可写层彻底清理掉。

理解状态流转对排障至关重要。比如容器启动后立刻退出,多半是启动命令执行失败,要看退出码和日志;比如 docker ps 默认只显示 running 状态的容器,想看所有容器要加 -a 参数。状态之间可以双向或单向迁移,但有几个铁律:只有 stopped 状态的容器才能被安全删除,paused 的容器必须先 unpause 才能停止。

盘子收了、蛋糕没了,但冷柜里的胚子还在——镜像没被删,随时可以几分钟再做一个。容器之所以“轻”,就是因为它只是一层薄薄的可写层,盖在大家共享的只读胚子上,删掉这层,胚子毫发无损。

Created→Running⇄Paused→Stopped→Deleted\displaystyle \text{Created} \rightarrow \text{Running} \rightleftarrows \text{Paused} \rightarrow \text{Stopped} \rightarrow \text{Deleted}
这是容器生命周期的标准状态迁移路径,单向箭头表示不可逆,双向箭头表示可以在运行与暂停之间来回切换。
  1. 初始状态Created 表示容器刚被 create 出来,像一张打印好的订单,还没有任何进程在跑。
    Created\text{Created}
  2. 运行状态Running 表示容器主进程已被启动,进入正常工作状态。
    Running\text{Running}
  3. 暂停与恢复Paused 与 Running 之间可双向切换,pause 冻结进程,unpause 解冻。
    Running⇄Paused\text{Running} \rightleftarrows \text{Paused}
  4. 终止与删除Stopped 是进程退出但文件系统保留,Deleted 才是彻底清理容器可写层。
    Stopped→Deleted\text{Stopped} \rightarrow \text{Deleted}

常用命令实战

后厨的墙上挂着一排趁手的工具:看库存的、进货的、下单的、查订单的、看烤箱状态的、进冷库的、倒垃圾的。一个熟练的店长闭着眼都能摸到对应的工具,蛋糕店才能高效运转。

Docker 的命令体系就是后厨的这套工具墙。镜像管理:docker images 查看本地所有镜像(库存清单),docker pull 从仓库拉取镜像(进货),docker rmi 删除本地镜像(清理过期库存)。容器操作:docker run 是最高频的一条命令,它等价于“如果本地没有镜像就 pull + create + start”一条龙;docker ps 查看运行中的容器(当前订单),加 -a 看全部;docker rm 删除容器。日志与进入:docker logs 查看容器输出(看烤箱状态),docker exec -it <容器> bash 进入容器内部排查问题(进后厨巡一圈)。资源查看:docker top 看容器内进程列表,docker stats 实时监控 CPU、内存、网络和磁盘 IO。

初学者最容易混淆的是 run 和 start:run 是从镜像新建容器并启动,start 是启动一个已存在的 stopped 容器。前者每次都会生成一个新的容器 ID,后者复用同一个容器。所以如果你改动了代码重新构建了镜像,一定要重新 run,而不是 start 旧容器——旧容器的可写层里还是老代码。

把这些命令用熟,你就能像店长管理后厨一样,条理清晰地管理一整台服务器上的几十个容器:知道库存有什么、哪些订单在跑、哪个容器出了问题、问题出在哪一层。

docker run≈pull+create+start\displaystyle \text{docker run} \approx \text{pull} + \text{create} + \text{start}
这个公式揭示了一条高频命令的本质:run 是拉取、创建、启动三步的合成动作,理解它就能理解为什么 run 一个新镜像会“自动下载”。
  1. run 复合命令docker run 是日常使用频率最高的命令,把三步合并成一步。
    docker run\text{docker run}
  2. pull 拉取如果本地没有指定镜像,先从 Docker Hub 或私有仓库把镜像下载到本地。
    pull\text{pull}
  3. create 创建基于镜像的文件系统,创建一个带独立可写层的容器实例。
    create\text{create}
  4. start 启动执行容器内配置好的启动命令,让主进程跑起来。
    start\text{start}

构建优化秘诀

好的蛋糕店有两间厨房:备料厨房里堆着面粉、鸡蛋和搅拌机,又脏又乱;出餐厨房只放烤好的胚子、奶油和水果,干净整洁。客人永远不会走进备料厨房——他们只关心端上桌的那块蛋糕,而不是后厨有多少工具。

镜像构建也遵循同样的哲学,这就是多阶段构建。在一个 Dockerfile 里写多个 FROM 指令,前几个阶段是“备料厨房”:装编译器、跑测试、构建产物;最后一个阶段是“出餐厨房”:从一个干净的基础镜像开始,只把前几个阶段构建出的产物 COPY 过来,所有编译器、临时文件、缓存统统不带走。最终镜像只包含运行必需的东西,体积大幅缩小。

除了多阶段构建,还有三个常见的优化手段。第一是镜像瘦身:尽量使用 alpine 等精简基础镜像、安装依赖后清理 apt 缓存、不要 COPY 多余的本地文件。第二是利用构建缓存:把变化最少的指令写在 Dockerfile 最前面(先 COPY 依赖清单文件再 RUN 安装,而不是先 COPY 全部代码),这样代码改动时依赖层还能命中缓存。第三是 .dockerignore:像 .gitignore 一样,把 node_modules、.git、日志等排除在构建上下文之外,既减小上传体积,又避免不该进镜像的文件混入。

端上桌的蛋糕只需要胚子加点缀,不需要把整个备料厨房一起端上来。多阶段构建让“备料”和“出餐”彻底分离,镜像体积可以从几百 MB 瘦到几十 MB,拉取时间从分钟级降到秒级,部署体验天差地别。

Vfinal=∑i=1mvi(m≪n)\displaystyle V_{\text{final}} = \sum_{i=1}^{m} v_i \quad (m \ll n)
这个公式说明:最终交付的镜像体积,只等于出餐阶段保留的那 m 层体积之和,而 m 远小于构建阶段的总层数 n。
  1. 最终体积V_final 代表最终推到仓库、部署到生产环境的镜像体积。
    VfinalV_{\text{final}}
  2. 求和符号把最终阶段所有层的体积累加起来,得到镜像总体积。
    ∑i=1mvi\sum_{i=1}^{m} v_i
  3. 远小于注释m 远小于 n,表示最终阶段丢弃了所有构建工具和中间产物,只留运行产物。
    m≪nm \ll n

安全加固实践

蛋糕店打烊之后,店长会做三件事:把冷柜锁上(限制访问)、切掉不必要的电源(限制资源)、给员工配门禁卡而不是大门钥匙(最小权限)。这三道防线看起来不起眼,却能挡住绝大多数安全事故。

容器安全加固的思路如出一辙。第一道防线是非 root 运行:容器内默认以 root 用户运行,如果应用被攻破,攻击者就拿到了容器内的最高权限。在 Dockerfile 里用 RUN useradd app 创建普通用户,再 USER app 切换,配合 Kubernetes 的 runAsNonRoot 强制约束,把攻击面压到最小。第二道防线是只读文件系统:用 --read-only 挂载容器根文件系统,应用就无法往容器可写层写入恶意文件,需要写临时文件时单独挂载 tmpfs 或数据卷。第三道防线是资源配额限制:用 --memory 512m --cpus 1 限制容器的内存和 CPU,防止某个失控容器把宿主机资源耗尽,拖垮同主机的其他容器。

此外还有一道防线容易被忽略:镜像漏洞扫描。用 trivy、clair 等工具扫描镜像中的操作系统包和应用依赖,找出已知 CVE 漏洞,在构建阶段就拦截问题镜像。安全不是上线后的补救,而是从 Dockerfile 第一行就开始的约束。

店铺安防做扎实了,才敢放心把蛋糕店开在云上这种人流密集的地方。同理,容器跑在共享内核的宿主机上,安全加固不是可选项而是必选项——每一道防线都在帮你降低“一锅端”的风险。

UID容器进程≠0\displaystyle \text{UID}_{\text{容器进程}} \neq 0
这个不等式是容器安全的第一原则:让容器内进程以非 root 用户运行,即使进程被攻破,也无法获得宿主机的管理员权限。
  1. 容器进程 UID指容器内主进程的用户 ID,Linux 下 0 代表 root。
    UID容器进程\text{UID}_{\text{容器进程}}
  2. 不等于 0要求进程不是 root 用户,从身份上限制其系统权限。
    ≠0\neq 0
  3. 整体含义进程以普通用户运行时,即使攻击者拿到 shell,提权也需要额外绕过系统内核防护。
    UID容器进程≠0\text{UID}_{\text{容器进程}} \neq 0

AI 应用场景

AI 模型推理服务容器化部署

阶段:模型部署与上线

解决的问题:AI 模型通常依赖特定版本的 Python、CUDA、PyTorch 等环境。传统方式在每台服务器手动配环境,版本冲突频发,换一台机器就崩。用 Docker 把模型权重、推理代码和依赖环境打成镜像,任何机器上 docker run 都能复现一致的推理环境。

具体案例:某团队把基于 PyTorch 的文本分类模型打包成镜像,基础镜像选用 pytorch/pytorch:2.0.1-cuda11.7(选胚子),COPY 推理服务代码,CMD 启动 uvicorn。镜像推到私有仓库后,三台 GPU 服务器分别拉取并启动容器,对外提供统一的推理 API,模型版本升级只需重新构建镜像并滚动重启容器。

CI/CD 流水线中的镜像构建与瘦身

阶段:持续集成与持续交付

解决的问题:AI 项目的训练代码和推理服务频繁迭代,每次提交都要重新构建镜像。如果用单阶段构建,每个镜像都带着完整的编译工具链,体积大、构建慢、拉取慢。多阶段构建和层缓存的组合拳,让构建又快、产物又小。

具体案例:某公司在 GitLab CI 中配置了流水线:第一个阶段用完整 Python 镜像安装训练依赖并跑测试,第二个阶段用 python:3.10-slim 作为运行镜像,只 COPY 训练好的模型产物和推理依赖清单,最终镜像从 1.2GB 瘦身到 280MB,流水线总耗时从 20 分钟降到 9 分钟。

团队统一模型训练环境

阶段:模型训练与实验管理

解决的问题:算法工程师各自本地环境不一致,经常出现“我这能跑、你那报错”的经典问题。把训练基础镜像统一封装,保证团队所有人、所有机器都在同一套依赖环境下实验。

具体案例:某算法团队在 Dockerfile 里固定了 CUDA 版本、cuDNN 版本、Python 包依赖,并在镜像里预装常用算法库。新同事入职第一天 docker pull 训练镜像即可开始跑实验,复现论文结果时直接指定镜像 tag,实验可复现性大幅提升。

Python 演示

python60 行
# -*- coding: utf-8 -*-"""模拟Docker分层镜像与容器可写层机制"""import os  class ImageLayer(object):    """只读镜像层: 保存文件路径到字节内容的映射"""    def __init__(self, name, files):        self.name = name        self.files = dict(files)        self.readonly = True     def read(self, path):        return self.files.get(path)  class Container(object):    """容器: 镜像只读层之上叠加一个私有可写层"""    def __init__(self, image_layers):        self.image_layers = image_layers        self.writable = {}  # 可写层: 修改或新增的文件     def read(self, path):        # 先查可写层, 再逐层向下查找镜像层        if path in self.writable:            return self.writable[path]        for layer in self.image_layers:            content = layer.read(path)            if content is not None:                return content        return None     def write(self, path, content):        # 写入可写层, 不改动任何镜像层        self.writable[path] = content  def main():    # 构建一个三层镜像: 基础层 + 依赖层 + 应用层    base = ImageLayer("ubuntu-base", {"/etc/os-release": b"Ubuntu 22.04 LTS"})    deps = ImageLayer("python-deps", {"/usr/bin/python3": b"python3.10",                                      "/usr/bin/pip3": b"pip3"})    app = ImageLayer("app-code", {"/app/main.py": b"print('hello docker')"})    image = [base, deps, app]     # 容器1: 读取镜像, 写入新配置    c1 = Container(image)    c1.write("/app/config.yaml", b"lr=0.01")    print("[容器1] 读到main.py:", c1.read("/app/main.py"))    print("[容器1] 读到config.yaml:", c1.read("/app/config.yaml"))     # 容器2: 与容器1共享同一份镜像, 但看不到容器1的写入    c2 = Container(image)    print("[容器2] 读到config.yaml:", c2.read("/app/config.yaml"))    print("[容器2] 读到os-release:", c2.read("/etc/os-release"))    print("镜像层未被修改:", base.files["/etc/os-release"] == b"Ubuntu 22.04 LTS")  if __name__ == "__main__":    main()
目的:用纯 Python 实现 Docker 分层镜像的核心机制——只读层共享 + 容器私有可写层,演示为什么容器之间互不干扰、镜像可以复用。
  1. class Container(object):每个容器实例拥有独立的可写层,对应 docker create 出来的容器
  2. self.image_layers = image_layers所有容器共享同一份只读镜像层,这就是镜像分层的复用机制
  3. self.writable = {}容器私有的可写层,存放对文件的修改和新增
  4. if path in self.writable:先查可写层,模拟容器内“修改优先看到自己写的”行为
  5. for layer in self.image_layers:按层从上往下查找文件,模拟联合文件系统的合并视图
  6. c1.write("/app/config.yaml", ...)写操作只落在可写层,不触碰任何镜像层
  7. c2.read("/app/config.yaml")容器2读不到容器1的写入,容器间完全隔离
  8. base.files["/etc/os-release"]镜像层内容原封不动,验证镜像的不可变性

常见误区

误区我一直以为容器和虚拟机差不多,都是在一台物理机上跑多个独立的“小机器”,虚拟机更重,容器更轻,仅此而已。

正解两者的底层原理完全不同。虚拟机通过 Hypervisor 模拟硬件,每个虚拟机里都有一个完整的操作系统内核;而容器和宿主机共享同一个内核,只靠 Linux 的 namespace(隔离视图)和 cgroup(限制资源)实现隔离。共享内核决定了容器更轻、启动更快,但也意味着容器逃逸会直接威胁宿主内核,安全边界没有虚拟机那么硬。

为何常见:从使用体验看,容器和虚拟机都能“跑一个独立的 Linux 环境”,用起来很像,容易让人忽略内核层面的本质差异。

误区docker rm 把容器删掉之后,里面的数据就全没了,代码、配置、数据库都得从头来,所以我不敢轻易删容器。

正解删掉容器只删掉了它的可写层,镜像本身还在磁盘上,随时可以基于镜像重新 run 一个全新的容器,代码和镜像里的配置都不会丢。会丢的只是容器运行期间写入可写层的数据(比如运行时生成的临时文件或 SQLite 文件)。如果你有真正需要持久化的数据,应该用 -v 挂载数据卷(volume),把数据写到宿主机目录或卷里,这样即使容器删了,数据也安然无恙。

为何常见:把“容器”和“镜像”当成同一个东西,没理解可写层是临时的、镜像层是永久的这一分层本质。

误区既然层越多镜像越复杂,我就把所有的操作都合并成一条 RUN,用 && 串起来,这样层少肯定更优。

正解层数不是评判 Dockerfile 优劣的唯一标准。过度合并 RUN 确实能减少层数,但会破坏构建缓存的粒度——只要其中一条命令变了,整层缓存全部失效,后续构建大幅变慢。更合理的做法是:把稳定的依赖安装放一层(缓存命中率高)、把易变的代码复制放最后一层。层数少不等于体积小,体积主要取决于每层装进去了什么。清理缓存文件、用精简基础镜像、多阶段构建,才是真正的瘦身手段。

为何常见:初学者容易把“层数”当做一个可以一刀切优化的数字指标,忽略了缓存机制和可维护性这些更重要的考量。

误区我能在容器里 sudo 执行任何命令,说明容器内的 root 就是超级管理员,和宿主机 root 权力一样大。

正解容器内的 root 只是 namespace 内的 root,它的能力被大幅限制:不能随便加载内核模块、不能修改宿主机网络、默认看不到宿主机其他进程。但要注意,容器内 root 依然是 Docker 守护进程的客户端,如果同时挂载了宿主机目录或设置了特权模式,容器 root 就能操作宿主机文件,等于拿到了宿主机 root。所以生产环境强烈建议用 USER 指令以非 root 运行应用,并配合 --read-only、capabilities 裁剪等加固手段,把容器内的权限最小化。

为何常见:在容器里执行 id 看到 uid=0 就下意识认为是真 root,没有意识到 namespace 隔离已经把 root 的实际权力大幅削弱了。

面试题

Q1. Docker 镜像的文件系统由什么组成?

单选题 · easy

  • A.一层层只读文件系统叠加← 正确答案
  • B.一个完整的操作系统内核
  • C.单个可执行二进制文件
  • D.完整虚拟机磁盘快照

答案:A。镜像由多层只读层叠加而成,每层对应 Dockerfile 的一条指令。镜像不含内核,容器共享宿主机内核;也不是单个二进制文件。就像冷冻蛋糕胚是分层叠出来的,而不是一个实心面团。

Q2. Dockerfile 中哪个指令用于指定基础镜像?

单选题 · easy

  • A.RUN
  • B.FROM← 正确答案
  • C.COPY
  • D.CMD

答案:B。FROM 指定基础镜像,相当于做蛋糕时“选胚子”,必须是 Dockerfile 的第一条非注释指令。RUN 是构建时执行命令,COPY 是拷贝文件,CMD 是容器启动时执行的命令,三者都不是选择基础镜像。

Q3. 容器被 docker rm 删除后,对镜像有什么影响?

单选题 · easy

  • A.镜像也会被删除
  • B.镜像保持不变← 正确答案
  • C.镜像中可写层保留
  • D.镜像被标记为过时

答案:B。容器删除只清理容器自己的可写层,镜像本身是只读的、共享的,完全不受影响。就像客人吃完蛋糕收盘子,冷柜里的冷冻蛋糕胚还好好躺着,随时可以再做一份。

Q4. 以下哪些属于容器的生命周期状态?

多选题 · mid

  • A.created← 正确答案
  • B.running← 正确答案
  • C.compiled
  • D.paused← 正确答案

答案:ABD。容器状态包括 created(已创建)、running(运行中)、paused(已暂停)、stopped(已停止)等。compiled 是编程语言里的编译状态,不是容器状态。生命周期状态的流转对应蛋糕从下单、制作、暂停到收盘的各个阶段。

Q5. 多阶段构建的主要目的是什么?

单选题 · mid

  • A.提高容器运行速度
  • B.减小最终镜像体积← 正确答案
  • C.增加镜像安全层
  • D.支持多平台部署

答案:B。多阶段构建用多个 FROM,前面的阶段做编译和依赖安装,最后一个阶段只 COPY 必要产物,丢弃所有构建工具和中间文件,最终镜像体积大幅减小。就像备料厨房和出餐厨房分离,客人只看到干净的成品蛋糕。

Q6. 哪个命令可以查看运行中容器的日志输出?

单选题 · mid

  • A.docker logs← 正确答案
  • B.docker ps
  • C.docker exec
  • D.docker inspect

答案:A。docker logs 查看容器标准输出和标准错误日志,相当于看后厨烤箱的状态。docker ps 查看容器列表,docker exec 进入容器执行命令,docker inspect 查看容器的详细配置信息。

Q7. 关于镜像分层缓存,下列说法正确的是?

多选题 · hard

  • A.只要某层未变化,后续层也能复用缓存
  • B.某层指令变化会导致该层及以后层缓存失效← 正确答案
  • C.COPY 文件内容未变时层缓存可命中← 正确答案
  • D.RUN 安装命令无论网络源是否变化都会缓存

答案:BC。层缓存是“从上到下”逐层校验的,只要某一层失效,它后面的所有层都得重新构建,A 是错的。RUN 执行结果依赖命令和基础层,网络源变化可能导致结果不同,D 太绝对。B 和 C 准确描述了缓存命中与失效的边界,和蛋糕店“底层胚子没变、上层装饰全换”的逻辑一致。

Q8. 容器与虚拟机的本质区别是什么?

单选题 · hard

  • A.容器共享宿主机内核← 正确答案
  • B.容器有独立内核
  • C.容器需要模拟硬件
  • D.容器只能运行 Linux

答案:A。容器通过 namespace 和 cgroup 隔离进程视图和资源,但所有容器共享宿主机内核;虚拟机通过 Hypervisor 虚拟化硬件,每个 VM 有独立内核。共享内核让容器启动更快、更省资源,但也意味着隔离边界不如虚拟机硬。

Q9. 以下哪些是容器安全加固的常见做法?

多选题 · hard

  • A.以非 root 用户运行← 正确答案
  • B.挂载只读文件系统← 正确答案
  • C.限制内存和 CPU 配额← 正确答案
  • D.关闭所有日志输出

答案:ABC。非 root 运行降低提权风险、只读文件系统防止运行期篡改、资源配额避免失控容器拖垮宿主机,都是标准加固手段。关闭日志输出不属于安全加固,反而会损害可观测性,让攻击行为更难被发现。

Q10. 多个容器共享同一镜像时,修改其中一个容器内的文件会发生什么?

单选题 · extreme

  • A.修改写入该容器可写层,其他容器不受影响← 正确答案
  • B.修改直接写入镜像层,所有容器可见
  • C.修改需要重启所有容器才能生效
  • D.修改会被拒绝并产生告警日志

答案:A。这体现了 copy-on-write(写时复制)机制:镜像层只读,容器内任何修改都发生在自己的可写层,其他容器根本看不见。镜像层是共享冷柜里的蛋糕胚,可写层是客人盘子里临时加的那颗草莓——草莓不会回到冷柜里去。