元数据卡
- 前置知识:终端基础(ch02)、能看懂简单命令行
- 预计时间:45 分钟
- 核心难度:(进阶)
- 阅读模式: 高度专注
- 完成标志:能写一个 Dockerfile、用 docker-compose 启动多服务、理解数据持久化
你在哪
你还在出发前的工坊里。工具墙已经挂得满满当当了——但你意识到一个问题:这套工坊是你的,不是别人的。你的代码换一台机器就水土不服。工坊的师傅在角落里翻出一个不起眼的箱子,拍了拍灰:隔壁队友的工坊布局跟你的不一样,你写的咒语(代码)在他那儿根本跑不起来。你需要一个自包含的移动工坊——连装备带环境,打包带走。
你的任务
把整面工坊墙压缩进一个背包大小的箱子。从此你不再说"我的电脑上能跑啊",而是递过去一个箱子说"开箱即用"。
本章分层
- 必读:能
docker run一个服务、理解镜像(Image)和容器(Container)的区别、能写出一个最小 Dockerfile- 选读:Docker Compose 编排多服务、Volume 持久化数据、Dockerfile 层缓存优化技巧
- 深水区:Docker 网络模型(bridge/host/overlay)、Namespace 和 Cgroups 底层机制、多阶段构建
本章不会要求你掌握
- Docker Compose 的全部配置选项——单容器时用不到
- Volume 的挂载策略和备份方案
- Kubernetes 容器编排——那是 Vol 7 的事
遭遇战 → 获得技能
先让事情坏一次
"我这配方没问题啊!"你对着图纸喊。
你的合金配方在你的锻造炉里淬出一把完美剑刃。但你把图纸拿去隔壁工坊,按同样的步骤操作——炸了。剑身上布满了裂纹。
"你炉温多少?" "1100 度。" "我的炉子只能烧 950,你那配方到我这儿根本达不到熔点……"
"还有你那淬火油是什么配比?我用的还是老配方呢……"
你已经不是第一次遇到这种问题了。每次换一个工坊,你的配方就像变了——完全不认。
你的成品在本工坊机能用。你把设计图拿去隔壁——他们按你的步骤走,结果完全不一样。
"你炉温多少?" "1100度。" "我的只能烧 950……" "你那淬火油呢?……"
你已经不是第一次遇到这种问题了。环境差异像不同的工坊布局——炉子在左边还是右边,风箱拉法不一样,这都会让你的成品变形。
你需要的不是"传代码",而是"传整个环境"。
这就是 Docker 要解决的问题。
镜像 vs 容器:蓝图和房子
"问题在哪?"你坐下来,开始梳理。"我的炉子是燃煤的,他的炉子是燃油的——要不要把燃料配方刻在图纸上?"
工坊主人摇头。"刻在图纸上没用的,没人会照着做。你需要的是把整间工坊打包。连炉子带工具带材料,一卷走。"
他从架子上拿出两张纸。第一张是工坊的完整建筑图纸,标注了每件工具的位置。第二张是一间已经盖好的小屋子的照片。
"这两样东西有什么区别?"他问你。
我先用最粗的绳子捆一捆这两个概念,不然后面容易散架。
镜像(Image) 是一份只读的蓝图——它记录了你需要什么系统、装什么软件、配什么环境。
容器(Container) 是用蓝图造出来的房子——你可以造很多间,每间独立,进去能住能折腾。弄坏了也无所谓,从蓝图重新造一间就是。
# 看看你最常用的房子蓝图长什么样
docker images
# 从蓝图造一间新房子(并启动它)
docker run hello-world
# 看看现在有哪几间房子在住人
docker ps你不需要提前看懂每一行。先动手,感受一下。
第一件事:确认你的工坊装好了 Docker。
docker --version如果看到了版本号,就往下走。
你的第一个容器
"比喻听了半天,纸上谈兵。"工坊主人拍了拍手。"来,动手。"
他把你拉到一台机器前。"Docker Hub 里有一堆别人画好的蓝图。你不需要从头画工坊的施工图——直接从别人那里拿一份现成的,立刻造房子。"
他敲了一行命令。"我要给你看的是:从一张蓝图到一间能住的房子,需要多久。"
Docker 有一个官方仓库叫 Docker Hub,里面放着各种别人造好的蓝图(镜像)。你想跑一个简单的 Web 服务器,不需要自己从头写——直接捡一份现成的 nginx 蓝图:
docker run -d -p 8080:80 --name my-web nginx等等,这一行里发生了什么?
docker run—— 用蓝图造个房子,并且住进去启动它-d—— 后台模式,房子建成后不占你的终端-p 8080:80—— 端口映射。你的工坊门口(本机 8080 端口)通往容器的门口(容器内 80 端口)。这样你从浏览器访问localhost:8080就能到容器里的 nginx--name my-web—— 给这间房子起个名字,方便以后管它nginx—— 蓝图的名称,Docker 会自动从仓库下载
现在打开浏览器,访问 http://localhost:8080。看到 nginx 的欢迎页面了吗?
停止并清除:
docker stop my-web
docker rm my-web一间房子,从落成到拆除,不过几秒。这就是容器的威力——用完即弃。
为什么容器比虚拟机轻?
你可以把虚拟机想象成在房间里再盖一个完整房间——有墙(Hypervisor)、地板(Guest OS)、水电(完整的内核),非常重。而容器只是在你现有的房间里用屏风隔出一块区域——共享墙和地板(宿主内核),只隔离家具和活动空间(进程、文件系统、网络)。
┌────────────────────────────┐
│ 你的电脑 │
│ ┌──────────────────────┐ │
│ │ 容器 A │ 容器 B │ 容器 C│ │
│ │ (进程隔离,共享内核) │ │
│ └──────────────────────┘ │
│ Linux 内核 │
│ 硬件 │
└────────────────────────────┘所以容器启动快到毫秒级,占空间只有几 MB 到几十 MB。这不是魔法,是共享的力量。
这个比喻在哪里失效?
"屏风隔断"能帮你理解容器为什么轻,但它不能解释真正的隔离机制——容器的隔离靠的不是物理隔断,而是 Linux 的 Namespace(让每个容器看到自己的独立 pid、net、mount 世界)和 Cgroups(限制每个容器的 CPU/内存用量)。当你需要理解容器安全性或配置复杂网络时,屏风模型就不够用了。
Dockerfile:把你的工坊画成蓝图
"你从 Docker Hub 拿别人的蓝图造了房子。"工坊主人点点头。"但你要在里面跑你自己的东西——Java 17、Maven、你的 Spring Boot 应用。别人给你画的蓝图可不包含这些。"
"所以我得自己画一张蓝图?"
"对。"他从桌上拿起一张白纸。"你想让别人在任何一台电脑上都能复刻你的环境?那就把步骤写下来——用 Docker 能读懂的格式。"
从仓库下载现成的镜像只是第一步。真正的力量在于自己画蓝图。
想象你有一个 Java 项目,需要 JDK 17 和 Maven,才能运行一个 Spring Boot 应用。现在你要把这个环境打包成一个自包含的箱子。
在项目根目录创建一个文件,叫 Dockerfile(注意没有扩展名):
# 基础镜像:从带有 JDK 17 的官方镜像开始
FROM eclipse-temurin:17-jdk
# 创建应用目录
WORKDIR /app
# 把当前目录下的文件复制到容器里的 /app
COPY . .
# 告诉 Docker 这个容器要暴露 8080 端口
EXPOSE 8080
# 容器启动时运行的命令
CMD ["java", "-jar", "my-app.jar"]等等,这段代码怎么解读?
每一行都在往蓝图里加一层——FROM 选地基,WORKDIR 划区域,COPY 搬材料进去,CMD 定好最后的咒语。
现在运行:
# 把蓝图变成镜像(注意结尾有个点,表示当前目录)
docker build -t my-app:v1 .
# 从镜像启动容器
docker run -d -p 8080:8080 --name my-app my-app:v1docker build 是把蓝图(Dockerfile)烧制成镜像的过程。-t my-app:v1 给镜像取了个名字和标签。. 告诉 Docker:在当前目录找 Dockerfile。
优化的 Dockerfile:层才是关键
等等——上面的 Dockerfile 有个坑。
每次你改了代码,COPY . . 这层就会重新执行。但你的依赖(pom.xml 里声明的包)可能根本没变,为什么要重新下载?Dockerfile 是按**层(layer)**构建的,每一层变更都会导致后面所有层重新构建。
你应该把不变的东西放在前面,变的东西放在后面:
FROM eclipse-temurin:17-jdk
WORKDIR /app
# 先把依赖描述文件复制进去
COPY pom.xml .
RUN mvn dependency:resolve
# 再复制代码——只有代码变了才会触发这层之后的构建
COPY src ./src
RUN mvn package -DskipTests
EXPOSE 8080
CMD ["java", "-jar", "target/my-app-1.0.jar"]"改动少的在前,改动多的在后"——这是 Dockerfile 优化的第一原则。
进阶:Docker Compose——多间房子的联动(等你需要数据库+应用同时运行时再回来)
单个容器太简单了。你的真实项目不是单打独斗——它可能需要数据库、缓存服务,多个程序要相互通信。
你可以手动给每间房子造个网,但每次重生都要敲一遍。这时候你需要 Docker Compose——一个用 YAML 文件一次性定义多间房子的方案。
version: '3.8'
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: mydb
ports:
- "3306:3306"
volumes:
- db-data:/var/lib/mysql
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb
depends_on:
- db
volumes:
db-data:然后一行命令起飞:
docker compose up -d查看所有容器的日志:
docker compose logs -f收摊:
docker compose down注意到 depends_on 了吗?它确保 MySQL 先启动,应用后启动。但注意——depends_on 只保证启动顺序,不保证MySQL 已经可以接受连接。你的应用代码里还需要加一个"等待 MySQL 就绪"的重试逻辑。
常见陷阱
以下案例在你只跑单容器时不会遇到,但迟早会碰见。先知道有那么一回事就好。
案例 1:"我的容器里的时间怎么是 UTC?"
你发现容器里的日志时间全是 UTC,跟你本地差 8 小时。
# 查问题
docker exec my-container date
# 输出:Mon Jun 23 08:00:00 UTC 2026
# 而你的时间是 16:00
# 解决:挂载宿主机的时区文件
docker run -v /etc/localtime:/etc/localtime:ro my-image或者在 Dockerfile 里固定时区:
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime案例 2:"重启容器后我的数据没了"
你运行了一个 MySQL 容器,写了几条数据进去,docker stop 再 docker start——数据还在。但你 docker rm 后重新 docker run——数据全没了。
容器的文件系统默认是活的,但死了就没了。要持久化数据,必须用 Volume(卷):
# 创建卷
docker volume create my-db-data
# 挂载卷到容器
docker run -v my-db-data:/var/lib/mysql mysql:8.0Volume 是 Docker 管理的存储空间,宿主机上的某个目录。即使容器被删除了,卷里还留着数据。
你能在宿主机上找到卷的真实路径:
docker volume inspect my-db-data
# 输出的 Mountpoint 就是宿主机上的路径案例 3:"容器里的日志太多撑爆磁盘了"
你的应用跑了几天,日志文件把宿主机磁盘塞满了。容器不控制日志大小。解决方案:启动时限制日志大小。
docker run --log-opt max-size=10m --log-opt max-file=3 my-image或者在 Compose 里全局配置:
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"以上问题等你真正把容器部署到服务器上时再回来看——本地开发几乎不会触发。
通关挑战
🗡 热身:运行
docker run -d -p 8080:80 nginx,访问 localhost:8080 确认能打开 nginx 欢迎页,然后用docker stop+docker rm清理它。挑战:为你的练习项目写一个 Dockerfile 和 docker-compose.yml。要求:项目包含至少两个服务(比如一个 Web 应用 + 一个数据库),用 Volume 持久化数据库数据。
观察:运行
docker stats查看容器的实时 CPU/内存占用。尝试在容器里跑一个压力测试,观察资源消耗曲线。排障:你的
docker compose up -d执行后,应用容器一直重启。查看docker compose logs app,发现 "Can't connect to MySQL"。问题很可能出在depends_on不保证 MySQL 就绪上。加一个等待脚本解决之。
验收标准
- 能解释镜像和容器的区别——"蓝图为镜像,房子为容器,蓝图只读,房子可住"
- 能写出一个优化过的 Dockerfile,并解释为什么把依赖和代码分开放
- 能用 docker-compose.yml 定义多服务应用并用一条命令启动
- 知道为什么容器重启后数据会丢失,以及如何用 Volume 解决
- 能 debug "容器内时区不对"和"容器日志撑爆磁盘"的常见问题
常见卡点
- "docker: command not found":Docker Desktop 没装好。去 docker.com 下载安装即可。WSL2 用户注意要在 Windows 上装 Docker Desktop 并启用 WSL2 集成。
- "Port is already allocated":宿主机上的 8080 端口已经被其他程序占用了。用
lsof -i :8080找出来是谁,杀掉它,或者换一个端口(比如-p 8081:80)。 - "docker compose: command not found":老版本的 Docker 用
docker-compose(中间有横杠)。新版用docker compose。如果你的版本不支持新命令,升级 Docker 或改用docker-compose。 - "无法从 Docker Hub 下载镜像":国内环境可能需要配置镜像加速器。在 Docker Desktop 的 Settings → Docker Engine 中修改 registry-mirrors。
- "docker exec 说容器没在运行":
docker exec只能用于正在运行的容器。先docker ps确认容器在不在。如果是退出状态的容器,先用docker start启动它。
现在不需要理解
- Docker 网络模型的细节(bridge/host/overlay):对单机开发来说,Compose 默认的 bridge 网络已经够用。多机跨主机网络是 Vol 7 分布式系统的内容。
- Docker 安全隔离的底层机制:Namespace 和 Cgroups 的细节会在 Vol 3 计算机系统里展开。
- Kubernetes:一个容器叫什么名字、怎么调度到哪台机器上,是 K8s 管的事。Vol 7 会讲编排。
- 多阶段构建(multi-stage build):用来减小最终镜像体积的高级技巧。知道有这回事就好,暂时用不到。
旅人笔记
一个镜像是不变的蓝图,一个容器是它跑起来的副本。Docker 让你把整面工坊墙——系统、依赖、配置、代码——全塞进一个箱子,别人打开箱子,你的东西原样工作。
→ 下一站预告
箱子打包好了。下一章,我们要带着这口箱子,走完"写代码 → 编译 → 打包 → 部署 → 上线"的完整路线——一个真实项目怎么从编辑器走到用户手上。