Docker 架构与部署:一条 docker run 到底走了三层

命令敲下去,容器就起来了------中间的活儿是谁干的?这篇先把 Docker 的三件套(客户端、宿主机、镜像仓库)拆开,看一条 docker run 怎么在这三层之间走完三步;再给出单机部署的六步顺序,以及把客户端单独挪到另一台机器的 C/S 分离做法。

本文概要:

项目 内容
文章主题 Docker 的 C/S 架构(Client / Host / Registry 三件套与一次 run 的完整链路)、版本演进、单机部署六步、C/S 分离部署
所属类别 云计算 → 容器技术 → Docker 基础
知识关键字 dockerd · docker run · Images · Containers · Registry · daemon.json · -H tcp://

主线是这样的:先搞清楚「谁在干活」(架构),再确认「装哪个版本」(版本),然后按顺序把它装起来(部署)。最后把客户端拆出去做远程管理(C/S 分离)。

架构那段看着抽象,却决定了后面所有的排错方向。命令报错到底是客户端的问题、守护进程的问题,还是仓库连不上,答案都在这一层。

一、三件套:客户端、宿主机、镜像仓库

Docker 用的是 C/S(客户端/服务端)架构,整体由三部分组成:

  • Client 客户端------你敲命令的那个终端;
  • Docker Host 宿主机------跑守护进程、存镜像、跑容器的那台机器;
  • Registry 镜像仓库------镜像的源头。

客户端与守护进程之间通过 Socket 通信,所有容器操作都是这么发出去的。

1.1 客户端:只发指令,不动手

Client 是用户交互入口,本身不直接创建容器,只把指令发给 Docker daemon。三条最常见的命令正好对应三个阶段:

命令 发给 daemon 之后做什么
docker build 读 Dockerfile,把构建结果存成本地镜像
docker pull 去 Registry 把镜像拉到本地 Images
docker run 基于本地镜像创建并启动容器

理解这一点很关键:客户端和服务端可以不在同一台机器上 。docker 命令后面加 -H 就能指向远端 daemon,这也是第四节 C/S 分离的理论基础。

1.2 宿主机:真正干活的地方

宿主机上跑的核心组件是 dockerd(Docker daemon),后台常驻进程,是整个架构的服务端。镜像构建、拉取、容器创建、启停、销毁,全是它做的。宿主机里只有两类资源:

Images 镜像------只读模板,可以理解成容器的安装包,里面是应用程序、依赖库、配置文件。

  • 可以通过 docker build 本地构建,也可以 docker pull 从仓库下载;
  • 镜像分层存储,多个镜像可以共享底层,节约磁盘;
  • 镜像本身只读,不能直接运行,必须先实例化成容器。

Containers 容器------镜像的运行实例,一个轻量级沙箱。

  • docker run 时,daemon 读本地镜像,在镜像之上增加一层可读写层,生成容器;
  • 一台宿主机可以同时跑多个容器,彼此隔离、互不可见;
  • 容器本质上是宿主机上受 Namespace 与 Cgroups 管控的进程;
  • 容器内产生的数据写在读写层,镜像本身不会被修改;容器删除后读写层数据默认丢失。

⚠️ 注意 :最后这条是新手最容易踩的坑------在容器里改了配置、存了数据,docker rm 之后全没了,还以为是镜像坏了。要持久化必须挂 volume 或绑定宿主机目录,不能指望容器的读写层。

1.3 Registry:镜像的源头

Registry 是集中存放镜像的地方。仓库注册服务器里存着很多类镜像,每类镜像又用不同的**标签(tag)**区分版本。比如 Ubuntu 这个分类下,就有 24.04、22.04、20.04 这些 tag。

按公开与否分两类:

  • 公开仓库:Docker Hub 最大,镜像数量庞大;国内不少云厂商也提供了镜像源,访问更稳定;
  • 私有仓库 :在本地网络里自建一个只能自己访问的仓库,常见方案是 Harbor 或官方的 registry:2。

镜像做好之后 docker push 传上去,别的机器 docker pull 拉下来,镜像的分发就完成了。

1.4 一次 run 的完整链路

以 docker run nginx 为例,三步是:

  1. 用户在 Client 执行 docker run nginx,请求发给 Docker daemon;
  2. daemon 发现本地没有 nginx 镜像,访问 Registry 下载,存进本机 Images;
  3. daemon 读取本地 nginx 镜像,创建容器需要的网络、存储等资源,运行 Container。

本地已经有这个镜像时,第二步直接跳过------这也是「第二次起容器明显更快」的原因。

