目录

虚拟机清单

主机名IP 地址操作系统核心实验角色
docker1192.168.52.171CentOS 7 x86_64Harbor 私有仓库主节点、Docker 基础实验、Convoy 卷插件、Portainer、Docker‑Swarm 管理节点
docker2192.168.52.165CentOS 7 x86_64Docker 客户端、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镜像由多层只读文件系统堆叠而成,自下而上依次为基础操作系统层、若干应用修改层。分层存储可以实现镜像层缓存复用,节省磁盘空间,加快镜像构建速度。镜像本身全部只读,不存在可写区域;只有启动为容器后,才额外生成独立读写层用来存放运行时改动。

    Logo

    DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

    更多推荐