“啊? 刚刚那个构建就结束了?”
在体验过 Docker 的便利之后,我开始瞒着组长,一点一点把自己的开发环境迁到 Docker 上。我写了一份叫 Dockerfile 的设计图,然后敲下了 docker build 命令。
第一次构建花了不少时间。下载 Ubuntu、安装 Java、再把项目需要的库,Jar,拉下来,总共花了大约 5 分钟。“果然第一次会慢一些啊。”
可当我只是改了一个代码拼写错误,再次执行构建时,我简直不敢相信自己的眼睛。刚按下 Enter,终端里就跳出了 Successfully built。整个过程只用了 0.1 秒。
“不会是坏掉了吧? 刚才还要 5 分钟,怎么突然一下就结束了?”
我忐忑地把服务跑起来看了看,结果代码修改得一点没错。那 Docker 里面到底发生了什么? 这种离谱速度的秘密,就藏在一种叫“层(Layer)”的独特存储方式里。

镜像(Image)与容器(Container)
在深入原理之前,先把两个最容易混淆的术语整理一下。
我们在部署时发送到服务器上的,不是“容器”,而是“镜像”。服务器只需要接收这个镜像并运行它即可。
层(Layer):透明赛璐璐的魔法
Docker 镜像并不是一个单体的大文件。它更像是把很多张“透明赛璐璐”,也就是 Layer,一层层叠在一起。Dockerfile 里写下的每一行命令,都会分别变成一层。
比如这样。
# 第1層:安裝 Ubuntu(OS)
FROM ubuntu:20.04
# 第2層:安裝 Java
RUN apt-get install openjdk-17-jdk
# 第3層:複製我的程式碼
COPY my-app.jar /app/my-app.jar
# 第4層:執行
CMD ["java", "-jar", "/app/my-app.jar"]
如果把 Dockerfile 里的内容慢慢拆开来看,就会发现它的结构其实没有那么复杂。“装上 Ubuntu,FROM;安装 Java,RUN;把我的代码带进去,COPY;再运行,CMD。”
归根结底,这只是把我们以前在 VM 里或者服务器终端上手动敲的“服务器配置过程” 原封不动地写成了一份文档。有了这份文档,Docker 不需要别人一步步指挥,就会自己把安装流程完成。(这个概念之后也会和我们要讲的 “CI/CD 自动化” 的核心原理连在一起。)
但 Docker 为什么不把这个过程一口气打包成一个整体,而偏偏要按行拆开,一层一层地堆起来呢?
答案就在于 复用,也就是缓存(Caching)。如果我修改代码后再次构建,Docker 会很聪明地判断:“第一层,OS,和第二层,Java,跟刚才完全一样啊? 那就没必要重建,直接复用之前做好的缓存层就行。”
然后它只会从发生变化的第三层,也就是拷贝代码那一步开始重新构建。所以第二次构建才会在 0.1 秒内结束。

[Code Verification] 用眼睛亲自确认层缓存
别只停留在口头上,说它快不算数,我们直接用代码来确认。只要写一个简单的 Dockerfile,然后连续构建两次就行。
FROM alpine:latest
RUN echo "1. 正在安裝基本工具..." && sleep 2 # 需要 2 秒的作業
COPY test.txt /app/test.txt
CMD ["cat", "/app/test.txt"]
$ docker build -t my-test:v1 .
# 結果:
# [2/3] RUN echo "1. 正在安裝基本工具..." ... 2.1s (耗時 2 秒)
$ docker build -t my-test:v2 .
# 結果:
# [2/3] RUN echo "1. 正在安裝基本工具..." ... CACHED (0 秒!)
你看到日志里清清楚楚出现的 CACHED 这个词了吗? 这就是 Docker 跳过了原本需要 2 秒的那一步的证据。
实务建议:顺序就是生命
一旦理解了层缓存的原理,你就会知道 Dockerfile 应该怎么写。核心原则很简单:“不常变化的内容放在下面,也就是前面;经常变化的内容放在上面,也就是后面。”
坏例子:
# 1. 先複製原始碼(經常變動!)
COPY . .
# 2. 安裝函式庫(幾乎不變)
RUN npm install
源代码一天会变动几十次。如果第 1 步发生变化,Docker 连后面的第 2 步,安装库,缓存也会一并作废,然后重新执行。你只是改了一行代码,却还得重新等一次 npm install。
好例子:
# 1. 先只複製函式庫清單(package.json)
COPY package.json .
# 2. 安裝函式庫(即使原始碼變動,這一步仍會被快取!)
RUN npm install
# 3. 複製原始碼
COPY . .
哪怕只是换一下顺序,构建速度也能提升 10 倍。这就是 Docker 新手和真正高手之间的差别。
实务建议 2:标签(Tag)就是生命线
在上面的代码里,我写了 docker build -t my-test:v1 .。这里冒号 : 后面的 v1,就是标签,Tag。
很多初学者嫌麻烦,不给镜像打标签。这样一来,Docker 就会自动给它加上 latest 这个标签。
这个打标签的小习惯,以后会帮你守住很多下班时间。
结语:把 0.1 秒的魔法握在手里
Docker 镜像并不是单纯的一堆文件,而是为了把环境以最高效率交付出去而层层堆叠起来的 技术结晶。只要理解层缓存,并把 Dockerfile 的顺序安排好,我们就能构建出轻量、快速、甚至能在 0.1 秒内完成构建的部署环境。
现在,把我电脑里的环境完美冻结成一个 Image 并发送到服务器上的准备,已经全部完成了。
但真到了要在服务器上启动这个镜像的时候,新的问题又出现了。一个 Web 服务并不是只靠一个服务器容器就能运行起来的。它还需要数据库、Redis,以及前端服务器。
这么多容器,难道要靠一条条命令一个个启动和关闭吗? 它们之间又该怎么通信? 当我们不再只讨论单个“镜像”,而是一个完整“应用”时,该怎么组织它? 下一次,我们就来看看 Docker 世界观进一步扩展后的概念:容器、服务,以及栈。