本文是对docker容器进行学习的随笔记录,主要记录docker容器的相关知识,如:原理、体系、环境支持、操作流程、命令行操作以及相关的技术生态系统。文章本身会根据作者对于知识的掌握深浅而进行筛选和记录,因此具有强烈的主观性。虽然是从各个专业书籍与博客搜集的记录,技术仍在进步,故此文章不具备时效性,请仔细分辨本文中的观点。若和现实生产与应用场景存在出入,作者不对因使用本笔记所产生的任何直接或间接损失承担责任
前言
关于docker容器在其他两篇文章中略有讨论过容器内部配置文件的修改,这只是docker容器的冰山一角。(见右边两篇博客)
Docker 容器的核心价值是:把“应用 + 运行环境依赖”封装成可移植、可复现、可隔离的交付单元,从而在开发、测试、上线、运维等环节减少环境差异与交付成本。例如:
1) 开发环境标准化(Dev Environment)
场景痛点:不同开发者电脑系统/版本/依赖不同,“我这能跑你那不能跑”。
容器用法:
- 用
Dockerfile固化语言运行时、依赖库、工具链版本(Node/Python/JDK/LLVM 等)。 - 用
docker compose一键拉起整套依赖:数据库、缓存、消息队列、对象存储等。 - VS Code Dev Containers / JetBrains Gateway 让 IDE 直接连接容器开发。
2)微服务架构与服务拆分
场景痛点:多个服务依赖不同语言/版本,传统部署隔离差、冲突多。
容器用法:
- 一个服务一个镜像;各自独立发布、扩缩容;
- 通过服务发现、网关、sidecar(如 service mesh)统一治理。
3) 测试环境快速搭建(Integration/E2E Testing)
场景痛点:测试环境搭建慢、依赖复杂,环境不干净导致测试不稳定。
容器用法:
docker compose拉起临时环境跑集成测试;- 使用 Testcontainers(Java/Go/Node 等)在测试时动态启动 MySQL/Redis/Kafka;
- 测完即销毁,保证每次环境干净一致。
还有很多便携之处就不一一举例了
安全方面
容器相对于传统的无容器部署,它可以更容易做到最小权限和实时监控。同时在滚动升级和漏洞修复方面也不逊色于传统的无容器部署。
网安的圈子里有一个攻击方式:容器逃逸。这就是黑客先进入docker容器创造的应用程序当中,再通过挂载危险路径/var/run/docker.sock或者通过privileged模式来操作/dev等模块,具体的细节在后面进行赘述。
第一章 Docker基础
1、Docker’s Orders
docker命令的执行一般都需要root权限,这是因为docker的命令行工具docker和Docker daemon是同一个二进制文件,同时后者负责接收并执行来自docker输入的命令,它的运行需要root权限。
在docker命令中有很多基础的命令需要重点记住:
| 子命令分类 | 子命令 |
| docker环境信息 | info、version |
| 容器生命周期管理 | create、exec、kill、pause、restart、rm、run、start、stop、unpause |
| 镜像仓库命令 | login、logout、pull、push、search |
| 镜像管理 | build、images、import、load、rmi、save、tag、commit |
| 容器运维操作 | attach、export、inspect、port、ps、rename、stats、top、wait、cp、diff、update |
| 容器资源管理 | volumn、network |
| 系统日志信息 | events、history、logs |



1.1 docker run
docker run是Docker的核心命令之一,所有容器的启动都需要这一命令来操作,例如:
sudo docker run ubuntu echo “Hello World”:
Docker 以 ubuntu15.10 镜像创建一个新容器,然后在容器里执行bin/echo“Hello world”,然后输出结果pulling from library/ubuntu:
拉取ubuntu镜像Hello World:
用ubuntu镜像输出Hello World