二、版本怎么认:三个阶段三种编号

Docker 的版本号换过两次格式,看到不同写法不用怀疑,是历史原因:

阶段 时间 格式 关键版本
语义化 1.x 2013 -- 2017.02 主.次.补丁,如 1.13.1 0.1(2013.06 首发,基于 LXC)、1.0(2014.06 生产可用,Docker Hub 上线)、1.12(内置 Swarm、引入 healthcheck)、1.13(1.x 最后一版,命令行重构)
日历版本 CE/EE 2017.03 -- 2022.06 YY.MM.Patch,分社区版 CE 与企业版 EE 17.03(首个日历版,runc 落地)、17.05(支持多阶段构建)、18.09(底层全面用 containerd)、20.10(经典稳定版,支持 cgroup v2 与 rootless,存量环境大量在用)、22.06(日历版终点,Compose V1 废弃)
回归语义化 2023.02 至今 主.次.补丁 23.0(默认启用 BuildKit)、24.0 / 25.0(生产稳定推荐)、29.x(最新,清理大量老旧接口)

选版本的经验:

  • 新装环境选 24.x / 25.x,稳且资料多;
  • 老环境看到 20.10.x 不要奇怪,那是过去几年最广泛部署的一版;
  • 29.x 起全新安装默认使用 containerd image store ,存储路径从 /var/lib/docker 变成 /var/lib/containerd,脚本里写死路径的要注意。

三、单机部署:六步走完

环境前提:CentOS 7 最小化安装,容器运行时用 runc,管理工具是 Docker Engine。Docker 支持几乎所有 Linux 发行版,也支持 macOS 与 Windows。

3.1 先解决「从哪装」:本地源

最小化安装的系统没有外网访问的稳定性保障,第一件事是把安装光盘挂成 yum 源:

bash 复制代码
[root@docker ~]# mkdir /dvd
[root@docker ~]# cp /etc/fstab /etc/fstab.bak
[root@docker ~]# echo '/dev/sr0 /dvd iso9660 defaults 0 0' >> /etc/fstab
[root@docker ~]# mount /dvd
mount: /dev/sr0 写保护,将以只读方式挂载
# 看不到内容时先检查虚拟机是否关联了 ISO
[root@docker ~]# ls /dvd
CentOS_BuildTag  EULA  images  LiveOS  repodata  RPM-GPG-KEY-CentOS-7

[root@docker ~]# mkdir /etc/yum.repos.d/bak
[root@docker ~]# mv /etc/yum.repos.d/*repo /etc/yum.repos.d/bak
[root@docker ~]# vim /etc/yum.repos.d/dvd.repo
[dvd]
name=local repo from dvd
baseurl=file:///dvd
gpgcheck=0

[root@docker ~]# yum clean all && yum makecache
元数据缓存已建立

报 mount: 在 /dev/sr0 上找不到媒体 就是虚拟机没挂 ISO 文件,先把光驱关联上再 mount。

3.2 防火墙不能关

这一步和很多教程相反:把 firewalld 开着,因为容器的端口映射依赖它的 NAT 功能。

bash 复制代码
[root@docker ~]# systemctl enable firewalld.service --now
[root@docker ~]# firewall-cmd --set-default-zone=trusted

默认区设为 trusted 是实验环境的省事做法(放行一切);生产环境应当精细放行,但不能直接 systemctl stop firewalld ------关掉之后 -p 80:80 的映射会静默失效,容器在跑但外面访问不到。

⚠️ 注意 :这是本篇第一个容易走错的地方。网上大量「装 Docker 先关防火墙」的说法来自 CentOS 6 时代;现在的 Docker 会自己往 firewalld 的 DOCKER 链里插规则,把 firewalld 停掉反而让规则链失效。

3.3 配 Docker 仓库

系统源里没有 docker-ce,得单独加一个源:

bash 复制代码
[root@docker ~]# cat > /etc/yum.repos.d/docker-ce.repo <<'EOF'
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.huaweicloud.com/docker-ce/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=0
EOF

$basearch 会自动展开成 x86_64,不用手改。gpgcheck=0 是为了跳过 GPG 校验,内网环境可以接受;对公网源建议改成 1 并导入官方 key。

📌 想完全离线部署,就把 rpm 包全部下载下来自己造一个本地源------目录结构、元数据生成、客户端配置的完整做法在《自定义 yum 软件源》那篇里,这里不重复。

3.4 装并启动

bash 复制代码
[root@docker ~]# yum install -y docker-ce
[root@docker ~]# systemctl enable docker.service --now
[root@docker ~]# docker --version
Docker version 26.1.3, build b72abbb

