Docker 概述与架构原理
Docker镜像交付,容器三级调度运行,推荐构建一次后pull+run部署。
Docker 不是一个”铁板一块”的单一程序,而是一套模块化架构:客户端、守护进程、运行时、镜像、容器、网络、存储、仓库……每个模块各司其职,又通过清晰的接口彼此协作。理解这些模块,是真正掌握 Docker 的钥匙。
本文结构:概念 → 原理 → 动手操作 → 命令行速查,所有架构图均使用 Mermaid 绘制。
目录
- Docker 的模块化设计思想
- 平台核心模块:Client / Daemon / containerd / runc / Registry
- 镜像模块:分层的只读模板
- 容器模块:隔离运行的进程
- 网络模块:连接与隔离
- 存储模块:数据持久化
- 镜像仓库模块:发布与分发
- 端到端实战:一条
docker run背后的故事 - 部署工作流:代码是怎么跑到服务器上的
- 命令行速查总表
- 小结
1. Docker 的模块化设计思想
传统虚拟机把”应用程序 + 依赖 + 完整操作系统”打包成一个巨大的镜像,由 Hypervisor 模拟整台硬件。Docker 换了一条路:只打包应用和依赖,直接共享宿主机的操作系统内核,再用 Linux 内核自带的隔离技术把进程”圈”起来。
模块化带来的收益:
| 特性 | 说明 |
|---|---|
| 可替换 | 可配置兼容的 OCI 运行时,如 runc、Kata 或 gVisor;containerd 是另一层的生命周期管理器 |
| 可组合 | 镜像 + 网络 + 卷可以自由组合成应用 |
| 职责单一 | 每个模块只做一件事,边界清晰、易排障 |
| 标准化 | 各模块之间用公开规范(OCI、REST API)通信 |
下面的全景图展示了 Docker 各模块之间的关系:
从图中可以看到清晰的”三段式”:
- 用户层:客户端 CLI,负责接收人的指令;
- 管理层:守护进程 dockerd,负责”头脑”工作——调度、编排、对象管理;
- 执行层:containerd → runc → 内核,负责真正把进程跑起来。
2. 平台核心模块:Client / Daemon / containerd / runc / Registry
2.1 Docker Client(客户端 CLI)
概念:用户与 Docker 交互的命令行程序,例如 docker run 中的 docker 就是客户端。
原理:客户端本身不做任何容器操作,主要把容器管理命令翻译成 Engine API 请求;构建等子命令还会调用相应组件。客户端与守护进程通过本地 unix:///var/run/docker.sock 通信,也可通过 -H 参数连接远程守护进程:
# 连接到远程机器的守护进程docker -H ssh://user@192.168.1.10 ps# 查看当前客户端 / 服务端信息docker versiondocker info2.2 Docker Daemon(守护进程 dockerd)
概念:常驻后台的服务进程,是 Docker 的”大脑”。
原理:它监听 REST API 请求,负责管理所有 Docker 对象(镜像、容器、网络、卷),并在需要时调用 containerd 执行真正的容器操作。containerd 通过 shim 进程管理容器进程。Docker 默认会在守护进程退出时停止容器;需要在符合限制的环境中启用 live-restore,才能在 daemon 不可用时保留容器运行。官方说明
# 查看守护进程与运行环境信息docker info# 在容器内查看(了解它跑在哪个内核上)docker run --rm alpine uname -r2.3 containerd(容器运行时管理器)
概念:一个工业级容器运行时管理器,CNCF 毕业项目,同时被 Kubernetes(通过 CRI)和 Docker 使用。
原理:containerd 负责镜像的下载与解包、容器存储、以及调用低层运行时启动容器。它实现了 OCI(Open Container Initiative)规范中的镜像与运行时标准,把”如何管理容器生命周期”与”具体怎么启动进程”两层分离。
2.4 runc(低层运行时)
概念:真正与内核打交道的程序,目前 Docker 默认的低层运行时。
原理:runc 根据 OCI 运行时规范,调用 Linux 内核的两个关键能力创建隔离环境:
- namespaces(命名空间):让进程拥有独立的 PID、Network、Mount、UTS、IPC、User 视图——容器里看到的”自己是 1 号进程""自己有网卡”,都是命名空间制造的幻觉;
- cgroups(控制组):限制并统计容器可用的 CPU、内存、磁盘 IO 等资源。
# 查看默认低层运行时(一般是 runc)docker info | grep -i runtime# 查看容器实际使用的 cgroupdocker run -d --name demo --memory 128m alpine sleep 3600docker inspect demo --format '{{.HostConfig.Memory}}'小结:
dockerd(管理)→containerd(生命周期管理)→runc(执行)→ 内核(隔离)。各层按接口分工,替换运行时需要满足兼容性和部署条件,并非任意组件都能直接互换。
2.5 Registry(镜像仓库)
概念:存放和分发镜像的服务,公开的有 Docker Hub,私有常见 Harbor、AWS ECR 等。
docker pull nginx # 从默认仓库 Docker Hub 拉取docker login # 登录(访问私有仓库需要)docker push myname/app:v1 # 推送本地镜像到仓库3. 镜像模块:分层的只读模板
3.1 概念
镜像是只读的模板,包含运行一个应用所需的全部内容:代码、运行时、库、环境变量、配置。镜像是”类”,容器是”实例”。
3.2 原理:分层与联合文件系统
镜像由多层只读层堆叠而成。RUN、COPY、ADD 等会形成文件系统层;CMD、ENTRYPOINT 等记录配置元数据,不新增文件系统层:
- 层之间用 UnionFS(联合文件系统) 叠加,对外表现为一个完整文件系统;
- 层是可共享、可缓存的:多个镜像共用同一基础层时只存一份;
- 容器启动时,Docker 在最上层再加一层可写层;修改文件采用 Copy-on-Write(写时复制)——先复制再修改,绝不动下层。
3.3 构建上下文(Build Context):docker build 到底在哪执行?
先回答一个高频疑问:是不是必须在 Dockerfile 所在的文件夹里执行 docker build?
准确答案是:可以在任意目录执行,只要正确指定构建上下文路径和 Dockerfile 路径。 看这条命令:
docker build -t myapp:v1 .# ↑ 这个 . 是"构建上下文路径",不是"Dockerfile 的位置"它背后发生的事是:
- 构建器根据
.确定本地构建上下文,并应用.dockerignore; - 默认使用的 BuildKit 按需传输和处理构建所需文件;旧式构建器才通常把整个上下文打包传给 daemon;
- 普通
COPY/ADD的本地源以这个上下文为根,不能通过../读取其外部文件;COPY --from则可读取其他构建阶段、镜像或命名上下文。
三种典型用法
# ① 最常用:Dockerfile 在当前目录,上下文 = 当前目录cd /home/user/myappdocker build -t myapp:v1 .
# ② Dockerfile 放在子目录,上下文仍是项目根目录(推荐的工程组织方式)docker build -f docker/Dockerfile -t myapp:v1 .
# ③ 无上下文,从标准输入读 Dockerfile(此时只能用 ADD 远程 URL,不能 COPY 本地文件)docker build -t myapp:v1 - < Dockerfile三个必踩的坑
# ❌ 1. 不要把系统根目录当构建上下文,避免暴露无关文件并增加扫描成本cd / && docker build -t x .# ❌ 2. COPY 不能跳出上下文# 不能借 ../ 读取上下文外文件;报错或路径规范化行为依构建器而异COPY ../config.yml /app/# ❌ 3. 不写 .dockerignore,COPY . . 可能带入 node_modules、.git 等无关文件.dockerignore 放在上下文根目录,语法类似 .gitignore:
.gitnode_modulesdist*.logDockerfile.dockerignore它既缩小传输体积,又避免本地依赖覆盖镜像内的依赖、导致构建缓存失效。
3.4 操作:用 Dockerfile 构建镜像
# 示例 Dockerfile:构建一个极简 Node.js 应用镜像# 基础镜像层FROM node:20-alpine# 设置工作目录WORKDIR /app# 先只拷贝依赖清单,利于缓存COPY package*.json ./# 安装依赖RUN npm install# 拷贝应用源码COPY . .# 声明端口(仅文档作用)EXPOSE 3000# 启动命令CMD ["node", "server.js"]docker build -t myapp:v1 . # 构建,-t 指定名称:标签docker images # 查看本地镜像docker history myapp:v1 # 查看构建历史(含不产生文件系统层的元数据指令)docker inspect myapp:v1 # 查看镜像元数据docker rmi myapp:v1 # 删除镜像docker image prune # 清理悬空镜像镜像命名格式:[仓库地址/]仓库名/镜像名:标签,缺省时仓库为 docker.io/library。
docker tag myapp:v1 myname/myapp:latest # 打新标签docker save -o myapp.tar myapp:v1 # 导出镜像为 tar 包docker load -i myapp.tar # 导入 tar 包4. 容器模块:隔离运行的进程
4.1 概念
容器是镜像的运行实例。它在宿主机上就是一个普通进程(带着一堆隔离),被命名空间、cgroup 包裹,拥有独立的文件系统、网络栈与进程视图。
4.2 原理:生命周期状态机
几个易混的细节:
docker stop先发SIGTERM优雅退出,超时后才SIGKILL(默认 10 秒,可用-t调整);docker restart先停止再启动;强制删除运行中容器用的是docker rm -f;- 前台运行(不
-d)时,按Ctrl-C相当于给 PID 1 发送 SIGINT。
4.3 操作与命令行
# 创建并启动容器:-d 后台、--name 命名、-p 端口映射docker run -d --name myapp -p 3000:3000 myapp:v1
docker ps # 查看运行中的容器docker ps -a # 查看所有容器(含已停止)docker logs -f myapp # 跟随查看日志docker exec -it myapp sh # 进入容器执行命令(排障利器)docker top myapp # 查看容器内进程docker stats # 实时查看资源占用(依赖 cgroups)docker cp myapp:/app/log.txt ./ # 与容器互拷文件
docker pause myapp # 暂停(冻结)docker unpause myapp # 恢复docker restart myappdocker stop myappdocker start myappdocker rm -f myapp # 删除容器(-f 先杀后删)docker container prune # 清理所有已停止容器排障流程口诀:先
logs看应用,再exec进容器查环境,最后inspect看配置。
5. 网络模块:连接与隔离
5.1 概念与原理
Docker 通过 veth 虚拟网卡 + 网桥 + iptables/NFTables 搭建虚拟网络。主要驱动类型:
| 驱动 | 适用场景 | 说明 |
|---|---|---|
bridge | 单机多容器互访 | 默认网络 docker0,自动分配 172.17.0.x 地址 |
host | 需要极致性能 | 容器直接用宿主机网络栈,无隔离,-p 失效 |
overlay | Swarm 跨主机 | 让跨节点的容器处于同一虚拟子网 |
macvlan | 需要容器像物理设备 | 给容器分配 MAC 地址,接入局域网 |
none | 网络完全隔离 | 只有 lo 回环接口 |
要点:普通 bridge 网络中的容器通常可经 NAT 出网,是否可达还受路由和防火墙影响;同一 bridge 内的容器默认可按 IP 互访,不同 bridge 默认隔离;容器间推荐加入同一个自定义 bridge 网络——此时可直接用容器名/服务名通信(内嵌 DNS),而默认 docker0 网络不支持按名字解析。
5.2 操作与命令行
# 创建自定义网络,并指定子网docker network create -d bridge --subnet 172.20.0.0/16 app-net
# 在指定网络中启动容器docker run -d --name web --network app-net myapp:v1docker run -d --name db --network app-net -e POSTGRES_PASSWORD=dev-only-change-me postgres:16
# 在 web 容器里直接用名字访问 db(内嵌 DNS)docker exec web ping -c 2 db
# 管理命令docker network ls # 列出网络docker network inspect app-net # 查看网络详情与成员docker network connect app-net old-container # 动态接入docker network disconnect app-net old-containerdocker network rm app-net # 删除(需先断开成员)
# 端口映射docker run -d -p 8080:80 nginx # 宿主机8080 -> 容器80docker run -d -P nginx # -P 随机映射所有 EXPOSE 端口docker port <container> # 查看端口映射关系6. 存储模块:数据持久化
6.1 概念与原理
容器被删除后,它的可写层随之消失,需要持久化的数据必须放到宿主机侧的挂载上。Docker 提供三种挂载:
| 类型 | 存储位置 | 典型用途 | 特点 |
|---|---|---|---|
| Volume 卷 | /var/lib/docker/volumes/(由 Docker 管理) | 数据库数据、应用数据 | 首选;可命名、可备份、可跨容器共享 |
| Bind mount 绑定挂载 | 宿主机任意路径 | 开发时共享源码、挂配置文件 | 直观,但依赖宿主目录结构 |
| tmpfs | 宿主机内存(可能被换出到 swap) | 临时数据、高速缓存 | 容器停止后清空;用于 Linux 容器,不保证绝不落盘 |
新写法推荐
--mount type=volume,src=myvol,dst=/data形式,语法更明确,比短写法-v更不易出错。
6.2 操作与命令行
# ---- Volume 卷 ----docker volume create pgdata # 创建命名卷docker volume ls # 列出卷docker volume inspect pgdata # 查看卷(含宿主机路径)docker volume rm pgdata # 删除卷docker volume prune # 清理无用卷
# 挂载卷:给 Postgres 持久化数据docker run -d --name pg \ -v pgdata:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=secret postgres:16
# ---- Bind mount:开发时共享源码(改动即时生效)----docker run -d -p 3000:3000 \ -v "$PWD":/app \ -w /app node:20 node server.js
# ---- tmpfs:内存文件系统 ----docker run -d --tmpfs /cache alpine sleep 3600
# ---- 查看容器挂载情况 ----docker inspect pg --format '{{json .Mounts}}'
# ---- 容器间共享同一卷 ----docker run -d --name writer -v share:/data alpine sh -c "echo hi > /data/f.txt; sleep 3600"docker run --rm -v share:/data alpine cat /data/f.txt7. 镜像仓库模块:发布与分发
7.1 概念
Registry 是镜像的”GitHub”。本地 Docker 称为缓存,远程仓库才是源头。完整镜像名是 [仓库地址]/[命名空间/]镜像名:标签。
7.2 命令行
docker search nginx # 搜索官方仓库镜像docker pull nginx:alpine # 显式拉取 alpine 标签;未写标签时才默认 latestdocker login # 登录 Docker Hubdocker tag myapp:v1 myname/myapp:1.0.0 # 打上自己的仓库标签docker push myname/myapp:1.0.0 # 推送docker logout # 退出登录
# 用容器镜像名区分官方与个人镜像docker pull nginx # docker.io/library/nginxdocker pull myname/myapp:1.0.0 # docker.io/myname/myapp8. 端到端实战:一条 docker run 背后的故事
把前面的模块串起来,看一次典型部署中每个模块做了什么。
第一步:动手操作(以第 3 节 Dockerfile 构建的 myapp:v1 为例)
# ① 构建镜像(镜像模块)docker build -t myapp:v1 .
# ② 建网络、建卷(网络/存储模块)docker network create app-netdocker volume create pgdata
# ③ 启动数据库与业务容器(容器模块)docker run -d --name pg --network app-net -v pgdata:/var/lib/postgresql/data postgres:16docker run -d --name web --network app-net -p 3000:3000 myapp:v1
# ④ 验证docker pscurl localhost:3000docker logs -f web第二步:内部原理——docker run -d -p 80:80 nginx 的完整时序:
对应关系:CLI = 客户端模块,dockerd = 管理模块,Registry = 仓库模块,containerd/runc = 运行时模块,namespaces/cgroups = 内核隔离机制。
9. 部署工作流:代码是怎么跑到服务器上的
学到这里,最容易混淆的问题是:“我写好的代码,到底是推到 Docker Hub 让服务器拉下来 build,还是服务器 git clone 源码再构建?”
先给结论:两个方案都有人用,但生产环境推荐”构建一次、分发镜像”。并且要记住 —— 代码走 git,成品走 Registry,两者不是一回事。
9.1 先分清三个角色
| 角色 | 存什么 | 常用服务 |
|---|---|---|
| 代码仓库 | 源码(人类可读的文本) | GitHub / GitLab / Gitee |
| Registry 镜像仓库 | 镜像(已构建好的二进制交付物,含代码 + 依赖 + 运行时) | Docker Hub / Harbor / 阿里云 ACR / AWS ECR |
| 服务器 | 只负责运行容器 | 任意装了 Docker 的机器 |
关键认知:docker build 时 COPY . /app 已经把源码烤进镜像了,所以生产服务器既不需要 git,也不需要 Node/Python/编译器,它只需要 docker pull + docker run。这正是容器化的价值所在。
9.2 两种部署方案对比
| 对比项 | 方案 A(分发镜像) | 方案 B(服务器构建) |
|---|---|---|
| 服务器需要工具链吗 | 通常只需容器运行环境与部署配置 | 需要源码获取工具和构建器;语言工具链可以放在 Dockerfile 的构建阶段中 |
| 部署速度 | 快(只 pull 镜像层,有缓存) | 慢(每次可能重新 build) |
| 回滚 | 重新部署旧镜像;耗时取决于拉取、启动及健康检查,数据库迁移另行处理 | 可重建旧提交;如果保留旧镜像,也可直接重新部署 |
| 环境一致性 | 强(构建环境固定) | 弱(依赖服务器当时的环境) |
| 适合场景 | 生产、团队协作、K8s | 个人小项目、内网单机、快速验证 |
9.3 方案 A 命令:构建一次,到处运行
# ===== 本地开发机 或 CI(GitHub Actions / GitLab CI):只在这里 build 一次 =====docker build -t myrepo/myapp:1.0.0 .docker push myrepo/myapp:1.0.0
# ===== 生产服务器:只 pull 和 run,绝不 build =====docker pull myrepo/myapp:1.0.0docker run -d -p 3000:3000 --name app myrepo/myapp:1.0.0
# 回滚:把 tag 换成上个版本,重启容器即可docker pull myrepo/myapp:0.9.0docker rm -f appdocker run -d -p 3000:3000 --name app myrepo/myapp:0.9.09.4 方案 B 命令:服务器上拉代码构建
# ===== 服务器上 =====git pull origin main # 先更新源码docker compose up -d --build # 然后构建并创建/更新服务容器
# 等价的手动写法docker build -t myapp:latest .docker run -d -p 3000:3000 --name app myapp:latest简单直接,适合个人项目和内网机器;代价是服务器要承担构建资源消耗,并维护构建器及源码;语言依赖可以安装在构建容器中。
9.5 bind mount 在”开发”与”生产”中的定位
很多人会把上一节的挂载方式和部署流程搞混。bind mount 是运行时行为,和 build、Registry 没有任何关系——它不打包、不上传、不构建,只是把宿主机的目录”借”给容器读写。
三条实践准则:
- 生产环境不要 bind mount 代码:镜像里已经有代码了,再挂宿主机目录会覆盖镜像内容,“镜像版本”就失去意义了——这是常见反模式;
- 生产用 volume 挂数据:数据库文件、用户上传、日志等(见第 6 节),因为容器删除后可写层会一起消失;
- 开发用 bind mount 挂源码:配合
nodemon、vite等热重载工具,改一行代码立即生效,不用反复 build。
# 开发环境典型写法:挂源码 + 热重载docker run -d -p 3000:3000 -v "$PWD":/app -w /app node:20 npm run dev
# 生产环境典型写法:代码在镜像里,只挂数据卷docker run -d -p 3000:3000 -v pgdata:/var/lib/postgresql/data myrepo/myapp:1.0.09.6 一句话总结
代码走 git,成品走镜像仓库;
build只做一次(本地或 CI),服务器只pull+run;bind mount是”运行时把宿主机目录借给容器”,不参与构建和分发。
10. 命令行速查总表
容器
| 命令 | 作用 |
|---|---|
docker run -d --name n -p 80:80 img | 创建并后台启动容器 |
docker ps / -a | 查看运行中 / 全部容器 |
docker start / stop / restart | 启 / 停 / 重启容器 |
docker pause / unpause | 暂停(冻结)/ 恢复 |
docker rm [-f] | 删除容器(-f 强制删运行中) |
docker exec -it n sh | 进入容器执行命令 |
docker logs -f n | 查看并跟随日志 |
docker cp n:/path ./ | 容器与宿主机互拷文件 |
docker top / stats / inspect n | 进程 / 资源 / 配置详情 |
docker port n | 查看端口映射 |
镜像
| 命令 | 作用 |
|---|---|
docker build -t name:tag . | 构建镜像 |
docker images | 列出本地镜像 |
docker pull / push | 拉取 / 推送镜像 |
docker tag | 打标签 |
docker history img | 查看分层历史 |
docker rmi / docker image prune | 删除镜像 / 清理悬空镜像 |
docker save / load | 导出 / 导入镜像为 tar |
docker commit | 把容器保存为新镜像(少用) |
网络与存储
| 命令 | 作用 |
|---|---|
docker network ls / create / rm | 网络列表 / 创建 / 删除 |
docker network connect / disconnect | 动态接入 / 断开网络 |
docker volume ls / create / rm / prune | 卷列表 / 创建 / 删除 / 清理 |
docker system df | 查看镜像、容器、卷占用 |
11. 小结
把 Docker 拆成模块来理解,你会发现它的设计其实非常优雅:
- 镜像是”交付物”,容器是”运行态”,两者通过分层与 CoW 紧密关联;
- 构建上下文决定了
COPY能看到什么,docker build要正确指定上下文路径,并配好.dockerignore; - dockerd → containerd → runc 是”管理 → 编排 → 执行”三级流水线,每层都可替换;
- namespace / cgroups 是内核提供的隔离地基,Docker 只是它的”友好的前端”;
- 网络、存储、仓库是三个横向支撑模块,分别解决”连通、持久、分发”问题;
- 部署流程上,代码走 git、成品走 Registry,服务器只
pull+run,build只在本地或 CI 做一次。
核对资料
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
评论区
正在加载 GitHub 评论…