在启动ubuntu这个容器后,docker会给这个容器分配一个ID以方便识别,就是上图中的fd8cda969ed2
如果想要进入容器进行操作的话,可以先用docker run启动一个容器并分配一个伪终端来操作
sudo docker run -i -t --name mytest ubuntu:latest /bin/bash
- -i 启动交互
- -t 分配一个伪终端
前两者可融合变为-it - –name 分配容器名字
除此之外,还有:
- -c 分配所占的CPU权重
- -m 分配容器内存
- -v 挂载一个数据卷volume
- -p 暴露端口
1.2 docker start/stop/restart
字面意思的命令
但docker start可以使用-i来启动交互模式,docker stop和docker restart可以使用-t来设定停止前的等待时间
1.3 docker pull/push
pull是从Docker registry拉取image或repository。也可以从Docker Hub拉取镜像资源
push是将本地的image或repository推送到Docker Hub的公共或者个人私有仓库
1.4 docker rmi/rm
docker rm用于删除容器docker rmi用于删除镜像
第二章 Docker核心原理
1、Docker的内核
Docker容器本质是借助宿主机的进程来运行,说白了他也是一个需要利用宿主机内存的进程。Docker利用namespace实现了资源隔离,利用cgroups实现了资源限制,又通过写时复制机制(copy-on-write)实现了高效的文件操作。
1.1 namespace资源隔离
每一个容器都是独立存在的,但又没有完全独立。前面一句看似很矛盾,但以人来打个比方的话,就是:
每个人是独立存在的,但又和社会脱离不了干系
每个容器启动之后,会借用linux内核中的namespace隔离的系统调用来解决最基本的6项隔离:
| namespace | 隔离内容 |
| UTS | 主机名与域名 |
| IPC | 信号量、消息队列和共享内存 |
| PID | 进程编号 |
| Netword | 网络设备、网络栈、端口等 |
| Mount | 挂载点 |
| User | 用户和用户组 |
1.1.1 进行namespace API操作的4种方式
a、clone()创建新进程的同时创建namespace
使用clone()来创建一个独立namespace的进程是最常见的做法,实际上clone()是linux系统调用fork()的一种通用的实现方式,可以用flags来控制使用多少功能。
b、查看/proc/[pid]/ns文件
在该路径文件下,可以查看指向不同namespace的文件,如果两个进程指向的namespace编号相同,就说明它们在同一个namespace下,否则便在不同namespace里面。
c、setns()加入一个已经存在的namespace
通过挂载的形式,可以在进程都结束的情况下把namespace保留下来,保留下来的目的是为了以后有进程加入做准备。在Docker中,使用docker exec命令在已经运行着的容器中执行一个新的命令,就需要用到该方法。
d、unshare()在原先进程上进行namespace隔离
这个和clone()很类似,不同的是,unshare()运行在原先的进程上,不需要启动一个新进程。
调用这个函数的主要作用就是不启动新进程的情况下,起到隔离的效果,相当于跳出原先的namespace进行操作。
*、附加:fork()系统调用
fork()的一个核心就是:这个函数被调用一次,却能够返回两次,而且可以根据返回值的不同就可以区分父进程和子进程:
- 在父进程中,fork()返回新创建子进程的进程ID
- 在子进程中,fork()返回0
- 如果出现错误,fork()返回一个负值
fork()的一个简单示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("I am child process, PID: %d\n", getpid());
} else if (pid > 0) {
// 父进程
printf("I am parent process, PID: %d, child PID: %d\n", getpid(), pid);
} else {
// fork 失败
perror("fork failed");
exit(1);
}
return 0;
}
输出:
I am parent process, PID: 1234, child PID: 1235
I am child process, PID: 1235
从输出就能看得出来fork()的一个作用就是生成父子进程
1.1.2 namespace
代码解释起来太过于痛苦,它的一个作用就是为了防止普通用户任意修改系统主机名导致set-user-ID相关的应用运行出错。
1.1.3 IPC namespace
1.1.4 PID namespace
⬇这里的东西推荐看看这本书 P33页开始⬇
Docker容器与容器云(第2版)⬆文件下载在这⬆
1.2 cgroups资源限制
cgroups在容器当中可以起到下面四个作用
- 资源限制
对任务使用的资源总额进行限制 - 优先级分配
通过分配的CPU时间片数量以及磁盘IO宽带大小,直接控制了任务运行的优先级 - 资源统计
统计系统的资源使用量,适合计费(唉,资本) - 任务控制
对任务执行挂起、回复等操作
1.2.1 子系统
子系统是cgroups的资源控制系统,同时每个子系统独立地控制一种资源。目前docker使用的子系统有9种,分别如下:
- blkio:为块设备设定输入/输出限制
- cpu:使用调度程序控制任务对CPU的使用
- cpuacct:自动生成cgroups中任务对CPU资源使用情况的报告
- cpuset:为cgroups中的人物分配独立的CPU和内存
- devices:开启或关闭cgroups中 任务对设备的访问
- freezer:挂起或回复cgroups中的任务
- memory:限定cgroups中任务的内存使用量,并且自动生成相应的资源使用报告
- perf_event:可以让cgroups中的任务进行统一的性能测试
- net_cls:通过等级识别符(classid)让linux流量控制程序识别cgroups中生成的数据包
Docker对cgroups本身没有做过增强,或者说cgroups现阶段可以满足Docker的需求。用户也不需要直接操作cgroups,所以对于cgroups只需要认识到底层运作当中有这么个东西就好了
2、Docker架构
Docker 的整体架构是一个典型的客户端-服务器(C/S)模式,主要由以下几大部分组成:
- Docker Client(客户端)
- Docker Daemon(守护进程 / dockerd)
- Docker 对象(Image、Container、Network、Volume 等)
- 底层依赖(Linux 内核特性 + 各种驱动/库)
2.1 Docker Client
- docker CLI 命令行工具
- Docker Desktop 的图形界面——Windows下的docker操控界面居多
- IDE 插件(VS Code、IntelliJ 等)
- CI/CD 工具中的 docker 命令
- 远程 API 调用(REST API)
作用:把我们输入的命令(如 docker run、docker build、docker pull 等)转换成 HTTP/REST 请求,发给 Docker Daemon。
2.2 核心:Docker Daemon
Docker 的真正“大脑”,一个常驻后台的进程。它负责相应来自Docker client的请求,然后翻译成docker能“听懂”的语言来调用完成容器管理操作
主要职责:
- 监听 API 请求(默认
unix socket:/var/run/docker.sock,也支持 tcp) - 管理 Docker 的全生命周期:build、pull、push、run、stop、rm 等
- 协调镜像、容器、网络、存储等各种子模块
2.3 底层:Linux 内核能力
Docker 本身不包含虚拟化能力,所有隔离都依赖 Linux 内核提供的原生特性:
| 内核特性 | 作用 | 对应隔离维度 |
|---|---|---|
| Namespaces | 进程、网络、用户、挂载、IPC、UTS 等隔离 | 看起来像独立系统 |
| cgroups | 资源限制(CPU、内存、IO、设备等) | 防止容器吃光宿主机 |
| Union FS / Overlay FS | 分层镜像、写时复制(CoW) | 镜像层高效叠加 |
| Netfilter / iptables | 网络 NAT、端口映射 | 容器间、内外通信 |
| Seccomp / AppArmor / SELinux | 安全加固(可选) | 限制系统调用 |
用户 → Docker Client
↓ (REST API)
Docker Daemon (dockerd)
├─ API Server
├─ Image 管理(distribution / layer / image / registry / reference)
├─ graphdriver(存储驱动:overlay2 / aufs / devicemapper …)
├─ execdriver → containerd + runc(执行驱动)
├─ network → libnetwork / containerd network
└─ volumedriver(卷驱动:local / 插件 …)
↓
底层:Linux Kernel
├─ namespaces
├─ cgroups
└─ Union FS / Overlay FS / Device Mapper / …
以上就是平常能够接触到的、比较浅层的架构。
以下开始就是Docker daemon文件当中会用到的几个驱动(driver),由这些driver来调用docker容器内部。它们分别是——execdriver、volumedriver、graphdriver
execdriver
execdriver是对Linux操作系统的namespaces、cgroups、apparmor、SELinux等容器运行所
需的系统操作进行的一层二次封装,其本质作用类似于LXC,但是功能要更全面。这也
就是为什么LXC会作为execdriver的一种实现而存在。当然,execdriver最主要的实现,也
是现在的默认实现,是Docker官方编写的libcontainer库。
说白了execdriver要负责管理进程管理包括启动、停止、重启等操作,还为容器提供了一个独立地执行环境,监控容器内的进程
volumedriver
volumedriver是volume数据卷存储操作的最终执行者,负责volume的增删改查,屏蔽不同
驱动实现的区别,为上层调用者提供一个统一的接口。Docker中作为默认实现的
volumedriver是local,默认将文件存储于Docker根目录下的volume文件夹里。其他的
volumedriver均是通过外部插件实现的。
graphdriver
graphdriver是所有与容器镜像相关操作的最终执行者。graphdriver会在Docker工作目录下
维护一组与镜像层对应的目录,并记下镜像层之间的关系以及与具体的graphdriver实现相
关的元数据。这样,用户对镜像的操作最终会被映射成对这些目录文件以及元数据的增
删改查,从而屏蔽掉不同文件存储实现对于上层调用者的影响。在Linux环境下,目前
Docker已经支持的graphdriver包括aufs、btrfs、zfs、devicemapper、overlay和vfs。
3、client和daemon
3.1 client模式
docker命令对应的源文件是docker/docker.go,它的使用方式如下
docker [OPTIONS] COMMAND [arg……]
'''
举例说明:平常启动容器后会经常调用一个命令 docker ps (查看镜像)
其中的ps就是flag
'''
其中OPTIONS参数成为flag,任何时候执行一个docker命令,Docker都需要先解析这些flag,然后按照用户声明的COMMAND向指定的子命令执行对应的操作
如果子命令为daemon,Docker会创建一个运行在宿主机的daemon进程,即执行daemon模式。其余子命令都会执行client模式。而这些处于client模式的docker命令工作流程包含以下步骤:
- 解析flag信息
解析一些重点信息如:Debug、LogLevel、Hosts、protoAddrparts - 创建client实例
client的创建就是在已有配置参数信息的基础上,调用api/client/cli.go#NweDockerCli - 执行具体的命令
Docker client对象创建成功后,剩下的执行具体命令的过程就交给cli/cli.go来处理了。
3.2 daemon模式
3.1说到只有一个进程会进入daemon模式,其余会进入client模式。那么这唯一一个进入了daemon模式的进程中,Docker daemon会通过一个server模块接收来自client的请求,然后根据请求类型,交由具体的方法去执行。因此daemon首先就需要启动并初始化这个server。同时,启动server后,Docker进程还需要初始化一个daemon对象(daemon/daemon.go)来负责处理server接收到的请求
关于Docker daemon工作路径

