Docker 并不是一项孤立的技术,它把 Linux 内核中已经存在多年的隔离能力整合成了一套面向开发者的友好工具。使用 Docker 时,我们打交道最多的对象有三个:镜像、容器和仓库。镜像负责描述应用运行所需的文件系统快照,容器是镜像的实际运行状态,仓库解决镜像的存储与分发。理解这三者还不够,容器化之所以比传统部署更轻量,主要得益于镜像分层、联合文件系统、Namespace 隔离和 Cgroups 资源控制。本文会从虚拟机与容器的差异讲起,逐步拆解容器化原理,并通过一个最小化的 Web 应用示例展示完整的构建与运行流程。

一、容器不是轻量级虚拟机:先厘清隔离边界
很多人刚接触 Docker 时容易把容器理解成一种更省资源的虚拟机,这个说法不准确。传统虚拟机通过 Hypervisor 在物理硬件之上虚拟出一套完整硬件环境,每个虚拟机都要安装独立的操作系统内核。容器则直接运行在宿主机操作系统上,共享同一个内核,只在用户空间层面做隔离。这个区别决定了容器的启动速度可以做到毫秒级,而虚拟机通常需要几十秒甚至更久。
可以通过一个表格更直观地对比二者差异:
| 比较项 | 传统虚拟机 | Docker 容器 |
|---|---|---|
| 隔离层级 | 硬件级,每个虚拟机有独立内核 | 进程级,共享宿主机内核 |
| 启动速度 | 几十秒到数分钟 | 通常毫秒到秒级 |
| 资源开销 | 每个实例需完整操作系统 | 仅应用与依赖,共享内核 |
| 安全边界 | 更强,内核独立 | 较弱,内核漏洞可能影响所有容器 |
容器的隔离能力来自 Linux 内核的 Namespace 机制。Namespace 可以把进程、网络、挂载点、主机名等资源划分成独立的视图,让容器内的进程认为自己拥有独立的系统环境。Cgroups 则负责限制 CPU、内存、磁盘 IO 等资源使用量,避免某个容器耗尽宿主机资源。Docker 没有重新发明这些能力,它最核心的价值在于把这些复杂的内核接口封装成简单易用的命令和 API,让普通开发者也能快速构建和运行隔离环境。
二、镜像、容器与仓库:Docker 运行的三个核心对象
镜像是一个只读的文件系统快照,采用分层结构构建。每一条 Dockerfile 指令都可能生成一个新的层,底层内容可以在不同镜像之间共享。例如两个 Node.js 应用都基于 node:20-alpine,那么这两个镜像会复用同一个基础层,宿主机只需要保存一份数据。这种设计不仅节省磁盘空间,也加快了镜像拉取和构建速度。
容器是镜像的运行实例。运行容器时,Docker 会在镜像的最上层添加一个可写层,所有文件修改和新进程产生的数据都写入这个可写层。多个容器可以共享同一个只读镜像而互不影响。容器停止后,可写层中的数据默认仍然保留,只有通过 docker rm 删除容器才会释放。如果希望数据在容器删除后依然存在,应当使用卷或绑定挂载,把重要数据写到宿主机目录或专用卷中。
仓库负责镜像的存储和分发。Docker Hub 是最常见的公共仓库,企业内网通常使用 Harbor 等私有仓库。镜像名称一般包含仓库地址、命名空间、名称和标签,例如 nginx:1.25 实际指向 docker.io/library/nginx:1.25。生产环境应尽量使用明确的版本号,不要依赖 latest 标签,因为它只是默认值,并不保证指向最新版本。
下面通过几条基础命令展示镜像拉取、容器启动和日志查看过程:
docker pull nginx:1.25 docker run -d --name web -p 8080:80 nginx:1.25 docker ps curl http://127.0.0.1:8080 docker logs web
三、Dockerfile 与分层缓存实践
实际项目中,镜像通常通过 Dockerfile 构建,而不是手动修改容器后再提交。Dockerfile 是一组声明式指令,常见的有 FROM 指定基础镜像、WORKDIR 设置工作目录、COPY 复制文件、RUN 执行命令、EXPOSE 声明端口、ENV 设置环境变量、CMD 与 ENTRYPOINT 指定启动命令。下面是一个最小化的 Node.js 应用 Dockerfile:
FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . EXPOSE 3000 CMD ["node","server.js"]
构建时 Docker 会逐条执行指令并生成层,每层都有缓存键。为了充分利用缓存,应当把不容易变化的内容放在前面。例如先复制 package.json 和 package-lock.json,再执行 npm install,最后复制剩余的源码文件。这样当源码变化时,依赖安装层仍然可以命中缓存,从而提升构建效率。构建上下文大小也会影响速度,使用 .dockerignore 文件可以排除 node_modules、.git、日志等无关内容,避免把它们发送到 Docker 守护进程。
CMD 和 ENTRYPOINT 的区别值得单独说明。CMD 定义容器启动时的默认命令,但在 docker run 后面追加命令时会被覆盖。ENTRYPOINT 定义固定入口,docker run 追加的内容会成为 ENTRYPOINT 的参数。两者可以组合使用,例如 ENTRYPOINT 指定启动脚本,CMD 提供默认参数。推荐使用 exec 形式 CMD ["node","server.js"],这样容器可以正常接收 SIGTERM 信号;如果使用 shell 形式,实际会以 /bin/sh -c 启动,信号处理可能不符合预期。
四、底层原理:Namespace、Cgroups 与联合文件系统
容器之所以看起来像独立系统,核心是 Linux Namespace。Namespace 对内核资源进行分区,让不同容器只能看到属于自己的那部分视图。例如 PID Namespace 让容器内的进程从 1 开始编号,但宿主机的进程仍然存在;Mount Namespace 隔离挂载点;Network Namespace 提供独立的网络栈和网卡;UTS Namespace 允许容器拥有自己的主机名。Docker 启动容器时会为每个容器创建一组 Namespace,容器内的应用无法直接看到宿主机或其他容器的进程,这是一种进程级隔离,与虚拟机的硬件级隔离有本质区别。
资源限制则由 Cgroups 完成。Cgroups 可以将进程分组,并为每组设置 CPU 配额、内存上限、块设备 IO 权重等限制。例如 docker run -m 512m --cpus 1.5 my-app 可以限制容器最多使用 512MB 内存和 1.5 个 CPU 核心。如果没有这些限制,单个容器可能消耗大量宿主机资源,影响其他服务。Docker 还支持限制 PID 数量、只读根文件系统等安全选项。
镜像分层依赖联合文件系统。OverlayFS 是最常见的实现,它把多个只读层和一个可写层叠加成一个统一视图。读取文件时优先从上层查找,修改文件时执行写时复制,把文件复制到可写层再进行修改,因此不会影响底层镜像内容。正是这种机制让镜像层只读且可共享,同时每个容器又具备独立的变化空间。联合文件系统的性能通常接近原生文件系统,但在大量小文件的写入场景下会存在一定开销,需要结合实际负载评估。
掌握镜像、容器、仓库三者的关系,并理解 Namespace、Cgroups 和联合文件系统的作用,就能在入门 Docker 时少走弯路。实际工作中,环境一致性、交付速度和资源利用率是容器化带来的最直接收益,而安全边界和持久化设计则是后续需要持续关注的重点。