知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 00 - 课程总览:容器到底是什么 > 这篇解决什么问题:你大概敲过 `docker run`,也照教程抄过一份 Dockerfile,但未必说得清"容器凭什么能隔离"、镜像里到底存了什么。 > 「云原生」课默认你已经会造镜像,本文补的是那之前的一段断层:从原理讲到交付闭环,把"镜像从哪来、怎么造、怎么送进仓库"讲透。 > 读完第 00 篇,你会知道容器不是"轻量虚拟机",也会知道后面五篇各自解决什么。 --- ## 1. 从"在我机器上能跑"说起 应用在开发机上跑得好好的,部署到测试机就报错,通常不是代码问题,而是环境里至少有一项对不上:JDK 版本(开发机 17、服务器 8,直接 `UnsupportedClassVersionError`)、时区(容器默认 UTC、数据库 `+08`,时间差 8 小时)、字符集(`LANG` 未设置为 POSIX,中文写入变问号)、中文字体(镜像里没有,导出报表全是方框)、系统依赖库版本(`glibc` / `libfreetype` 对不上,启动即 `UnsatisfiedLinkError`)、环境变量(`DB_HOST` / `PROFILE` 没设,连上了生产库)、路径分隔符(Windows 的 `\` 写进了配置,文件找不到)、配置文件位置(classpath 外挂目录不一致,走了默认配置)。 传统做法是写一份《部署手册》,把这些一项项在目标机器上装齐。问题是每换一台机器就要重来一遍,而"手册第 7 条漏执行了"这类事故,只有上线那一刻才暴露。 容器换了个思路:**不再去适配宿主机,而是让应用自带一整套运行环境**。  *图:传统部署与容器部署的对比:前者多应用共用宿主机环境,后者各自自带运行环境* ```text ┌───────────────────────────────┐ ┌───────────────────────────────┐ │ 传统部署:应用依赖宿主机环境 │ │ 容器部署:应用自带运行环境 │ ├───────────────────────────────┤ ├───────────────────────────────┤ │ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │ │ │ App (blog-app.jar) │ │ │ │ App (blog-app.jar) │ │ │ ├─────────────────────────┤ │ │ ├─────────────────────────┤ │ │ │ JDK(手动装,版本看运气)│ │ │ │ 依赖库 / 字体 / 配置 │ │ │ └─────────────────────────┘ │ │ ├─────────────────────────┤ │ │ ↓ 依赖 │ │ │ 运行时(JRE 17) │ │ │ ┌─────────────────────────┐ │ │ └─────────────────────────┘ │ │ │ 宿主操作系统(差异源头) │ │ │ ↑ 以上全部打包进镜像 │ │ └─────────────────────────┘ │ ├───────────────────────────────┤ │ ↓ 依赖 │ │ 宿主 OS + 容器运行时 │ │ ┌─────────────────────────┐ │ │ (只提供内核,不给应用依赖) │ │ │ 硬件 / 虚拟机 │ │ ├───────────────────────────────┤ │ └─────────────────────────┘ │ │ 硬件 / 虚拟机 │ └───────────────────────────────┘ └───────────────────────────────┘ 换台机器 → 照手册重装一遍 换台机器 → docker run 就完事 ``` 新手常把容器理解成"轻量虚拟机",但两者的结构和开销完全不同:  *图:虚拟机与容器的结构对比:虚拟机每台自带内核,容器共享宿主内核* ```text ┌────────────── 虚拟机 ────────────────┐ ┌────────────── 容器 ────────────────┐ │ App │ App │ App │ │ App │ App │ App │ ├────────┼────────┼───────────────────┤ ├────────┴────────┴─────────────────┤ │ Bins/ │ Bins/ │ Bins/ │ │ Bins/Libs(每个容器一份) │ │ Libs │ Libs │ Libs │ ├───────────────────────────────────┤ ├────────┼────────┼───────────────────┤ │ Docker Engine │ │ Guest │ Guest │ Guest │ ├───────────────────────────────────┤ │ OS │ OS │ OS ← 各自一套内核 │ │ Host OS(所有容器共享内核) │ ├────────┴────────┴───────────────────┤ ├───────────────────────────────────┤ │ Hypervisor │ │ 硬件 / 虚拟机 │ ├─────────────────────────────────────┤ └───────────────────────────────────┘ │ 硬件 / 虚拟机 │ └─────────────────────────────────────┘ 一台 GB 级,启动几十秒 一个几十 MB 级,启动毫秒级 ``` 关键差别在中间那层:虚拟机每台都装了一套 **Guest OS**(含独立内核),容器没有自己的内核,它直接把宿主机内核借来用,只打包了应用和用户态依赖。 | 对比项 | 虚拟机 | 容器 | |---|---|---| | 是否带内核 | 带,每个虚拟机一套 | 不带,共享宿主机内核 | | 典型体积 | GB 量级 | MB 到几百 MB | | 启动耗时 | 几十秒 | 毫秒到秒级 | | 隔离强度 | 硬件级,强 | 内核级,够用 | | 单机能跑几个 | 几个到几十个 | 几十到几百个 | > 一句话:**容器不是"更小的虚拟机",而是"被内核工具圈起来的一组进程",省掉的不只是体积,还有一整套内核。** --- ## 2. 容器到底隔离了什么:namespace 与 cgroup 容器能隔离,靠的是 Linux 内核早就提供的两组机制,Docker 只是把它们组装好、加了易用的接口。 **namespace 负责"看到什么"** —— 每个容器里的进程看到的是自己那一套视图: | namespace | 隔离的内容 | 带来的效果 | |---|---|---| | `pid` | 进程号空间 | 容器里自己的进程是 PID 1,看不到宿主上的其他进程 | | `net` | 网卡、IP、端口、路由表 | 每个容器有自己的 `eth0` 和独立端口空间,8080 不冲突 | | `mnt` | 挂载点 / 文件系统视图 | 容器看到自己的 `/` 来自镜像,看不到宿主的目录树 | | `uts` | 主机名与域名 | 容器内 `hostname` 是容器 ID,与宿主无关 | | `ipc` | 共享内存、信号量、消息队列 | 不同容器的进程无法互相通信 | | `user` | 用户 / 用户组 ID 映射 | 容器内的 root 可以映射成宿主的普通用户 |  *图:namespace 负责「看到什么」、cgroup 负责「能用多少」:两者合起来才是一个容器* ```text ┌──────────── 宿主机(只有一个内核)────────────┐ │ ┌──────── 进程 1234 = 容器 A ────────┐ │ │ │ pid → 它认为自己是 PID 1 │ │ │ │ net → 它只看到 eth0 / 172.17.0.2 │ │ │ │ mnt → 它看到自己的 / 来自镜像 │ │ │ │ uts → 它看到 hostname = a3f5c1e9 │ │ │ │ ipc → 它只看到自己的共享内存 │ │ │ │ user→ 它以为自己就是 root │ │ │ └────────────────────────────────────┘ │ │ ┌──────── 进程 1240 = 容器 B ────────┐ │ │ │ 同样六个 namespace,换一套完全不同的值 │ │ │ └────────────────────────────────────┘ │ └──────────────────────────────────────────────┘ ``` **cgroup 负责"用多少"** —— namespace 让进程看不见别人,但看不见不等于用不到,所以还要给每组进程设上限:CPU(`--cpus`)、内存(`--memory`)、磁盘 IO(`--device-write-bps`)、进程数(`--pids-limit`)。两者分工很清楚: | 机制 | 回答的问题 | 不设会怎样 | |---|---|---| | namespace | 我能**看到**什么 | 容器里能看见宿主的进程和文件,隔离形同虚设 | | cgroup | 我能**用**多少 | 一个容器跑满 CPU/内存,把整台机器上的邻居一起拖垮 | > 一句话:**容器 = 一组被 namespace 隔离、被 cgroup 限量的普通进程,它们共用宿主机那一个内核。** --- ## 3. 亲手验证隔离 原理听着抽象,不如自己看一眼。依次执行下面几条命令,你会亲眼看到"容器内的 PID 1,就是宿主机上的某个普通进程"。 ```bash # ── 在容器里看:只有一个进程 ──────────────────────────── docker run -it --rm --name demo alpine sh / # ps aux PID USER TIME COMMAND 1 root 0:00 sh ← 就这一个,它是 PID 1 7 root 0:00 ps aux / # hostname a3f5c1e9b2d4 ← 容器 ID 前 12 位,与宿主机无关 ``` ```bash # ── 在宿主机上看:同一个进程,另一个 PID ───────────────── docker top demo # UID PID PPID C STIME TTY TIME CMD # root 8123 8104 0 10:12 pts/0 00:00:00 sh docker inspect -f '{{.State.Pid}}' demo # 8123 ← 和 docker top 里的 PID 完全一致 # ── 再看 cgroup:故意撑爆内存限额 ─────────────────────── docker run -d --name memtest --memory 128m --memory-swap 128m alpine \ sh -c 'head -c 200m /dev/zero > /tmp/big && sleep 30' docker ps -a --filter name=memtest # Exited (137) docker inspect -f '{{.State.OOMKilled}}' memtest # true ← 137 = 128 + 9,被 SIGKILL 杀掉 ``` 同一个 `sh` 进程,在容器里自称 PID 1,在宿主上却是 PID 8123。它并没有被"虚拟化",只是被套上了 namespace 这层滤镜。内存那条命令则说明:**内存超限是直接杀,不是变慢**,这一点和 CPU 限流的表现完全不同,排查时要分清楚。 > 一句话:**容器不是"造出来的东西",而是被内核贴了标签的进程;你验证隔离,验的其实是内核的 namespace 视图。** --- ## 4. 镜像与容器的关系 最省事的类比是**类与实例**:镜像是类,容器是实例,一份镜像可以 `docker run` 出 N 个互不干扰的容器。更准确的说法是:镜像由一叠**只读层**堆成,容器是在这叠只读层顶上再加一张**可写层**。  *图:Dockerfile 经 build 生成只读镜像,镜像可起多个容器,容器删掉只丢可写层* ```text Dockerfile 镜像(只读,可复用) 容器(运行中) ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ FROM ... │ │ │ │ 可写层(容器专属)│ ← 新增 │ COPY ... │ ├──────────────────┤ ├──────────────────┤ │ RUN ... │ build │ 应用层(不可变) │ run │ 应用层(不可变) │ │ CMD ... │ ─────→ ├──────────────────┤ ─────→ ├──────────────────┤ │ │ │ 依赖层(不可变) │ │ 依赖层(不可变) │ │ │ ├──────────────────┤ ├──────────────────┤ │ │ │ 基础层(不可变) │ │ 基础层(不可变) │ └──────────────┘ └──────────────────┘ └──────────────────┘ 一份镜像 一个运行态 ``` 抓住三点:**层是只读且共享的**(10 个容器跑同一镜像,宿主机上只存一份只读层);**层是缓存的基础**(某一层没变,重建时就能复用,这是第 01 篇的优化重点);**可写层是易失的**(容器删掉,可写层一起删,运行时产生的数据全没)。 最后一条是新手最常踩的坑:往容器里写日志、传文件,一重启全没了。数据怎么留下来是第 02 篇要解决的问题,这里先记住结论。 > 一句话:**镜像是只读模板,容器是"只读模板 + 一层临时可写层";可写层随容器生死,不是存储方案。** --- ## 5. 这门课的路线 每一篇都是"先讲清为什么,再动手做一遍",不堆 API 手册。 | 篇 | 标题 | 讲什么 | 学完能做什么 | |---|---|---|---| | 00 | 课程总览:容器到底是什么 | 本文 | 明白容器隔离原理,装好环境并验证 | | 01 | 镜像与分层:Dockerfile 从写到跑 | 分层、缓存、多阶段构建、基础镜像选型 | 把 Spring Boot 项目打成体积合理、能上生产的镜像 | | 02 | 容器运行时:入口、网络、数据与优雅退出 | `ENTRYPOINT`/`CMD`、端口映射、数据卷、信号与退出码 | 让容器稳定跑起来,配置和数据都不丢 | | 03 | 镜像仓库:标签规范、推拉与私有仓库 | tag 语义、`push`/`pull`、Harbor、凭据 | 把镜像规范地存进仓库,团队之间能共享 | | 04 | CI/CD:从提交代码到镜像进仓库 | 流水线阶段、构建缓存、tag 与提交号绑定 | 提交代码后自动出镜像并推送 | | 05 | 实战:源码 → 镜像 → K8s 上线 | 串起前五篇 | 从一份源码一路做到 K8s 上可访问 | **刻意的取舍**:这门课**不讲** Swarm、不讲容器编排(那是「云原生」课 K8s 的活)、不讲 CRI/OCI 规范全文、不讲 runc 源码。这些不是不重要,而是当你还在纠结"Dockerfile 怎么写才不重下依赖"时,讲它们只会分散注意力。等镜像能又快又稳地产出来,再回去接 K8s 才顺。 --- ## 6. 环境准备 三种方式任选一种,后续命令通用: | 方案 | 适合谁 | 优点 | 注意事项 | |---|---|---|---| | Docker Desktop | Windows / macOS 桌面用户 | 一键安装,自带 Compose 与图形界面 | 默认走虚拟机,磁盘占用较大 | | 原生 Docker Engine | Linux 或 Windows + WSL2 | 行为最贴近生产,无额外虚拟化层 | 需要会用 `systemctl` 与 `daemon.json` | | 云主机自建 | 想练"真服务器"流程 | 最接近线上,端口/防火墙问题都能遇到 | 要花钱,练完记得关机 | 以 **Linux(CentOS 系)** 为例: ```bash # 1. 装工具并添加 Docker 官方源 sudo yum install -y yum-utils sudo yum-config-manager --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo # 2. 安装三个组件:引擎、命令行、底层运行时 sudo yum install -y docker-ce docker-ce-cli containerd.io # 3. 开机自启并立刻启动 sudo systemctl enable --now docker # 4. 免 sudo(可选,重新登录后生效) sudo usermod -aG docker $USER ``` 装完先别急着构建,**国内网络下一定要配镜像加速器**,否则拉基础镜像会卡到怀疑人生: ```bash sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<'EOF' { "registry-mirrors": [ "https://docker.m.daocloud.io", "https://hub-mirror.c.163.com" ], "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } } EOF # 改 daemon.json 必须走这两步,只 restart 不 reload 不生效 sudo systemctl daemon-reload sudo systemctl restart docker ``` 最后做三项验证,三条都过就算环境没问题: ```bash docker version # 能看到 Client 和 Server 两段,说明引擎真的起来了 docker info # 看 Storage Driver 与 Registry Mirrors 是否符合预期 docker run hello-world # 打印 Hello from Docker!,即拉镜像→起容器→跑进程整条链路通 ``` `docker version` 的期望输出是两段式(只有 Client 段说明守护进程没起来): ```text Client: Docker Engine - Community Version: 24.0.7 API version: 1.43 OS/Arch: linux/amd64 Server: Docker Engine - Community Engine: Version: 24.0.7 API version: 1.43 (minimum version 1.12) containerd: 1.6.25 runc: 1.1.9 ``` `docker info` 里重点确认这几行: ```text Server: Server Version: 24.0.7 Storage Driver: overlay2 ← 分层靠它,讲镜像层时会用到 Cgroup Driver: systemd Kernel Version: 5.15.0-91-generic CPUs: 4 Total Memory: 7.757GiB Registry Mirrors: https://docker.m.daocloud.io/ ← 加速器生效了 ``` > 一句话:**环境只需要三条验证命令都过:`docker version` 看两段、`docker info` 看加速器、`docker run hello-world` 看能不能跑。** --- ## 7. 阅读方式建议  *图:三遍读法:先只读建立认知,再跟着敲一遍命令,最后动手改镜像与构建顺序* ```text 第一遍:只读,不敲命令 ──→ 建立"容器为什么能隔离、镜像是什么"的认知 │ 第二遍:跟着敲 ──────────→ 第 3 节验证隔离的命令都手动跑一遍 │ 第三遍:动手改 ──────────→ 换基础镜像、换构建顺序,观察体积和构建时间的变化 ``` **一个提醒**:学容器最容易卡住的地方,不是命令记不住,而是**把容器当虚拟机用**——进容器装软件、改配置、存数据。这些改动都落在可写层,容器一删全没。下次冒出"进容器里改一下"的冲动时,先问自己:这个改动是不是该写进 Dockerfile 或挂载卷? --- ## 8. 踩过的坑 ```text 1. 容器里装的东西重启就没 → 改动落在可写层,容器删了层也没 → 长期改动写进 Dockerfile,数据挂 Volume 2. docker version 只有 Client 段 → dockerd 没启动,或用户不在 docker 组 → systemctl status docker;usermod -aG docker 后重新登录 3. 改了 daemon.json 不生效 → 只 restart 没 daemon-reload,或 JSON 语法写错 → 先校验 JSON,再 daemon-reload 后 restart 4. 拉镜像卡在 Pulling fs layer → 走默认 Docker Hub,网络不通 → 配 registry-mirrors,用 docker info 确认已生效 ``` --- ## 9. 本篇小结 ```text ┌────────────────────────────────────────────────────────┐ │ 本篇要带走的四句话 │ ├────────────────────────────────────────────────────────┤ │ 1. 容器不是轻量虚拟机,它没有自己的内核 │ │ 2. 隔离靠 namespace(看到什么)+ cgroup(用多少) │ │ 容器内的 PID 1,就是宿主机上的某个普通 PID │ │ 3. 镜像是只读层叠成的模板,容器 = 只读层 + 临时可写层 │ │ 删容器,可写层和数据一起没 │ │ 4. 环境只需三条命令验证:version / info / run hello-world │ └────────────────────────────────────────────────────────┘ │ v 知道了"容器是什么",下一个问题就是"镜像从哪来" ──→ 第 01 篇 ``` 下一篇:[01 - 镜像与分层:Dockerfile 从写到跑]
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入