第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图概括了容器从创建到销毁的完整路径:

容器生命周期状态转换 Created Running Paused Stopped Deleted 虚线表示保留容器可再启动 docker start docker pause docker unpause docker stop docker start docker rm docker create docker rm -f (直接删除)

避坑:我曾经直接用docker rm -f强制删除一个正在运行的容器,结果丢了一部分未落盘的日志。记住:-f会发SIGKILL,不给进程任何清理的机会。生产环境尽量先docker stop(发SIGTERM),等几秒再docker rm。

再细说一下各个状态的含义:

状态值含义典型命令
Created容器已创建但未启动,文件系统已就绪docker create
Running容器内进程正在执行,网络、存储可用docker start 或 docker run
Paused容器内进程被挂起(freeze),不消耗CPUdocker 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开发环境了。接下里可以多敲几遍命令,形成肌肉记忆——别怕,我刚开始也记不住这十几条,用得多了自然就熟了。