3.5 配镜像加速(不配基本拉不动)

直连 docker.io 在国内基本不可用:要么超时,要么几 KB 每秒。这一步配华为云的镜像加速器,再把自建仓库和第三方加速站讲清楚。这三类东西常被写成一段,结果谁也搞不清哪个字段管哪件事。

3.5.1 取华为云 SWR 的加速地址
  1. 登录华为云容器镜像服务 SWR 控制台;
  2. 左侧导航点「镜像加速器」;
  3. 复制页面上的加速器地址 ,形如 https://abcdef123456.mirror.swr.myhuaweicloud.com。

地址里那串字符是账号标识,每个账号一个,不能照抄别人的。目前开放加速器的局点:华南-广州、华北-北京四、华东-上海一、华东-上海二、华东二、华北-乌兰察布一、西南-贵阳一。

3.5.2 写进 daemon.json
bash 复制代码
[root@docker ~]# mkdir -p /etc/docker
[root@docker ~]# tee /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["https://abcdef123456.mirror.swr.myhuaweicloud.com"]
}
EOF
[root@docker ~]# systemctl daemon-reload
[root@docker ~]# systemctl restart docker
[root@docker ~]# docker info | grep -A1 "Registry Mirrors"
 Registry Mirrors:
  https://abcdef123456.mirror.swr.myhuaweicloud.com/

docker info 里出现这一行才算生效。没出现就是没读到配置,两个原因最常见:daemon.json 不在 /etc/docker/ 下;或者 /etc/sysconfig/docker、/etc/default/docker 里也写了 registry-mirrors,两处冲突时 daemon 直接起不来。

3.5.3 加速器的边界
项 官方口径
能加速什么 只覆盖 Docker Hub 上的常用开源镜像,不保证每个镜像、每个 tag 都能拉到
给谁用 面向华为云上容器产品与个人开发场景;生产环境建议把镜像同步到 SWR 私有仓库,再按私有地址拉
客户端版本 Docker 18.06 ~ 28.5

这一段不是免责声明,是排错依据。拉 nginx 很快、拉某个冷门镜像还是慢,属于覆盖范围问题,不是配置写错了。

3.5.4 三类「加速」别混着写

registry-mirrors 和 insecure-registries 是两个完全不同的字段。把私有仓库塞进 registry-mirrors,是最常见的一种误解。

类型 例子 本质上是什么 写进哪个字段
云厂商加速器 华为云 SWR 加速地址 替你访问 Docker Hub 的代理 registry-mirrors
第三方公共站 docker.m.daocloud.io、docker.1ms.run 同上,稳定性自负 registry-mirrors,排后面兜底
自建 Harbor http://hub.zhb.cloud 自己的私有仓库,存自己的镜像 insecure-registries(只开 HTTP 时才需要)

三条容易误会的地方:

  1. registry-mirrors 只对 docker.io 生效。 想加速 gcr.io、quay.io、registry.k8s.io 的镜像,加速站帮不上忙,得先把镜像同步到自己的仓库。

  2. 自建 Harbor 不是「公共镜像加速站」。 它不替你去 Docker Hub 拉东西,写进 registry-mirrors 不会生效;拉里面的镜像要写全名,并且先登录:

    bash 复制代码
    [root@docker ~]# docker login hub.zhb.cloud
    [root@docker ~]# docker pull hub.zhb.cloud/library/nginx:1.25
  3. HTTP 地址必须同时进 insecure-registries。 否则 daemon 拒绝明文连接,报 http: server gave HTTP response to HTTPS client。

实验环境里常见的组合是这样,两个字段各管一段:

bash 复制代码
[root@docker ~]# cat /etc/docker/daemon.json
{
  "registry-mirrors": ["https://abcdef123456.mirror.swr.myhuaweicloud.com"],
  "insecure-registries": ["hub.zhb.cloud"]
}

写完必须 systemctl restart docker。 daemon.json 只在 daemon 启动时读一次,改完不重启等于没改------这是本篇第二个常见卡点。

3.6 验收:两个容器才算过

第一个容器用 hello-world。它只做一件事:把整条链路走一遍------客户端联系 daemon、daemon 拉镜像、daemon 起容器、输出回传。

bash 复制代码
[root@docker ~]# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

看到 Hello from Docker! 说明客户端、守护进程、镜像仓库三段都通了。

第二个容器用 httpd,验证端口映射:

bash 复制代码
[root@docker ~]# docker run -d -p 80:80 httpd
817f24cca3514568f9b7a7cac6f183734077fb74caf4c62502acd2a4d0d29520

