Docker 全维度实验:Harbor 仓库 | 多种网络 | 卷存储 | cgroup 资源控制 | 权限管控 | Swarm集群 | Portainer 可视化管理实操教程
目录
- 一、环境准备
- 二、私有仓库认证与部署
- 三、Docker网络实验
- 四、Docker数据管理
- 五、可视化管理与资源控制
- 六、Swarm集群与收尾
- 七、知识储备
虚拟机清单
| 主机名 | IP 地址 | 操作系统 | 核心实验角色 |
|---|---|---|---|
| docker1 | 192.168.52.171 | CentOS 7 x86_64 | Harbor 私有仓库主节点、Docker 基础实验、Convoy 卷插件、Portainer、Docker‑Swarm 管理节点 |
| docker2 | 192.168.52.165 | CentOS 7 x86_64 | Docker 客户端、Harbor 客户端、Convoy 卷插件、Docker‑Swarm 工作节点 |
软件版本清单
| 组件 | 版本说明 |
|---|---|
| 容器引擎 | Docker(26.1.4) |
| 私有仓库 | harbor‑offline‑installer‑v2.14.0 |
| 第三方卷插件 | convoy‑v0.5.2 |
| 可视化管理 | portainer‑ce |
| 系统工具 | nfs‑utils、lxcfs‑2.0.5‑3.el7.centos.x86_64、cgtools |
| 镜像 | nginx、centos:7、busybox |
Docker客户端节点部署对接reg.westos.org私有仓库
本实验完成第二台服务器docker2的环境初始化,配置主机域名解析、安装docker软件,配置镜像加速与私有仓库证书,最终实现内网私有仓库reg.westos.org的镜像上传下载全流程验证。
两台主机分别修改/etc/hosts做域名解析,docker1填写docker2的IP,docker2填写docker1与reg.westos.org域名指向docker1节点IP,保证两台机器都可以通过域名访问私有仓库。docker2执行`hostnamectl set‑hostname docker2`修改主机名,完成主机标识设置。
docker1把yum源文件、系统内核参数配置文件通过scp远程拷贝同步到docker2,减少重复配置工作。docker2使用yum安装docker‑ce软件包,设置docker服务开机自启并立即启动服务。编辑/etc/docker/daemon.json配置镜像加速地址为内网私有仓库地址,重启docker,通过docker info确认镜像加速配置已经生效。
docker2创建证书存放目录`/etc/docker/certs.d/reg.westos.org`,docker1将私有仓库根证书ca.crt远程推送至docker2对应目录。证书正确放置后,docker2可以正常访问HTTPS私有仓库,执行curl访问仓库catalog接口,能够返回仓库内镜像列表,代表网络与证书全部正常。
在docker1上对镜像打标签push上传到私有仓库,docker2通过curl查看仓库目录可以看到新增镜像,执行docker pull成功下载镜像。重复完成nginx、centos镜像的打标签推送、拉取测试,验证多节点访问同一套内网私有仓库功能可用。
多节点使用内网Docker私有仓库,首先要保证所有客户端域名解析正确,能够解析仓库域名`reg.westos.org`到仓库服务器IP。客户端需要安装docker环境,配置daemon.json镜像加速,并且导入私有仓库的根证书,否则HTTPS访问会报证书报错。证书必须放置在`/etc/docker/certs.d/仓库域名/`目录下,文件名固定为ca.crt。推送镜像需要按照仓库域名/项目名/镜像名:版本的格式打tag,再执行push上传。其他客户端就可以直接pull下载镜像。curl访问仓库catalog接口是快速验证仓库连通性的手段,不需要登录docker就可以测试连通。
[root@docker1 ~]# vim /etc/hosts

[root@docker2 ~]# hostnamectl set-hostname docker2
[root@docker2 ~]# vim /etc/hosts

虚拟机docker1
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7d2c3553ee30 registry "/entrypoint.sh /etc…" 5 days ago Up 42 minutes 0.0.0.0:443->443/tcp, :::443->443/tcp, 5000/tcp registry
[root@docker1 ~]# cd /etc/yum.repos.d/
[root@docker1 yum.repos.d]# ls
CentOS-Base.repo docker-ce.repo epel.repo
[root@docker1 yum.repos.d]# scp docker-ce.repo docker2:/etc/yum.repos.d
[root@docker1 ~]# cd /etc/sysctl.d/
[root@docker1 sysctl.d]# ls
99-sysctl.conf docker.conf
[root@docker1 sysctl.d]# scp docker.conf docker2:/etc/sysctl.d
虚拟机docker2
[root@docker2 ~]# cd /etc/yum.repos.d/
[root@docker2 yum.repos.d]# ls
CentOS-Base.repo docker-ce.repo
[root@docker2 yum.repos.d]# yum install -y docker-ce.x86_64 docker-ce-cli.x86_64
[root@docker2 yum.repos.d]# systemctl enable --now docker.service
[root@docker2 yum.repos.d]# cd /etc/sysctl.d/
[root@docker2 sysctl.d]# ls
99-sysctl.conf docker.conf
[root@docker2 sysctl.d]# cd /etc/docker/
[root@docker2 docker]# vim daemon.json
{
"registry-mirrors": ["https://reg.westos.org"]
}
[root@docker2 docker]# systemctl restart docker.service
[root@docker2 docker]# docker info | grep reg
https://reg.westos.org/
[root@docker2 docker]# mkdir certs.d
[root@docker2 docker]# cd certs.d
[root@docker2 certs.d]# mkdir reg.westos.org
[root@docker2 certs.d]# cd reg.westos.org
虚拟机docker1
[root@docker1 reg.westos.org]# scp ca.crt docker2:/etc/docker/certs.d/reg.westos.org/
虚拟机docker2
[root@docker2 reg.westos.org]# ls
ca.crt
[root@docker2 reg.westos.org]# cd
[root@docker2 ~]# curl -k https://reg.westos.org/v2/_catalog
{"repositories":["busybox","nginx","webserver"]}
虚拟机docker1
[root@docker1 ~]# cd
[root@docker1 ~]# docker tag reg.westos.org/nginx:latest reg.westos.org/library/nginx:latest
[root@docker1 ~]# docker push reg.westos.org/library/nginx:latest
虚拟机docker2
[root@docker2 ~]# curl -k https://reg.westos.org/v2/_catalog
{"repositories":["busybox","library/nginx","nginx","webserver"]}
[root@docker2 ~]# docker pull nginx:latest
虚拟机docker1
[root@docker1 ~]# docker tag centos:7 reg.westos.org/library/centos:7
[root@docker1 ~]# docker push reg.westos.org/library/centos:7
虚拟机docker2
[root@docker2 ~]# curl -k https://reg.westos.org/v2/_catalog
{"repositories":["busybox","library/centos","library/nginx","nginx","webserver"]}
[root@docker2 ~]# docker pull centos:7
Docker私有registry开启HTTPS基础身份认证
本实验在原有 HTTPS 私有仓库的基础上开启基础账号密码身份验证,实现访问仓库时必须输入用户名和密码,提升私有镜像仓库的安全性。首先安装httpd‑tools工具包,使用`htpasswd`命令生成存放账号与加密密码的认证文件,依次创建了 xx、lee 两个仓库用户;`‑c`参数代表新建密码文件,后续新增用户不能再使用`‑c`,否则会清空原有账号信息。生成密码文件后先停止原先无认证的 registry 容器,重新运行 registry 容器并挂载认证目录,重启仓库服务让认证配置生效。
开启认证之后,匿名访问仓库接口会返回`authentication required`未授权报错,查询镜像列表必须带上用户名密码参数;直接推送镜像也会失败,提示缺少认证凭据。这时就需要执行`docker login reg.westos.org`登录私有仓库,登录成功后账号密码会以 Base64 编码的形式保存在`/root/.docker/config.json`配置文件当中,可以使用 base64 解码命令查看明文账号密码。登录完成后权限生效,就可以正常给镜像打标签并推送至带认证的私有仓库。
客户端 docker2 同样需要先执行`docker login`登录仓库账号,才具备拉取镜像的权限;如果直接简写镜像名称`docker pull busybox:latest`,Docker 会默认去外网公共仓库下载,造成超时失败,所以拉取私有仓库镜像时必须写全仓库域名`reg.westos.org/busybox:latest`,最终成功下载镜像。整套流程验证了带账号密码认证的私有仓库部署成功,只有登录授权后的主机才能够上传、下载仓库内的镜像资源。
[root@docker1 ~]# yum install -y httpd-tools
[root@docker1 ~]# mkdir auth
[root@docker1 ~]# htpasswd -cB auth/htpasswd xx
New password:
Re-type new password:
Adding password for user xx
[root@docker1 ~]# cat auth/htpasswd
xx:$2y$05$gzV0wL6N60Ju3W0Om2QRvO/QSbqW/fphLuubsUVtTHMsE5QCRiIIG
[root@docker1 ~]# htpasswd -B auth/htpasswd lee
New password:
Re-type new password:
Adding password for user lee
[root@docker1 ~]# cat auth/htpasswd
xx:$2y$05$gzV0wL6N60Ju3W0Om2QRvO/QSbqW/fphLuubsUVtTHMsE5QCRiIIG
lee:$2y$05$VdFnHm86wJ33oPejGbBlB.n.vMWih79PW3Yw6BLRSyUZmNpBjyREC
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7d2c3553ee30 registry "/entrypoint.sh /etc…" 5 days ago Up 2 hours 0.0.0.0:443->443/tcp, :::443->443/tcp, 5000/tcp registry
[root@docker1 ~]# docker rm -f registry
registry
[root@docker1 ~]# docker run -d -p 443:443 --restart=always --name registry -v /registry:/var/lib/registry -v /root/certs:/certs -e REGISTRY_HTTP_ADDR=0.0.0.0:443 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/westos.org.crt -e REGISTRY_HTTP_TLS_KEY=/certs/westos.org.key -v /root/auth:/auth -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry
0ae4cda748d4037b256f11ce499b15e22ad3bf45ab927fa5a94e986edb220be6
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0ae4cda748d4 registry "/entrypoint.sh /etc…" 5 seconds ago Up 4 seconds 0.0.0.0:443->443/tcp, :::443->443/tcp, 5000/tcp registry
[root@docker1 ~]# curl -k https://reg.westos.org/v2/_catalog
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"registry","Class":"","Name":"catalog","Action":"*"}]}]}
[root@docker1 ~]# curl -k https://reg.westos.org/v2/_catalog -u xx:westos
{"repositories":["busybox","library/centos","library/nginx","nginx","webserver"]}
[root@docker1 ~]# docker images busybox
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest c6348fa86ba0 3 months ago 4.45MB
[root@docker1 ~]# docker tag busybox:latest reg.westos.org/library/busybox:latest
[root@docker1 ~]# docker push reg.westos.org/library/busybox:latest
The push refers to repository [reg.westos.org/library/busybox]
0958e0fef2d6: Preparing
no basic auth credentials
[root@docker1 ~]# docker login reg.westos.org
Username: xx
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@docker1 ~]# cat /root/.docker/config.json
"auths": {
"reg.westos.org": {
"auth": "eHg6d2VzdG9z"
}
[root@docker1 ~]# echo eHg6d2VzdG9z | base64 -d
xx:westos
[root@docker1 ~]# docker push reg.westos.org/library/busybox:latest
The push refers to repository [reg.westos.org/library/busybox]
0958e0fef2d6: Pushed
latest: digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3 size: 527
[root@docker2 ~]# docker login reg.westos.org
Username: xx
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@docker2 ~]# docker pull busybox:latest
Error response from daemon: Get "https://registry-1.docker.io/v2/": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
[root@docker2 ~]# docker pull reg.westos.org/busybox
Using default tag: latest
latest: Pulling from busybox
ce10b8c6cf7c: Pull complete
Digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3
Status: Downloaded newer image for reg.westos.org/busybox:latest
reg.westos.org/busybox:latest
Harbor企业级私有仓库部署、镜像推送与客户端拉取运行验证
本实验先清理环境中旧的registry容器,再部署Harbor离线v2.14.0版本企业级私有仓库,配置HTTPS证书、管理员密码,开启Trivy漏洞扫描组件,完成镜像推送、客户端拉取、容器运行以及Web日志审计全套验证。
首先清理旧的registry容器与测试容器,解压Harbor离线安装包,复制模板配置文件`harbor.yml.tmpl`生成`harbor.yml`进行修改。配置hostname为`reg.westos.org`,填写HTTPS证书与私钥文件路径,设置harbor管理员admin账号密码为westos。在宿主机创建`/data`持久化目录,把证书目录迁移到`/data`下,保证容器可以读取证书文件。部署完成后通过`docker compose ps`确认全部11个组件正常启动。
浏览器访问`https://IP`,使用admin/westos登录Harbor网页后台。docker1节点执行`docker login reg.westos.org`输入管理员账号密码,登录成功后把busybox、nginx镜像打标签推送到library公共项目。网页端进入library项目,确认镜像已经成功上传。
切换至docker2客户端节点,先执行docker logout清除旧登录凭据,删除本地缓存的busybox、nginx镜像。docker2可拉取镜像,镜像下载完成,在Harbor网页日志页面可以看到docker2节点的拉取操作记录,实现操作审计。使用下载好的nginx镜像启动容器,curl访问容器IP,返回nginx默认页面,证明从私有仓库获取的镜像完整可用。测试结束后删除测试容器,完成整套功能验证。
Harbor采用离线包方式部署,核心配置文件为harbor.yml,需要配置仓库域名、HTTPS证书路径、管理员密码,`--with‑trivy`参数开启漏洞扫描功能,整套服务由docker‑compose编排管理。登录仓库之后才可以执行push推送镜像,镜像需要带上仓库域名与项目名称完整标签。客户端可拉取私有仓库镜像。
Harbor网页后台可以查看镜像上传、拉取的操作日志,实现操作审计,满足企业运维要求。从私有仓库拉取的镜像可以正常启动容器,业务功能不受仓库来源影响。
[root@docker1 ~]# docker rm -f registry
registry
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5b14c687d359 webserver:v4 "nginx -g 'daemon of…" 5 days ago Exited (0) 5 days ago web1
[root@docker1 ~]# docker rm -f web1
web1
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# ls
anaconda-ks.cfg certs httpd.tar os.sql
auth docker mysql.tar
base-debian12.tar harbor-offline-installer-v2.14.0.tgz nginx.tar
[root@docker1 ~]# ls
anaconda-ks.cfg certs httpd.tar os.sql
auth docker mysql.tar
base-debian12.tar harbor-offline-installer-v2.14.0.tgz nginx.tar
[root@docker1 ~]# tar zxf harbor-offline-installer-v2.14.0.tgz
[root@docker1 ~]# cd harbor/
[root@docker1 harbor]# ls
common.sh harbor.yml.tmpl LICENSE
harbor.v2.14.0.tar.gz install.sh prepare
[root@docker1 harbor]# cp harbor.yml.tmpl harbor.yml
[root@docker1 harbor]# vim harbor.yml


