容器技术学到一个阶段,很多人会卡在同一个坎上:命令敲得挺熟,docker run能跑起来,docker ps能看状态,但一旦要自己定制一个镜像、把服务塞进容器里跑通,就立刻抓瞎。这个坎的本质不是命令记不住,而是对"镜像和容器到底是什么关系""分层到底分的是什么""为什么我改了容器里的东西重启就没了"这些底层逻辑没打通。这篇内容就是围绕这个断层展开的,从容器技术的整体定位讲起,把镜像与容器的管理操作串起来,再落到定制简单镜像和容器内装服务这两件最考验理解深度的事情上。适合已经能敲基础命令、但还没真正把容器用顺手的人,也适合刚接触容器、想一次性把概念和操作对齐的读者。下面这些内容是我在实际折腾过程中沉淀下来的路径,不是命令手册的复述,重点在于每一步为什么这么做、不这么做会出什么问题。
1. 容器技术到底解决了什么层面的问题
1.1 从"在我机器上是好的"说起
容器这个词被讲得太多,反而容易让人抓不住它真正的价值点。我习惯用一个最朴素的场景来定位它:同一份代码,在开发机上跑得好好的,换一台机器就报依赖缺失、版本不对、路径找不到。这个问题的根源不在于代码,而在于代码运行所依赖的整个环境没有被一起打包和搬运。
传统做法是写一份长长的部署文档,把要装的依赖、要配的环境变量、要改的配置文件一条条列出来,然后靠人去执行。人执行就会出错,就会漏步骤,就会因为系统版本差异导致某一步的结果不一样。容器做的事情,本质上是把"应用加上它运行所需要的那一整套环境"打包成一个标准单元,让这个单元在任何支持容器的机器上都能以一致的方式启动。
这里有个关键认知需要先建立:容器打包的不只是应用二进制,还包括文件系统层面的依赖、运行时、库文件、配置。它共享宿主机内核,但在文件系统和进程视图上是隔离的。这个"共享内核、隔离视图"的定位,决定了容器比虚拟机轻,但也决定了它不能像虚拟机那样跑完全不同内核的系统。理解这一点,后面很多现象就顺了。
1.2 容器与虚拟机的边界在哪里
很多人一开始会把容器当成"轻量级虚拟机",这个类比有帮助但会误导。我用一张表把两者的差异摆清楚,这张表是我自己在给团队做分享时反复用的版本,重点看隔离层次和启动代价两栏。
| 对比维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离层次 | 硬件虚拟化,独立内核 | 进程级隔离,共享宿主内核 |
| 启动速度 | 分钟级 | 秒级甚至更短 |
| 资源开销 | 每台预留完整系统资源 | 按进程实际占用 |
| 镜像体积 | 通常GB级 | 可做到MB级 |
| 系统兼容 | 可跑不同内核系统 | 受宿主内核约束 |
这张表里最容易被忽略的是最后一行。容器共享宿主内核意味着你不能在一个Linux内核的宿主机上跑一个依赖不同内核特性的系统镜像,这不是配置问题,是架构决定的。我在早期就踩过这个坑,拿着一个为特定内核版本构建的镜像在另一套环境里跑,报的错很隐晦,排查了半天才意识到是内核层面的不匹配。
所以选容器还是虚拟机,判断标准不是"哪个更先进",而是"隔离强度要求"和"启动密度要求"哪个更关键。需要强隔离、需要跑异构系统的场景,虚拟机仍然是对的;需要快速启停、需要在一台机器上跑几十上百个隔离单元的场景,容器优势明显。两者不是替代关系,实际生产里经常是虚拟机里跑容器,各取所长。
1.3 镜像与容器的关系:一个必须掰清楚的核心概念
这是整个容器学习里最基础也最容易含糊的地方。我用一个类比把它钉死:镜像(Image)是类,容器(Container)是实例。镜像是只读的模板,容器是镜像跑起来之后的一个运行态。
这个类比的关键在于"只读"两个字。镜像一旦构建完成就是只读的,你不能直接修改一个镜像。你在容器里做的任何写操作,都不会回写到镜像里,而是写在一个临时的可写层上。容器删除,这个可写层就没了,你的修改也就没了。这就是为什么"我改了容器里的配置文件,重启容器就还原了"——不是重启还原了,是那个可写层被丢弃重建了。
理解了这个,你就能明白为什么定制镜像必须通过"构建"而不是"改容器"。改容器只是改了一个运行实例的临时层,构建镜像才是改模板。后面讲定制镜像的时候,所有操作都是围绕"如何正确地生成一个新模板"展开的。
还有一个衍生概念是分层。镜像由多层只读层叠加而成,每一层对应构建过程中的一条指令。分层的好处是复用:多个镜像共享相同的基础层,存储和传输都省。容器启动时在镜像的所有只读层之上再加一个可写层,所有写操作落在这一层。这个分层模型是理解镜像体积优化、构建缓存、以及为什么某些操作会让镜像变大的基础,后面会反复用到。
2. 镜像管理的实操路径与常见误区
2.1 镜像的获取、查看与清理节奏
镜像管理的日常操作其实不多,但节奏感很重要。获取镜像最直接的方式是从镜像仓库拉取,命令是docker pull,后面跟镜像名和标签。这里第一个要注意的点是标签:不写标签默认拉latest,而latest在多数仓库里只是一个普通标签,不代表"最新最稳",生产环境用固定版本标签是基本纪律。
拉下来之后用docker images查看本地镜像列表,能看到仓库名、标签、镜像ID、创建时间和体积。镜像ID那一列是短ID,实际完整ID更长,短ID够日常用,但在脚本里做精确匹配时要注意短ID可能冲突。
清理是容易被忽视的一环。容器跑多了、镜像拉多了,磁盘会被悄悄吃掉。docker image prune清理悬空镜像(没有标签、没有被任何容器引用的中间层),docker image rm删除指定镜像。这里有个顺序问题:如果一个镜像还被某个容器引用着(哪怕容器已停止),直接删镜像会失败。正确顺序是先删容器再删镜像,或者用强制删除但要清楚后果。我见过有人图省事一路-f强删,结果留下一些状态不一致的残留,后面排查很麻烦。
提示:定期清理前先
docker system df看一眼磁盘占用分布,镜像、容器、数据卷、构建缓存各占多少一目了然,比盲目删要靠谱得多。
2.2 镜像分层结构对体积的实际影响
前面提到镜像分层,这里展开讲它对体积的实际影响,因为这是定制镜像时最影响结果的知识点。每一条构建指令生成一层,层与层之间是叠加关系。关键在于:删除文件的操作不会减小前面层已经占用的空间。
举个具体的例子。假设你在某一层里下载了一个几百MB的安装包,然后在下一层里把它删掉。从最终容器的文件系统看,这个包确实不在了,但镜像体积不会因此减小,因为下载那一层已经把数据写进去了,删除只是在新的层里标记"这个文件不可见"。这就是为什么很多看起来"很干净"的镜像体积依然很大。
正确的做法是把"下载、使用、删除"放在同一条指令里完成,让它们只生成一层,这样中间产物不会留在最终镜像里。这个原则在定制镜像时会直接决定你写指令的方式,后面讲 Dockerfile 的时候会具体演示。
另一个影响体积的点是基础镜像的选择。同样是运行一个简单服务,基于完整发行版的基础镜像可能几百MB,基于精简版的可能几十MB,基于更极致的裁剪版可能只有几MB。选择基础镜像要在"体积"和"调试便利性"之间权衡:精简镜像体积小、攻击面小,但里面可能连常用的排查工具都没有,出问题时不好定位。我的习惯是开发调试阶段用完整一点的,最终交付再换精简的。
2.3 镜像仓库的推拉与标签管理
镜像要在多台机器间流转,就得靠仓库。推送到仓库用docker push,拉取用docker pull,前提是镜像名要符合仓库的命名规范:通常是"仓库地址/命名空间/镜像名:标签"的形式。本地构建的镜像如果只用了简单名字,推送前需要先docker tag打上符合规范的完整标签。
标签管理是个容易被轻视但很影响协作的点。同一个镜像可以有多个标签,标签本质上是指向镜像ID的别名。常见做法是给一个镜像同时打版本标签和latest标签,但要注意latest不会自动更新,你推了新版本之后得手动再推一次latest,否则它指向的还是旧镜像。这个细节我在协作项目里见过不止一次出问题,有人以为推了新版本latest就自动跟上了,结果别人拉latest拉到的还是旧的。
标签的另一个用途是标记镜像的用途或环境,比如加个-debug后缀标识带调试工具的版本。但标签不宜过多过乱,否则时间一长没人记得每个标签对应什么状态。我的经验是标签要么表达版本,要么表达明确的环境/用途差异,不要用它做临时备注。
3. 容器生命周期管理的完整链路
3.1 从创建到销毁:容器的几个关键状态
容器的生命周期比想象中要细。用docker create创建容器但不启动,此时容器处于 created 状态;docker start启动它进入 running;docker stop停止进入 exited;docker restart是停止再启动;docker rm删除。日常最常用的是docker run,它等于 create 加 start 的组合。
这里有个非常实用的参数组合值得单独说:docker run -d让容器在后台运行,-it让容器以交互模式运行并分配伪终端。前者适合跑服务,后者适合进容器里操作。新手常犯的错是用docker run不带-d跑一个服务,结果终端被占住,一按 Ctrl+C 容器就停了,误以为服务挂了,其实是容器跟着前台进程一起结束了。
还有一个高频困惑:容器里跑的服务,为什么docker stop之后数据就没了。这又回到可写层的问题——容器的可写层随容器删除而消失。如果服务需要持久化数据,必须用数据卷(volume)或绑定挂载(bind mount)把数据写到容器外部。这个点我在实际部署里反复强调,因为它是"容器里装服务"能否真正可用的分水岭。
3.2 端口映射与容器网络的基本认知
容器有自己的网络命名空间,容器内服务监听的端口默认在宿主机上访问不到,必须做端口映射。docker run -p 宿主机端口:容器端口是标准写法。这里容易混淆的是两个端口的位置:冒号左边是宿主机,右边是容器。写反了服务就访问不通,而且报错不明显,只是连不上。
端口映射背后是网络地址转换,宿主机在指定端口上监听,把流量转发到容器的对应端口。理解这一点有助于排查"映射了但访问不到"的问题:先确认容器内服务确实在监听那个端口(进容器用curl或netstat验证),再确认映射的宿主机端口没被占用,最后确认宿主机防火墙没拦。这个排查顺序是我踩过几次坑之后固定下来的,从内到外逐层验证,比乱试快得多。
容器之间要互相通信时,更推荐用自定义网络而不是默认的桥接网络。自定义网络里容器可以通过容器名互相解析,不用记IP。docker network create建网络,docker run --network指定网络。这个做法在跑多容器应用时几乎是必须的,因为容器重启后IP可能变,靠名字解析才稳定。
3.3 容器内进程与前台运行的关系
容器的生命周期绑定的是它的主进程(PID 1)。主进程退出,容器就退出。这个机制解释了很多现象:为什么docker run跑一个执行完就结束的命令,容器立刻变成 exited;为什么跑服务时服务必须在前台运行,不能自己后台化。
很多服务默认会把自己放到后台运行(daemonize),这在传统部署里是常态,但在容器里是灾难——服务后台化了,主进程(启动脚本)执行完就退出了,容器跟着退出,服务其实还在但容器显示已停止,管理就乱了。所以容器里跑服务,通常要加参数让它前台运行,比如很多服务有--foreground之类的选项,或者用专门的容器友好启动方式。
这个认知直接决定了"容器内安装部署服务"能不能成功。后面讲部署服务时,所有配置都是围绕"让服务作为主进程前台运行"来做的。如果这一点没处理好,你会遇到"容器启动就退出"的经典问题,而排查方向往往被误导到配置错误上,实际是进程模型的问题。
4. 定制简单镜像:从思路到落地
4.1 为什么定制镜像要用构建而不是改容器
在讲具体操作前,必须先把"为什么"讲透,否则操作就是死记。前面说过镜像是只读模板,容器是可写实例。如果你进到一个容器里,装软件、改配置,然后想把这个状态保存成新镜像,技术上可以用docker commit把容器的可写层提交成一个新镜像。但这条路我不推荐,原因有三个。
第一,commit出来的镜像不可复现。你不知道这个镜像里到底改了哪些东西,因为所有修改都是手工操作留下的痕迹,没有记录。第二,commit出来的镜像会包含大量临时文件和缓存,体积不可控。第三,也是最关键的,commit无法体现构建逻辑,别人拿到这个镜像不知道它是怎么来的,没法维护、没法审计、没法改。
正确的方式是写构建文件(Dockerfile),把"基于什么基础镜像、装什么、改什么配置、启动什么命令"用声明式的方式写下来,然后用docker build构建。这样构建过程可复现、可版本管理、可审查,镜像体积也可控。这是容器实践里的一条基本原则:镜像靠构建产生,不靠手工修改产生。
4.2 Dockerfile 的核心指令与书写逻辑
Dockerfile 的指令不多,常用的就那么几条,但每条背后的逻辑值得说清楚。FROM指定基础镜像,必须是第一条有效指令;RUN在构建时执行命令,通常用来装依赖;COPY把构建上下文里的文件复制进镜像;WORKDIR设置工作目录;ENV设环境变量;EXPOSE声明服务端口(注意这只是声明,不实际映射);CMD和ENTRYPOINT指定容器启动时的默认命令。
这里重点说RUN的写法,因为它直接关系到镜像体积。前面讲过删除不减小体积的原理,所以装依赖时要遵循"一条 RUN 里完成更新、安装、清理"。以基于 Debian 系的基础镜像装一个工具为例:
FROM debian:stable-slim RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/*这三步必须在同一条RUN里用&&串起来。如果拆成三条RUN,那么apt-get update产生的包索引会留在中间层,即使最后删了也不减体积。--no-install-recommends是避免装一堆非必要的推荐包,进一步控制体积。rm -rf /var/lib/apt/lists/*清理包索引缓存。这套写法是精简镜像的标准动作,几乎每个基于包管理器装东西的 Dockerfile 都该这么写。
CMD和ENTRYPOINT的区别也值得单独拎出来。ENTRYPOINT定义的是容器的入口,CMD提供默认参数,两者配合时CMD的内容会作为ENTRYPOINT的参数。如果只用CMD,那么docker run后面跟的命令会覆盖CMD。理解这个覆盖关系,才能设计出既好用又灵活的启动方式。我的习惯是:如果这个镜像就是为跑某一个固定服务准备的,用ENTRYPOINT加CMD给默认参数;如果是通用工具镜像,用CMD让使用者方便覆盖。
4.3 构建上下文与 .dockerignore 的坑
docker build的时候会指定一个构建上下文目录,通常是.。这个目录下的所有文件都会被发送给构建引擎,即使 Dockerfile 里根本没用到它们。这意味着如果你的项目目录里有个巨大的日志文件或者依赖缓存目录,构建会莫名其妙地变慢,因为它们在传输上浪费了时间。
解决办法是写.dockerignore文件,把不需要参与构建的路径排除掉,语法类似.gitignore。这个文件很多人不写,觉得无所谓,但在项目目录稍大的时候,写不写差别非常明显。我一般会把版本控制目录、依赖缓存目录、日志目录、本地配置文件都排除掉。
还有一个和上下文相关的坑:COPY指令的源路径是相对于构建上下文的,不能引用上下文之外的文件。有人想COPY ../config这种写法,会直接报错。正确做法是把需要的文件放进上下文目录,或者调整构建上下文的指定位置。这个限制初看很别扭,但它是为了保证构建的可移植性——构建只依赖上下文里的东西,换台机器也能复现。
4.4 一个可复现的简单镜像构建实例
把上面的原则落到一个具体例子上。假设要构建一个跑静态文件服务的小镜像,思路是:选一个精简基础镜像,装一个轻量服务程序,把静态文件复制进去,设置启动命令。
FROM alpine:3.19 RUN apk add --no-cache busybox-extras WORKDIR /srv COPY ./site /srv/site EXPOSE 8080 CMD ["httpd", "-f", "-p", "8080", "-h", "/srv/site"]这里选 alpine 是因为它体积极小,apk add --no-cache是 alpine 包管理器的写法,--no-cache避免留下包索引。WORKDIR设好工作目录后,COPY把本地site目录复制进去。CMD用 exec 形式(方括号数组)而不是 shell 形式,原因是 exec 形式让服务进程直接作为 PID 1 运行,能正确接收停止信号;shell 形式会多一层 shell 进程,信号传递可能出问题,导致docker stop时服务不能优雅退出。
构建命令是docker build -t my-static:1.0 .,-t打标签,最后的.是构建上下文。构建完用docker run -d -p 8080:8080 my-static:1.0跑起来,浏览器访问宿主机的 8080 就能看到静态页面。这个例子虽小,但把基础镜像选择、包管理清理、工作目录、复制、端口声明、前台启动、exec 形式这些关键点都覆盖了,是一个可以照着改的骨架。
5. 容器内安装部署服务的实战要点
5.1 服务前台化:容器化部署的第一道关
在容器里装服务,和在普通机器上装服务,最大的差异就在进程模型上。普通机器上装完服务,习惯用系统服务管理器把它托管起来后台跑;容器里没有那套东西(或者说有也不该依赖它),服务必须作为主进程前台运行。
以装一个 Web 服务为例,很多服务的默认启动方式会自己 fork 到后台,这在容器里会导致主进程退出、容器停止。解决办法通常是找该服务的前台运行参数,或者在启动脚本里用exec把服务进程替换成主进程。这个"exec 替换"的技巧很关键:脚本里最后一行用exec 服务命令,这样服务进程会取代脚本进程成为 PID 1,信号能正确传递,容器生命周期也和服务对齐。
我在实际部署里总结出一个检查清单:容器启动后立刻docker ps看状态是不是 Up,如果几秒内变成 Exited,八成是主进程退出了;再看docker logs有没有服务启动成功的日志;如果日志显示服务启动了但容器还是退了,那就是服务后台化了,主进程没留住。这个排查链路比盲目改配置高效得多。
5.2 配置文件、数据与环境变量的分离
容器化部署的一个核心原则是"不可变基础设施":镜像构建好之后不再改,运行时的差异通过外部注入。具体到服务部署,就是配置文件、数据、环境相关的参数都不该烤进镜像,而应该在运行时提供。
配置文件的处理有两种常见方式:一种是用环境变量覆盖服务配置,适合配置项不多的场景;另一种是把配置文件通过绑定挂载或配置卷在运行时挂进去。环境变量方式更符合容器习惯,但配置项一多就难管理;挂载方式灵活,但要保证挂载的文件格式正确。我的经验是:少量关键参数用环境变量,复杂配置用挂载,两者结合。
数据持久化必须用数据卷。容器内的数据目录如果不用卷,容器一删数据全没。用docker run -v 卷名:容器内路径把数据目录挂到卷上,卷的生命周期独立于容器,容器删了卷还在,重建容器挂同一个卷数据就回来了。这个机制是容器化服务能真正用于生产的前提,也是新手最容易忽略的一环。
环境变量注入用-e参数,比如docker run -e DB_HOST=xxx -e DB_PORT=3306 ...。要注意的是环境变量会出现在容器的进程环境里,敏感信息(比如密码)用环境变量不是最安全的做法,更稳妥的是用专门的密钥管理机制或者挂载只读的密钥文件。这一点在安全要求高的场景要特别注意。
5.3 依赖安装顺序与构建缓存的有效利用
在容器里装服务,依赖安装的顺序会显著影响构建效率和镜像质量。Dockerfile 的构建是有缓存的:某条指令如果和上次构建时完全一样(包括它依赖的前序层),就直接用缓存,不重新执行。利用这个机制,把"变化频率低"的步骤放前面,"变化频率高"的步骤放后面,能大幅减少重复构建的时间。
具体到装服务的场景:先装系统级依赖(变化少),再装服务本体(变化中等),最后复制应用代码和配置(变化最频繁)。如果顺序反了,每次改一行代码都会导致前面的依赖安装全部重来,构建时间成倍增加。这个优化在开发迭代阶段感受特别明显。
还有一个细节:COPY指令的缓存判断是基于文件内容的,文件内容没变就用缓存。所以如果先COPY整个项目目录再装依赖,那么项目里任何一个文件改动都会让依赖安装的缓存失效。正确做法是先只COPY依赖清单文件(比如包管理器的清单),装完依赖,再COPY其余代码。这样改代码不会触发依赖重装。这个技巧是提升构建效率的关键,值得在每一个 Dockerfile 里落实。
5.4 部署后的验证与常见故障定位
服务装进容器跑起来之后,验证不能只看容器状态是 Up 就完事。完整的验证链路是:容器状态 Up、容器日志无报错、容器内服务端口在监听、宿主机映射端口能访问、业务功能正常。这五步逐层验证,任何一步不通都能快速定位问题在哪一层。
常见故障里,占比最高的是端口问题。容器内服务监听的地址如果是127.0.0.1,那么从宿主机映射过来是访问不到的,因为容器内的回环地址只对容器自己可见。服务要监听0.0.0.0才能接受来自容器外部的连接。这个坑极其常见,表现是"容器里 curl 自己能通,宿主机访问不通",遇到这个现象第一反应就该检查监听地址。
第二常见的是权限问题。容器内以非 root 用户运行服务是安全最佳实践,但非 root 用户可能没有权限绑定低位端口(1024以下)或写某些目录。解决办法要么是调整端口到高位,要么是给目录正确的属主权限。第三是依赖缺失,精简基础镜像里可能缺某些运行时库,服务启动时报找不到库文件,这时候要么补装依赖,要么换基础镜像。
注意:排查容器内服务问题时,
docker exec -it 容器名 sh进容器内部验证是最直接的手段。但精简镜像里可能连 shell 都没有,所以选基础镜像时要把"是否方便排查"作为一个考量因素,或者在构建时预留调试工具。
6. 把镜像和容器用顺手的几个经验判断
6.1 什么时候该重建镜像,什么时候改容器就够
这个判断在实际工作里天天遇到。我的原则很简单:改动如果需要在下次启动时依然生效,就必须进镜像;如果只是临时调试,改容器就行。临时改容器用于快速验证想法,验证通过后把改动落到 Dockerfile 里重新构建,这是标准流程。
反过来的错误也很常见:有人为了省事,每次改配置都进容器手改,改完不落盘到构建文件,结果容器一重建配置就丢了,然后又手改一遍,陷入循环。这种操作在单机临时环境里勉强能忍,一旦涉及多环境部署就是灾难,因为没有任何地方记录了你到底改了什么。
判断的另一个维度是"这个改动是否应该被版本管理"。配置、依赖、启动命令这些属于镜像定义的一部分,应该进构建文件接受版本管理;运行时的数据、临时文件不属于镜像,应该走卷。把这两类东西分清楚,镜像和容器的职责就清晰了。
6.2 镜像体积和可维护性的平衡取舍
追求小体积是对的,但不能走极端。我见过为了把镜像压到最小,把基础镜像裁到连基本排查工具都没有,结果线上一出问题,进容器什么都干不了,只能重建一个带工具的临时镜像来排查,反而更费时间。
合理的做法是分场景:面向最终交付的镜像追求精简,但保留最基本的排查能力(比如一个 shell 和基础的网络工具);面向开发和调试的镜像可以完整一些,工具齐全。用多阶段构建可以在一个 Dockerfile 里兼顾两者——构建阶段用完整镜像装依赖、编译,最终阶段只复制产物到精简镜像。这个技巧在需要编译的场景里特别有用,能把编译工具链完全排除在最终镜像之外。
多阶段构建的写法是在 Dockerfile 里写多个FROM,后面的阶段可以用COPY --from=前一阶段名复制前一阶段的产物。这样最终镜像只包含运行所需的东西,体积能大幅下降,同时构建过程该有的工具一个不少。这个模式值得每个需要编译的镜像采用。
6.3 日常操作里值得固化的几个习惯
最后分享几个我固化下来的习惯,都是踩坑换来的。第一,给每个容器起有意义的名字(--name),不要依赖自动生成的随机名,否则容器一多根本分不清谁是谁。第二,跑服务统一加--restart策略,比如--restart unless-stopped,让容器在异常退出或宿主机重启后能自动拉起,避免服务静默挂掉没人知道。第三,养成看日志的习惯,docker logs -f跟踪日志,很多问题在日志里早有征兆。
第四,镜像标签用明确的版本号,不用latest跑生产。第五,定期docker system prune清理,但要先确认没有需要保留的停止容器和数据卷。第六,把常用的运行参数写进一个启动脚本或者编排文件,不要每次手敲一长串命令,手敲容易漏参数,漏一个端口映射或者漏一个卷挂载,排查起来都很费劲。
这些习惯单看都是小事,但组合起来决定了你用容器是"能用"还是"用顺"。容器技术的门槛不在命令数量,而在对这些机制的理解深度。把镜像与容器的关系、分层与可写层、进程模型、网络与端口这几件事想透,剩下的操作都是自然推导出来的结果。我在带人的过程中发现,凡是这几件事讲清楚的,后面上手都很快;凡是靠背命令入门的,遇到稍微变形的场景就卡住。所以如果只能记住一件事,就记住:镜像是只读模板,容器是带可写层的运行实例,所有操作都是围绕这个关系展开的。