第2章 Docker镜像与容器基础概念
学Docker,我建议你先搞明白两个东西:镜像和容器。很多人一开始就把它们混着叫,其实区别特别大。镜像是一套只读的模板,容器是根据模板跑起来的实际进程。你可以把镜像看作一个光盘镜像文件,容器则是用这个光盘启动的一台虚拟机——只不过Docker更轻量,启动只需要毫秒级。
本章我们就拆开来看:镜像为什么能分层?容器从生到死经历了哪些状态?常用的docker命令到底怎么用才不出错?走一遍,你的基本功就稳了。
2.1 镜像是只读模板,容器是运行实例
先说镜像。你执行docker pull ubuntu:22.04,下载下来的就是一个Ubuntu系统的快照。它包含了根文件系统、默认配置文件、环境变量,但不包含任何运行中的进程。镜像是不可变的——你拉下来是什么就是什么,往里面写东西?不行。
容器就不同了。用docker run ubuntu:22.04启动后,Docker会在镜像顶层加一个可写层(也叫容器层)。你对容器的修改全部写在这一层,镜像本身纹丝不动。我记得有个同事直接删了容器里的/etc/passwd,然后重启容器发现文件还在——他一脸懵,我告诉他:你删的是容器层,镜像层还在,重新run一下就回来了。这就是不可变的好处。
一个小类比:
- 镜像 = 类的定义(class),静态
- 容器 = 类的实例(object),动态、有状态
2.2 镜像分层:UnionFS 与写时复制
镜像是分层的,这是Docker最巧妙的设计。说白了,每个Dockerfile指令(FROM、RUN、COPY)都会生成一个层。这些层叠加起来,通过联合文件系统(UnionFS)挂载成一个完整的文件系统。
为什么分层?层的最大价值是复用。比如你拉python:3.11-slim,它底层共用debian:bullseye-slim。如果你本地已经有了这个基础层,Docker就只下载差异部分。我在项目中见过一个团队维护了50多个微服务镜像,基础层几乎一模一样,最终磁盘占用只有单层计算的三分之一。
看一个典型的分层结构:
docker history python:3.11-slim
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 2 weeks ago CMD ["python3"] 0B
b2c3d4e5f6g7 2 weeks ago ENTRYPOINT ["docker-entrypoint.sh"] 0B
c3d4e5f6g7h8 2 weeks ago COPY dir:... /usr/local/bin/ ... 1.2kB
d4e5f6g7h8i9 2 weeks ago RUN /bin/sh -c set -eux; ... 87.6MB
e5f6g7h8i9j0 3 weeks ago ENV PYTHON_VERSION=3.11.4 0B
f6g7h8i9j0k1 3 weeks ago FROM debian:bullseye-slim 77.1MB
每一层都是一个只读的tar包,保存着文件变更。多个容器共享同一条镜像时,这些层是共同引用的,互不干扰。当某个容器修改了文件,写时复制(Copy-on-Write)机制才会把文件从镜像层拷贝到容器层,再进行修改。这叫延迟拷贝,省空间也省时间。
| 特性 | 传统虚拟机 | Docker 镜像分层 |
|---|---|---|
| 磁盘占用 | 每个VM包含完整OS,GB级 | 共享基础层,MB级增量 |
| 启动速度 | 分钟级 | 秒级(层已缓存) |
| 分发方式 | 整个镜像上传/下载 | 只推送变更层 |
| 写操作 | 直接修改磁盘文件 | 写时复制到容器层 |
关键点:镜像分层让你只存一份基础层,节省的不仅是磁盘,还有网络带宽和构建时间。生产环境一定要利用好基础镜像缓存。
2.3 容器生命周期:从创建到删除
一个容器不是只存在「运行」和「停止」两种状态。实际上它有完整的生命周期,我习惯把它看作一个状态机。你想想看,你能不能把一个正在运行的容器直接删除?不能。Docker会强制你先停止它(除非加-f)。这就说明了状态转换是有约束的。
下面这张SVG图概括了容器从创建到销毁的完整路径:
避坑:我曾经直接用docker rm -f强制删除一个正在运行的容器,结果丢了一部分未落盘的日志。记住:-f会发SIGKILL,不给进程任何清理的机会。生产环境尽量先docker stop(发SIGTERM),等几秒再docker rm。
再细说一下各个状态的含义:
| 状态值 | 含义 | 典型命令 |
|---|---|---|
| Created | 容器已创建但未启动,文件系统已就绪 | docker create |
| Running | 容器内进程正在执行,网络、存储可用 | docker start 或 docker run |
| Paused | 容器内进程被挂起(freeze),不消耗CPU | docker pause |
| Stopped (Exited) | 主进程退出或被终止,容器文件系统仍保留 | docker stop |
| Deleted | 容器元数据和可写层被移除,不可恢复 | docker rm |
我的习惯:调试阶段我常用docker create + docker start,因为可以在启动前用docker cp注入调试脚本。线上我直接docker run -d一步到位。
2.4 常用 Docker 命令速查
命令不需要背,但要理解归类。Docker 命令主要分两拨:镜像管理和容器管理。我刚开始总是记混docker images和docker ps,后来记住「镜像用image,容器用container」就对了。
镜像相关:
# 拉取镜像
docker pull nginx:1.25-alpine
# 列出本地镜像
docker images
# 删除镜像(先删依赖容器)
docker rmi nginx:1.25-alpine
# 从Dockerfile构建镜像
docker build -t my-app:v1.0 .
# 查看镜像分层历史
docker history my-app:v1.0
容器相关:
# 创建并启动容器(前台运行)
docker run --name webserver -p 8080:80 nginx:1.25-alpine
# 创建并启动容器(后台运行)
docker run -d --name webserver -p 8080:80 nginx:1.25-alpine
# 列出运行中的容器
docker ps
# 列出所有容器(包括已停止)
docker ps -a
# 停止容器
docker stop webserver
# 启动已停止的容器
docker start webserver
# 重启容器
docker restart webserver
# 删除已停止的容器
docker rm webserver
# 强制删除运行中的容器
docker rm -f webserver
# 进入运行中的容器(交互式)
docker exec -it webserver bash
# 查看容器日志
docker logs -f webserver
# 查看容器详细配置(底层信息)
docker inspect webserver
重要:docker exec -it 是调试利器,但我在生产环境曾经因为忘记加-it导致命令无响应。记住:交互式调试必须加 -it(-i 保持 STDIN,-t 分配伪终端)。
还有一些组合拳,我整理成一个速查表,你可以贴在工位上:
| 场景 | 命令 |
|---|---|
| 快速起一个临时容器测试 | docker run --rm -it ubuntu:22.04 bash |
| 清理所有停止的容器 | docker container prune |
| 清理所有悬空镜像 | docker image prune |
| 查看容器资源占用 | docker stats |
| 从容器拷贝文件到宿主机 | docker cp container_id:/path/file ./ |
嗯,到这里你已经掌握了镜像与容器的核心概念。分层机制让你理解为什么Docker能省空间、启动快;生命周期让你知道容器在不同状态下的行为;配合日常命令,你已经能自己搭建一个简单的Docker开发环境了。接下里可以多敲几遍命令,形成肌肉记忆——别怕,我刚开始也记不住这十几条,用得多了自然就熟了。