一般而言,每次安装docker之后都会在
/var/lib路径下创建一个docker的文件夹,这里面就是Docker daemon的工作路径。其中里面containers就是创建的容器文件。在后面输入容器的ID就可以查看里面相对应的配置文件。注意:如果想要查看docker里面的文件是需要提权的,在没有做安全的防护下,会在日志当中留有记录,谨记请勿随便查看或修改内部的文件Docker根目录的结构:
/var/lib/docker/ ├── aufs # aufs驱动工作的目录 │ ├── diff # aufs文件系统的所有层的存储目录(新下载的镜像内 容就逐层保存在这里) │ ├── layers # 存储上述所有aufs层之间的关系等元数据 │ └── mnt # aufs文件系统的挂载点,如果容器中写了一个新文 件,会出现在这里 ├── containers # 容器的配置文件目录 │ ├── image/aufs # 存储镜像和镜像层信息,注意这里只是元数据,真 的镜像层内容保存在aufs/diff下 │ ├── imagedb # 存储所有镜像的元数据 │ ├── layerdb # 存储所有镜像层和容器层的元数据 │ ├── repositories.json # 记录镜像仓库中所有镜像的repository和tag名 │ └── distribution │ └── volumes # volumes的工作目录,存放所有volume数据和元数据
3.2.1 daemon对象
在Docker daemon进程在经过多层设置以及创建对象之后,最终创造出用户经常能见到的daemon对象实例:
- ID:根据传入的证书生成的容器ID,若没有传入则自动使用ECDSA加密算法生成。
- repository:部署所有Docker容器的路径。
- containers:用于存储具体Docker容器信息的对象。
- execCommands:Docker容器所执行的命令。
- referenceStore:存储Docker镜像仓库名和镜像ID的映射。
- distributionMetadataStore:v2版registry相关的元数据存储。
- trustKey:可信任证书。
- idIndex:用于通过简短有效的字符串前缀定位唯一的镜像。
- sysInfo:Docker所在宿主机的系统信息。
- configStore:Docker所需要的配置信息。
- execDriver:Docker容器执行驱动,默认为native类型。
- statsCollector:收集容器网络及cgroup状态信息。
- defaultLogConfig:提供日志的默认配置信息。
- registryService:镜像存储服务相关信息。
- EventsService:事件服务相关信息。
- volumes:volume所使用的驱动,默认为local类型。
- root:Docker运行的工作根目录。
- uidMaps:uid的对应图。
- gidMaps:gid的对应图。
- seccompEnabled:是否使用seccompute。
- nameIndex:记录键和其名字的对应关系。
- linkIndex:容器的link目录,记录容器的link关系
3.2.2 恢复已有的Docker容器
在Docker daemon启动时,会查看daemon.repository,也就是/var/lib/docker/containers中的内容。如果存在容器,则会将相应信息收集并进行维护,同时重启restart repolicy为always的容器
⬇/var/lib/docker/containers⬇