[root@docker ~]# ip -br a
ens33     UP    10.1.8.102/24
docker0   UP    172.17.0.1/16
veth7c57cce@if8  UP

[root@docker ~]# curl 10.1.8.102
<html><body><h1>It works!</h1></body></html>

docker0 上出现 172.17.0.1/16 是正常的------那是 Docker 默认网桥,容器从这段地址里分 IP。真正要确认的是:浏览器或 curl 能访问宿主机 IP 的 80 端口。

⚠️ 注意 :-p 80:80 的前后顺序是「宿主机端口 : 容器端口」,写反了不会报错但访问不到。宿主机 80 被占用时换成 -p 8080:80,访问时用 8080。

四、C/S 分离:把客户端挪到另一台机器

单机装完,把客户端拆出去的意义在于:管理入口和运行环境分离。运维在自己的机器上敲命令,镜像和容器都在服务端的机房机器上。

4.1 server 端:把 2375 打开

两条路,改的文件不同、效果一样:方式一直接改单元文件 ,图快,适合一次性实验;方式二 drop-in 覆盖 + daemon.json,不怕升级,适合长期维护的机器------这台机器最后用的是方式二。两种写法都写在这里,别只抄一半。

方式一:直接编辑单元文件
bash 复制代码
[root@docker ~]# yum install -y docker-ce
[root@docker ~]# vim /usr/lib/systemd/system/docker.service
# 在 ExecStart 行末尾追加 -H tcp://0.0.0.0:2375
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375
[root@docker ~]# systemctl daemon-reload
[root@docker ~]# systemctl enable docker.service --now

# 验证端口起来了
[root@docker ~]# lsof -i :2375
dockerd  2565  root  3u  IPv6  76021  0t0  TCP *:2375 (LISTEN)

三个参数各自的含义:-H fd:// 是 systemd 的 socket 激活方式,保留它;-H tcp://0.0.0.0:2375 是本次新加的 TCP 监听;--containerd=/run/containerd/containerd.sock 指定 containerd 的 socket 路径,容器的生命周期实际由 containerd 接管。

三个细节都要做到位:改的是 docker.service、改完必须 daemon-reload、fd:// 那一项不能删。只加新参数不动原来的,两个地址会同时监听。

问题是这个文件属于 docker-ce 包,yum update docker-ce 一次就装回原样,2375 悄悄消失。

方式二:drop-in 覆盖 + daemon.json(实际采用)

不碰发行版的文件,另开一份只写自己那几行的覆盖:

bash 复制代码
[root@docker ~]# mkdir -p /etc/systemd/system/docker.service.d
[root@docker ~]# vim /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock

监听地址不写在命令行里,交给 daemon.json:

bash 复制代码
[root@docker ~]# cat /etc/docker/daemon.json
{
  "hosts": [
    "unix:///var/run/docker.sock",
    "tcp://0.0.0.0:2375"
  ]
}

[root@docker ~]# systemctl daemon-reload
[root@docker ~]# systemctl restart docker
[root@docker ~]# lsof -i :2375
dockerd  2565  root  3u  IPv6  76021  0t0  TCP *:2375 (LISTEN)

两处不能省:

  • ExecStart= 那行空着不是笔误。 systemd 里同一指令第二次赋值是追加,先赋空值才能把原值整体清掉;少了它,两个 ExecStart 拼在一起,daemon 直接起不来。
  • override 里不能再出现 -H。 命令行有 -H、daemon.json 又有 hosts,dockerd 会拒绝启动,报 the following directives are specified both as a flag and in the configuration file: hosts。原来的 -H fd:// 也一并去掉------它和 unix:///var/run/docker.sock 是同一个地址的两种说法,改由 hosts 统一接管后,本机 docker 命令照常能用。
对照项 方式一:改单元文件 方式二:drop-in + daemon.json
监听地址写在哪 ExecStart 的 -H 参数 daemon.json 的 hosts
yum update docker-ce 之后 被覆盖,2375 丢失 不受影响
与 daemon.json 的 hosts 冲突,daemon 起不来 不冲突
适合 一次性实验 长期维护的机器

📌 systemctl edit --force docker.service 打开的其实就是 override.conf 这个文件,用它写和用 vim 写等价。改完用 systemctl cat docker 看合并后的最终结果,比对着两份文件猜更直接。

4.2 client 端:只装 CLI

客户端机器不需要 dockerd ,装一个 docker-ce-cli 就够:

bash 复制代码
[root@docker-cli ~]# yum install -y docker-ce-cli
[root@docker-cli ~]# docker -H 10.1.8.102 run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

4.3 验证:镜像确实在服务端

bash 复制代码
[root@docker-cli ~]# docker -H 10.1.8.102 images
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
httpd          latest    0a30f4c29d25   2 weeks ago     138MB
hello-world    latest    bf756fb1ae65   11 months ago   13.3kB

客户端本机 docker images 是空的------镜像和容器全都存在 server 端,客户端只是发指令。这一条能验证,就说明 C/S 分离真的成了。

⚠️ 注意 :2375 是明文、无认证的端口,能连上的人等于拿到服务端的 root 。只在隔离的实验网段开,别在公网机器上开。生产环境要用 2376 加 TLS 双向证书,或者干脆走 SSH 隧道。

五、Windows 与 macOS:Docker Desktop

非 Linux 桌面不用装 docker-ce,装 Docker Desktop 即可。它把 daemon、CLI、K8s 打包在同一个安装包里。

Windows 版的 Docker Desktop 需要跑在两种架构之上:

  • WSL 2(Windows Subsystem for Linux)------默认方式,轻量,文件性能好;
  • Hyper-V------旧方案,需要开启 Hyper-V 功能。

装完在 PowerShell 里执行 docker version 能同时看到 Client 与 Server 两段信息,就说明后端已经起来了。

⚠️ 注意 :Docker Desktop 在 Windows 上是跑在 WSL 2 的一个 Linux 发行版里的,不是原生 Windows 容器。想跑 Windows 容器得切到 Windows containers 模式,两者不能同时开。装之前先确认虚拟化已在 BIOS 中开启,否则 WSL 2 起不来。

六、部署排错速查

现象 原因 处理
Unable to find a match: docker-ce 没配 docker-ce.repo,或 $basearch 展开后路径不对 检查 /etc/yum.repos.d/docker-ce.repo 的 baseurl
Cannot connect to the Docker daemon 服务没起,或当前用户不在 docker 组 systemctl status docker;普通用户加组后重新登录
docker pull 卡在 Pulling fs layer 没配镜像加速,或加速站失效 改 daemon.json 的 registry-mirrors 后 restart
容器起来了但 80 端口访问不到 firewalld 被停掉,NAT 链失效 systemctl enable --now firewalld
docker -H ... 报 connection refused server 端没监听 2375 lsof -i :2375 确认,检查 service 文件与 daemon-reload
改了 daemon.json 没效果 daemon 只在启动时读一次 systemctl restart docker

📚 参考资料(官方文档)

附:术语英文全称速查表

缩写 英文全称 中文
C/S Client / Server 客户端 / 服务端
daemon --- 守护进程(常驻后台的服务程序)
CLI Command Line Interface 命令行界面
CE / EE Community Edition / Enterprise Edition 社区版 / 企业版
WSL Windows Subsystem for Linux Windows 的 Linux 子系统
NAT Network Address Translation 网络地址转换

把这篇压成一句话:客户端发指令,守护进程干活,仓库供货。 部署时六步顺序不能乱------源、防火墙、仓库、安装、加速、验收;验收要两个容器都过(hello-world 验链路、httpd 验端口)。C/S 分离只是把第一层的客户端搬到另一台机器,服务的、存的都还在 server 上;而 2375 那个端口,实验能开、生产不能开。

相关推荐
Decay__1 小时前
SQL Server 数据库全栈运维实战系列 — 合集索引(24 篇)
运维·数据库·sql·mysql·架构
安易算力6 小时前
Token计费系统设计与优化:算力API的后端架构实践
架构
Decay__9 小时前
12_MySQL InnoDB 内存与锁调优 —— Buffer Pool、redo log、隔离级别与锁等待
运维·数据库·sql·mysql·架构
一只爱撸猫的程序猿10 小时前
我看着一个 AI 引擎在 14 天里,从"记忆"走进了"搜索"
人工智能·后端·架构
java_logo11 小时前
Docker 部署 Dify:轻松搭建 LLM 应用开发平台
运维·docker·容器
方便面不加香菜12 小时前
Docker Container -- 综合实战案例详解
运维·docker·容器
Dawson Zhu15 小时前
从空间 Scaling 到时间 Scaling
人工智能·语言模型·架构·aigc·agi
修己xj17 小时前
在 AMD 机器上拉取 ARM 架构 Docker 镜像
docker
瑞克-桑切斯20 小时前
docker Harbor 单机部署手册
大数据·运维·elasticsearch·docker·容器