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 内核特性有三个:

误区澄清:很多人以为 Docker 镜像就是一个压缩包。其实镜像更接近一个 Git 仓库——由多层只读层组成,每一层都记录了相对于上一层的修改。

1.2 为什么公司非要用 Docker

我入职的时候公司测试环境的部署流程是这样的:

  1. 测试提测 → 通知运维
  2. 运维 SSH 到测试服务器 → git pull
  3. 运维手动跑数据库迁移
  4. 运维重启 Node 服务
  5. 如果失败,运维回滚代码

整个流程平均 30 分钟,失败率 20%+,每次失败运维都得翻聊天记录看上次执行过什么命令。后来我们用 Docker 改造后:

  1. 测试提测 → CI 自动构建镜像
  2. 推送到 Harbor
  3. 测试服务器 docker compose pull && docker compose up -d
  4. 如果失败,回滚到上一个 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 还在

卷有两种形式:

# 绑定挂载 - 适合开发环境热更新
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
                      ▼
                   (消失)

实战中我做过的最蠢的事情:

# 误以为 -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)。问题:

  1. 工具版本混乱(有的 Node 12,有的 Node 18)
  2. 新人入职配环境要半天
  3. 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 上配置:

效果:

这是 Docker 改造的第一个胜利,也是我真正理解 Docker 的起点。

五、本系列接下来会讲什么

如果你读到这里还没睡着,接下来会按这个顺序展开:

  1. ✅ 本篇:核心概念与生命周期(你现在在这里)
  2. Dockerfile 编写规范与缓存优化
  3. 镜像分层原理与体积瘦身
  4. docker-compose 编排实战
  5. …以此类推

下一篇会讲 Dockerfile,把上一节提到的 COPY 顺序、CMD exec 形式这些点展开,会拿公司一个真实 Node 项目的 Dockerfile 改写前后做对比。

参考资料