同时重启restart repolicy为always的容器
restart policy = always 是 Docker 中最“强硬”、也最常用于生产环境的自动重启策略。它的核心设计目标是:让容器尽可能一直处于运行状态
有自动重启就会有手动重启,对于always来说,就相当于在Windows下的一些流氓软件设置的开机自启动,除非自己设置了禁止开机自启动(取消always),否则他会一直在你启动电脑后立刻在后台占用你的内存;而一般软件不会设置开机自启动,只有启动了软件才会呈现在你面前。这就是一般的默认设置。下面一份表格可以比对出默认和设置了always的区别
| 情况 | –restart=always | –restart=unless-stopped | 推荐场景 |
|---|---|---|---|
| 容器 crash / 异常退出 | 立刻重启 | 立刻重启 | — |
| Docker daemon / 主机重启 | 自动启动(不管之前什么状态) | 自动启动(除非之前是手动停的) | — |
| 执行 docker stop 后 | 保持停止,但 daemon 重启后又起来 | 保持停止,daemon 重启后仍然保持停止 | 运维更友好 |
| 适合场景 | 极致高可用、不能容忍任何宕机 | 生产环境最常用(推荐) | 大多数真实生产环境 |
3.3 从client到daemon
client和daeon单独启动并初始化的流程已经了解了大概,那么一个已经在运行中的daemon该如何处理来自client的请求?
1、docker run
Client
- docker run之后,用户端进入client模式,并开始前面所描述的初始化流程。
- 新建出client并通过反射机制找到CmdRun,后者开始解析用户提供的容器参数后,发出两个强求:
- 创建容器
- 启动容器
Daemon
- daemon利用API Server来响应上述要求,新建一个container即可。注意,这其中container是能够知道管理它的daemon进程信息的。
2、启动容器
daemon会着手处理后续client发来的start请求操作,前面提到的创建namespace,配置cgroup,挂载rootfs,以及container所需的各项参数,都在创建容器过程中就设置好了,所以daemon会直接在start.go中执行daemon.ContainStart,这样就可以在宿主机上创建对应容器了。
3、Execdriver.Run
daemon做好了所有的准备工作,那创建的细节又会如何处理?ExecDriver.Run这时候就排上了用场(具体是哪种Driver由container来决定)
execdriver作为daemon的重要组成部分之一,它封装了对底层namespace、cgroup等所有对OS资源进行操作的所有方法。而Docker当中,execdriver的默认实现(native)就是libcontainer。
具体操作方式也是Docker daemon向execdriver提供三个参数,等待返回结果即可
- commandv:该容器需要的所有配置信息集合(container的属性之一);
- pipes:用于将容器stdin、 stdout、stderr重定向到daemon;
- startCallback():回调方法。



