Docker 核心概念与生命周期
Docker 核心概念与生命周期
写在前面
第一次接触 Docker 的时候我跟大多数人一样,装了个桌面版、跑了几个 docker run hello-world,觉得"哦,就是个轻量虚拟机嘛"。这种认知一直保持到我接手公司 Jenkins 集群的容器化改造,才彻底理解 Docker 跟虚拟机的根本差异——它不是"更好的虚拟机",它是"用进程模拟操作系统"。
这篇文章不讲 docker run 的参数表,只讲最重要的四件事:镜像是什么、容器是什么、卷是什么、网络是什么。把这四件事的边界搞清楚,后面的 Dockerfile、Compose、Kubernetes 全都顺理成章。
一、Docker 到底是什么
1.1 容器 ≠ 虚拟机
先说结论:容器是宿主机上的一个特殊进程,而不是一个完整的虚拟操作系统。
| 维度 | 虚拟机 | Docker 容器 |
|---|---|---|
| 隔离级别 | 硬件级(内核也隔离) | 进程级(共享宿主机内核) |
| 启动时间 | 30s ~ 数分钟 | 毫秒到秒级 |
| 镜像大小 | GB 级(完整 OS) | MB 到 GB 级(只带运行时) |
| 性能损耗 | 5%~15% | < 1% |
| 底层依赖 | Hypervisor + Guest OS | Linux 内核特性(Namespace/CGroup/UnionFS) |
Docker 用的 Linux 内核特性有三个:
- Namespace:实现进程、网络、文件系统、主机名等资源的隔离
- CGroup:限制 CPU、内存、磁盘 IO 等资源使用
- UnionFS:把多层文件系统叠在一起,形成镜像
误区澄清:很多人以为 Docker 镜像就是一个压缩包。其实镜像更接近一个 Git 仓库——由多层只读层组成,每一层都记录了相对于上一层的修改。
1.2 为什么公司非要用 Docker
我入职的时候公司测试环境的部署流程是这样的:
- 测试提测 → 通知运维
- 运维 SSH 到测试服务器 →
git pull - 运维手动跑数据库迁移
- 运维重启 Node 服务
- 如果失败,运维回滚代码
整个流程平均 30 分钟,失败率 20%+,每次失败运维都得翻聊天记录看上次执行过什么命令。后来我们用 Docker 改造后:
- 测试提测 → CI 自动构建镜像
- 推送到 Harbor
- 测试服务器
docker compose pull && docker compose up -d - 如果失败,回滚到上一个 tag
整个流程压到 3 分钟,失败率低于 1%。这不是 Docker 的功劳,是不可变基础设施 + 声明式部署的功劳,而 Docker 是实现这套理念最趁手的工具。
二、四大核心对象
2.1 镜像(Image)
镜像 = 一个只读的模板,可以用来创建容器。
# 拉取镜像
docker pull nginx:1.25-alpine
# 查看本地镜像
docker images
镜像由**层(Layer)**组成,每一层对应 Dockerfile 里的一条指令。拉取镜像时,Docker 会按层校验,只下载本地没有的层。这就是为什么同一个镜像的多个 tag 往往只占一份磁盘空间。
实战中我踩过一个镜像层的坑:某次优化前端镜像大小时,我把 COPY package.json ./ 和 RUN npm install 拆成了两步,然后又把 COPY . . 放在了前面。结果每次改一行代码都触发 npm install,构建从 2 分钟变成 8 分钟。正确的顺序是:
# 依赖文件先复制,装依赖(这一层会被缓存)
COPY package*.json ./
RUN npm install
# 源码后复制(源码变更不影响上面的层)
COPY . .
RUN npm run build
后面讲 Dockerfile 优化时会详细展开。
2.2 容器(Container)
容器 = 镜像的运行实例。
# 从镜像创建并启动容器
docker run -d --name web -p 8080:80 nginx:1.25-alpine
# 查看运行中的容器
docker ps
# 查看所有容器(包括停止的)
docker ps -a
# 停止容器
docker stop web
# 删除容器
docker rm web
容器是临时性的。这是我最早没理解透的点——我以为容器跟虚拟机一样,改了点东西重启还能保留。其实容器默认是可以随时删掉的,所有写操作都在容器层,删了就没了。
# 进容器看看,容器层其实是 UnionFS 最上面一层可写层
docker exec -it web sh
touch /tmp/test
ls /tmp/test
exit
docker stop web && docker rm web
docker run -d --name web2 nginx:1.25-alpine
docker exec -it web2 ls /tmp/test
# ls: cannot access '/tmp/test': No such file or directory
这就引出了第三个对象——卷。
2.3 卷(Volume)
卷 = 脱离容器生命周期的持久化存储。
# 创建命名卷
docker volume create mysql-data
# 把卷挂载到容器
docker run -d --name db \
-v mysql-data:/var/lib/mysql \
mysql:8.0
# 删除容器,数据还在
docker rm -f db
docker volume ls # mysql-data 还在
卷有两种形式:
- 命名卷(Named Volume):由 Docker 管理,存在
/var/lib/docker/volumes/ - 绑定挂载(Bind Mount):直接挂载宿主机的目录或文件
# 绑定挂载 - 适合开发环境热更新
docker run -d --name dev \
-v $(pwd)/src:/app/src \
-v $(pwd)/package.json:/app/package.json \
node:20-alpine
我一般这样选:生产用命名卷,开发用 bind mount。
后面专门有一篇讲数据持久化,会详细讨论 MySQL、Redis 这种有状态服务在 Docker 里该怎么玩。
2.4 网络(Network)
网络 = 容器之间、容器与外部通信的虚拟网络。
# 查看网络
docker network ls
# 创建自定义网络(关键!)
docker network create --driver bridge my-net
# 启动两个容器加入同一个网络
docker run -d --name api --network my-net my-api
docker run -d --name db --network my-net my-db
# 在 api 容器里,可以直接用容器名访问 db
docker exec -it api ping db
# PING db (172.18.0.3): 56 data bytes
# 64 bytes from 172.18.0.3: icmp_seq=0 ttl=64 time=0.123 ms
Docker 内置了 DNS 解析——同一个自定义网络内的容器,可以直接用容器名互相访问。这个特性我用了好几年才发现,之前一直手写 --link,还被告知 link 已经 deprecated。
后来有一次踩了坑——两个容器在默认的 bridge 网络里,容器名居然访问不到,只能通过 IP。原因是默认 bridge 不带 Docker DNS,只有自定义网络才有。这个坑后面讲网络时会重点说。
三、容器的生命周期
容器的状态机很简单,只有五个状态:
create
created ────────▶ running
▲ │
│ start │ stop
│ ▼
└────────── exited
│
│ rm
▼
(消失)
- created:已经
docker create但还没启动 - running:正在运行
- paused:被
docker pause暂停(进程冻结) - exited:停止运行(正常退出或崩溃)
- dead:无法启动(罕见,通常是配置错误)
实战中我做过的最蠢的事情:
# 误以为 -d 是"debug"模式
docker run -d nginx
# 容器启动后立刻退出了!
docker ps -a
# STATUS: Exited (0) 2 seconds ago
-d 是 detached(后台运行)的意思。容器退出的根本原因是 nginx 在前台模式下没有持续运行的进程——但这个例子不是 nginx 的问题,是镜像的 entrypoint 设置问题(以 nginx:alpine 为例,默认 CMD 是 ["nginx", "-g", "daemon off;"],所以前台运行 OK)。
但我后来在写自己镜像的时候确实犯过这个错:
# 错误写法 - CMD 用 shell 形式会被包装成 sh -c,主进程不是 CMD
CMD node server.js &
# 正确写法 - exec 形式,CMD 直接作为 PID 1
CMD ["node", "server.js"]
如果用错误写法,docker run -d 时 PID 1 是 sh,sh 启动 node 后就退出了,容器也跟着退出了。这个坑在 Dockerfile 篇会再展开。
四、第一个真实场景:把 Jenkins slave 容器化
公司原来有 20 台物理机做 Jenkins slave,每台装不同的工具链(Node/Python/Java/Go/iOS)。问题:
- 工具版本混乱(有的 Node 12,有的 Node 18)
- 新人入职配环境要半天
- iOS 构建机利用率只有 10%
容器化方案:
FROM jenkins/inbound-agent:latest
USER root
# 安装各种工具
RUN apt-get update && apt-get install -y \
python3 python3-pip \
openjdk-17-jdk \
&& rm -rf /var/lib/apt/lists/*
# Node 18
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs
USER jenkins
然后在 Jenkins master 上配置:
- 静态 slave → Kubernetes Pod 模板
- 任务结束后 Pod 自动销毁
- 工具装在镜像里,镜像里有什么,构建就用什么
效果:
- 机器数量从 20 台降到 0(全部按需创建)
- 新人入职配环境时间从 4 小时降到 5 分钟(等镜像构建)
- iOS 构建机成本降 70%(只在需要时创建 Mac 实例)
这是 Docker 改造的第一个胜利,也是我真正理解 Docker 的起点。
五、本系列接下来会讲什么
如果你读到这里还没睡着,接下来会按这个顺序展开:
- ✅ 本篇:核心概念与生命周期(你现在在这里)
- Dockerfile 编写规范与缓存优化
- 镜像分层原理与体积瘦身
- docker-compose 编排实战
- …以此类推
下一篇会讲 Dockerfile,把上一节提到的 COPY 顺序、CMD exec 形式这些点展开,会拿公司一个真实 Node 项目的 Dockerfile 改写前后做对比。