1_Docker核心原理与实践
1. 核心概念
Docker 体系围绕三个核心对象展开:镜像(Image)、容器(Container)、仓库(Repository)。
1.1 镜像
镜像是一个只读的文件系统模板,包含运行应用所需的代码、运行时、系统工具、库文件和配置。
与 ISO 不同,Docker 镜像并非单一打包文件,而是由多层文件系统联合组成的虚拟概念。底层技术是 Union FS(联合文件系统),每一层只记录相对于上一层的增量变化。分层设计带来两个直接好处:
- 共享基础层:多个镜像可以共享相同的底层(如同一个
ubuntu:22.04基础层),节省磁盘空间。 - 构建缓存:重新构建镜像时,未变更的层直接复用,加快构建速度。
1.2 容器
容器是镜像的运行实例。启动容器时,Docker 以镜像为基础层,在其上创建一个可读写的容器存储层。
容器存储层应保持无状态化,需要持久化的数据使用数据卷(Volume)或绑定挂载(Bind Mount)。这些方式跳过容器存储层,直接对宿主机(或网络存储)读写,性能和稳定性更高。数据卷的生存周期独立于容器——容器删除后,数据卷中的数据不会丢失。
1.3 仓库
仓库(Repository)用于集中存储同一软件的不同版本镜像,通过标签(Tag)区分版本。Registry(如 Docker Hub、Harbor)是仓库的托管服务,一个 Registry 下包含多个 Repository。
- 格式:
<仓库名>:<标签>,如ubuntu:22.04 - 不指定标签时默认使用
latest - 仓库名常以两段式路径出现,如
nginx/nginx-ingress,前者是用户名/组织名,后者是软件名
1.4 三者的关系
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
R["仓库 Repository"] -->|"docker pull"| I["镜像 Image"]
I -->|"docker run"| C["容器 Container"]
C -->|"docker commit"| I
I -->|"docker push"| R
C -..->|"挂载"| V["数据卷 Volume"]
classDef primary fill:#3B82F6,stroke:#2563EB,color:#fff
classDef success fill:#10B981,stroke:#059669,color:#fff
classDef warning fill:#F59E0B,stroke:#D97706,color:#fff
class R primary
class I success
class C,V warning工作流:从仓库 pull 镜像到本地 → 基于镜像 run 出容器 → 容器运行中产生的变更可通过 commit 保存为新镜像 → 新镜像 push 回仓库。容器运行时可挂载数据卷实现数据持久化。
2. 容器底层原理
容器和虚拟机有什么区别?回答这个问题,需要理解容器的两大支柱技术——Namespace 和 Cgroups。
2.1 从进程隔离说起
在 Linux 中,所有进程共享同一个内核、同一套文件系统、同一个网络栈,默认可以互相看到、互相通信。
容器的核心思路是:不启动新内核,而是通过内核提供的隔离机制,让一组进程 “ 以为 “ 自己运行在一个独立的系统中。
类比:一栋大楼(Linux 内核)中,每个房间(容器)共享水电网基础设施,但墙壁(Namespace)让住户互相看不到,电表和水表(Cgroups)限制每户能使用的资源总量。
2.2 Namespace:资源隔离
Linux Namespace 是内核提供的隔离机制,每种 Namespace 隔离一类系统资源。容器主要依赖以下六种(Linux 内核还提供 Cgroup 和 Time Namespace,Docker 20.10+ 默认启用 Cgroup Namespace):
| Namespace | 隔离内容 | 容器中的效果 |
|---|---|---|
| PID | 进程 ID | 容器内进程从 PID 1 开始编号,看不到宿主机的其他进程 |
| NET | 网络栈 | 容器拥有独立的网卡、IP 地址、端口空间和路由表 |
| MNT | 文件系统挂载点 | 容器有自己的根文件系统,看不到宿主机的目录结构 |
| UTS | 主机名和域名 | 容器可以设置自己的 hostname,不影响宿主机 |
| IPC | 进程间通信 | 容器内的信号量、消息队列、共享内存与其他容器隔离 |
| User | 用户和用户组 ID | 容器内的 root 用户可映射为宿主机的普通用户 |
直观验证:在容器内执行 ps aux 只能看到容器自己的进程;hostname 返回容器名称而非宿主机名。这些 “ 幻觉 “ 全部由 Namespace 制造。
2.3 Cgroups:资源限制
Namespace 解决了 “ 看到什么 “ 的问题,Cgroups 解决 “ 能用多少 “ 的问题。不加限制的容器可以吃掉宿主机所有 CPU 和内存。
Control Groups(Cgroups)用于限制、记录和隔离进程组的资源使用,主要控制以下资源:
| 子系统 | 控制内容 | 示例 |
|---|---|---|
| cpu | CPU 使用时间 | 限制容器最多使用 2 个核心 |
| memory | 内存用量 | 限制容器最多使用 512MB,超出触发 OOM Killer |
| blkio | 块设备 I/O | 限制容器的磁盘读写速率 |
| pids | 进程数量 | 防止容器内 fork bomb |
Docker 通过 docker run 的参数直接操作 Cgroups:
1 | # 限制容器最多使用 2 个 CPU 核心、512MB 内存 |
2.4 虚拟机 Vs 容器
理解了 Namespace 和 Cgroups,就能清晰区分二者的架构差异:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart TB
subgraph vm ["虚拟机架构"]
direction TB
HW1["硬件 Hardware"]
HV["Hypervisor"]
subgraph g1 ["Guest OS 1"]
A1["App A"]
L1["Libs"]
K1["完整内核"]
end
subgraph g2 ["Guest OS 2"]
A2["App B"]
L2["Libs"]
K2["完整内核"]
end
HW1 --> HV --> g1 & g2
end
subgraph ct ["容器架构"]
direction TB
HW2["硬件 Hardware"]
HK["宿主机内核"]
DR["Docker Engine"]
subgraph c1 ["Container 1"]
CA1["App A"]
CL1["Libs"]
end
subgraph c2 ["Container 2"]
CA2["App B"]
CL2["Libs"]
end
HW2 --> HK --> DR --> c1 & c2
end
classDef hw fill:#64748B,stroke:#475569,color:#fff
classDef hyper fill:#8B5CF6,stroke:#7C3AED,color:#fff
classDef kernel fill:#EF4444,stroke:#DC2626,color:#fff
classDef docker fill:#3B82F6,stroke:#2563EB,color:#fff
classDef app fill:#10B981,stroke:#059669,color:#fff
class HW1,HW2 hw
class HV hyper
class K1,K2,HK kernel
class DR docker
class A1,A2,CA1,CA2,L1,L2,CL1,CL2 app| 维度 | 虚拟机 | 容器 |
|---|---|---|
| 隔离机制 | Hypervisor + 独立内核 | Namespace + Cgroups(共享内核) |
| 启动速度 | 分钟级(需引导完整 OS) | 秒级(直接启动进程) |
| 资源开销 | 每个 VM 需独立内核 + 系统进程,通常 GB 级 | 几乎无额外开销,只有应用本身的消耗 |
| 镜像体积 | GB 级(包含完整 OS) | MB 级(仅包含应用及依赖) |
| 性能 | 有虚拟化层损耗 | 接近原生性能 |
| 安全边界 | 强隔离(独立内核,攻击面小) | 弱隔离(共享内核,内核漏洞会影响所有容器) |
| 典型场景 | 多租户强隔离、异构 OS 共存 | 微服务部署、CI/CD、开发环境一致性 |
简言之:虚拟机虚拟的是硬件,每个 VM 运行独立内核;容器虚拟的是操作系统环境,所有容器共享宿主机内核。容器更轻更快,但安全隔离不如虚拟机。
3. 镜像原理与操作
3.1 Union FS 与分层存储
Docker 镜像底层通过存储驱动(默认 overlay2)实现分层存储,overlay2 基于内核的 OverlayFS 提供联合挂载能力,将多个只读层叠加成一个统一的文件系统视图。每一条 Dockerfile 指令(RUN、COPY 等)都会产生新的一层。
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart TB
subgraph container ["容器运行时"]
RW["可读写层(容器层)"]
end
subgraph image ["镜像(只读)"]
L3["Layer 3: COPY app.js"]
L2["Layer 2: RUN npm install"]
L1["Layer 1: RUN apt-get update"]
L0["Base Layer: ubuntu:22.04"]
end
RW --> L3 --> L2 --> L1 --> L0
classDef writable fill:#F59E0B,stroke:#D97706,color:#fff
classDef readonly fill:#3B82F6,stroke:#2563EB,color:#fff
classDef base fill:#10B981,stroke:#059669,color:#fff
class RW writable
class L1,L2,L3 readonly
class L0 base- 镜像层:所有层都是只读的。多个容器共享同一镜像层时,底层文件不会被复制多份。
- 容器层:容器启动时在镜像最上方添加一个可读写层。容器内的文件修改都发生在这一层,容器删除后该层随之消失。
3.2 Copy-on-Write
当容器需要修改镜像层中的某个文件时,不会直接改动原始层,而是将该文件从只读层复制到可读写的容器层,再在容器层中修改。后续读取该文件时,从容器层读取修改后的版本,原始镜像层保持不变。
这一机制带来三个好处:
- 快速启动:启动容器不需要复制整个镜像文件系统,只需创建一个薄薄的可写层。
- 共享存储:100 个基于同一镜像的容器,底层只存一份镜像数据。
- 镜像不可变:镜像层永远不会被修改,保证可复现性。
3.3 镜像操作
3.3.1 获取镜像
1 | docker pull [Docker Registry 地址[:端口号]/]仓库名[:标签] |
- Registry 地址:格式为
<域名/IP>[:端口号],默认为 Docker Hub。 - 仓库名:两段式
<用户名>/<软件名>。Docker Hub 不指定用户名时默认为library(官方镜像)。
1 | docker pull ubuntu:22.04 |
3.3.2 列出本地镜像
1 | docker images |
列表包含仓库名、标签、镜像 ID、创建时间和占用空间。镜像 ID 是唯一标识,一个镜像可对应多个标签。
3.3.3 清理虚悬镜像
docker pull 或 docker build 都可能产生虚悬镜像——当新旧镜像同名时,旧镜像的名称被取消,出现仓库名和标签均为 <none> 的镜像(dangling image)。
1 | # 查看虚悬镜像 |
3.3.4 删除镜像
1 | docker image rm <镜像ID或名称> |
镜像操作掌握后,接下来看容器的完整生命周期管理。
4. 容器操作
容器的完整生命周期如下:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F'}}}%%
stateDiagram-v2
[*] --> Created: "docker create"
Created --> Running: "docker start"
[*] --> Running: "docker run"
Running --> Paused: "docker pause"
Paused --> Running: "docker unpause"
Running --> Stopped: "docker stop"
Running --> Stopped: "进程退出"
Stopped --> Running: "docker start"
Stopped --> Removed: "docker rm"
Removed --> [*]4.1 新建并启动
1 | docker run -it --rm ubuntu:22.04 bash |
-i:保持标准输入打开(交互模式)。-t:分配伪终端。-it组合使用可获得交互式 Shell。--rm:容器退出后自动删除,适合临时使用。ubuntu:22.04:指定基础镜像。bash:覆盖镜像默认的启动命令。
docker run 背后的完整流程:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart TD
A["docker run"] --> B{"本地存在镜像?"}
B -->|"否"| C["从仓库下载镜像"]
B -->|"是"| D["创建容器"]
C --> D
D --> E["创建 Namespace 隔离环境"]
E --> F["通过 Cgroups 设置资源限制"]
F --> G["挂载只读镜像层 + 可读写层"]
G --> H["桥接虚拟网络接口 + 分配 IP"]
H --> I["执行用户指定程序"]
I --> J["程序退出,容器终止"]
classDef primary fill:#3B82F6,stroke:#2563EB,color:#fff
classDef success fill:#10B981,stroke:#059669,color:#fff
classDef decision fill:#F59E0B,stroke:#D97706,color:#fff
classDef danger fill:#EF4444,stroke:#DC2626,color:#fff
class A primary
class B decision
class C,D,E,F,G,H,I success
class J danger4.2 后台运行
添加 -d 参数让容器在后台运行:
1 | docker run -d --name my-nginx -p 8080:80 nginx |
常用参数组合:
| 参数 | 作用 |
|---|---|
-d | 后台运行 |
--name | 指定容器名称 |
-p 宿主:容器 | 端口映射 |
-v 宿主:容器 | 数据卷挂载 |
-e KEY=VALUE | 设置环境变量 |
--restart always | 容器退出后自动重启 |
4.3 查看容器
1 | # 查看运行中的容器 |
4.4 进入容器
1 | docker exec -it <容器ID或名称> bash |
-it 组合使用获得交互式 Shell。如果容器内没有 bash(如 Alpine 镜像),使用 sh 代替。
4.5 启动 / 停止 / 重启
1 | docker container start <容器> # 启动已终止的容器 |
4.6 导出与导入
1 | # 导出容器快照为 tar 文件 |
docker load 导入镜像文件(保留完整构建历史),docker import 导入容器快照(仅保留当前状态,可重新指定标签等元数据)。
4.7 删除与清理
1 | # 删除已终止的容器 |
容器操作掌握了,接下来看如何通过仓库分发和管理镜像。
4.8 文件拷贝
docker cp 用于在容器和宿主机之间拷贝文件或目录,常用于调试场景(如拷出日志、临时注入配置文件)。
1 | # 宿主机 → 容器 |
注意:docker cp 不依赖容器是否运行,对已停止的容器同样有效。生产环境中应优先使用数据卷挂载,docker cp 仅作为临时调试手段。
4.9 容器访问宿主机服务
容器内的应用有时需要访问宿主机上运行的服务(如本地数据库、API 网关)。不同平台的处理方式有差异:
| 平台 | 宿主机地址 | 说明 |
|---|---|---|
| Linux(默认桥接网络) | 172.17.0.1(docker0 网关) | 默认值,可能因自定义网络配置而不同 |
| macOS / Windows | host.docker.internal | Docker Desktop 内置 DNS 解析 |
| Linux(Docker 20.10+) | host.docker.internal | 需显式启用 |
1 | # Linux 上显式启用 host.docker.internal |
Docker Compose 中的写法:
1 | services: |
推荐统一使用 host.docker.internal,避免硬编码 IP 地址,实现跨平台兼容。
5. 仓库与镜像分发
5.1 Docker Hub
Docker Hub 是 Docker 官方的公共镜像仓库。
1 | # 登录 |
镜像分两类:
- 官方镜像(如
nginx、redis):Docker 官方维护,单个单词命名。 - 用户镜像(如
bitnami/redis):带有用户名/组织名前缀。
5.2 推送镜像
1 | # 给本地镜像打标签 |
5.3 自动构建
生产环境通常通过 GitHub Actions、GitLab CI 等 CI/CD 工具实现镜像自动构建与推送,在代码提交或创建标签时触发流水线,完成构建、测试、推送到 Registry 的全流程。
5.4 私有仓库
当不希望镜像公开、或需要内网分发时,可以搭建私有仓库。
5.4.1 Docker Registry(轻量方案)
docker-registry 是官方的轻量级私有仓库工具,适合个人和小团队:
1 | # 启动私有 Registry |
5.4.2 Harbor(企业级方案)
Harbor 是 VMware 开源的企业级镜像仓库,在 Docker Registry 基础上增加了:
- 基于角色的访问控制(RBAC)
- 镜像漏洞扫描(集成 Trivy)
- 镜像签名与审计日志
- 多仓库复制(跨数据中心同步)
- Web UI 管理界面
适合团队和生产环境使用。
会拉会推之后,还差最后一步——如何在生产环境中安全、稳定地运行容器。
6. 生产实践
6.1 容器资源限制
不限制资源的容器是生产事故的隐患——一个失控的进程可能耗尽宿主机所有 CPU 或内存,拖垮同机的其他服务。
6.1.1 CPU 限制
1 | # 限制容器最多使用 1.5 个 CPU 核心 |
6.1.2 Memory 限制
1 | # 限制最大内存为 256MB,超出时触发 OOM Killer |
6.1.3 查看容器资源使用
1 | # 实时监控所有容器的 CPU、内存、网络、磁盘 I/O |
6.2 容器日志管理
容器日志默认以 JSON 文件存储在宿主机上(/var/lib/docker/containers/<容器ID>/<容器ID>-json.log)。不配置日志轮转,日志文件会无限增长直到撑满磁盘。
6.2.1 限制单容器日志大小
1 | # 单个日志文件最大 10MB,最多保留 3 个轮转文件 |
6.2.2 全局配置日志策略
编辑 /etc/docker/daemon.json,对所有新建容器生效:
1 | { |
修改后需重启 Docker:
1 | sudo systemctl restart docker |
6.2.3 集中式日志收集
生产环境通常将容器日志发送到 ELK、Loki 等集中式日志平台。Docker 支持多种日志驱动:
1 | # 使用 fluentd 驱动,将日志发送到日志收集器 |
6.3 容器健康检查
Docker 默认只通过主进程是否存活来判断容器状态。但进程存活不代表服务可用——例如 Web 服务进入死锁状态,进程还在但不响应请求。
6.3.1 配置健康检查
1 | docker run -d \ |
参数说明:
| 参数 | 作用 | 默认值 |
|---|---|---|
--health-cmd | 健康检查命令,返回 0 表示健康 | — |
--health-interval | 检查间隔 | 30s |
--health-timeout | 单次检查超时 | 30s |
--health-retries | 连续失败多少次判为 unhealthy | 3 |
--health-start-period | 容器启动后的宽限期,此期间失败不计数 | 0s |
对于启动较慢的应用(如 Java/Spring Boot),建议将 --health-start-period 设为合理值(如 30s-60s),避免容器在启动过程中被误判为 unhealthy。
6.3.2 健康状态流转
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F'}}}%%
stateDiagram-v2
[*] --> starting: "容器启动"
starting --> healthy: "检查通过"
starting --> healthy: "宽限期内失败不计数"
healthy --> unhealthy: "连续 N 次检查失败"
unhealthy --> healthy: "检查恢复通过"查看健康检查记录:
1 | docker inspect --format='{{json .State.Health}}' <容器名> | jq |
6.4 Docker 安全实践
容器共享宿主机内核,安全隔离不如虚拟机。以下是生产环境中应遵守的安全原则。
6.4.1 不要以 Root 运行应用
容器内默认以 root 运行,一旦容器被攻破,攻击者可能借此获得宿主机的控制权。
1 | # 以 UID 1000 运行容器 |
生产镜像通常在 Dockerfile 中通过 RUN useradd + USER 指令设置运行用户,并调整相关目录权限;--user 参数适用于镜像已做好权限适配的场景。
6.4.2 以只读模式运行
对于不需要写入文件系统的容器,启用只读文件系统:
1 | docker run -d --read-only \ |
--tmpfs 挂载临时文件系统,允许应用写入 /tmp、/run 等必要路径。
6.4.3 限制容器权限
1 | # 禁止容器获取额外权限 |
6.4.4 镜像安全扫描
定期扫描镜像中的已知漏洞:
1 | # 使用 Docker Scout(Docker 官方工具) |
6.5 日常使用技巧
6.5.1 远程执行 Docker 命令
DOCKER_HOST 环境变量指定 Docker 客户端连接的 daemon 地址:
1 | DOCKER_HOST=ssh://root@10.133.0.21 docker ps |
6.5.2 使用容器代替本地安装
无需在宿主机安装客户端工具,直接用容器:
1 | # 用不同版本的 Redis 客户端连接远程服务 |
好处:保持宿主机环境清洁,轻松切换工具版本,--rm 用完即删。
6.5.3 善用 Docker Compose
对于需要多个参数的 docker run 命令,用 Docker Compose 配置文件管理更清晰、可复现:
1 | services: |
1 | docker compose up -d # 后台启动 |
6.5.4 磁盘空间清理
1 | # 清理虚悬镜像 |
7. 最佳实践速查
| 场景 | ✅ Do | ❌ Don’t |
|---|---|---|
| 镜像选择 | 使用官方镜像,优先 Alpine 变体 | 使用来源不明的第三方镜像 |
| 镜像标签 | 使用语义化版本或 Git Hash | 生产环境使用 latest |
| 资源限制 | 为每个容器设置 CPU 和内存上限 | 让容器无限制使用宿主机资源 |
| 日志管理 | 配置日志轮转限制,集中收集 | 让日志文件无限增长 |
| 运行用户 | 以非 root 用户运行应用 | 以 root 身份运行生产容器 |
| 健康检查 | 配置 HEALTHCHECK 检测服务可用性 | 仅依赖进程存活判断容器健康 |
| 数据持久化 | 使用命名数据卷或 Bind Mount | 把重要数据写入容器存储层 |
| 启动命令 | 用 Docker Compose 管理多参数启动 | 用超长 docker run 命令 |
| 安全 | 最小权限原则 + 定期扫描镜像漏洞 | 使用 --privileged 运行容器 |
| 磁盘清理 | 定期执行 docker system prune | 从不清理虚悬镜像和停止的容器 |