[root@docker1 harbor]# cd
[root@docker1 ~]# mkdir /data
[root@docker1 ~]# ls
anaconda-ks.cfg certs harbor-offline-installer-v2.14.0.tgz nginx.tar
auth docker httpd.tar os.sql
base-debian12.tar harbor mysql.tar
[root@docker1 ~]# mv certs/ /data
[root@docker1 ~]# cd /data
[root@docker1 data]# ls
certs
[root@docker1 data]# cd certs/
[root@docker1 certs]# ls
westos.org.crt westos.org.key
[root@docker1 certs]# cd
[root@docker1 ~]# cd harbor/
[root@docker1 harbor]# ls
common.sh harbor.yml install.sh prepare
harbor.v2.14.0.tar.gz harbor.yml.tmpl LICENSE
[root@docker1 harbor]# ./install.sh --help
Note: Please set hostname and other necessary attributes in harbor.yml first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
Please set --with-trivy if needs enable Trivy in Harbor.
Please do NOT set --with-chartmuseum, as chartmusuem has been deprecated and removed.
Please do NOT set --with-notary, as notary has been deprecated and removed.
[root@docker1 harbor]# ./install.sh --with-trivy
✔ ----Harbor has been installed and started successfully.----
[root@docker1 harbor]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
harbor-core goharbor/harbor-core:v2.14.0 "/harbor/entrypoint.…" core 16 seconds ago Up 13 seconds (health: starting)
harbor-db goharbor/harbor-db:v2.14.0 "/docker-entrypoint.…" postgresql 16 seconds ago Up 14 seconds (health: starting)
harbor-jobservice goharbor/harbor-jobservice:v2.14.0 "/harbor/entrypoint.…" jobservice 16 seconds ago Up 7 seconds (health: starting)
harbor-log goharbor/harbor-log:v2.14.0 "/bin/sh -c /usr/loc…" log 16 seconds ago Up 15 seconds (health: starting) 127.0.0.1:1514->10514/tcp
harbor-portal goharbor/harbor-portal:v2.14.0 "nginx -g 'daemon of…" portal 16 seconds ago Up 14 seconds (health: starting)
nginx goharbor/nginx-photon:v2.14.0 "nginx -g 'daemon of…" proxy 16 seconds ago Up 12 seconds (health: starting) 0.0.0.0:80->8080/tcp, :::80->8080/tcp, 0.0.0.0:443->8443/tcp, :::443->8443/tcp
redis goharbor/redis-photon:v2.14.0 "redis-server /etc/r…" redis 16 seconds ago Up 14 seconds (health: starting)
registry goharbor/registry-photon:v2.14.0 "/home/harbor/entryp…" registry 16 seconds ago Up 14 seconds (health: starting)
registryctl goharbor/harbor-registryctl:v2.14.0 "/home/harbor/start.…" registryctl 16 seconds ago Up 14 seconds (health: starting)
trivy-adapter goharbor/trivy-adapter-photon:v2.14.0 "/home/scanner/entry…" trivy-adapter 16 seconds ago Up 13 seconds (health: starting)
[root@docker1 harbor]# docker compose logs registry
- 浏览器访问 docker1 IP地址 https://192.168.52.171
- 用户名:admin 密码:westos
[root@docker1 harbor]# cd
[root@docker1 ~]# docker logout reg.westos.org
Removing login credentials for reg.westos.org
[root@docker1 ~]# docker login reg.westos.org
Username: admin
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@docker1 ~]# docker push reg.westos.org/library/busybox:latest
The push refers to repository [reg.westos.org/library/busybox]
0958e0fef2d6: Pushed
latest: digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3 size: 527
[root@docker1 ~]# docker push reg.westos.org/library/nginx:latest
The push refers to repository [reg.westos.org/library/nginx]
a8fae4102358: Pushed
b0020123c41b: Pushed
f29840e7d6e5: Pushed
f109061e6486: Pushed
e8fb5de3ef79: Pushed
070ef859f070: Pushed
411a86676185: Pushed
latest: digest: sha256:e522a180e31d441de11e60f594308762d937bac46365a4e6ae12f1f8f559ce55 size: 1778
- 浏览器访问 https://192.168.52.171
- 点击 项目,点击 library,查看是否有 library/busybox、library/nginx
[root@docker2 ~]# docker logout reg.westos.org
Removing login credentials for reg.westos.org
[root@docker2 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 38becec41bfa 9 days ago 162MB
reg.westos.org/busybox latest c6348fa86ba0 3 months ago 4.45MB
centos 7 eeb6ee3f44bd 4 years ago 204MB
[root@docker2 ~]# docker rmi reg.westos.org/busybox nginx
[root@docker2 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos 7 eeb6ee3f44bd 4 years ago 204MB
[root@docker2 ~]# docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
ce10b8c6cf7c: Pull complete
Digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest
[root@docker2 ~]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
2e19782d7109: Pull complete
30576ad53d33: Pull complete
b8f66660faa6: Pull complete
657dd7fba849: Pull complete
c90544874aaf: Pull complete
8f655e1bd5c1: Pull complete
0a35a4e59186: Pull complete
Digest: sha256:e522a180e31d441de11e60f594308762d937bac46365a4e6ae12f1f8f559ce55
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
[root@docker2 ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 38becec41bfa 9 days ago 162MB
busybox latest c6348fa86ba0 3 months ago 4.45MB
centos 7 eeb6ee3f44bd 4 years ago 204MB
- 浏览器访问 docker1 IP地址 https://192.168.52.171
- 点击 日志,查看那是否有这两条日志记录,docker2从仓库拉取了 nginx、busybox

[root@docker2 ~]# docker run -d --name web1 nginx:latest
9e6424a3dce580806edffb95715a296cde135b65dedbd2afb67259967658a762
[root@docker2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9e6424a3dce5 nginx:latest "/docker-entrypoint.…" 4 seconds ago Up 2 seconds 80/tcp web1
[root@docker2 ~]# curl 172.17.0.2
<title>Welcome to nginx!</title>
[root@docker2 ~]# docker rm -f web1
web1
[root@docker2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Harbor 项目权限与用户管理实验
一、项目创建与访问隔离
在 Harbor 的 Web 管理界面中,通过「项目 - 新建项目」创建名为`test`的私有项目。
访问级别 不勾选「公开」,代表这是私有项目,只有被授权的用户才可以访问、拉取和推送该项目下的镜像;如果设为公开项目,所有用户无需认证都能拉取镜像。项目配额设为`-1`表示不限制该项目的镜像存储容量,镜像代理功能默认关闭。项目是 Harbor 中镜像资源隔离的基本单元,不同业务、不同团队可以分别创建独立项目,实现镜像资源的租户级隔离。
二、用户创建与项目角色授权
在「用户管理」中创建普通用户`xx`,配置邮箱、登录密码等基础信息。用户创建完成后,进入`test`项目的「成员」管理页面,将用户`xx`添加为项目成员,并分配「维护人员」角色。Harbor 提供分级角色权限体系:项目管理员拥有项目全部管理权限,维护人员可推送、拉取镜像并管理部分配置,开发者可推送和拉取,访客仅能拉取,受限访客只能拉取特定镜像。通过角色授权,可以实现细粒度的权限管控,让不同用户只拥有对应职责范围内的操作权限。
三、权限验证与镜像操作测试
退出管理员 admin 账号,使用`xx`账号登录 Harbor Web 界面,可以成功登录但只能看到有权限的`test`项目,无法访问其他项目和系统管理功能,验证了权限隔离生效。
在 docker2 客户端,使用`xx`账号执行`docker login reg.westos.org`完成仓库认证。将本地 busybox 镜像重新打标签为`reg.westos.org/test/busybox:latest`,标签中必须包含仓库域名、项目名、镜像名三层结构,才能正确匹配到 Harbor 的 test 私有项目。执行`docker push`成功上传镜像,说明维护人员角色拥有镜像推送权限。
拉取测试中,若简写为`test/busybox:latest`,Docker 会默认向公共镜像仓库发起请求,导致连接失败;必须写全完整路径`reg.westos.org/test/busybox:latest`,才能从 Harbor 私有项目中成功拉取镜像,验证了私有项目镜像的访问限制。总结
项目隔离机制
Harbor 以项目为单位实现镜像资源隔离,分为公开和私有两种访问级别。私有项目仅授权用户可访问,公开项目支持匿名拉取,通过项目划分可以满足多团队、多业务的镜像独立管理需求。
分级权限体系
Harbor 通过用户管理与项目角色绑定实现细粒度权限控制,不同角色对应不同的操作权限,能够按照职责分配用户权限,避免越权操作,符合企业安全运维规范。
镜像操作规范
访问私有项目镜像必须先完成账号认证,且镜像标签必须遵循「仓库域名 / 项目名 / 镜像名:版本」的完整格式,简写标签会导致 Docker 默认请求公共仓库而失败。这套权限与命名规则共同保障了私有镜像仓库的安全性与规范性。
- 浏览器访问 docker1 IP地址 https://192.168.52.171
- 点击 项目,点击 新建项目

- 点击 用户管理,点击创建用户

- 点击 项目,点击 test,点击 成员,点击 添加用户

- 点击 右上角 admin,点击退出
- 用户名 xx,密码 Xxwestos1,可以进行登录,但是权限有一定限制
- 退出,用 admin 身份登录
[root@docker2 ~]# docker login reg.westos.org
Username: xx
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
[root@docker2 ~]# docker tag busybox:latest reg.westos.org/test/busybox:latest
[root@docker2 ~]# docker push reg.westos.org/test/busybox:latest
The push refers to repository [reg.westos.org/test/busybox]
0958e0fef2d6: Pushed
latest: digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3 size: 527
[root@docker2 ~]# docker pull test/busybox:latest
Error response from daemon: Get "https://registry-1.docker.io/v2/": dial tcp 104.244.46.165:443: i/o timeout
[root@docker2 ~]# docker pull reg.westos.org/test/busybox:latest
latest: Pulling from test/busybox
Digest: sha256:92b1d1cae5f235812184415e63d9b24464116c58d3ba3c460b1eb0247f0f46e3
Status: Image is up to date for reg.westos.org/test/busybox:latest
reg.westos.org/test/busybox:latest
第二台 Harbor(从仓库)部署
一、主机域名解析与文件分发
首先在`docker1`与`docker2`两台服务器同时修改`/etc/hosts`本地域名解析文件,完成双向域名映射。`docker1`对应域名`reg.westos.org`,作为主 Harbor 仓库;`docker2`对应域名`slave.westos.org`,作为备用从仓库。配置 hosts 文件可以在不搭建 DNS 服务器的前提下,让两台主机都能够通过域名互相访问,这是后续 Harbor 之间镜像同步复制的基础前提。
随后使用`scp`远程拷贝命令,将 Harbor 离线安装包、yum 源配置文件从 docker1 发送至 docker2 节点,省去 docker2 重复下载资源的步骤,保证两台服务器使用完全一致的安装包与软件源环境。二、从节点基础环境与配置文件修改
docker2 节点解压离线安装包,复制生成`harbor.yml`配置文件并编辑。核心修改项:`hostname`填写从仓库域名`slave.westos.org`,指定 HTTPS 证书存放路径,同时设置 admin 管理员密码为`westos`。域名配置必须严格和后续生成证书时填写的通用名称保持一致,否则访问 Harbor 网页时会出现 HTTPS 证书域名不匹配报错。
接着安装`openssl11`证书生成工具,创建`/data/certs`目录用来存放证书与私钥文件,执行 openssl 命令生成自签名 HTTPS 证书,证书的通用名称填写`slave.westos.org`,与配置文件内主机名一一对应,保障 HTTPS 访问正常。三、执行部署与服务启动验证
证书生成完毕后,进入 harbor 安装目录,执行`./install.sh --with‑trivy`一键部署脚本。`--with‑trivy`参数开启镜像漏洞扫描组件,和主仓库 docker1 的安装参数保持统一,保证两个 Harbor 仓库功能组件完全一致。部署完成后通过`docker compose ps`查看全部容器状态,确认所有微服务容器正常运行。
最后使用浏览器访问 docker2 的 IP 地址`https://192.168.52.165`,输入账号`admin`,密码`westos`登录从仓库 Web 管理页面,验证从 Harbor 仓库部署成功。总结
本次实验完成了第二台独立 Harbor 从仓库的完整部署,两台仓库分别拥有独立域名`reg.westos.org`和`slave.westos.org`。hosts 域名解析是多台 Harbor 仓库通信的先决条件,HTTPS 自签名证书的域名必须和 harbor.yml 文件当中 hostname 完全匹配,否则会出现访问异常。两台 Harbor 搭建完成后,就可以继续配置主从镜像复制策略,实现镜像的备份、灾备与多仓库分发。
[root@docker1 ~]# vim /etc/hosts

[root@docker1 ~]# scp harbor-offline-installer-v2.14.0.tgz docker2:
root@docker2's password:
harbor-offline-installer-v2.14.0.tgz 100% 637MB 38.1MB/s 00:16
[root@docker1 ~]# cd /etc/yum.repos.d/
[root@docker1 yum.repos.d]# ls
CentOS-Base.repo docker-ce.repo epel.repo
[root@docker1 yum.repos.d]# scp epel.repo docker2:/etc/yum.repos.d/
root@docker2's password:
epel.repo 100% 664 384.6KB/s 00:00
[root@docker2 ~]# vim /etc/hosts

[root@docker2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker2 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker2 ~]# ls
anaconda-ks.cfg harbor-offline-installer-v2.14.0.tgz
[root@docker2 ~]# tar zxf harbor-offline-installer-v2.14.0.tgz
[root@docker2 ~]# cd harbor/
[root@docker2 harbor]# ls
common.sh harbor.yml.tmpl LICENSE
harbor.v2.14.0.tar.gz install.sh prepare
[root@docker2 harbor]# cp harbor.yml.tmpl harbor.yml
[root@docker2 harbor]# vim harbor.yml

![]()
[root@docker2 harbor]# cd
[root@docker2 ~]# yum install -y openssl11
[root@docker2 ~]# mkdir /data
[root@docker2 ~]# cd /data
[root@docker2 data]# ls
[root@docker2 data]# mkdir certs
[root@docker2 data]# openssl11 req -newkey rsa:4096 -nodes -sha256 -keyout certs/westos.org.key -addext "subjectAltName = DNS:slave.westos.org" -x509 -days 365 -out certs/westos.org.crt
Can't load /root/.rnd into RNG
140574418638656:error:2406F079:random number generator:RAND_load_file:Cannot open file:crypto/rand/randfile.c:98:Filename=/root/.rnd
Generating a RSA private key
.....................................................................++++
...........++++
writing new private key to 'certs/westos.org.key'
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
Country Name (2 letter code) [XX]:cn
State or Province Name (full name) []:shannxi
Locality Name (eg, city) [Default City]:xi'an
Organization Name (eg, company) [Default Company Ltd]:xiyou
Organizational Unit Name (eg, section) []:docker
Common Name (eg, your name or your server's hostname) []:slave.westos.org
Email Address []:
[root@docker2 data]# cd
[root@docker2 ~]# cd harbor/
[root@docker2 harbor]# ./install.sh --help
Note: Please set hostname and other necessary attributes in harbor.yml first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
Please set --with-trivy if needs enable Trivy in Harbor.
Please do NOT set --with-chartmuseum, as chartmusuem has been deprecated and removed.
Please do NOT set --with-notary, as notary has been deprecated and removed.
[root@docker2 harbor]# ./install.sh --with-trivy
✔ ----Harbor has been installed and started successfully.----
[root@docker2 harbor]# docker compose ps
- 浏览器访问 https://192.168.52.165
- 用户名:admin,密码:westos
Harbor 主从镜像复制
一、新建远程目标仓库连接
登录主仓库`reg.westos.org`的 Web 管理后台,进入仓库管理页面新建目标,用来建立
主 Harbor 和从仓库`slave.westos.org`之间的通信通道。提供者选择 Harbor,目标名称填写从仓库域名,目标 URL 填入从仓库服务器的 HTTPS 访问地址,访问 ID 和密码填写从仓库 admin 管理员账号信息。实验环境下取消勾选 验证远程证书,可以跳过自签名 HTTPS 证书校验,避免证书不信任导致连接失败,配置完成后点击测试连接,验证两台仓库网络连通正常。
二、配置推送‑基于模式的镜像复制规则
打开复制管理界面新建同步规则,复制模式选择`Push‑based`也就是主动推送模式,代表由当前主仓库主动把镜像发送给远端从仓库;另一种 Pull‑based 为拉取模式,由目标仓库主动拉取镜像。资源过滤器填写`library/**`,代表监控 library 项目下面所有镜像资源;目标仓库选择刚刚创建好的 slave 仓库。触发模式设置为事件驱动,只要主仓库上传新镜像就立刻触发同步任务;勾选删除本地资源时同时删除远程资源,保证主从两端镜像数据完全一致;勾选覆盖标签,当镜像标签更新时从仓库镜像会被同步覆盖,最后启用这条复制规则。
三、镜像推送与同步结果校验
在 docker1 主机上为本地镜像`webserver:v4`打上完整仓库标签,标签路径指向主仓库 library 项目,执行 docker push 命令上传镜像至主 Harbor。上传成功后主仓库检测到新镜像事件,立刻触发已经配置好的复制任务,将镜像自动同步发送给 slave 从仓库。分别刷新主仓库与从仓库 Web 页面,进入 library 项目查看镜像列表,检查`webserver`镜像以及对应的`v4`、`latest`标签是否成功同步到从仓库,以此验证主从镜像复制功能运行正常。
总结
本次实验实现 Harbor 主仓库到从仓库的镜像主动同步备份。仓库目标是两台 Harbor 建立通信的连接对象,复制模式 Push‑based 为主仓库主动推送镜像,事件驱动可以做到镜像上传后实时同步。主从复制可以用来实现镜像灾备、多机房分发,勾选同步删除选项能够保证两端镜像数据保持一致,在生产环境中经常用来保障容器镜像的数据安全。
- 浏览器访问https://192.168.52.171
- 用户名:admin,密码:westos
- 点击 仓库管理,点击新建目标

- 点击 复制管理,点击 新建规则

- 选中 复制规则slave.westos.org,查看复制任务是否正确执行并完成
- 浏览器访问https://192.168.52.165
- 刷新页面
- 点击 项目,点击 library,查看 是否存在 library/busybox,library/nginx
[root@docker1 yum.repos.d]# cd
[root@docker1 ~]# docker images webserver
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver v4 c1166fb727b3 5 days ago 62.3MB
webserver v3 d4aa4463f2ed 5 days ago 205MB
webserver v2 176662ca1d62 5 days ago 319MB
webserver v1 386d64ec58fb 5 days ago 553MB
[root@docker1 ~]# docker tag webserver:v4 reg.westos.org/library/webserver:v4
[root@docker1 ~]# docker tag webserver:v4 reg.westos.org/library/webserver:latest
[root@docker1 ~]# docker push reg.westos.org/library/webserver:v4
[root@docker1 ~]# docker push reg.westos.org/library/webserver:latest
- 浏览器访问https://192.168.52.171
- 刷新页面
- 点击 项目,点击 library,查看是否上传了 library/webserver
- 点击 复制管理,选中 复制规则slave.westos.org,查看复制任务是否正确执行并完成
- 浏览器访问https://192.168.52.171
- 刷新页面
- 点击 项目,点击 library,查看是否上传了 library/webserver
- 点击 library/webserver,查看 Tags 有 latest 和 v4
Harbor‑Trivy 漏洞扫描代理配置
正常情况下只要开启连接,打开虚拟网卡选项,无需手动进入容器配置环境变量,就可以直接在网页上发起扫描
一、Windows 端连接环境准备
Trivy 漏洞扫描组件运行时需要从外网下载漏洞数据库,虚拟机无法直接访问外网时,就可以借助 Windows 主机上的连接软件实现网络转发。首先在 Windows 系统打开 cmd 执行`ipconfig`,找到 VMnet8 虚拟网卡的 IPv4 地址,这个 IP 才是 NAT 模式下虚拟机可以连通的网关地址,不能使用 WiFi 或者物理网卡的公网 IP。接着查看 Clash 等连接软件的监听端口,确认混合连接端口号,后续将这个 IP 和端口填入 Harbor 扫描容器的连接环境变量。
二、进入扫描容器配置连接并下载漏洞库
通过`docker exec -it trivy‑adapter bash`命令进入 Trivy 扫描适配器容器内部,使用`env`查看当前环境变量,手动添加`HTTP_PROXY`与`HTTPS_PROXY`指向 Windows 主机的连接地址。配置完成后执行`trivy image --download‑db‑only`命令,仅下载漏洞数据库文件。下载成功之后,漏洞库文件就保存在容器内`/home/scanner/.cache/trivy/db`目录下,数据库文件`trivy.db`是后续镜像漏洞检测的核心数据文件。该数据库下载只需要执行一次,下载完成后就可以离线执行扫描任务。
三、网页端执行镜像漏洞扫描
漏洞数据库准备完毕后,登录 Harbor 的 Web 管理页面,进入 library 项目,选中`busybox`、`nginx`等镜像,点击漏洞扫描按钮触发检测任务。Trivy 会对比镜像文件和本地漏洞数据库,检测镜像内部包含的已知安全漏洞,扫描完成后即可查看每一个镜像对应的漏洞风险报告。正常情况下只要开启连接,打开虚拟网卡选项,无需手动进入容器配置环境变量,就可以直接在网页上发起漏洞扫描,手动配置连接容器环境变量属于进阶排查手段。
实验总结
Trivy 是 Harbor 内置的镜像漏洞扫描工具,扫描前必须下载外网的漏洞数据库,虚拟机无法直连外网时可以借助 Windows 主机 VMnet8 网卡的连接完成下载。漏洞数据库下载完成后会缓存到扫描容器的本地目录,后续扫描镜像不再需要重复下载。网页端发起漏洞扫描任务,Trivy 就可以比对缓存的漏洞库,检测容器镜像当中存在的安全缺陷,以此提升镜像仓库的安全性。
开启连接,打开 虚拟网卡选项
- 先看 Windows 本机 IP
按 `Win+R` → 输入 `cmd`,执行:
ipconfig
找到 VMware Network Adapter VMnet8(NAT 网卡),IPv4 地址就是虚拟机里填的连接 IP。
注意:不要用 Windows 物理网卡(Wi‑Fi / 以太网)的公网 IP,一定要 VMnet8 的 IP,虚拟机才能连通。
- 查连接软件的端口(10081)
[root@docker2 harbor]# docker exec -it trivy-adapter bash
scanner [ / ]$ env
SCANNER_TRIVY_VULN_TYPE=os,library
SCANNER_TRIVY_REPORTS_DIR=/home/scanner/.cache/reports
HOSTNAME=9062b2ffd697
SCANNER_TRIVY_GITHUB_TOKEN=
SCANNER_TRIVY_INSECURE=False
SCANNER_TRIVY_CACHE_DIR=/home/scanner/.cache/trivy
PWD=/
SCANNER_JOB_QUEUE_REDIS_NAMESPACE=harbor.scanner.trivy:job-queue
SCANNER_TRIVY_TIMEOUT=5m0s
HOME=/home/scanner
SCANNER_TRIVY_SECURITY_CHECKS=vuln
SCANNER_REDIS_URL=redis://redis:6379/5?idle_timeout_seconds=30
SCANNER_TRIVY_SKIP_JAVA_DB_UPDATE=False
SCANNER_TRIVY_OFFLINE_SCAN=False
SCANNER_LOG_LEVEL=info
TERM=linux
NO_PROXY=registryctl,log,postgresql,redis,.internal,trivy-adapter,jobservice,127.0.0.1,exporter,registry,db,.local,portal,core,nginx,localhost
SHLVL=1
HTTPS_PROXY=http://192.168.52.1:7890
HTTP_PROXY=http://192.168.52.1:7890
SCANNER_STORE_REDIS_NAMESPACE=harbor.scanner.trivy:store
SCANNER_JOB_QUEUE_REDIS_URL=redis://redis:6379/5?idle_timeout_seconds=30
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SCANNER_STORE_REDIS_URL=redis://redis:6379/5?idle_timeout_seconds=30
SCANNER_TRIVY_IGNORE_UNFIXED=False
SCANNER_TRIVY_SEVERITY=UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
SCANNER_TRIVY_SKIP_UPDATE=False
TRIVY_VERSION=v0.66.0
_=/usr/bin/env
scanner [ / ]$ trivy image --download-db-only
2026-09-03T12:47:49Z INFO [vulndb] Need to update DB
2026-09-03T12:47:49Z INFO [vulndb] Downloading vulnerability DB...
2026-09-03T12:47:49Z INFO [vulndb] Downloading artifact... repo="mirror.gcr.io/aquasec/trivy-db:2"
110.38 MiB / 110.38 MiB [----------------------------] 100.00% 2.12 MiB p/s 52s
2026-09-03T12:48:45Z INFO [vulndb] Artifact successfully downloaded repo="mirror.gcr.io/aquasec/trivy-db:2"
scanner [ / ]$ cd /home/scanner/
scanner [ ~ ]$ ls
bin ca-bundle.crt.original entrypoint.sh install_cert.sh
scanner [ ~ ]$ cd .cache
scanner [ ~/.cache ]$ ls
reports trivy
scanner [ ~/.cache ]$ cd trivy/
scanner [ ~/.cache/trivy ]$ ls
db
scanner [ ~/.cache/trivy ]$ cd db/
scanner [ ~/.cache/trivy/db ]$ ls
metadata.json trivy.db
scanner [ ~/.cache/trivy/db ]$ ls -l
total 1305236
-rw-r--r-- 1 scanner scanner 151 2026-09-03 12:48 metadata.json
-rw-r--r-- 1 scanner scanner 1336557568 2026-09-03 07:15 trivy.db
scanner [ ~/.cache/trivy/db ]$ exit
exit
- 正常来说,开连接后,直接可以进行漏洞扫描,上面的配置,仅增加知识
- 开启连接,打开 虚拟网卡选项
- 浏览器访问 https://192.168.52.171 或 https://192.168.52.165
- 点击 项目,点击 library,点击 library/busybox,选中,点击 漏洞扫描
- 点击 项目,点击 library,点击 library/nginx,选中,点击 漏洞扫描
手动为 none 网络模式的容器添加 veth 网卡
手动把一张虚拟网卡添加到`--network none`无网络的 busybox 容器中,从底层演示 Docker 的网络命名空间原理。
首先启动无网络容器`docker run -it --rm --network none busybox`,`--network none`模式下容器只有回环网卡`lo`,没有任何外部网卡。按下`Ctrl+P+Q`快捷键,不关闭容器,把容器放到后台运行。这条快捷键区别于`Ctrl+C`,不会终止容器进程。
接下来通过`docker inspect`拿到容器 PID,进入`/proc/容器PID/ns/`目录。`ns`目录下存放进程所有的命名空间文件,其中`net`链接就是容器专属的网络命名空间。通过软链接,把容器的网络命名空间挂载到宿主机`/var/run/netns/myns`,这样宿主机`ip netns`工具就能够识别、管理这个容器的网络环境。
然后创建一对`veth‑pair`虚拟网卡,`vetha`、`vethz`相当于一根虚拟网线的两头。使用`ip link set vethz netns myns`,将网卡的一端移入容器的网络命名空间,另一端留在宿主机。之后分别给宿主机侧`vetha`、容器侧`vethz`配置 10.0.0.1 与 10.0.0.2 的 IP 地址,并且启用 UP 激活两张网卡。
最后执行 ping 双向连通测试,宿主机与容器可以互相通信。回到容器内部执行`ip a`,就能够看到刚刚手动添加进去的 veth 网卡。完整复现了 Docker 网桥给容器分配网卡的底层流程。
本次实验完整演示 Linux 网络命名空间底层原理。`--network none`的容器初始无外部网卡,通过 /proc 目录找到容器网络命名空间,创建软链接让宿主机 ip 工具接管该命名空间。利用 veth‑pair 虚拟网线,一端留在宿主机,一端移入容器,配置 IP 之后宿主机和容器成功互通。
这个实操展示了 Docker 给容器添加网卡的底层实现,所有 docker 的容器网络,本质都是依靠网络命名空间 + veth 虚拟网卡实现网络隔离与通信。`Ctrl+P+Q`是后台退出交互式容器的关键快捷键,可以避免容器意外停止。
[root@docker1 ~]# docker run -it --rm --network none busybox
/ #
- Ctrl + P + Q 后台运行
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14543c9152d2 busybox "sh" About a minute ago Up About a minute serene_solomon
[root@docker1 ~]# docker attach serene_solomon
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
/ # read escape sequence
[root@docker1 ~]# docker inspect serene_solomon
[root@docker1 ~]# docker inspect serene_solomon | grep Pid
"Pid": 8774,
"PidMode": "",
"PidsLimit": null,
[root@docker1 ns]# cd /var/run
[root@docker1 run]# ls | grep netns
[root@docker1 run]# ip netns
[root@docker1 run]# mkdir netns
[root@docker1 run]# ls | grep netns
netns
[root@docker1 run]# cd netns
[root@docker1 netns]# ls
root@docker1 ~]# cd /proc
[root@docker1 proc]# ls | grep 8774
8774
[root@docker1 proc]# cd 8774/
[root@docker1 8774]# ls
[root@docker1 8774]# cd ns/
[root@docker1 ns]# ll
total 0
lrwxrwxrwx 1 root root 0 Sep 5 09:57 ipc -> ipc:[4026532495]
lrwxrwxrwx 1 root root 0 Sep 5 09:57 mnt -> mnt:[4026532493]
lrwxrwxrwx 1 root root 0 Sep 5 09:54 net -> net:[4026532498]
lrwxrwxrwx 1 root root 0 Sep 5 09:57 pid -> pid:[4026532496]
lrwxrwxrwx 1 root root 0 Sep 5 09:57 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep 5 09:57 uts -> uts:[4026532494]
[root@docker1 ns]# pwd
/proc/8774/ns
[root@docker1 ns]# ln -s /proc/8774/ns/net /var/run/netns/myns
[root@docker1 ns]# ip netns
myns
[root@docker1 ns]# ip a
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7a:d3:2e:c4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip link add vetha type veth peer name vethz
[root@docker1 ns]# ip a
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7a:d3:2e:c4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
96: vethz@vetha: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff
97: vetha@vethz: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 6a:33:ad:f7:c7:37 brd ff:ff:ff:ff:ff:ff
[root@docker1 ns]# ip netns exec myns ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip link set vethz netns myns
[root@docker1 ns]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: vethz@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@docker1 ns]# ip a a 10.0.0.1/24 dev vetha
[root@docker1 ns]# ip a
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7a:d3:2e:c4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
97: vetha@if96: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 6a:33:ad:f7:c7:37 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.0.1/24 scope global vetha
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip link set dev vetha up
[root@docker1 ns]# ip a
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7a:d3:2e:c4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
97: vetha@if96: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
link/ether 6a:33:ad:f7:c7:37 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.0.1/24 scope global vetha
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: vethz@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
[root@docker1 ns]# ip -n myns a a 10.0.0.2/24 dev vethz
[root@docker1 ns]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: vethz@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.0.2/24 scope global vethz
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip -n myns link set vethz up
[root@docker1 ns]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: vethz@if97: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.0.2/24 scope global vethz
valid_lft forever preferred_lft forever
inet6 fe80::20ca:36ff:fe5b:5e82/64 scope link
valid_lft forever preferred_lft forever
[root@docker1 ns]# ip r
default via 192.168.52.2 dev ens33 proto static metric 100
10.0.0.0/24 dev vetha proto kernel scope link src 10.0.0.1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.52.0/24 dev ens33 proto kernel scope link src 192.168.52.171 metric 100
[root@docker1 ns]# ip -n myns r
10.0.0.0/24 dev vethz proto kernel scope link src 10.0.0.2
[root@docker1 ns]# ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.251 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.095 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=0.112 ms
^C
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.095/0.152/0.251/0.071 ms
[root@docker1 ns]# ip netns exec myns ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.143 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.106 ms
64 bytes from 10.0.0.1: icmp_seq=3 ttl=64 time=0.111 ms
^C
--- 10.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.106/0.120/0.143/0.016 ms
[root@docker1 ns]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14543c9152d2 busybox "sh" 22 minutes ago Up 22 minutes serene_solomon
[root@docker1 ns]# docker attach serene_solomon
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: vethz@if97: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 scope global vethz
valid_lft forever preferred_lft forever
inet6 fe80::20ca:36ff:fe5b:5e82/64 scope link
valid_lft forever preferred_lft forever
/ # read escape sequence
Docker 自定义网桥 my_net1 搭建与内置 DNS 域名解析
本次实验分为环境清理、自定义网桥创建、容器 DNS 解析验证、启停稳定性验证四个阶段,核心是验证 Docker 自定义 bridge 网络的内置域名解析能力。
第一阶段是上一轮手动网络命名空间实验的环境收尾。首先在 myns 命名空间内将容器侧 vethz 网卡重命名为 eth0。随后强制删除旧的 busybox 测试容器,容器销毁后其对应的网络命名空间同步失效,再手动删除宿主机上残留的 netns 挂载点`myns`,完成实验环境复位。
第二阶段:创建自定义 bridge 网络。执行`docker network create -d bridge my_net1`创建用户自定义网桥,Docker 自动分配`172.19.0.0/16`子网,网关为`172.19.0.1`。宿主机上会生成对应`br-`前缀的虚拟网桥设备,通过`brctl show`可以看到网桥实体,`docker network ls`可查看新增的 my_net1 网络,驱动类型为 bridge,作用域为本地。
第三阶段:验证容器间名称解析。先后启动 nginx 容器 web1 和 busybox 测试容器,二者均接入 my_net1 网络。进入 busybox 容器后,直接使用容器名`web1`即可 ping 通对方,无需填写具体 IP 地址。查看容器内`/etc/hosts`文件,仅包含本机回环地址与自身 IP 映射,并没有 web1 的主机记录;查看`/etc/resolv.conf`可见 DNS 服务器为`127.0.0.11`,这是 Docker 内置的 DNS 解析服务,同自定义网络下的容器名称解析均由该服务完成,而非依赖本地 hosts 文件。
第四阶段:验证容器重启后的通信稳定性。先后停止、重启 demo 与 web1 容器,重新接入后依然可以通过容器名正常 ping 通。即使容器重启后内网 IP 发生变化,内置 DNS 也会自动更新解析记录,容器间通信不受 IP 变动影响。
[root@docker1 proc]# cd
[root@docker1 ~]# ip -n myns link set dev vethz down
[root@docker1 ~]# ip -n myns link set dev vethz name eth0
[root@docker1 ~]# ip -n myns link set dev eth0 up
[root@docker1 ~]# ip -n myns a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
96: eth0@if97: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 22:ca:36:5b:5e:82 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 10.0.0.2/24 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::20ca:36ff:fe5b:5e82/64 scope link
valid_lft forever preferred_lft forever
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14543c9152d2 busybox "sh" About an hour ago Up About an hour serene_solomon
[root@docker1 ~]# docker rm -f serene_solomon
serene_solomon
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# ip netns
myns
[root@docker1 ~]# ip netns delete myns
[root@docker1 ~]# ip netns
[root@docker1 ~]# docker network create -d bridge my_net1
764f0f93eae6f983e6dc206125fcfe72f316e887e4d19358b634df5347ed15eb
[root@docker1 ~]# docker network inspect my_net1
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
[root@docker1 ~]# ip a
98: br-764f0f93eae6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:36:f0:89:ea brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 brd 172.19.255.255 scope global br-764f0f93eae6
valid_lft forever preferred_lft forever
[root@docker1 ~]# brctl show
bridge name bridge id STP enabled interfaces
br-764f0f93eae6 8000.024236f089ea no
docker0 8000.02427ad32ec4 no
[root@docker1 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
a1fb7fb46bb3 bridge bridge local
141b4496ed4d host host local
764f0f93eae6 my_net1 bridge local
f72013194df3 none null local
[root@docker1 ~]# docker run -d --name web1 --network my_net1 nginx
e368773207dd7d8a83d20ef18275752f6aa7bd432848051886df7417e266196b
[root@docker1 ~]# docker run -it --rm --network my_net1 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
101: eth0@if102: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.3/16 brd 172.19.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ping web1
PING web1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.274 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.244 ms
64 bytes from 172.19.0.2: seq=2 ttl=64 time=0.158 ms
^C
--- web1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.158/0.225/0.274 ms
/ # cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.19.0.3 cb6f5c74fd38
/ # exit
[root@docker1 ~]# docker exec -it web1 bash
root@e368773207dd:/# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.19.0.2 e368773207dd
root@e368773207dd:/# cat /etc/resolv.conf
Generated by Docker Engine.
This file can be edited; Docker Engine will not make further changes once it
has been modified.
nameserver 127.0.0.11
options ndots:0
Based on host file: '/etc/resolv.conf' (internal resolver)
ExtServers: [114.114.114.114]
Overrides: []
Option ndots from: internal
root@e368773207dd:/# exit
exit
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e368773207dd nginx "/docker-entrypoint.…" 3 minutes ago Up 3 minutes 80/tcp web1
[root@docker1 ~]# docker run -it --name demo --network my_net1 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
103: eth0@if104: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.3/16 brd 172.19.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ping demo
PING demo (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.109 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.158 ms
^C
--- demo ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.109/0.133/0.158 ms
/ # ping web1
PING web1 (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.166 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.193 ms
^C
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.166/0.179/0.193 ms
/ # exit
[root@docker1 ~]# docker stop demo
demo
[root@docker1 ~]# docker stop web1
web1
[root@docker1 ~]# docker start demo
demo
[root@docker1 ~]# docker start web1
web1
[root@docker1 ~]# docker attach demo
/ # ping demo
PING demo (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.088 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.143 ms
^C
--- demo ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.088/0.115/0.143 ms
/ # ping web1
PING web1 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.485 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.199 ms
^C
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.199/0.342/0.485 ms
/ # exit
Docker自定义网络隔离与单容器多网卡跨网络接入
本次实验核心是验证Docker自定义网桥的网络隔离特性,以及实现单容器跨多网络接入的方法,完整展示不同自定义网络之间的默认隔离规则与打通方式。
第一阶段:创建第二个自定义网络。执行`docker network create --subnet 10.0.0.0/24 --gateway 10.0.0.1 my_net2`,手动指定网段与网关,创建出第二个独立的bridge网络。通过`docker network inspect`可确认子网配置,此时宿主机上存在my_net1(172.19.0.0/16)、my_net2(10.0.0.0/24)两套互相独立的虚拟网桥。随后启动nginx容器web2,接入my_net2网络。
第二阶段:默认跨网络隔离测试。尝试attach已停止的demo容器,系统报错`You cannot attach to a stopped container`,验证了attach命令仅能连接运行中的容器。启动demo容器后再次attach进入,该容器默认仅接入my_net1网络。执行ping测试:ping同网络的web1可以正常通信;ping另一个网络的web2直接报错`bad address`。这说明两个核心现象:第一,不同自定义网桥下的容器默认无法直接通信;第二,Docker内置DNS仅能解析同网络内的容器名称,跨网络的容器名无法解析。
第三阶段:底层隔离规则验证。查看iptables的nat表与filter表:nat表的POSTROUTING链为每个自定义网段都配置了MASQUERADE地址伪装规则,负责容器访问外部网络的地址转换;filter表的`DOCKER-ISOLATION-STAGE`系列链,就是实现不同网桥之间二层隔离的核心规则,默认阻止不同自定义网络之间的流量互通。
第四阶段:单容器动态接入多网络。执行`docker network connect my_net2 demo`,为已经运行的demo容器动态接入my_net2网络。再次通过`docker inspect demo`查看网络信息,demo容器此时拥有两个网卡、两个IP地址:my_net1中的172.19.0.2,以及my_net2中新分配的10.0.0.3,同时两个网络都有对应的DNS名称解析。
第五阶段:双网络连通验证。重新进入demo容器,再次执行ping测试:同网络的web1依旧可以通信,原本不通的web2也可以正常ping通。此时demo容器相当于同时处在两个网络中,通过两块虚拟网卡分别和两个网络内的容器互通。
Docker的每个自定义bridge网络都是独立的二层网络域,默认情况下不同网络之间通过iptables隔离链实现流量隔离,且内置DNS也仅支持同网络容器名解析,跨网络既无法解析名称也无法直接通信。这是Docker网络安全的默认机制,可实现不同业务环境的网络隔离。通过`docker network connect`命令可以为运行中的容器动态接入额外的网络,容器会新增对应网段的虚拟网卡,同时拥有多个IP地址。接入多网络的容器可以同时和多个网络内的容器通信,相当于充当了跨网络的节点,这种方式常用于需要同时访问多个隔离业务网络的网关类容器。
整个实验也验证了Docker网络隔离的底层实现:不同网桥对应独立的虚拟交换机,配合iptables隔离规则实现三层流量阻断;网络互通不需要手动修改防火墙规则,只需将容器同时接入对应网络即可。
[root@docker1 ~]# docker network create --subnet 10.0.0.0/24 --gateway 10.0.0.1 my_net2
1068915a8f4397651bf64aeeffa5234714828daa3319f91d3aeedcd29ab73374
[root@docker1 ~]# docker network inspect my_net2
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1"
}
[root@docker1 ~]# ip a
[root@docker1 ~]# brctl show
[root@docker1 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
a1fb7fb46bb3 bridge bridge local
141b4496ed4d host host local
764f0f93eae6 my_net1 bridge local
1068915a8f43 my_net2 bridge local
f72013194df3 none null local
[root@docker1 ~]# docker run -d --name web2 --network my_net2 nginx
336ed94927d838de17d565131df5b96c2b8e5d224b8f37ba0bf2f2621d4f389a
[root@docker1 ~]# docker attach demo
You cannot attach to a stopped container, start it first
[root@docker1 ~]# docker start demo
demo
[root@docker1 ~]# docker attach demo
/ # ping web1
PING web1 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.338 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.198 ms
^C
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.198/0.268/0.338 ms
/ # ping web2
ping: bad address 'web2'
/ # exit
[root@docker1 ~]# iptables -t nat -nL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 10.0.0.0/24 0.0.0.0/0
MASQUERADE all -- 172.19.0.0/16 0.0.0.0/0
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
[root@docker1 ~]# iptables -nL
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source destination
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
336ed94927d8 nginx "/docker-entrypoint.…" 37 minutes ago Up 37 minutes 80/tcp web2
0b2f622dd6c1 busybox "sh" About an hour ago Up About a minute demo
e368773207dd nginx "/docker-entrypoint.…" About an hour ago Up 17 minutes 80/tcp web1
[root@docker1 ~]# docker inspect demo
"Networks": {
"my_net1": {
"Gateway": "172.19.0.1",
"IPAddress": "172.19.0.2",
[root@docker1 ~]# docker network inspect my_net2
"Name": "web2",
"IPv4Address": "10.0.0.2/24",
[root@docker1 ~]# docker network connect my_net2 demo
[root@docker1 ~]# docker inspect demo
"Networks": {
"my_net1": {
"Gateway": "172.19.0.1",
"IPAddress": "172.19.0.2",
"IPPrefixLen": 16,
"DNSNames": [
"demo",
"0b2f622dd6c1"
]
},
"my_net2": {
"Gateway": "10.0.0.1",
"IPAddress": "10.0.0.3",
"IPPrefixLen": 24,
"DNSNames": [
"demo",
"0b2f622dd6c1"
[root@docker1 ~]# docker attach demo
/ # ping web1
PING web1 (172.19.0.3): 56 data bytes
64 bytes from 172.19.0.3: seq=0 ttl=64 time=0.288 ms
64 bytes from 172.19.0.3: seq=1 ttl=64 time=0.260 ms
^C
--- web1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.260/0.274/0.288 ms
/ # ping web2
PING web2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.712 ms
64 bytes from 10.0.0.2: seq=1 ttl=64 time=0.145 ms
^C
--- web2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.145/0.428/0.712 ms
/ # exit
Docker容器固定IP分配与container共享网络命名空间模式
本次实验分为两个核心部分:自定义网络下容器固定IP分配,以及`container`共享网络命名空间模式的验证与应用。
固定IP启动容器
在自定义网桥`my_net2`中,启动容器时添加`--ip 10.0.0.20`参数,可以手动为web3容器指定静态IP地址。
- 只有用户自定义的bridge网络支持IPAM静态地址分配,默认的docker0网桥无法使用--ip手动指定IP;
- 通过`docker inspect web3`可以验证,容器最终分配的IP与指定值完全一致,不会由Docker随机分配;
- 固定IP可以避免容器重启后地址变动,适合需要依赖固定地址提供服务的场景。container共享网络模式
`--network container:目标容器名`是Docker的一种特殊网络模式,核心是完全复用目标容器的网络命名空间。
- 新容器不会创建独立的虚拟网卡,也不会分配独立的IP地址,二者共享完全相同的网络栈、IP、端口空间、路由表和防火墙规则;
- 实验中分别以web3、web2、web1为目标启动busybox容器,执行`ip a`查看,新容器的eth0网卡地址与目标容器完全一致,验证了网络栈的共享特性;
- 两个容器仅网络层面共享,文件系统、进程空间依然互相隔离。共享网络的本地访问验证
最后以`container:web1`模式启动centos:7容器,进入后执行`curl localhost`,成功返回web1容器的Nginx默认页面。
- 因为二者共享同一个网络命名空间,本地80端口就是web1中Nginx监听的端口,直接访问本地地址即可命中对方的服务,无需通过容器IP跨网络访问;
- 这种模式常被用于网络故障排查、sidecar边车代理(如服务网格的流量代理注入)等场景,无需修改原容器即可扩展网络功能。本次实验验证了Docker自定义网络的静态IP能力与container共享网络模式的原理。自定义bridge网络支持通过--ip参数为容器分配固定地址,解决了默认动态IP重启变动的问题,适合服务地址需要固定的部署场景。
container网络模式的本质是共享网络命名空间,新容器与目标容器共用完整的网络协议栈,二者IP、端口完全一致,网络层面没有隔离。这种模式下直接访问localhost就能访问到目标容器的服务,通信效率最高。
共享网络模式是很多高级架构的底层基础,边车代理、网络抓包、日志采集等旁路工具,都可以通过这种方式在不侵入业务容器的前提下,接入到业务容器的网络环境中。
[root@docker1 ~]# docker network inspect my_net2
"Name": "demo",
"MacAddress": "02:42:0a:00:00:03",
"IPv4Address": "10.0.0.3/24",
"Name": "web2",
"MacAddress": "02:42:0a:00:00:02",
"IPv4Address": "10.0.0.2/24",
[root@docker1 ~]# docker run -d --name web3 --network my_net2 --ip 10.0.0.20 nginx
b56400ce5c63dc6c46e382c5cb809562d265699457cb5480b1c12b4fe390df6d
[root@docker1 ~]# docker inspect web3
"Networks": {
"my_net2": {
"IPAMConfig":
"IPv4Address": "10.0.0.20"
"Gateway": "10.0.0.1",
"IPAddress": "10.0.0.20",
"IPPrefixLen": 24,
"DNSNames": [
"web3",
[root@docker1 ~]# docker run -it --rm --network container:web3 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
118: eth0@if119: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:0a:00:00:14 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.20/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
/ # exit
[root@docker1 ~]# docker run -it --rm --network container:web2 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
110: eth0@if111: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
/ # exit
[root@docker1 ~]# docker run -it --rm --network container:web1 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
122: eth0@if123: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:13:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.3/16 brd 172.19.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # exit
[root@docker1 ~]# docker run -it --rm --network container:web1 centos:7
[root@e368773207dd /]# curl localhost
<title>Welcome to nginx!</title>
[root@e368773207dd /]# exit
exit
Docker 批量容器删除与 prune 系列资源清理命令
这是一套完整的 Docker 实验环境清理流程,分为批量清空容器和prune 系列闲置资源回收两个阶段。
批量强制删除所有容器 docker rm -f `docker ps -aq`
这是实验场景最常用的容器批量清理命令。
- `docker ps -aq`:`-a`匹配所有状态的容器,`-q`只输出容器 ID,生成完整的容器 ID 列表;
- 反引号是 Shell 命令替换语法,先执行内层命令,将所有容器 ID 作为参数传给外层的删除命令;
- `-f`强制删除参数,无需先停止容器,运行中的容器也会被直接终止并删除。
执行后所有容器被一次性清空,后续`docker ps -a`验证结果为空列表。prune 系列资源清理命令
`prune`是 Docker 统一的闲置资源清理指令,核心规则是**只清理未被任何容器引用 / 使用的资源**,正在运行的容器关联的资源不会被误删。
1. `docker network prune`
清理所有未被容器占用的自定义网络。只会删除用户创建的自定义网桥(如实验中的 my_net1、my_net2),Docker 内置的默认网络 bridge、host、none 永远不会被清理。
2. `docker volume prune`
清理所有未被任何容器挂载的 Docker 命名数据卷。仅作用于 Docker 管理的命名卷,宿主机手动绑定挂载的目录不受该命令影响。
3. `docker image prune`
默认仅清理**悬空镜像(dangling image)**,也就是标签为`<none>`、失去引用的中间镜像层。如果追加`-a`参数,会删除所有没有被容器使用的镜像,清理更彻底。
4. `docker system prune`
一站式综合清理命令,默认一次性清理停止的容器、闲置网络、悬空镜像以及构建缓存。注意默认不会清理数据卷,需要追加`-v`参数才会同时清理卷;追加`-a`会删除所有未使用镜像而非仅悬空镜像。
这套操作是 Docker 实验环境的标准复位流程。先强制清空所有容器,解除容器对网络、卷、镜像的占用,再通过 prune 命令分别回收各类闲置资源,也可以直接用 system prune 完成一站式清理。prune 命令遵循 “未使用才删除” 的安全规则,不会影响运行中容器依赖的资源。这套命令适合实验环境快速重置,但生产环境需要谨慎执行,避免误删备用容器、备份镜像和持久化数据卷。
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b56400ce5c63 nginx "/docker-entrypoint.…" About an hour ago Up About an hour 80/tcp web3
336ed94927d8 nginx "/docker-entrypoint.…" About an hour ago Up About an hour 80/tcp web2
0b2f622dd6c1 busybox "sh" 2 hours ago Up 11 minutes demo
e368773207dd nginx "/docker-entrypoint.…" 2 hours ago Up About an hour 80/tcp web1
[root@docker1 ~]# docker rm -f `docker ps -aq`
b56400ce5c63
336ed94927d8
0b2f622dd6c1
e368773207dd
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
a1fb7fb46bb3 bridge bridge local
141b4496ed4d host host local
764f0f93eae6 my_net1 bridge local
1068915a8f43 my_net2 bridge local
f72013194df3 none null local
[root@docker1 ~]# docker network prune
[root@docker1 ~]# docker volume prune
[root@docker1 ~]# docker image prune
[root@docker1 ~]# docker system prune
Docker端口映射双机制:iptables DNAT与docker-proxy原理
本次实验深入验证Docker端口映射的双轨实现机制:内核态iptables DNAT转发 + 用户态docker-proxy代理,清晰呈现了不同访问路径对应的流量处理逻辑。
阶段1:默认网桥的外网连通基础
启动默认bridge网络的busybox容器,ping公网域名成功,验证了容器默认可通过NAT访问外部网络。查看宿主机iptables的nat表,POSTROUTING链存在针对172.17.0.0/16网段的MASQUERADE地址伪装规则,这是容器访问外网的核心底层规则,将容器内网IP转换为宿主机IP后再向外转发。阶段2:-p端口映射的双机制同时生效
执行`docker run -d --name demo -p 80:80 nginx`启动带端口映射的容器,此时两套转发机制同步启用:
1. 内核态iptables DNAT规则:nat表的DOCKER链新增DNAT规则,将宿主机80端口的入站流量目标地址改写为容器IP 172.17.0.2:80,在内核层直接完成地址转换与转发,性能损耗极低,主要处理宿主机物理网卡IP(如192.168.52.171)的外部入站流量。
2. 用户态docker-proxy进程:宿主机启动docker-proxy进程,监听0.0.0.0:80端口,作为用户态四层代理转发流量。它的核心价值是处理本机localhost/127.0.0.1的访问流量——本地回环流量不经过iptables的PREROUTING链,DNAT规则无法生效,必须通过docker-proxy完成代理转发。此时无论是本地访问localhost,还是外部浏览器访问宿主机IP,都能正常访问Nginx服务。
阶段3:删除DNAT规则与代理进程的验证
执行`iptables -t nat -D DOCKER 2`手动删除端口映射的DNAT规则,内核层转发路径中断,但docker-proxy进程仍在运行。由于docker-proxy监听在0.0.0.0:80,所有到达宿主机80端口的流量都可以通过用户态代理兜底转发,因此浏览器访问宿主机IP依然可以正常访问。随后kill掉docker-proxy进程,两套转发机制全部失效。此时curl localhost直接报连接拒绝,浏览器访问宿主机IP也无法访问,端口映射完全中断。
阶段4:重启容器与双机制分工验证
执行`docker restart demo`重启容器,Docker daemon会自动修复端口映射配置:重新添加iptables DNAT规则,同时重新启动docker-proxy进程,两套机制自动恢复。此时再次杀掉docker-proxy进程,出现明确的分化现象:
- `curl localhost`:连接失败。本地回环地址的访问必须依赖docker-proxy,代理停止后无其他路径可转发。
- `curl 192.168.52.171`(宿主机IP):正常返回Nginx页面。宿主机网卡的入站流量走内核iptables DNAT路径,不需要经过用户态代理,因此不受proxy进程停止的影响。这就清晰验证了两条路径的分工边界:外部入站流量走内核DNAT高性能转发,本地回环访问靠docker-proxy用户态代理兜底。
Docker的-p端口映射采用内核与用户态双轨实现。iptables DNAT工作在内核层,负责宿主机物理IP的入站流量转发,性能更优;docker-proxy是用户态代理进程,专门处理本地回环地址的访问,弥补localhost流量不经过PREROUTING链、DNAT无法生效的缺陷。
删除DNAT规则后,docker-proxy仍可以兜底所有地址的端口访问;杀掉docker-proxy后,只要DNAT规则存在,宿主机IP的外部访问依然正常。容器重启后,Docker会自动重建iptables规则和proxy进程,保证端口映射的完整性。
理解双机制的分工可以精准排查端口故障:外部访问不通优先检查iptables规则与容器网络,localhost访问不通优先检查docker-proxy进程;生产环境中外部业务流量主要依赖内核DNAT转发,性能开销更低。
[root@docker1 ~]# docker run -it --rm busybox
/ # ping baidu.com
PING baidu.com (111.63.65.247): 56 data bytes
64 bytes from 111.63.65.247: seq=0 ttl=127 time=45.391 ms
64 bytes from 111.63.65.247: seq=1 ttl=127 time=42.373 ms
^C
--- baidu.com ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 42.373/43.882/45.391 ms
/ # ip r
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
/ # exit
[root@docker1 ~]# ip a
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:7a:d3:2e:c4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:7aff:fed3:2ec4/64 scope link
valid_lft forever preferred_lft forever
[root@docker1 ~]# iptables -t nat -nL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
[root@docker1 ~]# docker run -d --name demo -p 80:80 nginx
25220652dce179179e70c0bd832f1b4854e7467d6d35bccc22a1be621e63bfd8
[root@docker1 ~]# iptables -t nat -nL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:80
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80
[root@docker1 ~]# ss -lnt | grep :80
LISTEN 0 128 :80 :
LISTEN 0 128 [::]:80 [::]:
[root@docker1 ~]# ps ax | grep docker-proxy
14414 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80
14420 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.17.0.2 -container-port 80
14512 pts/0 S+ 0:00 grep --color=auto docker-proxy
[root@docker1 ~]# curl localhost
- 浏览器访问 192.168.52.171 可正常访问 ( 建议更换浏览器 或 清理缓存)
[root@docker1 ~]# iptables -t nat -nL
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80
[root@docker1 ~]# iptables -t nat -D DOCKER 2
[root@docker1 ~]# iptables -t nat -nL
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
- 浏览器访问 192.168.52.171,仍可访问
[root@docker1 ~]# ps ax | grep docker-proxy
14414 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80
14420 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.17.0.2 -container-port 80
14531 pts/0 S+ 0:00 grep --color=auto docker-proxy
[root@docker1 ~]# kill 14414 14420
[root@docker1 ~]# ps ax | grep docker-proxy
14537 pts/0 S+ 0:00 grep --color=auto docker-proxy
[root@docker1 ~]# curl localhost
curl: (7) Failed connect to localhost:80; Connection refused
- 浏览器访问 192.168.52.171,不可访问
[root@docker1 ~]# docker restart demo
demo
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
25220652dce1 nginx "/docker-entrypoint.…" 10 minutes ago Up 7 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp demo
[root@docker1 ~]# iptables -t nat -nL
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80
[root@docker1 ~]# ps ax | grep docker-proxy
14678 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80
14683 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.17.0.2 -container-port 80
14765 pts/0 S+ 0:00 grep --color=auto docker-proxy
[root@docker1 ~]# kill 14678 14683
- 浏览器访问 192.168.52.171,仍可访问
[root@docker1 ~]# curl localhost
curl: (7) Failed connect to localhost:80; Connection refused
[root@docker1 ~]# curl 192.168.52.171
<h1>Welcome to nginx!</h1>
<p>If you see this page, nginx is successfully installed and working.
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
25220652dce1 nginx "/docker-entrypoint.…" 13 minutes ago Up 3 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp demo
[root@docker1 ~]# docker rm -f demo
demo
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Docker Macvlan跨主机网络搭建与连通性验证
本次实验核心是搭建 Docker macvlan 类型网络,实现跨宿主机容器之间的二层直接通信,全程不经过虚拟网桥、不做NAT地址转换,容器直接以独立主机身份接入物理局域网。
阶段1:实验环境初始化
实验开始前先完成环境复位:
- 执行`docker compose down`停止并清理compose编排的所有服务,释放占用的容器与网络资源;
- 执行`docker system prune`一键清理停止的容器、闲置网络、悬空镜像,确保实验环境干净无残留。随后在VMware中完成底层网络拓扑配置:给docker1、docker2两台虚拟机各添加一块自定义网卡,统一绑定到VMnet1仅主机模式。两台虚拟机的第二块网卡(设备名ens36)处于同一个二层局域网,作为后续macvlan网络的物理父接口。
阶段2:宿主机物理网卡配置
两台虚拟机执行完全相同的网卡配置流程:
1. 进入网卡配置目录`/etc/sysconfig/network-scripts/`,复制现有网卡模板生成`ifcfg-ens36`配置文件;
2. 编辑配置文件,核心参数:`BOOTPROTO=none`(不配置IP地址,仅启用网卡做二层转发)、`DEVICE=ens36`指定网卡名、`ONBOOT=yes`设置开机自启;
3. 通过`nmcli`命令重载网络配置,删除系统自动生成的默认连接,确保手动配置生效;
4. 执行`ip link set ens36 promisc on`开启网卡混杂模式。这是macvlan网络的必要前提:macvlan会为每个容器生成独立的MAC地址,父网卡必须开启混杂模式,才能接收目的MAC不是自身的数据包,否则容器无法正常收发数据。阶段3:创建Macvlan Docker网络
两台宿主机分别执行完全相同的网络创建命令:
docker network create -d macvlan --subnet 10.0.0.0/24 --gateway 10.0.0.1 --parent=ens36 macvlan1
核心参数说明:
`-d macvlan`:指定网络驱动为macvlan,区别于默认的bridge网桥模式;
`--parent=ens36`:指定macvlan的父物理网卡,所有容器的虚拟接口都直接挂载在这块物理网卡之上;
- 手动指定统一的子网与网关,保证两台宿主机上的macvlan网络处于同一个二层网段。创建完成后通过`docker inspect macvlan1`可以验证父网卡与子网配置,macvlan模式不会生成虚拟网桥,网络直接对接物理网卡层。
阶段4:跨主机容器连通性验证
- docker1宿主机启动busybox容器,接入macvlan1网络,指定静态IP`10.0.0.11`;
- docker2宿主机启动busybox容器,接入同一个macvlan网络,指定静态IP`10.0.0.12`;
- 两个容器内互相ping对方的IP,均能正常通信、零丢包。这验证了macvlan网络的核心特性:跨主机的容器直接通过物理二层网络通信,没有宿主机NAT转换,没有虚拟网桥转发损耗,网络性能接近物理网卡,容器本身就像局域网内一台独立的物理主机。
本次实验完整实现了Docker macvlan跨主机网络的搭建与连通性验证。macvlan模式下容器直接挂载在宿主机物理网卡上,拥有独立的MAC地址与IP地址,跨主机容器可以直接二层通信,无需端口映射与NAT转换,网络性能远高于bridge网桥模式,适合对网络延迟、吞吐量要求高的业务场景。macvlan的必要前提是父网卡开启混杂模式,且所有宿主机必须处于同一个二层局域网;同时macvlan默认无法直接和宿主机本身通信,这是其强隔离特性决定的。和overlay跨主机网络相比,macvlan不需要额外的集群存储组件,配置更简单,但只能在同网段二层环境下使用。
[root@docker2 harbor]# docker compose down
[root@docker2 harbor]# docker network ls
NETWORK ID NAME DRIVER SCOPE
1f0e85ea419d bridge bridge local
a712dffe57b8 host host local
502826df0e3e none null local
[root@docker2 harbor]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker2 harbor]# docker system prune
[root@docker2 harbor]# ip a
[root@docker1 ~]# ip a
- 切换到 VMware
- 虚拟机docker1
- 添加 网络适配器,设置为 自定义,且选择 VMnet1 (仅主机模式)
- 完成后,点击确定

- 虚拟机docker2
- 添加 网络适配器,设置为 自定义,且选择 VMnet1 (仅主机模式)
- 完成后,点击确定

[root@docker1 ~]# ip a
140: ens36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:95:e3:fa brd ff:ff:ff:ff:ff:ff
inet 192.168.32.133/24 brd 192.168.32.255 scope global noprefixroute dynamic ens36
valid_lft 1572sec preferred_lft 1572sec
inet6 fe80::c20b:b9f0:d92c:6535/64 scope link noprefixroute
valid_lft forever preferred_lft forever
[root@docker2 harbor]# ip a
59: ens36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:42:38:6b brd ff:ff:ff:ff:ff:ff
inet 192.168.32.134/24 brd 192.168.32.255 scope global noprefixroute dynamic ens36
valid_lft 1733sec preferred_lft 1733sec
inet6 fe80::d77c:c6cc:1867:48d2/64 scope link noprefixroute
valid_lft forever preferred_lft forever
[root@docker1 ~]# cd /etc/sysconfig/network-scripts/
[root@docker1 network-scripts]# ls
[root@docker1 network-scripts]# cp ifcfg-ens33 ifcfg-ens36
[root@docker1 network-scripts]# vim ifcfg-ens36
- 编辑 ifcfg-ens36 文件
- 仅保留这些内容

[root@docker1 network-scripts]# nmcli c reload
[root@docker1 network-scripts]# nmcli c s
NAME UUID TYPE DEVICE
ens33 6705122d-3472-4103-913e-97708de557a6 ethernet ens33
Wired connection 1 39a703a5-ebd7-3a3a-9ed2-9bb4b719c593 ethernet ens36
System ens36 418da202-9a8c-b73c-e8a1-397e00f3c6b2 ethernet --
[root@docker1 network-scripts]# nmcli c down Wired\ connection\ 1
Connection 'Wired connection 1' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/6)
[root@docker1 network-scripts]# nmcli c del Wired\ connection\ 1
Connection 'Wired connection 1' (39a703a5-ebd7-3a3a-9ed2-9bb4b719c593) successfully deleted.
[root@docker1 network-scripts]# nmcli c s
NAME UUID TYPE DEVICE
ens33 6705122d-3472-4103-913e-97708de557a6 ethernet ens33
System ens36 418da202-9a8c-b73c-e8a1-397e00f3c6b2 ethernet ens36
[root@docker1 network-scripts]# ip a
140: ens36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:95:e3:fa brd ff:ff:ff:ff:ff:ff
inet6 fe80::20c:29ff:fe95:e3fa/64 scope link
valid_lft forever preferred_lft forever
[root@docker1 network-scripts]# ip link set ens36 promisc on
[root@docker1 network-scripts]# ip a
140: ens36: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
[root@docker2 harbor]# cd /etc/sysconfig/network-scripts/
[root@docker2 network-scripts]# ls
[root@docker2 network-scripts]# cp ifcfg-ens33 ifcfg-ens36
[root@docker2 network-scripts]# vim ifcfg-ens36
- 编辑 ifcfg-ens36 文件
- 仅保留这些内容

[root@docker2 network-scripts]# nmcli c reload
[root@docker2 network-scripts]# nmcli c down Wired\ connection\ 1
Connection 'Wired connection 1' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/3)
[root@docker2 network-scripts]# nmcli c delete Wired\ connection\ 1
Connection 'Wired connection 1' (b84dcfd4-9b66-3f9e-a865-7e8429fac072) successfully deleted.
[root@docker2 network-scripts]# nmcli c s
NAME UUID TYPE DEVICE
ens33 6705122d-3472-4103-913e-97708de557a6 ethernet ens33
System ens36 418da202-9a8c-b73c-e8a1-397e00f3c6b2 ethernet ens36
[root@docker2 network-scripts]# ip a
59: ens36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:42:38:6b brd ff:ff:ff:ff:ff:ff
inet6 fe80::20c:29ff:fe42:386b/64 scope link
valid_lft forever preferred_lft forever
[root@docker2 network-scripts]# ip link set ens36 promisc on
[root@docker2 network-scripts]# ip a
59: ens36: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
[root@docker1 network-scripts]# cd
[root@docker1 ~]# docker network create -d macvlan --subnet 10.0.0.0/24 --gateway 10.0.0.1 -o parent=ens36 macvlan1
94478b38ceb5d007f282342916685fd0cba03fa923f654fe789b2ebf1337c0d9
[root@docker1 ~]# docker inspect macvlan1
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1"
}
"Options":
"parent": "ens36"
[root@docker2 network-scripts]# cd
[root@docker2 ~]# docker network create -d macvlan --subnet 10.0.0.0/24 --gateway 10.0.0.1 -o parent=ens36 macvlan1
2db7a6baa48d6858ddbee7f2b2509a47087cb9e4747af06483d5afb7d889e1f5
[root@docker2 ~]# docker inspect macvlan1
[root@docker2 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
1f0e85ea419d bridge bridge local
a712dffe57b8 host host local
2db7a6baa48d macvlan1 macvlan local
502826df0e3e none null local
[root@docker1 ~]# docker run -it --rm --network macvlan1 --ip 10.0.0.11 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
142: eth0@if140: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:0a:00:00:0b brd ff:ff:ff:ff:ff:ff
inet 10.0.0.11/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
[root@docker2 ~]# docker run -it --rm --network macvlan1 --ip 10.0.0.12 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
60: eth0@if59: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:0a:00:00:0c brd ff:ff:ff:ff:ff:ff
inet 10.0.0.12/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ping 10.0.0.11
PING 10.0.0.11 (10.0.0.11): 56 data bytes
64 bytes from 10.0.0.11: seq=0 ttl=64 time=1.705 ms
64 bytes from 10.0.0.11: seq=1 ttl=64 time=1.609 ms
64 bytes from 10.0.0.11: seq=2 ttl=64 time=1.347 ms
^C
--- 10.0.0.11 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.347/1.553/1.705 ms
切换到docker1
/ # ping 10.0.0.12
PING 10.0.0.12 (10.0.0.12): 56 data bytes
64 bytes from 10.0.0.12: seq=0 ttl=64 time=1.405 ms
64 bytes from 10.0.0.12: seq=1 ttl=64 time=1.017 ms
64 bytes from 10.0.0.12: seq=2 ttl=64 time=1.450 ms
^C
--- 10.0.0.12 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.017/1.290/1.450 ms
/ # exit
切换到docker2
/ # exit
Docker绑定挂载与命名卷挂载行为对比
本次实验对比验证了Docker两种数据挂载方式的核心差异:宿主机目录绑定挂载(bind mount)与 Docker命名卷(named volume),重点验证了两者在空挂载时的文件复制行为差异。
第一阶段:宿主机目录绑定挂载
1. 先进入Docker卷默认存储目录`/var/lib/docker/volumes/`,确认当前无自定义命名卷;随后启动带端口映射的Nginx容器,使用`-v /opt/website/:/usr/share/nginx/html`将宿主机`/opt/website`目录绑定挂载到容器的网页根目录。
2. 启动后执行`curl localhost`返回403 Forbidden。核心原因:绑定挂载的本质是目录覆盖,宿主机的空目录会直接替换容器内原有的网页目录,Nginx找不到`index.html`首页文件,因此返回访问拒绝错误。
3. 进入宿主机`/opt/website`目录,目录初始为空;手动执行`echo`写入`index.html`文件后,再次访问本地80端口,页面内容同步更新。
4. 进入容器内部查看`/usr/share/nginx/html`,文件内容与宿主机完全一致,验证了绑定挂载的实时同步特性:宿主机修改文件,容器内立即生效,反之亦然。第二阶段:Docker命名卷
1. 执行`docker volume create vol1`手动创建Docker命名卷,通过`docker volume inspect vol1`可以查看卷的真实存储路径:`/var/lib/docker/volumes/vol1/_data`,该目录由Docker统一管理,用户无需手动规划路径。
2. 删除旧的web1容器,重新启动容器并使用`-v vol1:/usr/share/nginx/html`挂载命名卷。此时访问`curl localhost`直接返回Nginx默认欢迎页,没有出现403错误。
3. 进入命名卷的真实存储目录`/var/lib/docker/volumes/vol1/_data`,可以看到`50x.html`和`index.html`两个Nginx默认网页文件。
核心机制:当 空的命名卷 首次挂载到容器内有内容的目录时,Docker会自动将容器内该目录的原有文件复制到命名卷中,不会出现空目录覆盖的问题,这是命名卷与绑定挂载最关键的区别。
本次实验清晰对比了两种Docker数据挂载的行为差异。绑定挂载直接使用宿主机指定目录,空目录挂载会覆盖容器内原有文件,容易导致服务403报错,优势是文件路径明确、方便宿主机直接编辑管理。命名卷由Docker统一管理存储,空卷首次挂载时会自动拷贝容器内的原始文件,服务启动即可正常运行,兼容性更好;同时命名卷支持Docker的卷管理命令,更适合容器化数据持久化的标准场景。
简单来说,需要宿主机频繁修改配置、代码的场景适合用绑定挂载;需要持久化业务数据、追求标准化和兼容性的场景,优先使用Docker命名卷。
[root@docker1 ~]# cd /var/lib/docker/volumes/
[root@docker1 volumes]# ls
backingFsBlockDev metadata.db
[root@docker1 volumes]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 volumes]# docker run -d --name web1 -p 80:80 -v /opt/website:/usr/share/nginx/html nginx
ad4bfef03418c1278cda4ec54b2b2bd97167daab077de2aa795785ac96d7060c
[root@docker1 volumes]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ad4bfef03418 nginx "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp web1
[root@docker1 volumes]# curl localhost
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.31.4</center>
</body>
</html>
[root@docker1 volumes]# cd /opt/website/
[root@docker1 website]# ls
[root@docker1 website]# echo www.westos.org > index.html
[root@docker1 website]# curl localhost
www.westos.org
[root@docker1 website]# echo www.linux.org > index.html
[root@docker1 website]# curl localhost
www.linux.org
[root@docker1 website]# docker exec -it web1 bash
root@ad4bfef03418:/# cd /usr/share/nginx/html/
root@ad4bfef03418:/usr/share/nginx/html# ls
index.html
root@ad4bfef03418:/usr/share/nginx/html# cat index.html
www.linux.org
root@ad4bfef03418:/usr/share/nginx/html# exit
exit
[root@docker1 website]# docker volume create vol1
vol1
[root@docker1 website]# docker volume ls
DRIVER VOLUME NAME
local vol1
[root@docker1 website]# docker volume inspect vol1
"Mountpoint": "/var/lib/docker/volumes/vol1/_data",
"Name": "vol1",
[root@docker1 website]# docker rm -f web1
web1
[root@docker1 website]# docker run -d --name web1 -p 80:80 -v vol1:/usr/share/nginx/html nginx
09cfebf8aa5a9fdbab3fd9a0bdfac57529c9e901348efe3270d50883302836c7
[root@docker1 website]# curl localhost
<h1>Welcome to nginx!</h1>
<p>If you see this page, nginx is successfully installed and working.
[root@docker1 website]# pwd
/opt/website
[root@docker1 website]# cd /var/lib/docker/volumes/vol1/
[root@docker1 vol1]# ls
_data
[root@docker1 vol1]# cd _data/
[root@docker1 _data]# ls
50x.html index.html
[root@docker1 _data]# docker rm -f web1
web1
Docker数据挂载权限控制:读写/只读模式与单文件/目录挂载特性
本次实验系统验证了Docker数据挂载的 权限控制粒度 与 不同挂载形式的行为特性,包含单文件读写挂载、单文件只读挂载、目录读写挂载、目录只读挂载四组对比场景。
一、单文件读写挂载
启动centos:7容器,执行单文件挂载:
`-v /etc/yum.repos.d/CentOS-Base.repo:/etc/yum.repos.d/CentOS-Base.repo`
默认不加权限参数即为 读写模式。
- 进入容器内的yum源目录,编辑`CentOS-Base.repo`文件,在头部新增一行`#`注释符;
- 退出容器后,在宿主机上查看同一路径的源文件,新增的`#`同步存在。
这验证了单文件挂载的双向同步特性:容器与宿主机访问的本质是同一个文件,容器内的修改会直接写入宿主机原文件,改动实时生效。二、单文件只读挂载
第二次启动容器,在单文件挂载路径后追加`:ro`(read-only)参数,设置为只读模式。
- 进入容器后使用vi编辑该文件,编辑器底部标注`[readonly]`,无法保存修改;
- 只读模式下容器仅拥有读取权限,所有写入、修改、删除操作都会被系统拒绝,适合配置文件、证书等仅需读取的场景,避免容器内误操作修改宿主机源文件。三、目录挂载的读写与只读对比
容器同时挂载两个宿主机目录,设置不同权限:
1. /data1:/data1(默认读写模式)
进入容器内`/data1`目录,执行`touch file1`可以正常创建文件;退出容器后,宿主机`/data1`目录下同步出现file1文件。读写目录挂载支持双向的文件增删改操作,数据完全实时同步。2. /data2:/data2:ro(只读模式)
进入容器内`/data2`目录,执行`touch file2`直接报错`touch: cannot touch 'file2': Read-only file system`,无法创建、修改文件。只读目录挂载会对整个目录及其所有子文件统一施加写入限制,适合挂载静态资源、归档日志等无需修改的目录。四、匿名卷补充
实验末尾启动nginx容器时,仅指定容器内路径`-v /usr/share/nginx/html`,未指定宿主机路径或卷名,Docker会自动创建一个匿名卷(以随机长ID命名)。在`docker volume ls`中可以看到新增的匿名卷条目,这类卷由Docker自动管理,适合临时数据、无需人工干预的数据存储场景。
本次实验完整验证了Docker挂载的权限控制能力与不同挂载形式的行为差异。无论是单文件还是目录挂载,都支持通过`:ro`参数设置只读模式,限制容器的写入操作,保护宿主机源文件不被修改;默认的读写模式则支持双向实时同步,容器内的修改直接作用于宿主机。单文件挂载适合单个配置文件的注入与共享,目录挂载适合批量文件、数据目录的持久化。只读挂载在生产中常用于配置文件、静态资源、证书目录的挂载,既满足容器读取需求,又避免误操作导致的配置变更。仅指定容器内路径的挂载方式会生成匿名卷,由Docker统一管理,适合临时数据存储场景。
[root@docker1 _data]# cd
[root@docker1 ~]# docker run -it --rm -v /data1:/data1 -v /data2:/data2:ro -v /etc/yum.repos.d/CentOS-Base.repo:/etc/yum.repos.d/CentOS-Base.repo centos:7
[root@6c5938682df9 /]# cd /etc/yum.repos.d/
[root@6c5938682df9 yum.repos.d]# ls
CentOS-Base.repo CentOS-Media.repo CentOS-fasttrack.repo
CentOS-CR.repo CentOS-Sources.repo CentOS-x86_64-kernel.repo
CentOS-Debuginfo.repo CentOS-Vault.repo
[root@6c5938682df9 yum.repos.d]# vi CentOS-Base.repo
- 编辑 CentOS-Base.repo 文件,添加一个# 号

[root@6c5938682df9 yum.repos.d]# exit
exit
[root@docker1 ~]# vim /etc/yum.repos.d/CentOS-Base.repo
- 编辑 CentOS-Base.repo 文件,查看虚拟主机 文件中 是否有这个添加的# 号

[root@docker1 ~]# docker run -it --rm -v /data1:/data1 -v /data2:/data2:ro -v /etc/yum.repos.d/CentOS-Base.repo:/etc/yum.repos.d/CentOS-Base.repo:ro centos:7
[root@a2182fb08caf /]# cd /etc/yum.repos.d/
[root@a2182fb08caf yum.repos.d]# vi CentOS-Base.repo
![]()
[root@a2182fb08caf yum.repos.d]# cd /
[root@a2182fb08caf /]# ls
anaconda-post.log data1 dev home lib64 mnt proc run srv tmp var
bin data2 etc lib media opt root sbin sys usr
[root@a2182fb08caf /]# cd /data
bash: cd: /data: No such file or directory
[root@a2182fb08caf /]# cd /data1
[root@a2182fb08caf data1]# ls
[root@a2182fb08caf data1]# touch file1
[root@a2182fb08caf data1]# ls
file1
[root@a2182fb08caf data1]# cd ..
[root@a2182fb08caf /]# cd /data2
[root@a2182fb08caf data2]# ls
[root@a2182fb08caf data2]# touch file2
touch: cannot touch 'file2': Read-only file system
[root@a2182fb08caf data2]# exit
exit
[root@docker1 ~]# cd /data1
[root@docker1 data1]# ls
file1
[root@docker1 data1]# docker run -d --name web1 -p 80:80 -v /usr/share/nginx/html nginx
844b715f14102982c407df7ff5bf7d346d7f4323c00c82c2fb73b5db36d26825
[root@docker1 data1]# docker volume ls
DRIVER VOLUME NAME
local 6fdd427b24c85d0b3ce2dc26b5f9661284dd33410eb1f958ca6f2a8ca6b4acd1
local vol1
Docker Convoy第三方卷插件对接NFS跨主机数据共享
本次实验搭建NFS共享存储+Convoy第三方卷插件,实现多台Docker主机之间容器数据共享,解决原生Docker卷只能单机持久化的短板,实验分为NFS共享部署、Convoy插件部署两大阶段。
第一阶段:NFS共享存储服务部署
首先清理docker1上遗留的旧容器,保证实验环境干净。两台节点都预先安装`nfs‑utils`工具包,提供NFS客户端与服务端功能。
在docker1(NFS服务端)创建共享目录`/mnt/nfs`,编辑`/etc/exports`写入共享规则`/mnt/nfs *(rw,sync,no_root_squash)`。参数含义:允许所有主机访问该目录、读写权限、文件同步写入磁盘、关闭root用户权限压缩,客户端root访问时保留root权限。
启动nfs服务后,用`showmount -e`命令验证共享发布成功。接着在docker2(NFS客户端)挂载远程共享目录,挂载完成后执行文件读写测试,在docker2创建`file1`,docker1可以立刻看到该文件,双向删除修改均可同步,证明NFS跨主机文件共享链路已经通。第二阶段:Convoy 卷插件双节点部署
Convoy属于Docker老牌第三方存储卷插件,可以对接NFS这类外部存储,让Docker容器直接使用远端共享存储,从而实现跨主机容器数据共享迁移。
先在docker1上传解压`convoy‑v0.5.2.tar.gz`安装包,将可执行程序`convoy`、`convoy‑pdata‑tools`复制到系统全局命令目录`/usr/local/bin`,保证系统可以直接识别这条命令。
接着创建Docker插件配置目录`/etc/docker/plugins/`,生成`convoy.spec`文件,文件内写入Convoy守护进程的socket通信地址,Docker引擎依靠这个文件找到Convoy插件。
后台启动`convoy daemon`守护进程,指定存储后端类型`vfs`,存储路径指向本地已经挂载好的NFS目录`/mnt/nfs`;启动成功之后生成套接字文件`/var/run/convoy/convoy.sock`,Docker引擎就可以和Convoy进程通信。
之后将整套Convoy程序通过`scp`远程拷贝传输到docker2节点,docker2执行一模一样的部署流程,两台主机的Convoy进程都指向同一个NFS共享目录。这样两台Docker主机创建出来的Convoy卷,底层实际存储都落在NFS服务器,天然实现卷数据互通。
本次实验首先搭建NFS网络共享存储,实现两台虚拟机之间文件实时双向同步,为后续Docker跨主机数据共享打下底层存储基础。Convoy作为Docker第三方存储插件,架起了Docker引擎与外部NFS存储之间的桥梁,弥补原生Docker卷仅能单机使用的缺陷。双节点同时部署Convoy并且对接同一个NFS目录后,两个Docker主机上的容器就可以读写同一份持久化数据,容器也能够在集群不同主机间迁移并且保留原有数据。该方案是Docker集群环境下实现容器数据共享、无状态服务之外有状态业务数据持久化的经典实现方式。
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
844b715f1410 nginx "/docker-entrypoint.…" 23 minutes ago Up 23 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp web1
[root@docker1 ~]# docker rm -f web1
web1
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# yum install -y nfs-utils
[root@docker2 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker2 ~]# yum install -y nfs-utils
[root@docker1 ~]# mkdir /mnt/nfs
[root@docker1 ~]# vim /etc/exports
- 编辑 /etc/exports 文件
![]()
[root@docker1 ~]# systemctl start nfs
[root@docker1 ~]# showmount -e
Export list for docker1:
/mnt/nfs *
[root@docker2 ~]# showmount -e docker1
Export list for docker1:
/mnt/nfs *
[root@docker2 ~]# mkdir /mnt/nfs
[root@docker2 ~]# mount 192.168.52.171:/mnt/nfs /mnt/nfs
[root@docker2 ~]# df
192.168.52.171:/mnt/nfs 49251072 10326272 38924800 21% /mnt/nfs
[root@docker2 ~]# cd /mnt/nfs
[root@docker2 nfs]# touch file1
[root@docker1 ~]# cd /mnt/nfs
[root@docker1 nfs]# ls
file1
[root@docker1 nfs]# rm -f file1
[root@docker1 nfs]# cd
[root@docker1 ~]# ls
- 将文件 convoy-v0.5.2.tar.gz 移入虚拟机docker1
[root@docker1 ~]# ls
[root@docker1 ~]# tar zxf convoy-v0.5.2.tar.gz
[root@docker1 ~]# ls
[root@docker1 ~]# cd convoy/
[root@docker1 convoy]# ls
convoy convoy-pdata_tools SHA1SUMS
[root@docker1 convoy]# cp -p * /usr/local/bin
[root@docker1 convoy]# cd /usr/local/bin
[root@docker1 bin]# ls
convoy convoy-pdata_tools SHA1SUMS
[root@docker1 bin]# mkdir -p /etc/docker/plugins/
[root@docker1 bin]# echo "unix:///var/run/convoy/convoy.sock" > /etc/docker/plugins/convoy.spec
[root@docker1 bin]# cd /etc/docker/plugins
[root@docker1 plugins]# ls
convoy.spec
[root@docker1 plugins]# cat convoy.spec
unix:///var/run/convoy/convoy.sock
[root@docker1 plugins]# convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs &
[root@docker1 plugins]# ps ax | grep convoy
18060 pts/0 Sl 0:00 convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs
18068 pts/0 S+ 0:00 grep --color=auto convoy
[root@docker1 plugins]# ls
convoy.spec
[root@docker1 plugins]# cat convoy.spec
unix:///var/run/convoy/convoy.sock
[root@docker1 plugins]# ll /var/run/convoy/convoy.sock
srwxr-xr-x 1 root root 0 Sep 5 15:46 /var/run/convoy/convoy.sock
[root@docker1 plugins]# cd
[root@docker1 ~]# cd convoy/
[root@docker1 convoy]# scp convoy* docker2:/usr/local/bin
[root@docker2 nfs]# cd /usr/local/bin
[root@docker2 bin]# ls
convoy convoy-pdata_tools
[root@docker2 bin]# mkdir -p /etc/docker/plugins/
[root@docker2 bin]# echo "unix:///var/run/convoy/convoy.sock" > /etc/docker/plugins/convoy.spec
[root@docker2 bin]# cd /etc/docker/plugins
[root@docker2 plugins]# ls
convoy.spec
[root@docker2 plugins]# convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs &
[root@docker2 plugins]# ps ax | grep convoy
8092 pts/0 Sl 0:00 convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs
8098 pts/0 S+ 0:00 grep --color=auto convoy
[root@docker2 plugins]# cat convoy.spec
unix:///var/run/convoy/convoy.sock
[root@docker2 plugins]# ll /var/run/convoy/convoy.sock
srwxr-xr-x 1 root root 0 Sep 5 15:52 /var/run/convoy/convoy.sock
Convoy第三方存储卷跨主机容器数据迁移验证
本实验在上一步NFS+Convoy环境部署完成的基础之上,完成第三方存储卷的创建、识别、跨主机共享读写、容器迁移验证,测试Docker跨主机数据持久化能力。
首先清理环境中遗留的本地匿名卷,`docker volume prune`命令会删除没有被任何容器挂载的匿名local卷,释放磁盘空间。随后手动删除旧的本地卷vol1,消除环境干扰,为创建Convoy类型的卷做好准备。
接下来使用`convoy create vol1`命令创建第三方存储卷。该卷并非存储在docker本机磁盘,底层由vfs驱动托管,实际文件路径指向NFS共享目录`/mnt/nfs/vol1`。执行`convoy list`可以查看卷的后端详细信息;`docker volume ls`能够识别出新创建出来的卷,驱动类型显示为`convoy`,代表Docker引擎已经成功调用第三方存储插件管理该卷。
在docker‑2节点执行查询,可以直接查询到vol1卷,这是因为两台主机的Convoy进程都对接同一个NFS共享目录,卷的元数据和实际数据存放在远端存储,因此卷信息天然在两台节点同时可见。进入NFS共享目录可以看到Convoy自动生成了vol1数据文件夹以及config配置文件。
之后在docker1启动nginx容器,将convoy卷vol1挂载到网页根目录。Nginx自动生成默认页面文件,接着手动修改`index.html`网页内容,写入自定义测试文本。访问容器IP,页面成功读取修改后的内容,证明容器可以正常读写Convoy卷。切换至docker2节点进入NFS目录查看,修改后的网页文件已经同步过来,验证了跨主机文件共享生效。
最后执行容器迁移测试:删除docker‑1上运行的nginx容器,再切换到docker‑2节点,使用完全相同的卷挂载命令 新建同名容器。容器启动成功并且访问页面可以读取到之前修改的网页内容。这一步证明容器销毁后数据保存在NFS共享卷中,在另一台主机重建容器依旧可以恢复原有业务数据。
本次实验完整验证了Convoy第三方存储卷对接NFS之后跨主机数据共享的效果。原生Docker本地卷只能单节点使用,而基于NFS后端的Convoy卷,实际数据存放在网络共享存储上,两台Docker主机都可以识别、读写同一份卷数据。
容器删除并不会销毁卷内的数据,业务数据独立于容器生命周期而持久保存。当容器从一台主机销毁,在另一台主机重新启动并挂载同一个Convoy卷,业务数据可以无缝恢复,从而实现容器跨主机迁移。该方案适合小规模Docker集群,用来实现有状态应用的数据共享与故障迁移。
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# docker volume ls
DEBU[0503] Handle plugin activate: POST /Plugin.Activate pkg=daemon
DEBU[0503] Response: {
"Implements": [
"VolumeDriver"
]
} pkg=daemon
DEBU[0503] Handle plugin list volume: POST /VolumeDriver.List pkg=daemon
DEBU[0503] Successfully got volume list for docker. pkg=daemon
DEBU[0503] Response: {} pkg=daemon
DRIVER VOLUME NAME
local 6fdd427b24c85d0b3ce2dc26b5f9661284dd33410eb1f958ca6f2a8ca6b4acd1
local vol1
[root@docker1 ~]# docker volume prune
[root@docker1 ~]# docker volume ls
DEBU[0518] Handle plugin list volume: POST /VolumeDriver.List pkg=daemon
DEBU[0518] Successfully got volume list for docker. pkg=daemon
DEBU[0518] Response: {} pkg=daemon
DRIVER VOLUME NAME
local vol1
[root@docker1 ~]# docker volume rm vol1
vol1
[root@docker1 ~]# docker volume ls
[root@docker1 ~]# convoy create vol1
DEBU[0662] Calling: POST, /volumes/create, request: POST, /v1/volumes/create pkg=daemon
DEBU[0662] event=create object=volume opts=map[Size:0 EndpointURL: VolumeName:vol1 VolumeIOPS:0 BackupURL: VolumeDriverID: VolumeType: PrepareForVM:false] pkg=daemon reason=prepare volume=vol1
DEBU[0662] Created volume event=create object=volume pkg=daemon reason=complete volume=vol1
DEBU[0662] Response: vol1 pkg=daemon
vol1
[root@docker1 ~]# convoy list
"Name": "vol1",
"Driver": "vfs",
"Path": "/mnt/nfs/vol1",
"VolumeName": "vol1"
[root@docker1 ~]# docker volume ls
DRIVER VOLUME NAME
convoy vol1
[root@docker1 ~]# docker volume inspect vol1
"Driver": "convoy",
"Name": "vol1",
"Scope": "local"
[root@docker2 plugins]# cd
[root@docker2 ~]# convoy list
"Name": "vol1",
"Driver": "vfs",
"Path": "/mnt/nfs/vol1",
[root@docker2 ~]# docker volume ls
DRIVER VOLUME NAME
local 8b520e6b796c41a6f01e386fe1a61932a9617c63e9ded97fc7fb4474a6cd48e3
local 534b0c7299f4a98aa2568425e80a58aa5a21c0dffa7259bcc0f195adcf4ab36b
local 544d976d512ddc5aff816a3feb5e926c07f4fca5ddf526f7cff859b30fa4e518
local 07883669f64bd0637a8da8dcee20ff0dba5a8790c8ff22c100f338e19f07488b
local 81789242a01b11b3afcfebc34365aecd3e8a9b49b8a6f64b823d12a1d764d5a8
local ae9af2060a624ab7c212622ca7657210ead51b370d50dfa7f6b34f5f21be310e
local b4ca7bfd62476313351e898398b65e77552e2bdc9763f6bf7cf7d2abc2c8349e
local c6c8ff8177285874a8bd87a803524448a587642bd34e16104349c0d4eaf8cdd5
local dff7cfe9d52c06e707ea5950659e59db94e1a231b519d4b9143d8dc973ffecdb
convoy vol1
[root@docker2 ~]# docker volume prune
[root@docker1 ~]# cd /mnt/nfs
[root@docker1 nfs]# ls
config vol1
[root@docker1 nfs]# cd vol1/
[root@docker1 vol1]# pwd
/mnt/nfs/vol1
[root@docker1 vol1]# cd
[root@docker1 ~]# docker run -d --name web1 -v vol1:/usr/share/nginx/html nginx
"Name": "vol1",
"Mountpoint": "/mnt/nfs/vol1"
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
64992f8c4417 nginx "/docker-entrypoint.…" 5 seconds ago Up 3 seconds 80/tcp web1
[root@docker1 ~]# cd /mnt/nfs
[root@docker1 nfs]# ls
[root@docker1 nfs]# cd vol1/
[root@docker1 vol1]# ls
50x.html index.html
[root@docker1 vol1]# curl 172.17.0.2
[root@docker1 vol1]# echo www.westos.org > index.html
[root@docker1 vol1]# cat index.html
www.westos.org
[root@docker1 vol1]# curl 172.17.0.2
www.westos.org
[root@docker2 ~]# cd /mnt/nfs
[root@docker2 nfs]# cd vol1/
[root@docker2 vol1]# ls
50x.html index.html
[root@docker2 vol1]# cat index.html
www.westos.org
在任意一台 docker 节点执行命令,就可以看到后端真实地址:
df -h /mnt/nfs/vol1
[root@docker1 vol1]# cd
[root@docker1 ~]# docker rm -f web1
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker2 vol1]# docker run -d --name web1 -v vol1:/usr/share/nginx/html nginx
[root@docker2 vol1]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cb1a4ee1d6c5 nginx "/docker-entrypoint.…" 8 seconds ago Up 6 seconds 80/tcp web1
[root@docker2 vol1]# curl 172.17.0.2
www.westos.org
[root@docker2 vol1]# docker rm -f web1
[root@docker2 vol1]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Convoy插件实验环境清理
本部分为Convoy插件实验的环境清理环节,完成Convoy守护进程关闭、Docker插件配置删除、本地卷元数据清理,把两台docker节点恢复到实验前干净状态。
在docker2节点,`ps ax | grep convoy`确认convoy守护进程正在后台运行。由于之前使用`&`把进程放到后台执行,使用`fg`命令把后台convoy进程切换到前台,按下`Ctrl+C`发送中断信号,进程收到信号执行关闭逻辑,输出日志`Caught signal interrupt: shutting down`,清理socket套接字文件,进程终止。再次过滤进程,已经不存在convoy守护进程。
停止插件进程之后执行`systemctl restart docker`重启docker引擎,清除docker内部对convoy插件的缓存。接着进入`/etc/docker/`目录,递归删除`plugins`目录,该目录存放`convoy.spec`插件声明文件,删除后docker不再识别Convoy第三方驱动。
再进入docker数据目录`/var/lib/docker/volumes`,该目录保存docker本地卷的元数据,执行`rm -fr *`清空本地卷目录,清除本地残留的卷记录。
注意:NFS共享目录`/mnt/nfs`的数据不会被这条命令删除,因为NFS数据存储在远端服务器,不在本机`/var/lib/docker`路径内。
docker1节点执行完全一致的清理操作:`fg`切回后台convoy进程,Ctrl+C终止守护进程,进程输出套接字关闭报错日志,代表正常退出。同样清空本机`/var/lib/docker/volumes`本地卷元数据目录。执行`docker volume ls`查看,本机已经看不到convoy类型的卷。
> 关键点:Convoy卷真实业务数据保存在NFS共享存储,本机仅仅保存元数据,清理本机docker环境不会删除NFS上实际业务文件。
该实验环节完成Convoy插件整套环境的清理回收,通过`fg`切换后台守护进程至前台,使用`Ctrl+C`优雅关闭convoy守护进程,释放unix套接字资源。删除docker的plugins插件配置目录,重启docker服务清除插件缓存,再清空本机docker volumes目录清除本地卷元数据。本机清理操作只会删除本地的插件配置与元数据,存储在NFS共享目录的真实卷数据不受影响。这套清理流程可以把两台docker主机恢复至实验初始状态,方便后续开展其他存储相关实验,同时区分清楚本地docker目录和外部NFS存储的数据边界,避免误删业务数据。
[root@docker2 vol1]# cd
[root@docker2 ~]# ps ax | grep convoy
8092 pts/0 Sl 0:00 convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs
8620 pts/0 S+ 0:00 grep --color=auto convoy
[root@docker2 ~]# fg
convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs (wd: /etc/docker/plugins)
^CCaught signal interrupt: shutting down.
DEBU[9756] Cleaning up environment... pkg=daemon
ERRO[9756] http server erroraccept unix /var/run/convoy/convoy.sock: use of closed network connection pkg=daemon
[root@docker2 ~]# ps ax | grep convoy
8623 pts/0 S+ 0:00 grep --color=auto convoy
[root@docker2 ~]# systemctl restart docker
[root@docker2 ~]# cd /etc/docker/
[root@docker2 docker]# ls
certs.d daemon.json plugins
[root@docker2 docker]# rm -fr plugins/
[root@docker2 docker]# ls
certs.d daemon.json
[root@docker2 ~]# cd /var/lib/docker/
[root@docker2 docker]# ls
buildkit engine-id network plugins swarm volumes
containers image overlay2 runtimes tmp
[root@docker2 docker]# cd volumes/
[root@docker2 volumes]# ls
backingFsBlockDev metadata.db
[root@docker2 volumes]# rm -fr *
[root@docker2 volumes]# ls
[root@docker2 volumes]# systemctl restart docker
[root@docker2 volumes]# cd
[root@docker1 ~]# ps ax | grep convoy
18060 pts/0 Sl 0:00 convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs
25982 pts/0 S+ 0:00 grep --color=auto convoy
[root@docker1 ~]# fg
convoy daemon --drivers vfs --driver-opts vfs.path=/mnt/nfs (wd: /etc/docker/plugins)
^CCaught signal interrupt: shutting down.
DEBU[10650] Cleaning up environment... pkg=daemon
ERRO[10650] http server erroraccept unix /var/run/convoy/convoy.sock: use of closed network connection pkg=daemon
[root@docker1 ~]# docker volume ls
DRIVER VOLUME NAME
convoy vol1
[root@docker1 ~]# cd /etc/docker/
[root@docker1 docker]# ls
certs.d daemon.json plugins
[root@docker1 docker]# rm -fr plugins/
[root@docker1 docker]# docker volume rm vol1
Error response from daemon: get vol1: error while checking if volume "vol1" exists in driver "convoy": Post "http://plugin.moby.localhost/VolumeDriver.Get": dial unix /var/run/convoy/convoy.sock: connect: no such file or directory
[root@docker1 docker]# cd /var/lib/docker/volumes/
[root@docker1 volumes]# rm -fr *
[root@docker1 volumes]# ls
[root@docker1 volumes]# cd
[root@docker1 ~]# systemctl restart docker
[root@docker1 ~]# cd -
/var/lib/docker/volumes
[root@docker1 volumes]# ls
backingFsBlockDev metadata.db
[root@docker1 volumes]# cd
[root@docker1 ~]# docker volume ls
DRIVER VOLUME NAME
Portainer‑CE可视化Docker管理平台部署与使用
本实验完成Portainer‑CE可视化Docker管理平台部署,演示网页端图形化管理容器、镜像、网络的完整操作流程,替代传统纯命令行操作。
首先开启代理保证镜像下载连通性,拉取portainer镜像,通过curl下载官方compose编排文件,查看yaml配置文件后,使用`docker compose -f`指定配置文件后台启动Portainer服务。`docker ps`确认Portainer容器正常运行,该服务默认监听9443端口,提供HTTPS网页访问入口。
浏览器访问`https://192.168.52.171:9443`进入初始化页面,首先设置管理员密码,密码长度必须大于等于12位。初始化需要`setup_token`,通过`docker logs portainer`查看容器日志,提取日志输出内的setup_token字符串粘贴到网页表单。填入令牌、设置管理员账号后创建用户,防火墙已经关闭直接跳过环境提示,进入本地Docker管理界面。
页面识别本机local的Docker环境,底层对接`/var/run/docker.sock`本地套接字,代表已经接管本机Docker引擎。进入容器管理页面,点击Add container新建容器,填写容器名称、镜像名称,配置端口映射,完成参数填写后点击Deploy the container部署容器。部署完成后在服务器执行`docker ps`,可以看到网页创建的web1容器正常运行,curl访问验证业务可访问。
在Portainer页面勾选运行中的web1容器执行删除操作,页面提供选项可以选择同步删除非持久化存储卷,点击Remove完成容器销毁。平台左侧导航栏分别提供Images镜像管理、Networks网络管理面板,可以直接在页面执行镜像删除、网络删除,后台可以使用docker命令行核对操作结果,图形界面操作和docker原生命令效果完全等价。
Portainer‑CE是Docker主流可视化管理工具,借助docker‑compose完成容器化部署,通过9443端口提供HTTPS管理页面。初始化阶段需要读取容器日志获取setup_token完成管理员账号注册,网页通过对接docker.sock套接字接管本地Docker引擎。在WebUI界面可以图形化完成容器创建、部署、启停、删除,同时支持镜像、网络资源的可视化维护,所有页面操作最终都会转化为底层Docker接口调用,和shell命令行执行效果完全一致。可视化界面降低容器运维上手门槛,适合快速查看集群状态、批量操作资源,实际生产环境通常会做好访问权限管控,避免对外开放直接暴露管理页面。


![]()

- 开启代理
[root@docker1 ~]# docker pull portainer/portainer-ce:latest
[root@docker1 ~]# curl -L https://downloads.portainer.io/ce-sts/portainer-compose.yaml -o portainer-compose.yaml
[root@docker1 ~]# ls
portainer-compose.yaml
[root@docker1 ~]# cat portainer-compose.yaml
[root@docker1 ~]# docker compose -f portainer-compose.yaml up -d
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
61155da59ecf portainer/portainer-ce:sts "/portainer" 6 seconds ago Up 4 seconds 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp, 9000/tcp portainer
- 浏览器访问 https://192.168.52.171:9443
- 创建的密码,确保长度不少于12位
- 对于 Setup token,执行这条命令
[root@docker1 ~]# docker logs portainer
- 在输出内容里面寻找 setup_token,粘贴填写即可

- 点击 Create user
- 由于已经关闭防火墙,点击 Skip
- 点击 Get Started
- 点击 local

- 点击 container

- 点击 Add container
![]()
- 填写完成后,点击 Deploy the container

- 这是 Portainer 社区版创建容器的 Web 表单,全部选项对应 docker run 的参数。
- Name 设置容器名称
- Image 指定仓库与镜像标签
- Always pull the image 开关控制每次部署是否重新拉取镜像
- Port mapping 完成端口映射,Host 代表宿主机端口,Container 代表容器内部端口,选择 TCP/UDP 通信协议。
- Access control 属于商业付费功能,社区版无法开启。
- 全部参数填写完成,点击`Deploy the container`就会调用 Docker 接口完成容器创建启动。
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
335d9bb3b878 nginx:latest "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp web1
61155da59ecf portainer/portainer-ce:sts "/portainer" About an hour ago Up 57 minutes 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp, 9000/tcp portainer
[root@docker1 ~]# curl localhost
- 选中 web1,删除


- 点击 左侧 Images,可以管理镜像
- 删除 webserver:v2
[root@docker1 ~]# docker images webserver
REPOSITORY TAG IMAGE ID CREATED SIZE
webserver v4 c1166fb727b3 7 days ago 62.3MB
webserver v3 d4aa4463f2ed 7 days ago 205MB
webserver v1 386d64ec58fb 8 days ago 553MB
- 点击 左侧 Networks,可以管理网络
- 删除 macvlan1
[root@docker1 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
8fb6e93e4243 bridge bridge local
141b4496ed4d host host local
f72013194df3 none null local
8c7456d1bd85 portainer_network bridge local
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
61155da59ecf portainer/portainer-ce:sts "/portainer" 2 hours ago Up About an hour 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:9443->9443/tcp, :::9443->9443/tcp, 9000/tcp portainer
[root@docker1 ~]# docker rm -f portainer
portainer
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Docker底层cgroup CPU资源控制
本实验演示Linux cgroup资源控制机制,查看Docker容器对应的CPU资源限制文件,理解Docker底层依靠cgroup完成硬件资源隔离。
执行`docker run -d --name demo nginx`后台创建demo容器,`docker ps`可以确认容器正常运行。容器启动之后,会在宿主机`/sys/fs/cgroup/cpu/docker/`目录生成以容器ID命名的cgroup子目录,这个目录就是该容器的CPU资源控制组。
进入容器ID对应的目录,里面`cpu.shares`文件记录CPU权重,默认值是1024。cpu.shares是CPU相对权重,多个容器争抢CPU时间时按照这个数值比例分配,1024为系统默认基准值,不代表固定CPU数量。
回到cpu总目录,同样存在全局`cpu.shares`。执行`docker rm -f demo`强制删除容器,对应容器ID目录消失
`cpu.cfs_period_us`代表CPU调度周期,默认`100000`微秒即100毫秒;`cpu.cfs_quota_us`为周期内允许使用的CPU时间,`‑1`代表不做限制,容器可以用尽全部CPU算力。cgroup属于内核提供的资源隔离接口,Docker启动容器时自动创建对应cgroup目录,修改目录内文件就可以限制容器CPU使用权;销毁容器后,cgroup相关配置文件随之失效。没有手动配置CPU限制的容器,使用内核默认参数,不做CPU硬限制。
Docker容器的资源限制底层依赖Linux内核cgroup子系统,容器启动后会在`/sys/fs/cgroup`下生成专属控制目录。`cpu.shares`设置CPU相对权重,默认1024,用于多个容器竞争CPU时分配时间片。`cpu.cfs_quota_us`与`cpu.cfs_period_us`实现CPU硬时间限制,默认‑1代表无上限。
创建容器自动生成cgroup子目录,删除容器之后对应容器ID目录消失。整个实验直观看到Docker并没有虚拟化硬件,只是复用内核cgroup机制完成CPU资源隔离,这也是容器轻量化的底层原理。
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 ~]# docker run -d --name demo nginx
dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
dddbda9b14b2 nginx "/docker-entrypoint.…" 4 seconds ago Up 2 seconds 80/tcp demo
[root@docker1 ~]# cd /sys/fs/cgroup/
[root@docker1 cgroup]# ls
cpu
[root@docker1 cgroup]# cd cpu
[root@docker1 cpu]# ls
docker
[root@docker1 cpu]# cd docker/
[root@docker1 docker]# ls
dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082
[root@docker1 docker]# cd dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082
[root@docker1 dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082]# ls
cpu.shares
[root@docker1 dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082]# cat cpu.shares
1024
[root@docker1 dddbda9b14b2f2b36271075dff82f96e073712561abb4481669d4682bb03a082]# cd ..
[root@docker1 docker]# cd ..
[root@docker1 cpu]# ls
cpu.shares
[root@docker1 cpu]# cat cpu.shares
1024
[root@docker1 cpu]# docker rm -f demo
demo
[root@docker1 cpu]# cd docker
[root@docker1 docker]# ls
cpu.cfs_quota_us
cpu.rt_period_us
[root@docker1 docker]# cat cpu.cfs_quota_us
-1
[root@docker1 docker]# cat cpu.cfs_period_us
100000
[root@docker1 docker]# cd
Docker cgroup CPU硬配额与CPU权重
本实验对比Docker的两种CPU限制方式:cpu‑quota/cpu‑period硬限制 和 cpu‑shares相对权重限制,依托Linux cgroup CPU子系统完成实操,`md5sum /dev/zero`用来产生持续高CPU压力。
使用`--cpu‑period=100000 --cpu‑quota=20000`启动容器,period代表调度周期100000微秒,quota代表周期内允许使用20000微秒CPU时间,整体上限为20%CPU。容器内部后台运行压力命令,宿主机top观察,单个进程CPU占用约20%;容器内再启动第二条压力进程,两个进程CPU总和依旧被压制在20%。进入cgroup对应容器目录查看`cpu.cfs_quota_us`,数值为20000,印证硬限制生效。完成测试后,`fg`把后台进程切到前台,Ctrl+C终止压力进程,执行exit退出容器,`--rm`参数会自动销毁容器。
实验演示CPU热插拔内核接口,`/sys/devices/system/cpu/cpu1/online`文件,写入0代表下线该CPU核心,写入1重新上线,用来模拟硬件核心变更。
接着测试`--cpu‑shares 100`相对权重模式,shares默认基准值1024,设置100代表该容器CPU权重很低。容器内执行压测命令,在只有这一个压力容器时,它依旧可以占满CPU;再启动第二个普通权重(默认1024)的压力容器,CPU资源发生争抢,两个容器按照权重比例分配,100:(1024),shares为100的容器只能拿到约10%CPU。shares只在多个容器竞争CPU的时候才生效,容器单独运行时不受shares约束。
进入cgroup目录读取`cpu.shares`文件,可以看到配置写入的权重数值。测试结束,终止容器内压测进程,退出容器自动销毁,宿主机top观察CPU负载回落至正常水平。
> 核心区分:`cpu‑quota`是硬上限,无论系统忙闲,最多只能使用指定比例CPU;`cpu‑shares`是相对权重,只有CPU资源紧张争抢时才起作用,空闲时容器可以吃满全部CPU。
本实验验证cgroup两套CPU控制机制。`--cpu‑quota`与`--cpu‑period`组合实现CPU硬配额,不管宿主机负载高低,容器总CPU占用被固定上限,适合用来限制容器最大算力消耗。`--cpu‑shares`为CPU相对权重,仅在多个容器争抢CPU资源时生效,CPU空闲时容器不受权重约束,可以占用全部CPU资源。
`md5sum /dev/zero`用来制造CPU压力,top命令观察宿主机CPU实际占用,`/sys/fs/cgroup`目录可以直接查看内核生效的限制参数。生产环境中,对业务容器做最大算力约束一般使用quota硬限制;shares多用于多容器之间优先级调度。
[root@docker1 ~]# docker run -it --rm --cpu-period=100000 --cpu-quota=20000 centos:7
[root@c4a464e8821b /]# md5sum /dev/zero &
[1] 7
- 再开一个docker1终端
- 执行 top 命令
- 可以看到,上限是20%

[root@c4a464e8821b /]# md5sum /dev/zero &
[2] 8
- 在第二个终端,查看 top 监控
- 可以看到,两者相为上限 20%

- 在第二个终端查看
[root@docker1 ~]# cd /sys/fs/cgroup/cpu/docker
[root@docker1 docker]# ls
805dd91349a2d2c32642b668b0ffca3334aedb1be2d597490943e33052bc702f
[root@docker1 docker]# cd 805dd91349a2d2c32642b668b0ffca3334aedb1be2d597490943e33052bc702f
[root@docker1 805dd91349a2d2c32642b668b0ffca3334aedb1be2d597490943e33052bc702f]# ls
cpu.cfs_quota_us
[root@docker1 805dd91349a2d2c32642b668b0ffca3334aedb1be2d597490943e33052bc702f]# cat cpu.cfs_quota_us
20000
- 切换到第一个终端
[root@c4a464e8821b /]# fg
md5sum /dev/zero
^C
[root@c4a464e8821b /]# fg
md5sum /dev/zero
^C
[root@c4a464e8821b /]# exit
exit
[root@docker1 ~]# lscpu
[root@docker1 ~]# cd /sys/devices/system/cpu
[root@docker1 cpu]# ls
cpu1
[root@docker1 cpu]# cd cpu1
[root@docker1 cpu1]# ls
cache crash_notes_size firmware_node online subsystem uevent
crash_notes driver node0 power topology
[root@docker1 cpu1]# cat online
1
[root@docker1 cpu1]# echo 0 > online
[root@docker1 cpu1]# cat online
0
[root@docker1 cpu1]# lscpu
[root@docker1 cpu1]# cd /sys/fs/cgroup
[root@docker1 cgroup]# ls
cpu
[root@docker1 cgroup]# cd cpu
[root@docker1 cpu]# cd docker
[root@docker1 docker]# ls
cpuacct.usage_percpu cpu.shares
[root@docker1 docker]# cat cpu.shares
1024
[root@docker1 ~]# docker run -it --rm --cpu-shares 100 centos:7
[root@cece12663e15 /]# md5sum /dev/zero &
[1] 15
- 在第二个终端,查看 top 监控
- 可以看到占满CPU
- 再开一个虚拟机docker1终端
[root@docker1 ~]# docker run -it --rm centos:7
[root@e1f957173e15 /]# md5sum /dev/zero &
[1] 15
- 在第二个终端,查看 top 监控
- 可以看到 两个进程 总和为 100%,且 docker 进程 只能占 100/1024,大约为 CPU 的10%

- 结束第三个终端 md5sum /dev/zero &进程
[root@e1f957173e15 /]# fg
md5sum /dev/zero
^C
[root@e1f957173e15 /]# exit
exit
[root@docker1 ~]# cd /sys/fs/cgroup/cpu/docker/
[root@docker1 docker]# ls
06296ad1df8d71d11755b81ff2b8fcc3f9272cebebd0674660caa28a52429600
[root@docker1 docker]# cd 06296ad1df8d71d11755b81ff2b8fcc3f9272cebebd0674660caa28a52429600/
[root@docker1 06296ad1df8d71d11755b81ff2b8fcc3f9272cebebd0674660caa28a52429600]# ls
cpu.shares
[root@docker1 06296ad1df8d71d11755b81ff2b8fcc3f9272cebebd0674660caa28a52429600]# cat cpu.shares
100
- 结束第一个终端 md5sum /dev/zero &进程
[root@cece12663e15 /]# fg
md5sum /dev/zero
^C
[root@cece12663e15 /]# exit
exit
- 查看 第二个终端的 top 监控,是否回到正常数值
- Ctrl + C 结束 top 监控
Linux cgroup memory内存子系统
本实验围绕Linux cgroup memory内存子系统,演示内存资源限制的底层文件、手动创建控制组、配置用户cgroup规则,模拟内存超限触发OOM kill的完整过程。
`/sys/fs/cgroup/memory/docker`是Docker容器对应的内存cgroup目录,`memory.limit_in_bytes`为内存硬限制,`memory.memsw.limit_in_bytes`是内存+交换分区总限制。没有配置内存限制时,文件会显示超大数值,代表不做内存上限约束。
安装`libcgroup‑tools`工具,使用`cgcreate -g memory:x1`手动创建名为x1的内存控制组,生成一整套内存控制接口文件,包含内存上限、统计、OOM控制、swap相关参数。也可以直接mkdir创建x2目录手动生成cgroup。修改配置文件写入内存字节数值,重启`cgconfig.service`服务,配置就会生效,读取文件确认内存限制已写入。
编辑`/etc/cgrules.conf`,实现**用户与cgroup绑定**,指定普通用户xx所有进程自动归入x1内存控制组。重启cgred服务使规则生效,切换为xx普通用户,在`/dev/shm`内存文件系统执行dd命令生成大文件,大量消耗内存,触发cgroup内存阈值,进程被内核直接Killed杀死。通过du查看,文件只生成部分,验证内存限制规则对普通用户进程生效。
cgroup内存子系统提供大量控制文件,既可以限制物理内存,也可以限制swap,还可以查看内存使用统计、OOM事件计数。Docker限制容器内存,本质就是修改这一组cgroup内存文件。普通业务用户也可以通过cgrules.conf给普通操作系统用户做内存配额。
memory cgroup是Linux内核用于管控进程内存占用的子系统,`memory.limit_in_bytes`设置物理内存上限,`memory.memsw.limit_in_bytes`管控内存加交换分区总和。既可以由Docker自动为容器生成cgroup,也可以手动通过cgcreate、mkdir创建自定义内存控制组。
通过`/etc/cgrules.conf`可以实现普通用户进程自动绑定指定cgroup,当进程内存占用超过设定阈值,内核触发OOM机制直接杀掉进程。这套机制不只是给Docker容器使用,也可以直接对宿主机普通操作系统用户做内存资源隔离与限制。
[root@docker1 ~]# cd /sys/fs/cgroup
[root@docker1 cgroup]# ls
memory
[root@docker1 cgroup]# cd memory/
[root@docker1 memory]# ls
docker
[root@docker1 memory]# cd docker/
[root@docker1 docker]# ls
memory.limit_in_bytes
memory.memsw.limit_in_bytes
[root@docker1 docker]# pwd
/sys/fs/cgroup/memory/docker
[root@docker1 docker]# cat memory.limit_in_bytes
9223372036854771712
[root@docker1 docker]# cat memory.memsw.limit_in_bytes
9223372036854771712
[root@docker1 docker]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 docker]# cd /sys/fs/cgroup/memory/
[root@docker1 memory]# yum install -y libcgroup-tools.x86_64
[root@docker1 memory]# cgcreate -g memory:x1
[root@docker1 memory]# ls
x1
[root@docker1 memory]# cd x1
[root@docker1 x1]# ls
cgroup.clone_children memory.memsw.failcnt
cgroup.event_control memory.memsw.limit_in_bytes
cgroup.procs memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks
memory.max_usage_in_bytes
[root@docker1 x1]# cd ..
[root@docker1 memory]# mkdir x2
[root@docker1 memory]# cd x2
[root@docker1 x2]# ls
cgroup.clone_children memory.memsw.failcnt
cgroup.event_control memory.memsw.limit_in_bytes
cgroup.procs memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks
memory.max_usage_in_bytes
[root@docker1 x2]# cd ..
[root@docker1 memory]# ls
x1 x2
[root@docker1 memory]# cgdelete -g memory:x2
[root@docker1 memory]# ls
x1
[root@docker1 memory]# cd x1
[root@docker1 x1]# ls
memory.limit_in_bytes
memory.memsw.limit_in_bytes
[root@docker1 x1]# cat memory.limit_in_bytes
9223372036854771712
[root@docker1 x1]# cat memory.memsw.limit_in_bytes
9223372036854771712
[root@docker1 x1]# bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
20010241024
209715200
^C
(interrupt) Exiting bc.
[root@docker1 x1]# echo 209715200 > memory.limit_in_bytes
[root@docker1 x1]# echo 209715200 > memory.memsw.limit_in_bytes
[root@docker1 x1]# cgexec -g memory:x1 dd if=/dev/zero of=/dev/shm/testfile bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.232711 s, 451 MB/s
[root@docker1 x1]# cgexec -g memory:x1 dd if=/dev/zero of=/dev/shm/testfile bs=1M count=200
Killed
[root@docker1 x1]# cgexec -g memory:x1 dd if=/dev/zero of=/dev/shm/testfile bs=1M count=300
Killed
[root@docker1 x1]# cd /dev/shm
[root@docker1 shm]# ls
testfile
[root@docker1 shm]# du -h testfile
199M testfile
[root@docker1 shm]# free -m
total used free shared buff/cache available
Mem: 1980 370 101 208 1509 1227
Swap: 2047 0 2047
[root@docker1 shm]# rm -f testfile
[root@docker1 shm]# free -m
total used free shared buff/cache available
Mem: 1980 370 300 9 1309 1426
Swap: 2047 0 2047
[root@docker1 shm]# reboot
[root@docker1 ~]# cd /sys/fs/cgroup/memory/
[root@docker1 memory]# ls
- reboot重启之后,/sys/fs/cgroup/memory/x1 不存在了
[root@docker1 memory]# vim /etc/cgconfig.conf

group x1 {
memory {
memory.limit_in_bytes = "209715200";
memory.memsw.limit_in_bytes= "209715200";
}
}
[root@docker1 memory]# systemctl restart cgconfig.service
[root@docker1 memory]# ls
x1
[root@docker1 memory]# cd x1
[root@docker1 x1]# ls
memory.limit_in_bytes memory.memsw.limit_in_bytes
[root@docker1 x1]# cat memory.limit_in_bytes
209715200
[root@docker1 x1]# cat memory.memsw.limit_in_bytes
209715200
[root@docker1 memory]# useradd xx
[root@docker1 memory]# vim /etc/cgrules.conf
- 编辑 /etc/cgrules.conf 文件
- 添加一行内容
![]()
xx memory x1/
[root@docker1 memory]# systemctl restart cgred.service
[root@docker1 memory]# su - xx
[xx@docker1 ~]$ cd /dev/shm
[xx@docker1 shm]$ dd if=/dev/zero of=bigfile bs=1M count=200
Killed
[xx@docker1 shm]$ ls
bigfile
[xx@docker1 shm]$ du -h bigfile
199M bigfile
[xx@docker1 shm]$ logout
Docker内存限制与memory cgroup对应
本实验演示Docker内存限制参数与底层memory cgroup的对应关系,直观看到docker启动参数直接写入内核cgroup控制文件。
执行`docker run -d --name demo --memory 200M --memory‑swap 200M nginx`启动nginx容器。`--memory 200M`限制容器物理内存最大200MB;`--memory‑swap 200M`代表内存+swap总上限200MB,等价于容器完全禁止使用swap交换分区。
容器启动成功后,进入宿主机`/sys/fs/cgroup/memory/docker/`下该容器ID的目录,读取`memory.limit_in_bytes`、`memory.memsw.limit_in_bytes`,数值为209715200字节,换算正好等于200M,证明docker把内存限制直接写入cgroup内存控制文件。目录内tasks文件保存容器内部全部进程PID,查看tasks可以看到容器内部所有进程编号,内核就是通过tasks把进程归入该内存控制组,实现内存配额管控。
执行`docker rm -f demo`强制删除容器,容器停止销毁,对应的cgroup容器ID目录也随之清理,`docker ps -a`确认容器已经消失。
Docker的内存限制底层完全依赖Linux memory cgroup,设置的内存参数会直接转换成字节写入内核文件,内核负责监控容器进程内存占用,超出阈值就触发OOM杀死容器进程。
Docker使用`--memory`设置物理内存上限,`--memory‑swap`控制内存加交换分区总大小,参数最终会转化为字节写入宿主机memory cgroup对应的控制文件。cgroup目录下tasks文件记录容器全部进程PID,内核以此识别哪些进程需要遵守这套内存配额。
当容器被删除,对应的cgroup目录自动回收清理。容器内存限制不是Docker软件本身实现,是依托Linux内核cgroup子系统完成资源隔离,这也是容器轻量化的底层原理。
[root@docker1 memory]# cd
[root@docker1 ~]# docker run -d --name demo --memory 200M --memory-swap 200M nginx
[root@docker1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e761c5dd8def nginx "/docker-entrypoint.…" 57 seconds ago Up 55 seconds 80/tcp demo
[root@docker1 ~]# cd /sys/fs/cgroup/memory/docker
[root@docker1 docker]# ls
e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7
[root@docker1 docker]# cd e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7
[root@docker1 e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7]# cat memory.limit_in_bytes
209715200
[root@docker1 e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7]# cat memory.memsw.limit_in_bytes
209715200
[root@docker1 e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7]# cat tasks
1893
1935
1936
[root@docker1 e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7]# docker top demo
UID PID PPID C STIME TTY TIME CMD
root 1893 1871 0 22:59 ? 00:00:00 nginx: master process nginx -g daemon off;
101 1935 1893 0 22:59 ? 00:00:00 nginx: worker process
101 1936 1893 0 22:59 ? 00:00:00 nginx: worker process
[root@docker1 e761c5dd8def50b10f2df2a893fea2f4b0b723d4103000870917afc599aca2b7]# cd
[root@docker1 ~]# docker rm -f demo
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
lxcfs修复容器/proc资源显示
本实验演示`lxcfs`工具的作用,解决容器内`/proc`伪文件系统显示宿主机真实硬件信息的问题。原生Docker做内存、CPU资源限制后,容器内部`free`、`top`读取`/proc`下文件,依旧看到宿主机完整内存CPU,会让容器内程序误判硬件资源,lxcfs专门用来修复该问题。
安装`lxcfs`rpm软件包,启动`lxcfs.service`服务,通过`ps ax |grep lxcfs`确认进程正常运行。`/var/lib/lxcfs`是lxcfs的挂载目录,内部提供虚拟的proc目录,包含cpuinfo、diskstats、meminfo、swaps、uptime等内核信息文件。
启动容器时使用`‑v`参数,把lxcfs虚拟proc文件映射进容器的`/proc`对应路径,同时通过`‑m 256m`限制容器内存为256M。进入容器执行`free ‑m`,可以看到容器识别到总内存只有256M,不再读取宿主机物理内存,实现容器内部正确识别资源配额。测试完成执行exit退出容器,再使用`docker rm ‑f`清理容器。
lxcfs本质是FUSE用户态文件系统,拦截读取/proc的请求,读取cgroup的限制数值,返回容器配额,而不是宿主机真实硬件数据。原生docker没有该能力,如果不部署lxcfs,容器内部工具看到的是宿主机硬件,容易引发业务程序错误判断可用资源。
原生Docker容器的`/proc`目录直接复用宿主机内核文件,即便做了内存CPU限制,容器内部`free`、top依旧读取宿主机全部硬件信息,容易造成业务程序资源判断出错。lxcfs是FUSE文件系统工具,对外提供虚拟的proc文件,读取数据时读取cgroup限制,向容器返回容器自身配额。
容器启动时通过数据卷挂载,把lxcfs虚拟proc文件覆盖容器内部`/proc`对应文件,容器内部命令就可以正确识别分配给容器的资源上限。在Kubernetes、docker生产环境,lxcfs常用来解决容器内部查看资源信息不准的问题。
[root@docker1 ~]# ls
lxcfs-2.0.5-3.el7.centos.x86_64.rpm
[root@docker1 ~]# yum install -y lxcfs-2.0.5-3.el7.centos.x86_64.rpm
[root@docker1 ~]# systemctl start lxcfs.service
[root@docker1 ~]# ps ax | grep lxcfs
1983 ? Ssl 0:00 /usr/bin/lxcfs /var/lib/lxcfs/
1995 pts/0 S+ 0:00 grep --color=auto lxcfs
[root@docker1 ~]# cd /var/lib/lxcfs
[root@docker1 lxcfs]# ls
cgroup proc
[root@docker1 lxcfs]# cd proc/
[root@docker1 proc]# ls
cpuinfo diskstats meminfo stat swaps uptime
[root@docker1 proc]# cd
[root@docker1 ~]# docker run -it -m 256m
> -v /var/lib/lxcfs/proc/cpuinfo:/proc/cpuinfo:rw
> -v /var/lib/lxcfs/proc/diskstats:/proc/diskstats:rw -v /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw -v /var/lib/lxcfs/proc/stat:/proc/stat:rw
> -v /var/lib/lxcfs/proc/swaps:/proc/swaps:rw
> -v /var/lib/lxcfs/proc/uptime:/proc/uptime:rw
> centos:7
[root@6ae7ae7d6254 /]# free -m
total used free shared buff/cache available
Mem: 256 3 252 9 0 252
Swap: 256 0 256
[root@6ae7ae7d6254 /]# exit
exit
[root@docker1 ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6ae7ae7d6254 centos:7 "/bin/bash" 46 seconds ago Exited (0) 25 seconds ago clever_borg
[root@docker1 ~]# docker rm -f clever_borg
clever_borg
Docker blkio块IO限流cgroup
本实验使用Linux `blkio` cgroup块IO子系统,演示Docker对容器磁盘读写速率做限流,限制容器磁盘读写的带宽与IOPS,避免单个容器大量磁盘IO吃光宿主机磁盘性能。
`/sys/fs/cgroup/blkio/`是块IO控制组目录,`blkio.throttle.write_bps_device`为写带宽限制文件。通过`docker run --help |grep device`可以查看blkio相关参数:`--device‑read‑bps`、`--device‑write‑bps`按字节限制读写带宽;`--device‑read‑iops`、`--device‑write‑iops`限制每秒IO次数。
执行`docker run -it --name demo --device‑write‑bps /dev/sda:30MB centos`启动容器,对磁盘`/dev/sda`设置容器写带宽上限30MB/s。切换另外一个终端,进入blkio/docker下对应容器ID目录,`blkio.throttle.write_bps_device`文件里面记录设备主设备号、次设备号、限制字节速率,30MB换算字节为`30*1024*1024`。
进入容器内部,使用`dd`命令搭配`oflag=direct`绕开操作系统页缓存,做直接IO压测磁盘写速度,可以观察实际写入速度被压制在30MB/s附近,证明blkio限流生效。测试完成退出容器,执行`docker rm -f demo`删除容器,对应的blkio cgroup目录自动释放。
blkio cgroup只对块设备IO生效,内存缓存命中的IO不会被限流。Docker磁盘IO限流底层直接操作blkio内核文件,用来防止容器疯狂读写磁盘,抢占其他业务的磁盘IO资源。
blkio是Linux内核cgroup块IO子系统,专门用于管控进程块设备读写带宽和IOPS。Docker提供对应的启动参数,可以限制容器读、写的每秒字节数以及每秒IO次数,限制作用针对指定磁盘块设备。
压测时需要使用direct直接IO绕过页缓存,才能观察到真实磁盘限速效果。该机制主要用于隔离磁盘IO,防止某个容器大量读写磁盘,耗尽宿主机磁盘性能。容器销毁之后,对应的blkio控制组配置自动回收清理。
[root@docker1 ~]# cd /sys/fs/cgroup/blkio/
[root@docker1 blkio]# ls
blkio.throttle.write_bps_device
[root@docker1 blkio]# docker run --help | grep device
--blkio-weight-device list Block IO weight (relative
device weight) (default [])
--device list Add a host device to the
--device-cgroup-rule list Add a rule to the cgroup
allowed devices list
--device-read-bps list Limit read rate (bytes per
second) from a device
--device-read-iops list Limit read rate (IO per
second) from a device
--device-write-bps list Limit write rate (bytes
per second) to a device
--device-write-iops list Limit write rate (IO per
second) to a device
--gpus gpu-request GPU devices to add to the
[root@docker1 blkio]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 blkio]# docker run -it --name demo --device-write-bps /dev/sda:30MB centos:7
- 再开一个docker1 终端
[root@docker1 ~]# cd /sys/fs/cgroup/blkio/
[root@docker1 blkio]# ls
blkio.throttle.write_bps_device
docker
[root@docker1 blkio]# cd docker/
[root@docker1 docker]# ls
7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e
[root@docker1 docker]# cd7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e
-bash: cd7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e: command not found
[root@docker1 docker]# cd 7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e
[root@docker1 7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e]# ls
blkio.throttle.write_bps_device
[root@docker1 7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e]# bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
3010241024
31457280
^C
(interrupt) Exiting bc.
[root@docker1 7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e]# ll /dev/sda
brw-rw---- 1 root disk 8, 0 Sep 6 09:06 /dev/sda
[root@docker1 7a9ffe9303927bbee6a180fe842fc62cc08eaf932a851ad786b881c3dde43b4e]# echo "8:0 31457280" > blkio.throttle.write_bps_device
- 切换到虚拟机docker1第一个终端
[root@7a9ffe930392 /]# dd if=/dev/zero of=/bigfile bs=1M count=200 oflag=direct
200+0 records in
200+0 records out
209715200 bytes (210 MB) copied, 6.62826 s, 31.6 MB/s
[root@7a9ffe930392 /]# exit
exit
[root@docker1 blkio]# docker rm -f demo
demo
[root@docker1 blkio]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Docker Capabilities权限能力
本实验演示Docker的Linux capability内核能力机制,区分普通容器、`--privileged`特权容器、单独添加特定cap权限三者之间权限差异。
默认启动busybox容器,即使容器内部是root用户,也被内核capabilities裁剪,没有完整主机权限。执行`ip link set down eth0`关闭网卡,直接报错`Operation not permitted`,容器内root不等于宿主机root,很多系统操作被内核禁止。
使用`--privileged`启动容器,该参数会给容器开放全部内核权限。容器内部可以执行fdisk查看宿主机磁盘分区,也可以随意修改网卡IP地址,几乎拥有宿主机全部操作权限,风险很高,生产环境尽量避免使用。
使用`--cap‑add NET_ADMIN`,只单独授予网络管理能力,不开启全部特权。容器内可以修改网卡IP地址,网络相关操作放行,但fdisk查看磁盘这类块设备操作依旧没有权限,实现最小权限分配。
对比`--cap‑add SYS_ADMIN`,SYS_ADMIN并不包含网络管理权限,因此依旧无法修改网卡IP。这说明不同cap能力职责划分明确,需要什么权限就授予对应cap,不能笼统依靠SYS_ADMIN。
Docker通过capabilities裁剪容器root的权限,实现安全隔离。
`--privileged`是把所有cap全部打开,权限过大;
生产推荐使用`--cap‑add`按需添加单项能力,遵循最小权限原则。
Linux capability把超级用户root的权限拆分成很多独立小能力,Docker默认会剥夺容器root大部分cap,所以容器内root并不能随意操作宿主机硬件与网络。`--privileged`会开启全部内核权限,容器几乎不受限制,安全风险大。
可以通过`--cap‑add`精准授予某一项能力,例如`NET_ADMIN`专门负责网络配置修改,只给业务需要的权限。SYS_ADMIN并不涵盖网络管理权限,不同cap各司其职。生产环境尽量不使用privileged,采用按需添加cap的方式实现权限管控。
[root@docker1 ~]# docker run -it --rm busybox
/ # id
uid=0(root) gid=0(root) groups=0(root),10(wheel)
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
14: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ip link set down eth0
ip: SIOCSIFFLAGS: Operation not permitted
/ # exit
[root@docker1 ~]# docker run -it --rm --privileged busybox
/ # fdisk -l
Disk /dev/sda: 50.0G, 53687091200 bytes, 104857600 sectors
6527 cylinders, 255 heads, 63 sectors/track
Units: cylinders of 16065 * 512 = 8225280 bytes
Device Boot StartCHS EndCHS StartLBA EndLBA Sectors Size Id Type
/dev/sda1 * 0,32,33 130,170,40 2048 2099199 2097152 1024M 83 Linux
/dev/sda2 130,170,41 1023,254,63 2099200 104857599 102758400 48.9G 8e Linux LVM
Disk /dev/dm-0: 46.9G, 50457477120 bytes, 98549760 sectors
6134 cylinders, 255 heads, 63 sectors/track
Units: cylinders of 16065 * 512 = 8225280 bytes
Disk /dev/dm-0 doesn't contain a valid partition table
Disk /dev/dm-1: 2048M, 2147483648 bytes, 4194304 sectors
261 cylinders, 255 heads, 63 sectors/track
Units: cylinders of 16065 * 512 = 8225280 bytes
Disk /dev/dm-1 doesn't contain a valid partition table
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ip a a 10.0.0.10/24 dev eth0
/ # ip a
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 10.0.0.10/24 scope global eth0
valid_lft forever preferred_lft forever
/ # exit
[root@docker1 ~]# docker run -it --rm --cap-add NET_ADMIN busybox
/ # fdisk -l
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ip a a 10.0.0.10/24 dev eth0
/ # ip a
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 10.0.0.10/24 scope global eth0
valid_lft forever preferred_lft forever
/ # fdisk -l
/ # exit
[root@docker1 ~]# docker run -it --rm --cap-add SYS_ADMIN busybox
/ # fdisk -l
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
/ # ip a a 10.0.0.10/24 dev eth0
ip: RTNETLINK answers: Operation not permitted
/ # exit
Docker Swarm 集群与 Portainer 可视化部署
本实验完成两节点 Docker Swarm 集群部署,实现容器编排、服务扩缩容、滚动更新回滚,最后部署 Portainer 作为 Swarm 集群可视化管理面板。
实验前期先清理 docker 环境,修改两台机器`daemon.json`配置统一镜像加速源,修改配置后必须重启 docker 服务,保证两个节点可以正常拉取镜像。Harbor 私有仓库提前部署完成,作为集群内部镜像仓库。
执行`docker swarm init`在 docker1 初始化集群,该节点成为 Manager Leader 管理节点,命令输出 worker 加入令牌与 manager 加入令牌。docker2 使用 worker 令牌加入集群,通过`docker node ls`查看集群所有节点状态,区分管理节点与工作节点。
使用`docker service create`创建 swarm 服务,指定副本数、端口发布,ingress overlay 网络提供集群入口负载均衡。两个节点访问 8000 端口都可以访问业务,请求会被 ingress 网络调度到集群任意副本容器。进入容器修改页面内容,循环 curl 访问,能够观察到负载均衡在不同节点的副本之间轮换。
`docker service scale`实现服务副本水平扩容、缩容,快速增加或者减少容器实例数量。`docker service update`执行滚动更新,逐步替换旧版本容器为新版本镜像;出现问题可以执行`docker service rollback`快速回滚到上一个可用版本,实现业务无损发布与故障回退。
在已经搭建完成的 Docker Swarm 集群环境之上,部署 Portainer‑CE 可视化管理平台,采用 stack 堆栈方式适配 Swarm 编排模式,同时对接内部 Harbor 私有仓库`reg.westos.org`完成镜像缓存,实现网页端图形化管理整个 Swarm 集群。
首先查阅官方文档,选择针对 Linux 环境下 Docker Swarm 的安装方案,通过 curl 命令下载官方提供的 portainer‑agent‑stack.yml 堆栈模板文件。实验环境使用内网 Harbor 私有仓库,因此需要提前在 Harbor 网页界面创建公开的 portainer 项目,用于存放 portainer 相关镜像。
接下来拉取 portainer‑ce 与 agent 两个官方镜像,通过 docker tag 重新打标签,push 上传至`reg.westos.org`私有仓库,集群内所有节点就可以从内网仓库拉取镜像,避免公网网络问题。清理之前测试用的 web‑svc 服务,执行`docker stack deploy`命令,使用yaml 文件部署 portainer 堆栈。部署完成通过`docker stack ls`、`docker stack services portainer`查看堆栈状态,portainer_portainer 为普通 replicated 副本模式,portainer_agent 使用 global 模式,会在集群每一个节点都运行 agent 代理容器,采集各个节点的 docker 信息。
部署完成浏览器访问`https://192.168.52.171:9443`,首次登录需要创建管理员账号。页面提示需要 setup token,执行`docker service logs portainer_portainer`查看服务日志获取 token。创建账号后跳过初始化引导,进入管理界面,选中 primary 集群环境,即可看到完整 Swarm 集群节点信息。
实验最后回到 Harbor 仓库页面,可以审计看到 Portainer 从私有仓库拉取镜像的操作日志,能够完整记录镜像 pull 行为,验证集群节点正常使用内网私有仓库。
Docker Swarm 是 Docker 原生容器编排工具,只需要简单命令即可搭建多节点集群,依靠 ingress overlay 网络实现集群级负载均衡。service 服务具备副本管理、水平扩缩容、滚动更新、一键回滚能力,区别于单机 docker 容器,服务会由集群调度器自动选择节点运行容器。Portainer CE 是 Docker 生态常用的可视化管理工具,针对 Swarm 集群必须使用 stack 堆栈部署,agent 组件采用 global 全局部署,保证集群每个节点都上报状态信息。实验中将 Portainer 镜像上传到内部 Harbor 仓库,集群所有节点优先走内网拉取镜像,消除外网依赖。
部署完成后通过 9443 的 HTTPS 网页入口,替代复杂 docker 命令行,可以图形化查看节点、服务、副本、容器日志。Harbor 审计日志可以完整留存镜像拉取记录,方便运维审计。该部署方式适合生产环境 Swarm 集群可视化运维。
[root@docker1 ~]# docker system prune
[root@docker2 ~]# docker system prune
[root@docker1 ~]# vim /etc/docker/daemon.json
{
"registry-mirrors": ["https://reg.westos.org"]
}
[root@docker2 ~]# vim /etc/docker/daemon.json
{
"registry-mirrors": ["https://reg.westos.org"]
}
- 统一镜像下载源
- 如果 有修改 daemon.json,需要做如下操作
systemctl restart docker
[root@docker1 ~]# cd harbor/
[root@docker1 harbor]# docker compose up -d
[root@docker1 harbor]# docker compose ps
- 浏览器访问 https://192.168.52.171
[root@docker1 harbor]# docker pull nginx
[root@docker2 ~]# docker pull nginx
[root@docker1 harbor]# cd
[root@docker1 ~]# docker swarm init --help
[root@docker1 ~]# docker swarm init
Swarm initialized: current node (4mh4spucsu2z5hv8x5rf0zhdn) is now a manag er.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3vijmnwfkgkhf13ie0s9a4s1x4cz186vuxs n4h9nog42yrkk2e-86vt7upcdm4kjmq2z2t6wdupz 192.168.52.171:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
[root@docker2 ~]# docker swarm join --token SWMTKN-1-3vijmnwfkgkhf13ie0s9a 4s1x4cz186vuxsn4h9nog42yrkk2e-86vt7upcdm4kjmq2z2t6wdupz 192.168.52.171:2377
This node joined a swarm as a worker.
[root@docker1 ~]# docker swarm join-token manager
[root@docker1 ~]# docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
4mh4spucsu2z5hv8x5rf0zhdn * docker1 Ready Active Leader 26.1.4
4bdfma78bdmwf6ribe0k1xznj docker2 Ready Active 26.1.4
[root@docker1 ~]# docker service create --help
[root@docker1 ~]# docker service create --name web-svc --replicas 2 --publish 8000:80 --limit-cpu 0.5 --limit-memory 256M nginx:latest
[root@docker1 ~]# curl docker1:8000
<title>Welcome to nginx!</title>
[root@docker2 ~]# curl docker2:8000
<title>Welcome to nginx!</title>
[root@docker1 ~]# docker ps | grep nginx:latest
e2e7c7d84fc0 nginx:latest "/docker-entrypoint.…" 6 minutes ago Up 6 minutes 80/tcp web-svc.2.squ6nagesu89aqfw3tb0b39yv
[root@docker1 ~]# docker service ps web-svc
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
0sjlgdpbvhms web-svc.1 nginx:latest docker2 Running Running 5 minutes ago
squ6nagesu89 web-svc.2 nginx:latest docker1 Running Running 6 minutes ago
[root@docker1 ~]# docker exec -it web-svc.2.squ6nagesu89aqfw3tb0b39yv bash
root@e2e7c7d84fc0:/# cd /usr/share/nginx/html/
root@e2e7c7d84fc0:/usr/share/nginx/html# echo docker1 > index.html
root@e2e7c7d84fc0:/usr/share/nginx/html# exit
exit
[root@docker2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4d85eae24ffa nginx:latest "/docker-entrypoint.…" 9 minutes ago Up 9 minutes 80/tcp web-svc.1.0sjlgdpbvhms6nd14btjlef4n
[root@docker2 ~]# docker exec -it web-svc.1.0sjlgdpbvhms6nd14btjlef4n bash
root@4d85eae24ffa:/# cd /usr/share/nginx/html/
root@4d85eae24ffa:/usr/share/nginx/html# echo docker2 > index.html
root@4d85eae24ffa:/usr/share/nginx/html# exit
exit
[root@docker1 ~]# for i in {1..10}; do curl docker2:8000; done;
docker2
docker1
docker2
docker1
docker2
docker1
docker2
docker1
docker2
docker1
[root@docker2 ~]# for i in {1..10}; do curl docker1:8000; done
docker2
docker1
docker2
docker1
docker2
docker1
docker2
docker1
docker2
docker1
[root@docker1 ~]# docker network ls
rkt13l361tvo ingress overlay swarm
[root@docker1 ~]# docker network inspect ingress
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1"
[root@docker1 ~]# docker service scale web-svc=6
web-svc scaled to 6
overall progress: 6 out of 6 tasks
1/6: running
2/6: running
3/6: running
4/6: running
5/6: running
6/6: running
verify: Service web-svc converged
[root@docker1 ~]# docker service ps web-svc
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
0sjlgdpbvhms web-svc.1 nginx:latest docker2 Running Running 14 minutes ago
squ6nagesu89 web-svc.2 nginx:latest docker1 Running Running 15 minutes ago
uh8ehqruebo1 web-svc.3 nginx:latest docker2 Running Running less than a second ago
lzvuhn4bcm8e web-svc.4 nginx:latest docker1 Running Running 18 seconds ago
ioco874qe3el web-svc.5 nginx:latest docker2 Running Running less than a second ago
w14hw63bac7p web-svc.6 nginx:latest docker1 Running Running 18 seconds ago
[root@docker1 ~]# docker service scale web-svc=2
web-svc scaled to 2
overall progress: 2 out of 2 tasks
1/2: running
2/2: running
verify: Service web-svc converged
[root@docker1 ~]# docker service ps web-svc
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
0sjlgdpbvhms web-svc.1 nginx:latest docker2 Running Running 15 minutes ago
squ6nagesu89 web-svc.2 nginx:latest docker1 Running Running 17 minutes ago
[root@docker1 ~]# docker service update web-svc --image webserver:v4
web-svc
overall progress: 2 out of 2 tasks
1/2: running
2/2: running
verify: Service web-svc converged
[root@docker1 ~]# docker ps | grep webserver
751367c4552a webserver:v4 "nginx -g 'daemon of…" About a minute ago Up About a minute 80/tcp, 443/tcp web-svc.2.kqkfqmdofs3am7z3o32yqo2p3
[root@docker1 ~]# docker service rollback web-svc
web-svc
rollback: manually requested rollback
overall progress: rolling back update: 2 out of 2 tasks
1/2: running
2/2: running
verify: Service web-svc converged
[root@docker1 ~]# docker ps | grep nginx:latest
97a23b77f206 nginx:latest "/docker-entrypoint.…" 24 seconds ago Up 18 seconds 80/tcp web-svc.2.85uqcxfgan26ydo9r43hxcmqi



[root@docker1 ~]# curl -L https://downloads.portainer.io/ce-lts/portainer-agent-stack.yml -o portainer-agent-stack.yml
[root@docker1 ~]# ls
[root@docker1 ~]# vim portainer-agent-stack.yml
- 浏览器访问 https://192.168.52.171
- 点击项目,点击 新建项目

- 开启连接
[root@docker1 ~]# docker pull portainer/portainer-ce:lts
[root@docker1 ~]# docker pull portainer/agent:lts
[root@docker1 ~]# docker tag portainer/portainer-ce:lts reg.westos.org/portainer/portainer-ce:lts
[root@docker1 ~]# docker push reg.westos.org/portainer/portainer-ce:lts
[root@docker1 ~]# docker tag portainer/agent:lts reg.westos.org/portainer/agent:lts
[root@docker1 ~]# docker push reg.westos.org/portainer/agent:lts
[root@docker1 ~]# docker service rm web-svc
web-svc
[root@docker1 ~]# docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
[root@docker1 ~]# docker stack deploy -c portainer-agent-stack.yml portainer
[root@docker1 ~]# docker stack ls
NAME SERVICES
portainer 2
[root@docker1 ~]# docker stack services portainer
ID NAME MODE REPLICAS IMAGE PORTS
yl8t9bnxp3i3 portainer_agent global 2/2 portainer/agent:lts
itc7ib6dkkjv portainer_portainer replicated 1/1 portainer/portainer-ce:lts *:8000->8000/tcp, *:9000->9000/tcp, *:9443->9443/tcp
- 浏览器访问 https://192.168.52.171:9443
- 执行此命令,查看 Setup token
[root@docker1 ~]# docker service logs portainer_portainer
- 填写完成后,点击 Create user
- 点击 Skip
- 点击 Get started

- 点击 primary

- 浏览器访问 https://192.168.52.171
- 可以看到从 portainer共有仓库拉取记录

Docker‑Swarm集群销毁与环境资源清理
本部分为实验收尾清理操作,完成Docker Swarm集群解散,并且清理集群残留网络、存储卷、无用容器镜像,把两台节点docker环境恢复到实验前干净状态。
普通worker节点执行`docker swarm leave`,即可正常退出swarm集群。而manager管理节点不能直接leave,必须带上`--force`强制参数,强制销毁本节点的集群管理角色,集群就此解散。集群销毁之后,原先swarm自动创建的overlay、ingress集群网络会自动删除。
执行`docker system prune`清理停止的容器、未使用镜像、闲置网络,但该命令默认不会删除数据卷volume。查看网络列表可以确认,swarm相关网络已经消失,仅保留bridge、host、none以及harbor业务网桥。
`docker volume prune`只会清理没有被容器挂载的闲置卷,Portainer持久化数据卷仍然保留,所以需要手动执行`docker volume rm`删除portainer相关volume,彻底清除Portainer的账号、集群配置数据。docker2节点同样执行system prune做环境清理,查看网络与卷列表确认残留资源全部清理完毕。
> 注意:清理操作区分风险,`system prune`不会删除正在运行业务;手动rm volume会永久丢失持久化数据,生产环境谨慎执行。
退出Swarm集群要区分节点角色,worker节点直接leave,manager节点必须加`--force`强制退出。集群解散后ingress、overlay这类swarm专属网络会自动回收。
[root@docker2 ~]# docker swarm leave
Node left the swarm.
[root@docker1 ~]# docker swarm leave --force
Node left the swarm.
[root@docker1 ~]# docker system prune
[root@docker1 ~]# docker network ls
NETWORK ID NAME DRIVER SCOPE
0cd056d3cfec bridge bridge local
6fe8bbc11b77 harbor_harbor bridge local
141b4496ed4d host host local
f72013194df3 none null local
[root@docker1 ~]# docker volume prune
[root@docker1 ~]# docker volume ls
local portainer_data
local portainer_portainer_data
[root@docker1 ~]# docker volume rm portainer_data portainer_portainer_data
[root@docker2 ~]# docker system prune
[root@docker2 ~]# docker network ls
[root@docker2 ~]# docker volume ls
知识储备
Harbor 基于 docker‑compose 启停销毁重建
进入 harbor 工作目录,`docker compose ps`查看全部组件运行状态,可以看到 portal、registry、db、redis、trivy‑adapter 等 11 个组件。`docker compose restart`执行服务重启,逐个停止再启动全部容器,容器 ID 保持不变,原有数据不会丢失,适合配置修改后热重启服务。
执行`docker compose down`,会停止并删除所有 Harbor 相关容器,同时删除 compose 自动创建的 harbor_harbor 网桥。执行`docker ps -a`确认容器全部消失,但查看宿主机`/data`目录,数据库、镜像仓库存储、证书、trivy 扫描库、日志等目录文件完整保留。
/data 是 Harbor 的数据持久化目录,容器删除不会清空该目录,保证仓库镜像、用户项目配置不会丢失。
再执行`docker compose up -d`后台重新拉起整套 Harbor 服务,会复用`/data`目录原有数据,容器重建,但仓库镜像、账号、项目配置全部保留,浏览器访问地址依旧可以正常登录使用。再次执行 down 操作,容器与网络再次被清理,持久化数据依旧保存在宿主机磁盘。
注意:`compose down`不清理宿主机 /data 目录;只有手动删除 /data 目录,才会彻底销毁 Harbor 全部业务数据。
[root@docker1 ~]# cd harbor/
[root@docker1 harbor]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
harbor-log goharbor/harbor-log:v2.14.0 "/bin/sh -c /usr/loc…" log 41 hours ago Up 3 minutes (healthy) 127.0.0.1:1514->10514/tcp
harbor-portal goharbor/harbor-portal:v2.14.0 "nginx -g 'daemon of…" portal 41 hours ago Up 3 minutes (healthy)
nginx goharbor/nginx-photon:v2.14.0 "nginx -g 'daemon of…" proxy 41 hours ago Restarting (1) 16 seconds ago
registry goharbor/registry-photon:v2.14.0 "/home/harbor/entryp…" registry 41 hours ago Up 3 minutes (healthy)
registryctl goharbor/harbor-registryctl:v2.14.0 "/home/harbor/start.…" registryctl 41 hours ago Up 3 minutes (healthy)
[root@docker1 harbor]# docker compose restart
[+] Restarting 10/10
✔ Container harbor-db Started 5.0s
✔ Container registry Started 6.1s
✔ Container harbor-jobservice Started 4.5s
✔ Container trivy-adapter Started 4.8s
✔ Container harbor-portal Started 6.2s
✔ Container registryctl Started 6.0s
✔ Container nginx Started 5.3s
✔ Container harbor-core Started 5.8s
✔ Container harbor-log Started 11.3s
✔ Container redis Started 4.3s
[root@docker1 harbor]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
harbor-core goharbor/harbor-core:v2.14.0 "/harbor/entrypoint.…" core 41 hours ago Up 50 seconds (healthy)
harbor-db goharbor/harbor-db:v2.14.0 "/docker-entrypoint.…" postgresql 41 hours ago Up 51 seconds (healthy)
harbor-jobservice goharbor/harbor-jobservice:v2.14.0 "/harbor/entrypoint.…" jobservice 41 hours ago Up 47 seconds (healthy)
harbor-log goharbor/harbor-log:v2.14.0 "/bin/sh -c /usr/loc…" log 41 hours ago Up 45 seconds (healthy) 127.0.0.1:1514->10514/tcp
harbor-portal goharbor/harbor-portal:v2.14.0 "nginx -g 'daemon of…" portal 41 hours ago Up 50 seconds (healthy)
nginx goharbor/nginx-photon:v2.14.0 "nginx -g 'daemon of…" proxy 41 hours ago Up 51 seconds (healthy) 0.0.0.0:80->8080/tcp, :::80->8080/tcp, 0.0.0.0:443->8443/tcp, :::443->8443/tcp
redis goharbor/redis-photon:v2.14.0 "redis-server /etc/r…" redis 41 hours ago Up 52 seconds (healthy)
registry goharbor/registry-photon:v2.14.0 "/home/harbor/entryp…" registry 41 hours ago Up 50 seconds (healthy)
registryctl goharbor/harbor-registryctl:v2.14.0 "/home/harbor/start.…" registryctl 41 hours ago Up 50 seconds (healthy)
trivy-adapter goharbor/trivy-adapter-photon:v2.14.0 "/home/scanner/entry…" trivy-adapter 41 hours ago Up 51 seconds (healthy)
- 浏览器访问 https://192.168.52.171
[root@docker1 harbor]# docker compose down
[+] Running 11/11
✔ Container trivy-adapter Removed 0.6s
✔ Container nginx Removed 0.7s
✔ Container registryctl Removed 0.6s
✔ Container harbor-jobservice Removed 0.7s
✔ Container harbor-core Removed 3.3s
✔ Container harbor-portal Removed 0.3s
✔ Container harbor-db Removed 0.5s
✔ Container registry Removed 0.4s
✔ Container redis Removed 0.5s
✔ Container harbor-log Removed 10.3s
✔ Network harbor_harbor Removed 0.1s
[root@docker1 harbor]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
[root@docker1 harbor]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@docker1 harbor]# ls /data
ca_download certs database job_logs redis registry secret trivy-adapter
- 浏览器访问 https://192.168.52.171
[root@docker1 harbor]# docker compose up -d
[+] Running 11/11
✔ Network harbor_harbor Created 0.2s
✔ Container harbor-log Started 1.1s
✔ Container harbor-db Started 2.3s
✔ Container harbor-portal Started 2.5s
✔ Container redis Started 2.3s
✔ Container registry Started 2.6s
✔ Container registryctl Started 2.5s
✔ Container trivy-adapter Started 3.0s
✔ Container harbor-core Started 3.2s
✔ Container harbor-jobservice Started 4.2s
✔ Container nginx Started 4.5s
[root@docker1 harbor]# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
harbor-core goharbor/harbor-core:v2.14.0 "/harbor/entrypoint.…" core 39 seconds ago Up 36 seconds (healthy)
harbor-db goharbor/harbor-db:v2.14.0 "/docker-entrypoint.…" postgresql 39 seconds ago Up 37 seconds (healthy)
harbor-jobservice goharbor/harbor-jobservice:v2.14.0 "/harbor/entrypoint.…" jobservice 39 seconds ago Up 33 seconds (healthy)
harbor-log goharbor/harbor-log:v2.14.0 "/bin/sh -c /usr/loc…" log 39 seconds ago Up 38 seconds (healthy) 127.0.0.1:1514->10514/tcp
harbor-portal goharbor/harbor-portal:v2.14.0 "nginx -g 'daemon of…" portal 39 seconds ago Up 36 seconds (healthy)
nginx goharbor/nginx-photon:v2.14.0 "nginx -g 'daemon of…" proxy 39 seconds ago Up 34 seconds (healthy) 0.0.0.0:80->8080/tcp, :::80->8080/tcp, 0.0.0.0:443->8443/tcp, :::443->8443/tcp
redis goharbor/redis-photon:v2.14.0 "redis-server /etc/r…" redis 39 seconds ago Up 37 seconds (healthy)
registry goharbor/registry-photon:v2.14.0 "/home/harbor/entryp…" registry 39 seconds ago Up 36 seconds (healthy)
registryctl goharbor/harbor-registryctl:v2.14.0 "/home/harbor/start.…" registryctl 39 seconds ago Up 37 seconds (healthy)
trivy-adapter goharbor/trivy-adapter-photon:v2.14.0 "/home/scanner/entry…" trivy-adapter 39 seconds ago Up 36 seconds (healthy)
[root@docker1 harbor]# docker compose down
[+] Running 11/11
✔ Container nginx Removed 0.7s
✔ Container trivy-adapter Removed 0.6s
✔ Container registryctl Removed 0.5s
✔ Container harbor-jobservice Removed 0.6s
✔ Container harbor-portal Removed 0.2s
✔ Container harbor-core Removed 3.8s
✔ Container redis Removed 0.5s
✔ Container harbor-db Removed 0.5s
✔ Container registry Removed 0.5s
✔ Container harbor-log Removed 10.3s
✔ Network harbor_harbor Removed 0.1s
[root@docker1 harbor]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Docker config.json 认证凭证存储与 base64 解码原理
该配置文件 `/root/.docker/config.json` 是Docker登录私有镜像仓库之后自动生成的凭证文件,用于保存仓库的认证信息,免去每次拉取镜像重复输入账号密码。文件内`auth`字段存储的字符串是用户名与密码以`用户名:密码`格式拼接后,经过Base64编码得到的结果。Base64仅属于编码而非加密,可以被直接反向解码,因此该文件存在一定的安全隐患,不可随意对外泄露。
通过管道命令将编码字符串交由`base64 -d`进行解码,最终得到原始账号密码`xx:westos`。而使用`$`符号读取该串字符会被Shell识别为环境变量,由于不存在对应变量,执行结果为空,无法实现解码操作。本次操作验证了Docker私有仓库凭据的存储原理,也掌握了从认证配置文件逆向还原登录账号密码的方法。
[root@docker1 ~]# cat /root/.docker/config.json
{
"auths": {
"reg.westos.org": {
"auth": "eHg6d2VzdG9z"
}
}
}
[root@docker1 ~]# echo eHg6d2VzdG9z | base64 -d
xx:westos
Docker三大标准:封装、传输、运行
原理:Docker 的核心目标就是解决软件环境不一致、跨主机部署麻烦的问题,依靠镜像统一完成 封装‑传输‑运行 完整生命周期,这也是容器技术最核心的三个环节。
封装 就是把应用程序、程序依赖库、配置文件、环境变量全部打包进镜像。它会将运行这个软件所需要的一切环境一次性封装完成,做到环境定型,和底层宿主机系统分离开,在哪台机器上镜像内容都不会发生改变。封装完成后最终产出 Docker 镜像文件,作为后续分发的原材料。
传输 指镜像可以在不同服务器之间进行搬运分发。镜像能够上传到私有仓库、公有仓库保存,其他主机就可以从仓库下载镜像;整个传输过程镜像内容不会被篡改,保证拿到的程序环境和打包时完全一模一样,实现一次打包,多处分发。
运行 就是基于下载好的镜像启动生成容器。镜像属于静态的文件包,本身不能直接运行,当使用 docker run 命令时,Docker引擎就会读取镜像,创建出一个独立、隔离的运行环境也就是容器,应用程序就在容器当中启动执行。同一个镜像可以一次性启动出多个相互独立的容器实例。
总结:
封装、传输、运行构成 Docker 容器完整工作流程。封装负责把应用连同全部运行环境打包成镜像;传输实现镜像跨主机分发共享;运行将静态镜像实例化为动态可工作的容器。三者配合就可以做到开发、测试、生产环境完全一致,消除环境差异带来的部署故障。
Docker镜像组成
原理:Docker镜像本质是多层只读文件堆叠,采用分层存储机制,每一层都是一个独立的文件系统,层与层之间相互依赖。镜像没有可写层,属于静态、只读的文件包,不能直接运行。
最底层是基础镜像层,一般为centos、ubuntu这类精简操作系统文件系统,它是整个镜像的根基。往上每执行一条Dockerfile指令(RUN、COPY、ADD等)就会生成一个新的镜像层,每一层只保存和上一层相比改动、新增的文件,不会重复存储全部数据。所有上层修改并不会改动下层,修改文件时docker会触发复制‑写机制,把文件复制到上层再修改。
最顶部是容器层(读写层),注意:读写层并不属于镜像本身。当`docker run`启动容器的时候,才会在只读镜像之上额外添加一层可读写容器层。所有容器运行产生的修改、新建文件都会保存在读写层;镜像本体依旧保持原样。
总结:
Docker镜像由多层只读文件系统堆叠而成,自下而上依次为基础操作系统层、若干应用修改层。分层存储可以实现镜像层缓存复用,节省磁盘空间,加快镜像构建速度。镜像本身全部只读,不存在可写区域;只有启动为容器后,才额外生成独立读写层用来存放运行时改动。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)