Docker结课实践考核

16551 字
83 分钟
Docker结课实践考核

《Docker 容器技术》结课实践考核报告#

[TOC]


第一阶段:环境准备#

系统优化#

Terminal window
'开启模板机后'
root@ubuntu:~# mkdir -p /server/script && cd /server/script
root@ubuntu:~# vim base_config.sh
#!/bin/bash
# author=Jiuzhao
sed -i '/use_pty/a\Defaults editor=/usr/bin/vim, env_editor' /etc/sudoers
# visudo 修改编辑器为vim
sed -i '/^%sudo/c\%sudo ALL=(ALL:ALL) ALL,NOPASSWD:ALL' /etc/sudoers
# 以%sudo开头的行,全部替换为后面的内容
# 所有用sudo命令的用户,都不需要使用密码
lvextend -L 30G -r /dev/mapper/ubuntu--vg-ubuntu--lv > /dev/null
# 扩容逻辑卷
# PS1 变量
cat >> ~/.bashrc <<EOF
# 我的PS1变量
PS1='\[\e[34;1m\]\u@\[\e[0m\]\[\e[32;1m\]\H\[\e[0m\]\[\e[31;1m\] \W\[\e[0m\]\\$ '
EOF
source ~/.bashrc
# 更换软件源
sed -i '/^URI/c\URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/' /etc/apt/sources.list.d/ubuntu.sources
apt-get update > /dev/null
# 优化下载速度(小调优,不是核心)
cat > /etc/apt/apt.conf.d/99local << 'EOF'
# 允许同时下载多个包(并发下载)
# 默认是串行的(一个个下载)
Acquire::http::Pipeline-Depth "5";
# 同时建立多个连接
Acquire::Queue-Mode "access";
Acquire::Languages "none";
# 不用每次都下载翻译文件,省带宽
Acquire::http::No-Cache "false";
EOF
# 安装常用的软件包
apt-get install -y vim tree wget bash-completion lrzsz net-tools sysstat iotop iftop htop unzip netcat-openbsd nmap telnet bc psmisc apache2-utils dnsutils nethogs sshpass expect cowsay sl
# 其他优化
> /etc/issue && > /etc/issue.net
# 字符花
cd /etc/update-motd.d/
ls | grep -v 00 | xargs rm -rf
cat > /etc/update-motd.d/99-local << 'SCRIPT'
#!/bin/bash
cat << 'EOF'
(@) * (@) * (@)
: * (@) * (@) * .;
(@) * (@) * (@) * (@)
* ; * ; (@) * ; * :
;\ \ \ \| / / /;
\\ \ Y/ / /
`_\ |/ _' '
/ \\Y// \
( ,-}={-, )
\_//((\_/
//))(\
(/ ))
(/
EOF
SCRIPT
chmod +x 99-local
# 赋权 --> 返回目录
cd -
# ssh连接优化
cat >>/etc/ssh/sshd_config<<EOF
UseDNS no
# 相当于网络命令的-n选项,这个就是说不解析为主机名,直接成IP地址.
GSSAPIAuthentication no
# 关闭GSS认证.
EOF
systemctl restart ssh.service
# 调整时区
timedatectl set-timezone Asia/Shanghai
# 类似于SElinux
systemctl disable --now apparmor &> /dev/null
# 关闭防火墙
systemctl disable --now ufw &> /dev/null
# 安装Iptables持久化工具
# 默认有弹窗
# 防止 debconf 交互
export DEBIAN_FRONTEND=noninteractive
debconf-set-selections << EOF
iptables-persistent iptables-persistent/autosave_v4 boolean true
iptables-persistent iptables-persistent/autosave_v6 boolean true
EOF
# 安装(不会卡住)
apt-get install -y iptables-persistent
# 开机自启
# systemctl enable --now iptables.service
systemctl daemon-reload
# ssh允许root登录
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
echo 'root:1' | sudo chpasswd
systemctl restart ssh
# 文件描述符
# 分开软硬限制
cat >> /etc/security/limits.conf << 'EOF'
* soft nofile 65535
* hard nofile 65535
root soft nofile 65535
root hard nofile 65535
EOF
# Ubuntu 的 PAM 默认没有启用 pam_limits.so
echo "session required pam_limits.so" >> /etc/pam.d/common-session
echo "session required pam_limits.so" >> /etc/pam.d/common-session-noninteractive
# 手动添加
# Systemd 服务的限制 --> 有些服务不受 limits.conf 直接控制
echo "DefaultLimitNOFILE=65535" >> /etc/systemd/system.conf
echo "DefaultLimitNOFILE=65535" >> /etc/systemd/user.conf
reboot
# 重启系统生效

主机名 & IP#

Terminal window
root@ubuntu:~# vim change_host_ip.sh
#!/bin/bash
#author: jiuzhao
#desc: change ip and hostname
#version: v1.0
[ $# -ne 2 ] && {
echo "Usage: $0 <hostname> <new_ip>"
echo "Example: $0 web01 192.168.1.200"
exit 1
}
# 获取IP地址的最后一段
# 例如192.168.1.200获取200,因为两个网卡都要进行修改
ip=$(hostname -I | awk '{print $1}' | awk -F. '{print $NF}')
# $NF最后一列
# -F. 以.为分隔符
new_ip=$(echo $2 | awk -F. '{print $NF}')
# $2是我们刚才输入进去的IP地址
# 新主机名
new_hostname=$1
# 修改IP(假设使用ifcfg-eth0)
sed -i "s#10.0.0.$ip#10.0.0.$new_ip#g" /etc/netplan/50-cloud-init.yaml
sed -i "s#172.31.128.$ip#172.31.128.$new_ip#g" /etc/netplan/50-cloud-init.yaml
# 修改主机名
hostnamectl hostname $new_hostname
# ubuntu中可以不要set-hostname --> 优化为hostname
# 重启网络
netplan apply
echo "hostname changed to $new_hostname IP changed to $2 "
Tip
  • 以上两条严格意义来讲不算脚本
    • 都是用基础命名堆上去的
    • 连循环, 判断, 函数都没有
Terminal window
'模版机只需要这两个脚本即可'
root@ubuntu:/server/script# tree ./
Command 'tree' not found
root@ubuntu:/server/script# apt install tree
root@ubuntu:/server/script# tree
./
├── base_config.sh
└── change_host_ip.sh
1)赋权
root@ubuntu:/server/script# ls | xargs chmod +x
2)先运行系统优化脚本
root@ubuntu:/server/script# ./base_config.sh
resize2fs 1.47.0 (5-Feb-2023)
3)打快照
'这个以后就是我们的模板机'

image-20260516094323184
image-20260516094323184

创建链接克隆#

image-20260516104258358
image-20260516104258358

  • 脚本修改主机名和IP地址

image-20260516105445827
image-20260516105445827

  • 另外两台也是同理
    • 同时打下快照

image-20260516105951965
image-20260516105951965

Docker 环境安装#

install-docker.sh
#!/bin/bash
#================================
# 作者: 久棹
# 用途: 在 Linux 上自动安装/卸载 Docker 容器引擎(二进制方式)
# 适用系统: CentOS 7/8/9, Ubuntu 18.04+, Debian 10+ 等 systemd 系统
#================================
#================================
# 第一部分: 定义变量
# 把常用的路径、版本号、下载地址定义为变量,方便后续修改和维护
#================================
# --- Docker 相关配置 ---
# Docker 的版本号(要与 download 目录下的 tgz 文件名匹配)
DOCKER_VERSION=29.1.4
# Docker 压缩包的文件名
DOCKER_FILENAME=docker-${DOCKER_VERSION}.tgz
# Docker 二进制包在 docker.com 上的下载地址
DOCKER_URL=https://download.docker.com/linux/static/stable/x86_64/${DOCKER_FILENAME}
# --- Docker Compose 相关配置 ---
# Docker Compose 的版本号
DOCKER_COMPOSE_VERSION=2.29.2
# Docker Compose 在 GitHub 上的文件名(下载后的原始文件名)
DOCKER_COMPOSE_FILENAME=docker-compose-linux-x86_64
# Docker Compose 在 GitHub 上的下载地址
# 注意: GitHub 在国内可能访问不稳定,需要确保网络通畅(最好提前下载)
DOCKER_COMPOSE_URL=https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/${DOCKER_COMPOSE_FILENAME}
# --- 安装路径配置 ---
# Docker 二进制文件解压后的安装目录
# /usr/local 常用于"本地安装软件"的目录
# 解压后二进制文件位于 /usr/local/docker/ 下
DOCKER_BASEDIR=/usr/local
# systemd 服务文件的存放路径(systemd 从这里读取服务配置)
SYSTEMD_DIR=/usr/lib/systemd/system
# Docker 配置文件目录
DOCKER_CONFIG=/etc/docker
# --- 本地目录 ---
# 预下载文件的存放目录
DOWNLOAD=./download
# 加载操作系统的 ID 变量(如 centos、ubuntu),用于判断系统类型
. /etc/os-release
OS_VERSION=$ID
#================================
# 第二部分: 工具函数
#================================
#----------------------------------------------------
# 函数名: prepare
# 作用: 下载指定的文件(如果本地已经有了就跳过下载,节省时间)
# 参数:
# $1 - 要下载的文件名
# $2 - 文件的下载地址(URL)
# 使用示例: prepare "docker-26.1.4.tgz" "https://..."
#----------------------------------------------------
function prepare(){
if [ ! -f ${DOWNLOAD}/$1 ]; then
echo "正在下载 $1 ..."
wget -T 10 -t 3 "$2" -O ${DOWNLOAD}/$1
if [ $? -ne 0 ]; then
rm -f ${DOWNLOAD}/$1
tput setaf 1
echo "错误: 无法下载 $1 ,请检查网络连接!"
echo "下载地址: $2"
tput sgr0
exit 100
fi
echo "下载完成: $1"
else
echo "文件已存在,跳过下载: $1"
fi
}
#================================
# 第三部分: 安装函数
#================================
function InstallDocker(){
# --- 第 1 步: 检查是否以 root 用户运行 ---
if [ "$(id -u)" -ne 0 ]; then
tput setaf 1
echo "错误: 请使用 root 用户或 sudo 运行此脚本!"
echo "用法: sudo bash $0 install"
tput sgr0
exit 1
fi
echo "========================================="
echo " 开始安装 Docker v${DOCKER_VERSION}"
echo "========================================="
# --- 第 2 步: 根据操作系统安装依赖(wget) ---
if [ "$OS_VERSION" == "centos" ] || [ "$OS_VERSION" == "rhel" ]; then
rpm -qa | grep -q wget || yum -y install wget
echo "已安装 CentOS/RHEL 系统依赖"
elif [ "$OS_VERSION" == "ubuntu" ] || [ "$OS_VERSION" == "debian" ]; then
dpkg -l | grep -q wget || apt -y install wget
echo "已安装 Ubuntu/Debian 系统依赖"
fi
# --- 第 3 步: 下载 Docker 二进制包 ---
prepare ${DOCKER_FILENAME} ${DOCKER_URL}
# --- 第 4 步: 下载 Docker Compose ---
prepare ${DOCKER_COMPOSE_FILENAME} ${DOCKER_COMPOSE_URL}
# --- 第 5 步: 解压 Docker 到安装目录 ---
# Docker 官方静态包解压后会生成 docker/ 目录
echo "正在解压 Docker 二进制包 ..."
# 如果目录不存在就创建它
if [ ! -d ${DOCKER_BASEDIR}/docker ]; then
install -d ${DOCKER_BASEDIR}/docker
fi
tar xf ${DOWNLOAD}/${DOCKER_FILENAME} -C ${DOCKER_BASEDIR}
echo "Docker 二进制文件已安装到: ${DOCKER_BASEDIR}/docker/"
# --- 第 6 步: 安装 Docker Compose ---
# Docker Compose 是一个独立的二进制文件,用于编排多容器应用
# 把它放到 docker 的 bin 目录下,统一管理
# 复制的同时改了名
cp ${DOWNLOAD}/${DOCKER_COMPOSE_FILENAME} ${DOCKER_BASEDIR}/docker/docker-compose
chmod +x ${DOCKER_BASEDIR}/docker/docker-compose
echo "Docker Compose 已安装到: ${DOCKER_BASEDIR}/docker/docker-compose"
# --- 第 7 步: 创建软链接到 /usr/bin/ ---
# /usr/bin/ 在系统的 PATH 中,创建软链接后可以直接在终端使用命令
# -s: 创建符号链接(软链接)
# -f: 如果链接已存在则覆盖
echo "正在创建软链接 ..."
ln -sf ${DOCKER_BASEDIR}/docker/* /usr/bin/
echo "已创建软链接,现在可直接使用 docker、dockerd 等命令"
# --- 第 8 步: 安装命令自动补全 ---
# Docker CLI 内置 completion 子命令,可生成 bash/zsh 补全脚本
# bash-completion 包提供动态加载补全的框架
echo "正在配置命令自动补全 ..."
if [ "$OS_VERSION" == "centos" ] || [ "$OS_VERSION" == "rhel" ]; then
rpm -qa | grep -q bash-completion || yum -y install bash-completion
elif [ "$OS_VERSION" == "ubuntu" ] || [ "$OS_VERSION" == "debian" ]; then
dpkg -l | grep -q bash-completion || apt -y install bash-completion
fi
# 生成 docker 命令补全(bash)
docker completion bash > /usr/share/bash-completion/completions/docker 2>/dev/null
# 生成 docker-compose 命令补全(bash)
${DOCKER_BASEDIR}/docker/docker-compose completion bash > /usr/share/bash-completion/completions/docker-compose 2>/dev/null
# /etc/bash_completion.d/ 只兼容旧版语法,新版 docker 补全写入会报错,已移除
echo "已安装 Docker / Docker Compose 命令自动补全"
echo "提示: 重新登录或执行 'source /etc/profile.d/bash_completion.sh' 使其生效"
# --- 第 9 步: 配置镜像加速 ---
# /etc/docker/daemon.json 是 Docker 守护进程的配置文件
# registry-mirrors 配置国内镜像加速源,解决从 Docker Hub 拉取镜像慢的问题
mkdir -p ${DOCKER_CONFIG}
cat > ${DOCKER_CONFIG}/daemon.json <<EOF
{
"registry-mirrors": ["https://docker.1ms.run"]
}
EOF
echo "已配置镜像加速: ${DOCKER_CONFIG}/daemon.json"
# --- 第 10 步: 生成 systemd 服务文件 ---
cat > ${SYSTEMD_DIR}/docker.service <<EOF
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target firewalld.service
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/bin/dockerd
ExecReload=/bin/kill -s HUP \$MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
StartLimitBurst=3
StartLimitInterval=60s
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
Delegate=yes
KillMode=process
OOMScoreAdjust=-500
[Install]
WantedBy=multi-user.target
EOF
echo "systemd 服务文件已生成: ${SYSTEMD_DIR}/docker.service"
# --- 第 11 步: 启动 Docker 并设置开机自启 ---
systemctl daemon-reload
systemctl enable --now docker &> /dev/null
echo "Docker 服务已启动并设置为开机自启"
# ===== 新增:创建 Docker Compose 插件软链接(仅当不存在时) =====
mkdir -p /usr/libexec/docker/cli-plugins/
# 先判断软链接是否存在,已存在则跳过
if [ ! -L /usr/libexec/docker/cli-plugins/docker-compose ]; then
# 软链接不存在,再判断源文件是否存在,避免无效链接
if [ -f /usr/local/docker/docker-compose ]; then
# 源文件存在,且软链接不存在则,创建软链接
ln -s /usr/local/docker/docker-compose /usr/libexec/docker/cli-plugins/docker-compose
echo "软链接已创建:/usr/libexec/docker/cli-plugins/docker-compose -> /usr/local/docker/docker-compose"
else
echo "警告:源文件 /usr/local/docker/docker-compose 不存在,跳过软链接创建。"
fi
else
echo "软链接已存在,跳过。"
fi
# --- 第 12 步: 验证安装结果 ---
echo ""
echo "========================================="
tput setaf 2
echo " 安装完成!版本信息如下:"
tput sgr0
echo "========================================="
echo ""
docker version
echo ""
docker-compose version
echo ""
tput setaf 3
echo "安装成功!欢迎使用久棹 Docker 二进制安装脚本~"
tput sgr0
}
#==================
# 第四部分: 卸载函数
#==================
function UninstallDocker(){
# 检查 root 权限
if [ "$(id -u)" -ne 0 ]; then
tput setaf 1
echo "错误: 请使用 root 用户或 sudo 运行此脚本!"
tput sgr0
exit 1
fi
echo "========================================="
echo " 开始卸载 Docker"
echo "========================================="
# --- 第 1 步: 停止服务并禁用开机自启 ---
# disable --now: 同时完成 disable(取消开机自启)和 stop(停止服务)
systemctl disable --now docker &> /dev/null
echo "已停止 Docker 服务"
# --- 第 2 步: 删除 systemd 服务文件 ---
rm -f ${SYSTEMD_DIR}/docker.service
echo "已删除 systemd 服务文件"
# --- 第 3 步: 删除 Docker 程序文件 ---
# 递归删除整个 docker 二进制目录
rm -rf ${DOCKER_BASEDIR}/docker
echo "已删除 Docker 程序文件"
# --- 第 4 步: 删除 Docker 配置和数据目录 ---
# /var/lib/docker - Docker 的镜像、容器、卷等数据存储目录
# /var/lib/containerd - containerd 的持久化数据目录
rm -rf /var/lib/docker /var/lib/containerd
rm -rf ${DOCKER_CONFIG}
echo "已删除 Docker 数据和配置目录"
# --- 第 5 步: 删除软链接 ---
# 清理之前在 /usr/bin/ 下创建的软链接
# 这些是 Docker 静态包解压后包含的全部二进制文件
rm -f /usr/bin/{docker,dockerd,docker-init,docker-proxy,containerd,containerd-shim-runc-v2,ctr,runc,docker-compose}
echo "已删除软链接"
# --- 第 6 步: 删除命令补全文件 ---
rm -f /usr/share/bash-completion/completions/docker
rm -f /usr/share/bash-completion/completions/docker-compose
rm -f /etc/bash_completion.d/docker
rm -f /etc/bash_completion.d/docker-compose
echo "已删除命令自动补全文件"
# --- 第 7 步: 重新加载 systemd 配置 ---
systemctl daemon-reload
tput setaf 5
echo "卸载成功!欢迎再次使用久棹 Docker 二进制安装脚本~"
tput sgr0
}
#=============================
# 第五部分: 主函数 —— 脚本的入口
#=============================
function main(){
# $1 是运行脚本时传入的第一个参数
case $1 in
install|i)
InstallDocker
;;
remove|r)
UninstallDocker
;;
*)
echo "用法: $0 {install|i|remove|r}"
echo ""
echo " 示例:"
echo " 安装: $0 install"
echo " 卸载: $0 remove"
echo " 简写: $0 i (安装)"
echo " 简写: $0 r (卸载)"
;;
esac
}
# 调用 main 函数,把命令行的第一个参数传进去
main $1
Terminal window
1)上传脚本和文件
root@SH-master tmp# rz
....
root@SH-master tmp# ls
autoinstall-docker.zip
2)解压
root@SH-master tmp# unzip autoinstall-docker.zip -d /root/
root@SH-master tmp# cd /root
root@SH-master ~# tree ./
./
├── download
│   ├── docker-29.1.4.tgz
│   └── docker-compose-linux-x86_64
└── install-docker.sh
3)赋权安装
root@SH-master ~# chmod +x install-docker.sh
root@SH-master ~# ./install-docker.sh
用法: ./install-docker.sh {install|i|remove|r}
示例:
安装: ./install-docker.sh install
卸载: ./install-docker.sh remove
简写: ./install-docker.sh i (安装)
简写: ./install-docker.sh r (卸载)
root@SH-master ~# ./install-docker.sh i
=========================================
开始安装 Docker v29.1.4
=========================================
已安装 Ubuntu/Debian 系统依赖
文件已存在,跳过下载: docker-29.1.4.tgz
文件已存在,跳过下载: docker-compose-linux-x86_64
正在解压 Docker 二进制包 ...
Docker 二进制文件已安装到: /usr/local/docker/
Docker Compose 已安装到: /usr/local/docker/docker-compose
正在创建软链接 ...
已创建软链接,现在可直接使用 docker、dockerd 等命令
正在配置命令自动补全 ...
已安装 Docker / Docker Compose 命令自动补全
提示: 重新登录或执行 'source /etc/profile.d/bash_completion.sh' 使其生效
已配置镜像加速: /etc/docker/daemon.json
systemd 服务文件已生成: /usr/lib/systemd/system/docker.service
Docker 服务已启动并设置为开机自启
=========================================
安装完成!版本信息如下:
=========================================
Client:
Version: 29.1.4
API version: 1.52
Go version: go1.25.5
Git commit: 0e6fee6
Built: Thu Jan 8 19:55:48 2026
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 29.1.4
API version: 1.52 (minimum version 1.44)
Go version: go1.25.5
Git commit: 08440b6e
Built: Thu Jan 8 19:58:07 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.2.0
GitCommit: 1c4457e00facac03ce1d75f7b6777a7a851e5c41
runc:
Version: 1.3.4
GitCommit: v1.3.4-0-gd6d73eb
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Docker Compose version v2.29.2
安装成功!欢迎使用久棹 Docker 二进制安装脚本~
4)拷贝安装
root@SH-master ~# ls
download install-docker.sh
root@SH-master ~# tar zcf hh.tar.gz download/ install-docker.sh
root@SH-master ~# ls
download hh.tar.gz install-docker.sh
root@SH-master ~# scp hh.tar.gz test@10.0.0.91:/home/test/
test@10.0.0.91's password: '
hh.tar.gz 100% 96MB 174.2MB/s 00:00
root@SH-master ~# scp hh.tar.gz test@10.0.0.92:/home/test/
test@10.0.0.92's password: '
hh.tar.gz 100% 96MB 174.2MB/s 00:00
root@SH-slave01 ~# ls /home/test/
hh.tar.gz
root@SH-slave01 ~# cd /home/test/
root@SH-slave01 test# tar xf hh.tar.gz
root@SH-slave01 test# tree ./
./
├── download
│   ├── docker-29.1.4.tgz
│   └── docker-compose-linux-x86_64
├── hh.tar.gz
└── install-docker.sh <-- 💚绿色拥有执行权限
📌 tar包会保留执行权限
root@SH-slave01 test# ./install-docker.sh i
'另外一台同理'

Swarm集群初始化#

Terminal window
1)hosts解析
root@SH-master ~# cat >> /etc/hosts <<EOF
10.0.0.90 SH-master harbor.shihao.com
10.0.0.91 SH-slave01
10.0.0.92 SH-slave02
10.0.0.90 java.shihao.com py.shihao.com
EOF
'harbor.shihao.com' --> 为后面域名解析做准备
'java.shihao.com py.shihao.com' --> 为后面两个应用做准备
root@SH-master ~# ping -W2 -c2 SH-master
PING SH-master (10.0.0.90) 56(84) bytes of data.
64 bytes from SH-master (10.0.0.90): icmp_seq=1 ttl=64 time=0.027 ms
64 bytes from SH-master (10.0.0.90): icmp_seq=2 ttl=64 time=0.034 ms
--- SH-master ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1046ms
rtt min/avg/max/mdev = 0.027/0.030/0.034/0.003 ms
root@SH-master ~# ping -W2 -c2 SH-slave01
PING SH-slave01 (10.0.0.91) 56(84) bytes of data.
64 bytes from SH-slave01 (10.0.0.91): icmp_seq=1 ttl=64 time=0.376 ms
64 bytes from SH-slave01 (10.0.0.91): icmp_seq=2 ttl=64 time=0.225 ms
--- SH-slave01 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1023ms
rtt min/avg/max/mdev = 0.225/0.300/0.376/0.075 ms
root@SH-master ~# ping -W2 -c2 SH-slave02
PING SH-slave02 (10.0.0.92) 56(84) bytes of data.
64 bytes from SH-slave02 (10.0.0.92): icmp_seq=1 ttl=64 time=0.349 ms
64 bytes from SH-slave02 (10.0.0.92): icmp_seq=2 ttl=64 time=0.464 ms
--- SH-slave02 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1008ms
rtt min/avg/max/mdev = 0.349/0.406/0.464/0.057 ms
'都是可以ping通的'
# 另外两台也做类似的操作
2)集群初始化
root@SH-master ~# docker swarm init --advertise-addr 10.0.0.90:2377
Swarm initialized: current node (qhnv1am8kzlcmy14ut8wfljhe) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-4e1jktyl6rivm40dea50sbkvjc74bscfzxhh7906k3io941i5s-0oollg7t9xuwsrnqxl5jdikja 10.0.0.90:2377
root@SH-slave01 test# docker swarm join --token SWMTKN-1-4e1jktyl6rivm40dea50sbkvjc74bscfzxhh7906k3io941i5s-0oollg7t9xuwsrnqxl5jdikja 10.0.0.90:2377
This node joined a swarm as a worker.
root@SH-slave02 test# docker swarm join --token SWMTKN-1-4e1jktyl6rivm40dea50sbkvjc74bscfzxhh7906k3io941i5s-0oollg7t9xuwsrnqxl5jdikja 10.0.0.90:2377
This node joined a swarm as a worker.
'看到 "joined a swarm as a worker" 说明加入成功'
3)Manager 节点验证集群状态
root@SH-master ~# docker node ls
ID HOSTNAME STATUS STATUS VERSION
qhnxxx * SH-master Ready Active "Leader" 29.1.4
5uvxxx SH-slave01 Ready Active 29.1.4
x85xxx SH-slave02 Ready Active 29.1.4
'* 号表示当前所在节点,Leader 表示它是集群主管'

📌 Swarm 集群就绪:1 Manager + 2 Worker,引擎版本 29.1.4

phase1-swarm-init.sh#

#!/bin/sh
# ============================================
# 第一阶段:Docker 安装 + Swarm 集群初始化
# 作者: 久棹
# 前置: 三节点已手动执行 base_config.sh + change_host_ip.sh
# 用法: 在 master 节点以 root 执行
# dash phase1-swarm-init.sh
# ============================================
# ---------- 颜色定义 ----------
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
CYAN='\033[36m'
BOLD='\033[1m'
RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
warn() { printf '%b\n' "${YELLOW}[WARN]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
# ---------- 节点信息 ----------
MASTER_IP="10.0.0.90"
SLAVE01_IP="10.0.0.91"
SLAVE02_IP="10.0.0.92"
SSH_USER="test"
SSH_PASS="1"
SCRIPT_DIR="/server/scripts"
# SSH 并 sudo 执行命令 (TERM=dumb 抑制 tput 报错)
ssh_sudo() {
local ip="$1"; local cmd="$2"
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; echo ${SSH_PASS} | sudo -S sh -c '${cmd}'"
}
# ---------- 1. 安装 Docker ----------
step "1.1 安装 Docker Engine (三节点)"
# 安装函数:上传 + 安装 + 验证
install_docker_on() {
local ip="$1"
local label="$2"
local silent="$3"
info "安装 Docker 到 ${label} (${ip}) ..."
# 上传安装包
sshpass -p "$SSH_PASS" scp -o StrictHostKeyChecking=no -r \
${SCRIPT_DIR}/autoinstall-docker ${SSH_USER}@${ip}:/tmp/ > /dev/null 2>&1
# 执行安装
if [ "$silent" = "yes" ]; then
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; cd /tmp/autoinstall-docker && echo ${SSH_PASS} | sudo -S bash install-docker.sh i" > /dev/null 2>&1
else
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; cd /tmp/autoinstall-docker && echo ${SSH_PASS} | sudo -S bash install-docker.sh i"
fi
# 用 docker version 验证安装是否成功(不受 tput 干扰)
sleep 2
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"echo ${SSH_PASS} | sudo -S docker --version" > /dev/null 2>&1
if [ $? -eq 0 ]; then
ok "${label} Docker 安装完成"
else
err "${label} Docker 安装失败"
fi
}
install_docker_on "$MASTER_IP" "master" "no"
install_docker_on "$SLAVE01_IP" "slave01" "yes"
install_docker_on "$SLAVE02_IP" "slave02" "yes"
# ---------- 2. 配置 hosts 解析 ----------
step "1.2 配置 /etc/hosts"
for ip in $MASTER_IP $SLAVE01_IP $SLAVE02_IP; do
ssh_sudo "$ip" '
grep -q "SH-master" /etc/hosts || {
echo "10.0.0.90 SH-master harbor.shihao.com java.shihao.com py.shihao.com" >> /etc/hosts
echo "10.0.0.91 SH-slave01" >> /etc/hosts
echo "10.0.0.92 SH-slave02" >> /etc/hosts
}
'
if [ $? -eq 0 ]; then
ok "$ip hosts 配置完成"
fi
done
# ---------- 3. Swarm 集群初始化 ----------
step "1.3 Swarm 集群初始化"
info "在 master 上初始化 Swarm ..."
INIT_OUT=$(ssh_sudo "$MASTER_IP" "docker swarm init --advertise-addr ${MASTER_IP}:2377 2>&1")
TOKEN=$(echo "$INIT_OUT" | grep -oP 'SWMTKN-\S+' | head -1)
# 如果已初始化,直接用 join-token
if [ -z "$TOKEN" ]; then
TOKEN=$(ssh_sudo "$MASTER_IP" "docker swarm join-token -q worker 2>&1")
fi
JOIN_CMD="docker swarm join --token ${TOKEN} ${MASTER_IP}:2377"
SHORT_TOKEN=$(echo "$TOKEN" | cut -c1-16)
ok "Swarm 初始化完成,Token: ${SHORT_TOKEN}..."
# ---------- 4. Worker 加入集群 ----------
step "1.4 Worker 节点加入集群"
for ip in $SLAVE01_IP $SLAVE02_IP; do
info "Worker $ip 加入集群 ..."
ssh_sudo "$ip" "$JOIN_CMD"
if [ $? -eq 0 ]; then
ok "$ip 加入成功"
else
err "$ip 加入失败"
fi
done
# ---------- 5. 验证 ----------
step "1.5 集群状态验证"
printf '\n'
ssh_sudo "$MASTER_IP" "docker node ls"
printf '\n'
ALL_READY=$(ssh_sudo "$MASTER_IP" "docker node ls --format '{{.Status}}' | grep -c 'Ready'")
if [ "$ALL_READY" = "3" ]; then
ok "Swarm 集群就绪:1 Manager + 2 Worker,3/3 Ready"
else
warn "部分节点状态异常,请检查"
fi
printf '\n'
info "第一阶段完成!集群 IP:${BOLD}${MASTER_IP}:2377${RESET}"
printf '\n'

第二阶段:Harbor 私有镜像仓库#

🧱 为什么要自建 私有镜像仓库?

  • Swarm 集群中有多个节点,如果在 Manager 上构建了镜像,Worker 节点本地并没有该镜像

  • 把镜像推送到私有镜像仓库,Worker 就可以直接 docker pull 拉取——一处构建,集群共享


为什么选择 Harbor❓️

Harbor 是 VMware 公司开源的==企业级镜像仓库==,主要特点如下:

  • 提供安全、可靠的私有镜像存储和分发
  • 底层使用 Docker Compose 来管理 Harbor 服务
  • 提供图形化管理界面(Web UI)

image-20260516131603539
image-20260516131603539

Terminal window
harbor:
🦄 基于官方的"docker registry"进行二次开发
🦄 是一个适合企业级使用的镜像仓库
🔖第三方仓库: --> 轩辕镜像站(docker.xuanyuan.run)
'我们的镜像全部都是从👆拉取的'
1)第三方镜像仓库配置
root@SH-master ~# echo 镜像密码 | docker login -u 镜像账户 --password-stdin docker.xuanyuan.run
https://docs.docker.com/go/credential-store/
"Login Succeeded"
2)上传解压
root@SH-master ~# cd /tmp
root@SH-master tmp# rz
......
root@SH-master tmp# ls harbor-offline-installer-v2.14.3.tgz
harbor-offline-installer-v2.14.3.tgz
root@SH-master tmp# tar xf harbor-offline-installer-v2.14.3.tgz -C /usr/local/
3)修改配置文件
root@SH-master tmp# cd /usr/local/harbor/
root@SH-master harbor# ls
common.sh harbor.v2.14.3.tar.gz harbor.yml.tmpl install.sh LICENSE prepare
root@SH-master harbor# cp harbor.yml{.tmpl,}
# 把备份文件复制出来
"把后缀tmpl去掉"
root@SH-master harbor# ls
common.sh harbor.v2.14.3.tar.gz harbor.yml harbor.yml.tmpl install.sh LICENSE prepare
root@SH-master harbor# vim harbor.yml
hostname: 10.0.0.90
http:
port: 80
# 后面再启用HTTPS
harbor_admin_password: passwd
# 修改登录密码
database:
password: root123
max_idle_conns: 100
max_open_conns: 900
conn_max_lifetime: 5m
conn_max_idle_time: 0
data_volume: /var/lib/harbor
# 更改数据卷目录
# 如果卸载harbor的话,要把这个目录手动删除掉
trivy:
ignore_unfixed: false
skip_update: false
skip_java_db_update: false
db_repository: ghcr.io/aquasecurity/trivy-db
java_db_repository: ghcr.io/aquasecurity/trivy-java-db
offline_scan: false
security_check: vuln
insecure: false
timeout: 5m0s
jobservice:
max_job_workers: 10
max_job_duration_hours: 24
job_loggers:
- STD_OUTPUT
- FILE
logger_sweeper_duration: 1
notification:
webhook_job_max_retry: 3
webhook_job_http_client_timeout: 3
log:
level: info
local:
rotate_count: 50
rotate_size: 200M
location: /var/log/harbor
_version: 2.15.0
proxy:
http_proxy:
https_proxy:
no_proxy:
components:
- core
- jobservice
- trivy
upload_purging:
enabled: true
age: 168h
interval: 24h
dryrun: false
cache:
enabled: false
expire_hours: 24
Terminal window
4)安装harbor服务
root@SH-master harbor# ./install.sh
[Step 0]: checking if docker is installed ...
Note: docker version: 29.1.4
[Step 1]: checking docker-compose is installed ...
Note: docker-compose version: 2.29.2
[Step 2]: loading Harbor images ...
........
[Step 5]: starting Harbor ...
[+] Running 10/10
Network harbor_harbor Created
Container harbor-log Started
Container harbor-db Started
Container harbor-portal Started
Container registry Started
Container registryctl Started
Container redis Started
Container harbor-core Started
Container nginx Started
Container harbor-jobservice Started
----Harbor has been installed and started successfully.----
5)测试验证
root@SH-master harbor# docker-compose ls
NAME STATUS CONFIG FILES
harbor running(9) /usr/local/harbor/docker-compose.yml
===========================
http://10.0.0.90/
✅️用户名: admin
✅️密码: passwd

image-20260516132914501
image-20260516132914501

新建项目#

image-20260516133048690
image-20260516133048690

image-20260516133055543
image-20260516133055543

Terminal window
1)拉镜像&打标签
root@SH-master harbor# docker pull docker.xuanyuan.run/mysql:8.0.36
8.0.36: Pulling from mysql
Status: Downloaded newer image for docker.xuanyuan.run/mysql:8.0.36
root@SH-master harbor# docker tag docker.xuanyuan.run/mysql:8.0.36 10.0.0.90/kpyun/mysql:8.0.36
root@SH-master harbor# docker images
10.0.0.90/kpyun/mysql:8.0.36 a53272402242 839MB
2)推送尝试
root@SH-master harbor# docker push 10.0.0.90/kpyun/mysql:8.0.36
The push refers to repository [10.0.0.90/kpyun/mysql]
dial tcp 10.0.0.90:443: connect: connection refused
'没有配置证书,所以有问题'

HTTPS证书#

Terminal window
root@SH-master ~# mkdir mycat && cd mycat
root@SH-master mycat# mkdir private certs
root@SH-master mycat# tree ./
./
├── certs
└── private
# 创建专门的目录
✅️private: # 存放私钥
✅️certs: # 来存放各种证书
root@SH-master mycat# chmod 700 private
1)生成“根证书” (Root CA)
root@SH-master mycat# openssl genrsa -out private/rootCA.key 2048
# 生成根私钥
root@SH-master mycat# openssl req -x509 -new -nodes -key private/rootCA.key -sha256 -days 3650 \
-out certs/rootCA.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyRootCA/CN=MyRootCA"
2)生成“中间证书”
root@SH-master mycat# openssl genrsa -out private/intermediate.key 2048
# 生成中间人的私钥
root@SH-master mycat# openssl req -new -key private/intermediate.key -out intermediate.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyIntermediate/CN=MyIntermediate"
# 生成中间人的申请文件 (CSR)
root@SH-master mycat# openssl x509 -req -in intermediate.csr -CA certs/rootCA.crt -CAkey private/rootCA.key -CAcreateserial \
-out certs/intermediate.crt -days 1825 -sha256 \
-extfile <(echo -e "basicConstraints=CA:TRUE\nkeyUsage=critical,keyCertSign,cRLSign")
# 用“根证书”给“中间人”盖章
Certificate request self-signature ok
subject=C = CN, ST = Beijing, L = Beijing, O = MyIntermediate, CN = MyIntermediate
3)生成“服务器证书”
# 创建扩展配置文件
# 为了支持域名,必须用配置文件,不能直接写在命令里
root@SH-master mycat# cat > server01.ext <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = harbor.shihao.com
IP.1 = 10.0.0.90
IP.2 = 127.0.0.1
EOF
root@SH-master mycat# openssl genrsa -out private/server.key 2048
# 生成服务器私钥
root@SH-master mycat# openssl req -new -key private/server.key -out server.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyServer/CN=harbor.shihao.com"
# 生成请求文件
root@SH-master mycat# openssl x509 -req -in server.csr -CA certs/intermediate.crt -CAkey private/intermediate.key -CAcreateserial \
-out certs/server.crt -days 365 -sha256 -extfile server01.ext
# 用“中间人”签发服务器证书
Certificate request self-signature ok
subject=C = CN, ST = Beijing, L = Beijing, O = MyServer, CN = harbor.shihao.com✅️
4)合并证书链
# Nginx 需要看到完整的链条(服务器证书 + 中间证书)
# 否则浏览器不知道中间人是谁
root@SH-master mycat# cat certs/server.crt certs/intermediate.crt > certs/fullchain01.crt
# 合并成一个 fullchain01.crt
5)生成另外两个应用的证书
root@SH-master mycat# cat > server02.ext <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = java.shihao.com
DNS.2 = py.shihao.com
IP.1 = 10.0.0.90
IP.2 = 10.0.0.91
IP.3 = 10.0.0.92
EOF
# 生成 Nginx 用的服务器密钥
'两个密钥各管各的'
root@SH-master mycat# openssl genrsa -out private/nginx-server.key 2048
# 生成 CSR(CN 用主域名 java.shihao.com)
root@SH-master mycat# openssl req -new -key private/nginx-server.key -out nginx-server.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyServer/CN=java.shihao.com"
# 用中间证书签发
root@SH-master mycat# openssl x509 -req -in nginx-server.csr -CA certs/intermediate.crt -CAkey private/intermediate.key -CAcreateserial \
-out certs/nginx-server.crt -days 365 -sha256 -extfile server02.ext
"非常成功"
Certificate request self-signature ok ✅️
subject=C = CN, ST = Beijing, L = Beijing, O = MyServer, CN = java.shihao.com ✅️
# 合并完整证书链
root@SH-master mycat# cat certs/nginx-server.crt certs/intermediate.crt > certs/fullchain02.crt
# 最终文件结构
root@SH-master mycat# tree ./
./
├── certs
├── fullchain01.crt # Harbor 完整链(server + intermediate)
├── fullchain02.crt # Nginx 完整链(nginx-server + intermediate)
├── intermediate.crt # 中间证书
├── intermediate.srl
├── nginx-server.crt # Nginx 服务器证书
├── rootCA.crt # 根证书
├── rootCA.srl
└── server.crt # Harbor 服务器证书
├── intermediate.csr
├── nginx-server.csr
├── private
├── intermediate.key
├── nginx-server.key
├── rootCA.key
└── server.key
├── server01.ext
├── server02.ext
└── server.csr

安装 CA 根证书到系统信任链#

Terminal window
'三台服务器都要做:把根证书导入系统信任'
1)Ubuntu 添加自定义 CA
root@SH-master mycat# cp certs/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crt
root@SH-master mycat# update-ca-certificates
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.
2)分发到 slave 节点
root@SH-master mycat# scp certs/rootCA.crt test@10.0.0.91:/tmp/
root@SH-master mycat# scp certs/rootCA.crt test@10.0.0.92:/tmp/
'在 SH-slave01 和 SH-slave02 上分别执行'
root@SH-slave01 ~# cp /tmp/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crt
root@SH-slave01 ~# update-ca-certificates
=========================================
root@SH-slave02 ~# cp /tmp/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crt
root@SH-slave02 ~# update-ca-certificates
3)验证证书信任 (任意节点)
root@SH-master mycat# cat certs/rootCA.crt certs/intermediate.crt > certs/ca-bundle.crt
'合并 CA 信任链' --> root + intermediate 拼在一起当 CA bundle
root@SH-master mycat# openssl verify -CAfile certs/ca-bundle.crt certs/server.crt
certs/server.crt: OK
root@SH-master mycat# openssl verify -CAfile certs/ca-bundle.crt certs/nginx-server.crt
certs/nginx-server.crt: OK

配置 Harbor 启用 HTTPS#

Terminal window
1)编辑 Harbor 配置,启用 HTTPS
root@SH-master mycat# cd /usr/local/harbor/
root@SH-master harbor# vim harbor.yml
hostname: harbor.shihao.com # 改为域名
# http 段注释掉(或删除)
# http:
# port: 80
https: # 新增 HTTPS 段
port: 443 # Swarm Nginx 的 443 冲突 <-- 后面应用调接口
certificate: /root/mycat/certs/fullchain01.crt
private_key: /root/mycat/private/server.key
harbor_admin_password: passwd
database:
password: root123
# ...其余配置不变
Terminal window
2)重新部署 Harbor(install.sh 会自动重建容器)
root@SH-master harbor# ./install.sh
# 输出省略...
----Harbor has been installed and started successfully.----
3)验证 HTTPS
root@SH-master harbor# docker-compose ps
NAME ... PORTS
nginx ... 0.0.0.0:443->8443/tcp
# 443 端口已监听
root@SH-master harbor# grep harbor /etc/hosts
10.0.0.90 SH-master harbor.shihao.com
root@SH-master harbor# curl -sI https://harbor.shihao.com/
HTTP/1.1 200 OK 返回正常,证书链路可信
Server: nginx

配置所有节点信任 Harbor#

Terminal window
1)编辑 daemon.json
'三台都要做:Docker daemon 信任自签 Harbor'
root@SH-master ~# vim /etc/docker/daemon.json
{
"registry-mirrors": ["https://docker.1ms.run"],
"insecure-registries": ["harbor.shihao.com"]
}
Terminal window
2)MASTER需要重启
root@SH-master harbor# docker-compose down -t 0
'停止compose集群'
root@SH-master ~# systemctl restart docker
root@SH-master harbor# docker-compose up -d
# 重新启动
3)slaver也要重启
'得是修改了daemon.json文件后'
root@SH-slave01 ~# systemctl restart docker
root@SH-slave02 ~# systemctl restart docker
4)推送测试
root@SH-master harbor# docker login -u admin -p passwd harbor.shihao.com
Login Succeeded ✅️
root@SH-master ~# docker tag docker.xuanyuan.run/mysql:8.0.36 harbor.shihao.com/kpyun/mysql:8.0.36
root@SH-master ~# docker push harbor.shihao.com/kpyun/mysql:8.0.36
The push refers to repository [harbor.shihao.com/kpyun/mysql]
d3f5c7b8a9e1: Pushed
...
8.0.36: digest: sha256:xxxx size: xxx
'推送成功✅'

image-20260516160923288
image-20260516160923288

Windows物理机#

Terminal window
1)Windows --> hosts 解析
'你自己的 Windows 浏览器端也配一下 hosts'
10.0.0.90 harbor.shihao.com
10.0.0.90 java.shihao.com
10.0.0.90 py.shihao.com
'客户端 hosts 三个域名都指向 10.0.0.90(master 节点)'
# nginx 通过 placement 约束固定在 master + mode:host 端口直接绑定
2)物理机导入根证书
# 后续用浏览器访问 --> 不会有证书警告
root@SH-master harbor# cd /root/mycat/certs
root@SH-master certs# sz rootCA.crt

image-20260516161453102
image-20260516161453102

image-20260516161527265
image-20260516161527265

image-20260516161635347
image-20260516161635347

image-20260516161722844
image-20260516161722844

拉取镜像测试#

Terminal window
1)slave01
root@SH-slave01 test# docker pull harbor.shihao.com/kpyun/mysql:8.0.36
8.0.36: Pulling from kpyun/mysql
cf55ff1c80af: Pull complete
c38d8660e1fa: Pull complete
......
Digest: sha256:65cxxx....
Status: Downloaded newer image for harbor.shihao.com/kpyun/mysql:8.0.36
root@SH-slave01 test# docker images
harbor.shihao.com/kpyun/mysql:8.0.36 65ce08897519 824MB
2)slave02
root@SH-slave02 test# docker pull harbor.shihao.com/kpyun/mysql:8.0.36
Status: Downloaded newer image for harbor.shihao.com/kpyun/mysql:8.0.36
root@SH-slave02 test# docker images
harbor.shihao.com/kpyun/mysql:8.0.36 65ce08897519 824MB

phase2-harbor.sh#

#!/bin/sh
# ============================================
# 第二阶段:Harbor 私有镜像仓库 (HTTPS 443)
# 作者: 久棹
# 用法: 在 master 节点以 root 执行
# dash phase2-harbor.sh
# ============================================
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
CYAN='\033[36m'
BOLD='\033[1m'
RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
MASTER_IP="10.0.0.90"
SLAVE01_IP="10.0.0.91"
SLAVE02_IP="10.0.0.92"
SSH_USER="test"
SSH_PASS="1"
HARBOR_PASS="passwd"
# SSH 远程执行(仅用于 slave 节点,master 本地执行)
ssh_sudo() {
local ip="$1"; local cmd="$2"
local escaped
escaped=$(printf '%s' "$cmd" | sed 's/"/\\"/g')
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; echo ${SSH_PASS} | sudo -S bash -c \"${escaped}\""
}
# ---------- 1. 解压 Harbor ----------
step "2.1 解压 Harbor 离线包"
if [ ! -f /tmp/harbor-offline-installer-v2.14.3.tgz ]; then
err "Harbor 离线包不存在!请先上传 harbor-offline-installer-v2.14.3.tgz 到 /tmp/"
exit 1
fi
tar xf /tmp/harbor-offline-installer-v2.14.3.tgz -C /usr/local/
ok "Harbor 包解压到 /usr/local/harbor/"
# ---------- 2. 生成 CA 证书链 ----------
step "2.2 自建 CA 证书链"
mkdir -p /root/mycat/certs
mkdir -p /root/mycat/private
chmod 700 /root/mycat/private
# 根证书
openssl genrsa -out /root/mycat/private/rootCA.key 2048 2>/dev/null
openssl req -x509 -new -nodes -key /root/mycat/private/rootCA.key -sha256 -days 3650 \
-out /root/mycat/certs/rootCA.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyRootCA/CN=MyRootCA" 2>/dev/null
# 中间证书
openssl genrsa -out /root/mycat/private/intermediate.key 2048 2>/dev/null
openssl req -new -key /root/mycat/private/intermediate.key -out /tmp/intermediate.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyIntermediate/CN=MyIntermediate" 2>/dev/null
cat > /tmp/intermediate.ext << EOF
basicConstraints=CA:TRUE
keyUsage=critical,keyCertSign,cRLSign
EOF
openssl x509 -req -in /tmp/intermediate.csr -CA /root/mycat/certs/rootCA.crt \
-CAkey /root/mycat/private/rootCA.key -CAcreateserial \
-out /root/mycat/certs/intermediate.crt -days 1825 -sha256 \
-extfile /tmp/intermediate.ext 2>/dev/null
# Harbor 服务器证书
cat > /tmp/server01.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = harbor.shihao.com
IP.1 = 10.0.0.90
IP.2 = 127.0.0.1
EOF
openssl genrsa -out /root/mycat/private/server.key 2048 2>/dev/null
openssl req -new -key /root/mycat/private/server.key -out /tmp/server.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyServer/CN=harbor.shihao.com" 2>/dev/null
openssl x509 -req -in /tmp/server.csr -CA /root/mycat/certs/intermediate.crt \
-CAkey /root/mycat/private/intermediate.key -CAcreateserial \
-out /root/mycat/certs/server.crt -days 365 -sha256 -extfile /tmp/server01.ext 2>/dev/null
cat /root/mycat/certs/server.crt /root/mycat/certs/intermediate.crt > /root/mycat/certs/fullchain01.crt
# Nginx 应用证书
cat > /tmp/server02.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = java.shihao.com
DNS.2 = py.shihao.com
IP.1 = 10.0.0.90
IP.2 = 10.0.0.91
IP.3 = 10.0.0.92
EOF
openssl genrsa -out /root/mycat/private/nginx-server.key 2048 2>/dev/null
openssl req -new -key /root/mycat/private/nginx-server.key -out /tmp/nginx-server.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MyServer/CN=java.shihao.com" 2>/dev/null
openssl x509 -req -in /tmp/nginx-server.csr -CA /root/mycat/certs/intermediate.crt \
-CAkey /root/mycat/private/intermediate.key -CAcreateserial \
-out /root/mycat/certs/nginx-server.crt -days 365 -sha256 -extfile /tmp/server02.ext 2>/dev/null
cat /root/mycat/certs/nginx-server.crt /root/mycat/certs/intermediate.crt > /root/mycat/certs/fullchain02.crt
# 验证证书
cat /root/mycat/certs/rootCA.crt /root/mycat/certs/intermediate.crt > /tmp/ca-bundle.crt
openssl verify -CAfile /tmp/ca-bundle.crt /root/mycat/certs/server.crt 2>/dev/null
openssl verify -CAfile /tmp/ca-bundle.crt /root/mycat/certs/nginx-server.crt 2>/dev/null
ok "CA 证书链生成完成"
# ---------- 3. 配置 Harbor HTTPS ----------
step "2.3 配置 Harbor HTTPS"
cd /usr/local/harbor
# 从模板复制
cp harbor.yml.tmpl harbor.yml
# 修改主机名为域名
sed -i "s/hostname: .*/hostname: harbor.shihao.com/" harbor.yml
# 注释掉 http 段
sed -i 's/^http:/#http:/' harbor.yml
sed -i 's/^ port: 80/# port: 80/' harbor.yml
# 启用 https 段并配置证书路径
sed -i 's/^#https:/https:/' harbor.yml
sed -i 's/^# port: 443/ port: 443/' harbor.yml
sed -i 's|^#\{0,1\} certificate:.*| certificate: /root/mycat/certs/fullchain01.crt|' harbor.yml
sed -i 's|^#\{0,1\} private_key:.*| private_key: /root/mycat/private/server.key|' harbor.yml
# 修改密码 + 数据卷
sed -i "s/harbor_admin_password: .*/harbor_admin_password: passwd/" harbor.yml
sed -i "s|data_volume: .*|data_volume: /var/lib/harbor|" harbor.yml
# 直接安装(install.sh 内置镜像导入 + prepare + compose up)
./install.sh
ok "Harbor HTTPS 安装完成 (443)"
# ---------- 4. 三节点导入根证书 ----------
step "2.4 三节点导入根证书 + Docker 信任 Harbor"
# Master 自己导入
cp /root/mycat/certs/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crt
update-ca-certificates --fresh 2>/dev/null
ok "Master 根证书已导入"
# 分发到 slave 节点
for ip in $SLAVE01_IP $SLAVE02_IP; do
info "分发根证书到 $ip ..."
sshpass -p "$SSH_PASS" scp -o StrictHostKeyChecking=no \
/root/mycat/certs/rootCA.crt ${SSH_USER}@${ip}:/tmp/rootCA.crt
ssh_sudo "$ip" "
cp /tmp/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crt
update-ca-certificates --fresh 2>/dev/null
"
ok "$ip 根证书已导入"
done
# ---------- 5. 配置 Docker daemon.json + 重启 ----------
step "2.5 配置 daemon.json + 重启 Docker(三节点)"
# Master: 停 Harbor → 写 daemon.json → 重启 Docker → 启 Harbor
cd /usr/local/harbor && docker-compose down -t 0
cat > /etc/docker/daemon.json << EOF
{
"registry-mirrors": ["https://docker.1ms.run"],
"insecure-registries": ["harbor.shihao.com"]
}
EOF
systemctl restart docker
ok "$MASTER_IP Docker 已重启"
cd /usr/local/harbor && docker-compose up -d
ok "Harbor 已重新启动"
# Slave 节点:写 daemon.json → 重启 Docker
for ip in $SLAVE01_IP $SLAVE02_IP; do
ssh_sudo "$ip" '
cat > /etc/docker/daemon.json << EOF
{
"registry-mirrors": ["https://docker.1ms.run"],
"insecure-registries": ["harbor.shihao.com"]
}
EOF
systemctl restart docker
'
ok "$ip Docker 已重启"
done
sleep 3
# ---------- 6. 登录验证 ----------
step "2.6 登录第三方仓库 + Harbor 验证"
echo "Oldboy123.com" | docker login -u 13353958307 --password-stdin docker.xuanyuan.run
echo ""
echo "=== Harbor 状态 ==="
cd /usr/local/harbor && docker-compose ps
echo ""
echo "=== HTTPS 验证 ==="
curl -skI https://harbor.shihao.com/ 2>&1 | head -3
echo ""
docker login -u admin -p ${HARBOR_PASS} harbor.shihao.com
ok "Harbor 就绪!https://harbor.shihao.com 管理员: admin / passwd"
printf '\n'

第三阶段:Java应用镜像构建(RuoYi-Vue)#

image-20260516165917464
image-20260516165917464

==RuoYi-Vue== 是一个基于经典技术栈的Java EE企业级快速开发平台,它整合了Spring Boot、Spring Security、MyBatis、JWT和Vue等主流技术

  • 帮助开发者快速搭建稳定、安全且可扩展的企业应用系统

参考文档📄

  • RuoYi-Vue 前后端分离,需要 4 个组件:
    • MySQL、Redis
    • Java 后端(Spring Boot)
    • 前端 Nginx(Vue 静态文件 + 反向代理)

其中 MySQL / 后端 / 前端 需要写 Dockerfile 构建镜像

Redis 直接取官方镜像推送到 Harbor 即可

环境准备#

Terminal window
1)克隆(下载)代码
root@SH-master ~# git clone https://gitee.com/y_project/RuoYi-Vue.git
Cloning into 'RuoYi-Vue'...
remote: Enumerating objects: 21949, done.
'下载的时候,也会给你创建一个目录'
root@SH-master ~# ls
RuoYi-Vue
root@SH-master ~# ls RuoYi-Vue/
bin LICENSE README.md ruoyi-common ruoyi-generator.....
2)安装Jdk + Maven

image-20260516170844809
image-20260516170844809

image-20260516170516850
image-20260516170516850

Terminal window
3)上传解压
root@SH-master ~# cd /tmp
root@SH-master tmp# rz
.........
root@SH-master tmp# ls | egrep "maven|jdk|node"
apache-maven-3.9.15-bin.tar.gz
jdk-17.0.12_linux-x64_bin.tar.gz
node-v24.15.0-linux-x64.tar.xz
root@SH-master tmp# tar xf jdk-17.0.12_linux-x64_bin.tar.gz -C /usr/local/
root@SH-master tmp# tar xf apache-maven-3.9.15-bin.tar.gz -C /usr/local/
root@SH-master tmp# tar xf node-v24.15.0-linux-x64.tar.xz -C /usr/local/
4)添加环境变量
root@SH-master tmp# cd
root@SH-master ~# vim /etc/profile.d/env.sh
#!/bin/bash
# java
export JAVA_HOME=/usr/local/jdk-17.0.12
export PATH=$PATH:$JAVA_HOME/bin
# maven
export MAVEN_HOME=/usr/local/apache-maven-3.9.15
export PATH=$PATH:$MAVEN_HOME/bin
# node.js
export NODEJS_HOME=/usr/local/node-v24.15.0-linux-x64
export PATH=$PATH:$NODEJS_HOME/bin
root@SH-master ~# source /etc/profile.d/env.sh
5)检查测试
root@SH-master ~# java --version
java 17.0.12 2024-07-16 LTS
Java(TM) SE Runtime Environment (build 17.0.12+8-LTS-286)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.12+8-LTS-286, mixed mode, sharing)
root@SH-master ~# mvn -v
Apache Maven 3.9.15 (98b2cdbfdb5f1ac8781f537ea9acccaed7922349)
Maven home: /usr/local/apache-maven-3.9.15
Java version: 17.0.12, vendor: Oracle Corporation, runtime: /usr/local/jdk-17.0.12
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-31-generic", arch: "amd64", family: "unix"
root@SH-master ~# node -v
v24.15.0
6)项目目录结构
root@SH-master ~# mkdir -p /root/ruoyi-compose/{dockerfile,conf,data}
root@SH-master ~# tree /root/ruoyi-compose/
/root/ruoyi-compose/
├── conf # Nginx 配置、my.cnf
├── data # jar 包、SQL 文件、dist 静态文件
└── dockerfile # 三个 Dockerfile
7)镜像准备
'此镜像可运行jre包'
root@SH-master ~# docker pull docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal
Status: Downloaded newer image for docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal
root@SH-master ~# docker tag docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal eclipse-temurin:21-jre-ubi9-minimal
==========================
'nginx镜像'
root@SH-master ~# docker pull docker.xuanyuan.run/nginx
Using default tag: latest
root@SH-master ~# docker tag docker.xuanyuan.run/nginx:latest nginx:latest
==========================
"Redis 镜像(直接拉取 + 推送)"
root@SH-master ~# docker pull docker.xuanyuan.run/redis:7.2.8
root@SH-master ~# docker tag docker.xuanyuan.run/redis:7.2.8 harbor.shihao.com/kpyun/redis:7.2.8
root@SH-master ~# docker push harbor.shihao.com/kpyun/redis:7.2.8
==========================
root@SH-master ~# docker images
eclipse-temurin:21-jre-ubi9-minimal 0b68af521c74 492MB
nginx:latest 06aa3d7be10b 240MB

数据库镜像(ruoyi-mysql)#

Terminal window
1)准备 SQL 初始化文件(从 RuoYi 源码目录)
root@SH-master ~# cd /root/RuoYi-Vue
root@SH-master RuoYi-Vue# cp ./sql/quartz.sql ./sql/ry_20260417.sql /root/ruoyi-compose/data/
# 确认
root@SH-master RuoYi-Vue# ls /root/ruoyi-compose/data/
quartz.sql ry_20260417.sql
2)准备 my.cnf(字符集 + 排序规则)
root@SH-master RuoYi-Vue# cat > /root/ruoyi-compose/conf/my.cnf << 'EOF'
[mysqld]
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
socket=/var/run/mysqld/mysqld.sock
default-character-set = utf8mb4
!includedir /etc/mysql/conf.d/
EOF
3)编写 Dockerfile
root@SH-master RuoYi-Vue# vim /root/ruoyi-compose/dockerfile/mysql-1.0
FROM mysql:8.0.36
LABEL maintainer="jiuzhao"
EXPOSE 3306
COPY ./conf/my.cnf /etc/my.cnf
COPY ./data/quartz.sql /docker-entrypoint-initdb.d/
COPY ./data/ry_20260417.sql /docker-entrypoint-initdb.d/
Terminal window
4)构建 + 推送
root@SH-master RuoYi-Vue# cd /root/ruoyi-compose
root@SH-master ruoyi-compose# docker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/ruoyi-mysql:1.0 .
Successfully tagged harbor.shihao.com/kpyun/ruoyi-mysql:1.0
root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-mysql:1.0
58a83c1f4ebd: Pushed

Java 后端镜像(ruoyi-backend)#

Terminal window
1)修改配置文件:数据库和 Redis 地址改为容器名
root@SH-master ruoyi-compose# cd /root/RuoYi-Vue
# MySQL 地址(容器化后用容器名)
root@SH-master RuoYi-Vue# vim ./ruoyi-admin/src/main/resources/application-druid.yml
# 主库数据源
master:
url: jdbc:mysql://java-db:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: jiu
password: oldboy123.com
# 数据库的用户名和密码
Terminal window
# Redis 地址
root@SH-master RuoYi-Vue# vim ./ruoyi-admin/src/main/resources/application.yml
# redis 配置
redis:
host: redis-server
port: 6379
Terminal window
2)Maven 打包
root@SH-master RuoYi-Vue# mvn clean package -Dmaven.test.skip=true
[INFO] BUILD SUCCESS

image-20260516193328009
image-20260516193328009

Terminal window
root@SH-master RuoYi-Vue# ls ruoyi-admin/target/ruoyi-admin.jar
ruoyi-admin/target/ruoyi-admin.jar
root@SH-master RuoYi-Vue# cp ruoyi-admin/target/ruoyi-admin.jar /root/ruoyi-compose/data/
3)编写 Dockerfile
root@SH-master RuoYi-Vue# vim /root/ruoyi-compose/dockerfile/jre-1.0
FROM eclipse-temurin:21-jre-ubi9-minimal
LABEL maintainer="jiuzhao"
EXPOSE 8080
COPY ./data/ruoyi-admin.jar /
CMD ["java", "-jar", "/ruoyi-admin.jar"]
Terminal window
4)构建 + 推送
root@SH-master RuoYi-Vue# cd /root/ruoyi-compose
root@SH-master ruoyi-compose# docker build -f ./dockerfile/jre-1.0 -t harbor.shihao.com/kpyun/ruoyi-backend:1.0 .
Successfully tagged harbor.shihao.com/kpyun/ruoyi-backend:1.0
root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-backend:1.0
8379cb2e116b: Pushed
c06b8ad3393f: Pushed
53f074f73cee: Pushed
1.0: digest: sha256:d77exxx......

前端镜像(ruoyi-frontend)#

Terminal window
1)修改后端接口地址
"容器化后指向 Swarm 服务名 java-web"
root@SH-master ruoyi-compose# cd /root/RuoYi-Vue/ruoyi-ui
root@SH-master ruoyi-ui# grep "baseUrl" vue.config.js
const baseUrl = 'http://localhost:8080' // 后端接口
root@SH-master ruoyi-ui# sed -i "s#localhost:8080#java-web:8080#g" vue.config.js
root@SH-master ruoyi-ui# npm install --registry=https://registry.npmmirror.com

image-20260516194738473
image-20260516194738473

image-20260516194853390
image-20260516194853390

Terminal window
root@SH-master ruoyi-ui# npm run build:prod
# 生成 dist/ 静态文件目录
root@SH-master ruoyi-ui# ls dist/
favicon.ico html index.html robots.txt static styles
root@SH-master ruoyi-ui# cp -r dist/ /root/ruoyi-compose/data/
2)编写 Nginx 子配置文件
root@SH-master ruoyi-ui# vim /root/ruoyi-compose/conf/default.conf
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;
}
location /prod-api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://java-web:8080/;
}
# springdoc proxy
location ~ ^/v3/api-docs/(.*) {
proxy_pass http://java-web:8080/v3/api-docs/$1;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Terminal window
3)编写 Dockerfile
root@SH-master ruoyi-ui# vim /root/ruoyi-compose/dockerfile/nginx-1.0
FROM nginx:latest
LABEL maintainer="jiuzhao"
EXPOSE 80
COPY ./conf/default.conf /etc/nginx/conf.d/
COPY ./data/dist /usr/share/nginx/html
RUN chown -R nginx:nginx /usr/share/nginx/html
Terminal window
4)构建 + 推送
root@SH-master ruoyi-ui# cd /root/ruoyi-compose
root@SH-master ruoyi-compose# docker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/ruoyi-frontend:1.0 .
Successfully tagged harbor.shihao.com/kpyun/ruoyi-frontend:1.0
root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-frontend:1.0

单机 Compose 验证(推 Swarm 前先跑通)#

Terminal window
root@SH-master ruoyi-compose# vim /root/ruoyi-compose/docker-compose.yml
name: ruoyi
services:
java-db:
image: harbor.shihao.com/kpyun/ruoyi-mysql:1.0
container_name: java-db
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "ry-vue"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "oldboy123.com"
command:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
- "--default-authentication-plugin=mysql_native_password"
volumes:
- java-mysql-data:/var/lib/mysql
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
redis-server:
image: harbor.shihao.com/kpyun/redis:7.2.8
container_name: redis-server
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
java-web:
image: harbor.shihao.com/kpyun/ruoyi-backend:1.0
container_name: java-web
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://java-db:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: jiu
SPRING_DATASOURCE_PASSWORD: oldboy123.com
SPRING_REDIS_HOST: redis-server
SPRING_REDIS_PORT: 6379
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 15s
timeout: 15s
retries: 3
depends_on:
java-db:
condition: service_healthy
redis-server:
condition: service_healthy
ruoyi-ui:
image: harbor.shihao.com/kpyun/ruoyi-frontend:1.0
container_name: ruoyi-ui
ports:
- "8088:80"
networks:
- ruoyi-net
depends_on:
java-web:
condition: service_healthy
networks:
ruoyi-net:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.33.0.0/16
gateway: 172.33.0.1
volumes:
java-mysql-data:
Terminal window
# 启动测试
root@SH-master ruoyi-compose# docker-compose up -d

image-20260516200936414
image-20260516200936414

image-20260516200959120
image-20260516200959120

Terminal window
root@SH-master ruoyi-compose# docker-compose ps
"确认 4 个容器均 Up + Healthy"
浏览器访问 http://10.0.0.90:8088 --> 若依登录页

image-20260516201117486
image-20260516201117486

image-20260516201154298
image-20260516201154298

Terminal window
# 测试通过后停掉
root@SH-master ruoyi-compose# docker-compose down -t 0
浏览器访问: --> '登录Harbor仓库'
https://harbor.shihao.com/harbor/projects/2/repositories
# 查看镜像

image-20260516201454262
image-20260516201454262

四个 Java 项目镜像全在 Harbor 中,Worker 节点可直接拉取

phase3-java-build.sh#

#!/bin/sh
# ============================================
# 第三阶段:Java 若依 (RuoYi-Vue) 镜像构建
# 作者: 久棹
# 用法: 在 master 节点以 root 执行
# dash phase3-java-build.sh
# ============================================
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
# ---------- 1. 安装 JDK + Maven + Node.js ----------
step "3.1 安装 JDK + Maven + Node.js"
cd /tmp
# JDK
if [ -f /tmp/jdk-17.0.12_linux-x64_bin.tar.gz ]; then
tar xf /tmp/jdk-17.0.12_linux-x64_bin.tar.gz -C /usr/local/
echo "JDK 17 已安装"
else
err "jdk-17.0.12_linux-x64_bin.tar.gz 不存在!请先上传到 /tmp/"
exit 1
fi
# Maven
if [ -f /tmp/apache-maven-3.9.15-bin.tar.gz ]; then
tar xf /tmp/apache-maven-3.9.15-bin.tar.gz -C /usr/local/
echo "Maven 3.9 已安装"
else
err "apache-maven-3.9.15-bin.tar.gz 不存在!请先上传到 /tmp/"
exit 1
fi
# Node.js
if [ -f /tmp/node-v24.15.0-linux-x64.tar.xz ]; then
tar xf /tmp/node-v24.15.0-linux-x64.tar.xz -C /usr/local/
echo "Node.js 24 已安装"
else
err "node-v24.15.0-linux-x64.tar.xz 不存在!请先上传到 /tmp/"
exit 1
fi
# 环境变量
cat > /etc/profile.d/env.sh << 'EOF'
#!/bin/bash
# java
export JAVA_HOME=/usr/local/jdk-17.0.12
export PATH=$PATH:$JAVA_HOME/bin
# maven
export MAVEN_HOME=/usr/local/apache-maven-3.9.15
export PATH=$PATH:$MAVEN_HOME/bin
# node.js
export NODEJS_HOME=/usr/local/node-v24.15.0-linux-x64
export PATH=$PATH:$NODEJS_HOME/bin
EOF
. /etc/profile.d/env.sh
echo "环境变量已配置"
java --version 2>&1 | head -1
mvn -v 2>&1 | head -1
node -v 2>&1
ok "JDK 17 + Maven 3.9 + Node.js 24 安装完成"
# ---------- 2. 项目目录 + 克隆源码 ----------
step "3.2 克隆 RuoYi-Vue 源码 + 创建项目目录"
mkdir -p /root/ruoyi-compose/dockerfile /root/ruoyi-compose/conf /root/ruoyi-compose/data
cd /root
if [ -d /root/RuoYi-Vue ]; then
echo "RuoYi-Vue 目录已存在,跳过 clone"
else
git clone https://gitee.com/y_project/RuoYi-Vue.git
echo "RuoYi-Vue 克隆完成"
fi
ok "RuoYi-Vue 源码就绪,项目目录已创建"
# ---------- 3. 拉取基础镜像 ----------
step "3.3 拉取基础镜像"
. /etc/profile.d/env.sh
# 登录第三方仓库
echo "Oldboy123.com" | docker login -u 13353958307 --password-stdin docker.xuanyuan.run
# 登录 Harbor
docker login -u admin -p passwd harbor.shihao.com
# 拉取基础镜像
docker pull docker.xuanyuan.run/mysql:8.0.36
docker tag docker.xuanyuan.run/mysql:8.0.36 mysql:8.0.36
docker pull docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal
docker tag docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal eclipse-temurin:21-jre-ubi9-minimal
docker pull docker.xuanyuan.run/nginx:latest
docker tag docker.xuanyuan.run/nginx:latest nginx:latest
docker pull docker.xuanyuan.run/redis:7.2.8
docker tag docker.xuanyuan.run/redis:7.2.8 harbor.shihao.com/kpyun/redis:7.2.8
docker push harbor.shihao.com/kpyun/redis:7.2.8
echo "基础镜像准备完毕"
ok "基础镜像拉取完成,Redis 已推送 Harbor"
# ---------- 4. MySQL 镜像 ----------
step "3.4 构建 ruoyi-mysql 镜像"
. /etc/profile.d/env.sh
# my.cnf(完整版)
cat > /root/ruoyi-compose/conf/my.cnf << 'EOF'
[mysqld]
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
socket=/var/run/mysqld/mysqld.sock
default-character-set = utf8mb4
!includedir /etc/mysql/conf.d/
EOF
# Dockerfile
cat > /root/ruoyi-compose/dockerfile/mysql-1.0 << EOF
FROM mysql:8.0.36
LABEL maintainer="jiuzhao"
EXPOSE 3306
COPY ./conf/my.cnf /etc/my.cnf
COPY ./data/quartz.sql /docker-entrypoint-initdb.d/
COPY ./data/ry_20260417.sql /docker-entrypoint-initdb.d/
EOF
# SQL 文件从 RuoYi 源码目录复制
cp /root/RuoYi-Vue/sql/quartz.sql /root/ruoyi-compose/data/ 2>/dev/null
cp /root/RuoYi-Vue/sql/ry_20260417.sql /root/ruoyi-compose/data/ 2>/dev/null
cd /root/ruoyi-compose
docker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/ruoyi-mysql:1.0 .
docker push harbor.shihao.com/kpyun/ruoyi-mysql:1.0
echo "ruoyi-mysql:1.0 构建+推送完成"
ok "ruoyi-mysql 镜像构建完成"
# ---------- 5. Java 后端镜像 ----------
step "3.5 构建 ruoyi-backend 镜像"
. /etc/profile.d/env.sh
cd /root/RuoYi-Vue
# 修改数据库连接配置(容器化)
sed -i "s#jdbc:mysql://localhost:3306/ry-vue#jdbc:mysql://java-db:3306/ry-vue#g" \
./ruoyi-admin/src/main/resources/application-druid.yml 2>/dev/null || true
sed -i "s/username: root/username: jiu/" \
./ruoyi-admin/src/main/resources/application-druid.yml 2>/dev/null || true
sed -i "s/password: password/password: oldboy123.com/" \
./ruoyi-admin/src/main/resources/application-druid.yml 2>/dev/null || true
sed -i "s/host: localhost/host: redis-server/" \
./ruoyi-admin/src/main/resources/application.yml 2>/dev/null || true
# Maven 打包
mvn clean package -Dmaven.test.skip=true -q
cp ruoyi-admin/target/ruoyi-admin.jar /root/ruoyi-compose/data/
# Dockerfile
cat > /root/ruoyi-compose/dockerfile/jre-1.0 << EOF
FROM eclipse-temurin:21-jre-ubi9-minimal
LABEL maintainer="jiuzhao"
EXPOSE 8080
COPY ./data/ruoyi-admin.jar /
CMD ["java", "-jar", "/ruoyi-admin.jar"]
EOF
cd /root/ruoyi-compose
docker build -f ./dockerfile/jre-1.0 -t harbor.shihao.com/kpyun/ruoyi-backend:1.0 .
docker push harbor.shihao.com/kpyun/ruoyi-backend:1.0
echo "ruoyi-backend:1.0 构建+推送完成"
ok "ruoyi-backend 镜像构建完成"
# ---------- 6. 前端镜像 ----------
step "3.6 构建 ruoyi-frontend 镜像"
. /etc/profile.d/env.sh
cd /root/RuoYi-Vue/ruoyi-ui
# 修改后端接口地址
sed -i "s#localhost:8080#java-web:8080#g" vue.config.js 2>/dev/null || true
# npm 构建
npm install --registry=https://registry.npmmirror.com --silent
npm run build:prod
# 复制 dist
cp -r dist/ /root/ruoyi-compose/data/
# Nginx 子配置(含 springdoc 代理)
cat > /root/ruoyi-compose/conf/default.conf << 'EOFNG'
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
index index.html;
}
location /prod-api/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://java-web:8080/;
}
# springdoc proxy
location ~ ^/v3/api-docs/(.*) {
proxy_pass http://java-web:8080/v3/api-docs/$1;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
EOFNG
# Dockerfile
cat > /root/ruoyi-compose/dockerfile/nginx-1.0 << EOF
FROM nginx:latest
LABEL maintainer="jiuzhao"
EXPOSE 80
COPY ./conf/default.conf /etc/nginx/conf.d/
COPY ./data/dist /usr/share/nginx/html
RUN chown -R nginx:nginx /usr/share/nginx/html
EOF
cd /root/ruoyi-compose
docker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/ruoyi-frontend:1.0 .
docker push harbor.shihao.com/kpyun/ruoyi-frontend:1.0
echo "ruoyi-frontend:1.0 构建+推送完成"
ok "ruoyi-frontend 镜像构建完成"
# ---------- 7. 单机 Compose 验证 ----------
step "3.7 单机 Compose 验证若依4件套"
cat > /root/ruoyi-compose/docker-compose.yml << 'EOFYML'
name: ruoyi
services:
java-db:
image: harbor.shihao.com/kpyun/ruoyi-mysql:1.0
container_name: java-db
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "ry-vue"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "oldboy123.com"
command:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
- "--default-authentication-plugin=mysql_native_password"
volumes:
- java-mysql-data:/var/lib/mysql
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
redis-server:
image: harbor.shihao.com/kpyun/redis:7.2.8
container_name: redis-server
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
java-web:
image: harbor.shihao.com/kpyun/ruoyi-backend:1.0
container_name: java-web
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://java-db:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: jiu
SPRING_DATASOURCE_PASSWORD: oldboy123.com
SPRING_REDIS_HOST: redis-server
SPRING_REDIS_PORT: "6379"
networks:
- ruoyi-net
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
interval: 15s
timeout: 15s
retries: 3
depends_on:
java-db:
condition: service_healthy
redis-server:
condition: service_healthy
ruoyi-ui:
image: harbor.shihao.com/kpyun/ruoyi-frontend:1.0
container_name: ruoyi-ui
ports:
- "8088:80"
networks:
- ruoyi-net
depends_on:
java-web:
condition: service_healthy
networks:
ruoyi-net:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.33.0.0/16
gateway: 172.33.0.1
volumes:
java-mysql-data:
EOFYML
cd /root/ruoyi-compose
docker-compose up -d
echo "等待服务启动..."
sleep 30
echo ""
echo "=== 验证 ==="
docker-compose ps
echo ""
curl -sI http://127.0.0.1:8088 2>&1 | head -5
echo ""
echo "访问 http://10.0.0.90:8088 验证若依登录页"
echo "验证通过后,执行: cd /root/ruoyi-compose && docker-compose down -t 0"
ok "单机 Compose 验证部署完成"
echo ""
info "==========================================="
info "请在浏览器访问: ${BOLD}http://10.0.0.90:8088${RESET}"
info "确认若依登录页正常后,执行关闭命令:"
info " ${BOLD}cd /root/ruoyi-compose && docker-compose down -t 0${RESET}"
info "==========================================="
ok "第三阶段完成!4个 Java 镜像已推送 Harbor"

第四阶段:Python Flask 名言应用#

三个容器:py-db(MySQL)+ py-web(Flask+Gunicorn)+ py-nginx(Nginx 反向代理)

项目目录结构#

Terminal window
root@SH-master ~# mkdir -p /root/py-compose/{dockerfile,conf,data,templates}
root@SH-master ~# tree /root/py-compose/
/root/py-compose/
├── conf # Nginx 子配置文件
├── data # SQL 初始化脚本
├── dockerfile # 三个 Dockerfile
└── templates # Flask Jinja2 模板

数据库镜像(py-mysql)#

Terminal window
1)准备 SQL 初始化文件
root@SH-master ~# vim /root/py-compose/data/quote-init.sql
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS `quote`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'jiu'@'%' IDENTIFIED BY 'passwd';
GRANT ALL PRIVILEGES ON `quote`.* TO 'jiu'@'%';
FLUSH PRIVILEGES;
USE `quote`;
CREATE TABLE IF NOT EXISTS `quota` (
`ID` INT AUTO_INCREMENT PRIMARY KEY,
`QUOTE` TEXT NOT NULL,
`author` VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO `quota` (`QUOTE`, `author`) VALUES
('The only way to do great work is to love what you do.', 'Steve Jobs'),
('生活不止眼前的苟且,还有诗和远方。', '高晓松'),
('人生如逆旅,我亦是行人。', '苏轼'),
('In the middle of difficulty lies opportunity.', 'Albert Einstein'),
('天行健,君子以自强不息。', '《周易》'),
('Stay hungry, stay foolish.', 'Steve Jobs'),
('博观而约取,厚积而薄发。', '苏轼'),
('路漫漫其修远兮,吾将上下而求索。', '屈原'),
('海内存知己,天涯若比邻。', '王勃'),
('Do or do not. There is no try.', 'Yoda'),
('人固有一死,或重于泰山,或轻于鸿毛。', '司马迁'),
('不积跬步,无以至千里。', '荀子'),
('To be or not to be, that is the question.', 'William Shakespeare'),
('山重水复疑无路,柳暗花明又一村。', '陆游'),
('业精于勤,荒于嬉。', '韩愈'),
('Knowledge is power.', 'Francis Bacon'),
('长风破浪会有时,直挂云帆济沧海。', '李白'),
('学而不思则罔,思而不学则殆。', '孔子'),
('All that we see or seem is but a dream within a dream.', 'Edgar Allan Poe'),
('非淡泊无以明志,非宁静无以致远。', '诸葛亮');
Terminal window
2)准备 my.cnf
root@SH-master ~# vim /root/py-compose/conf/my.cnf
[mysqld]
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
socket=/var/run/mysqld/mysqld.sock
default-character-set = utf8mb4
!includedir /etc/mysql/conf.d/
Terminal window
3)编写 Dockerfile
root@SH-master ~# vim /root/py-compose/dockerfile/mysql-1.0
FROM mysql:8.0.36
LABEL maintainer="jiuzhao"
EXPOSE 3306
COPY ./conf/my.cnf /etc/my.cnf
COPY ./data/quote-init.sql /docker-entrypoint-initdb.d/
Terminal window
4)构建 + 推送
root@SH-master ~# cd /root/py-compose
root@SH-master py-compose# docker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/py-mysql:1.0 .
Successfully tagged harbor.shihao.com/kpyun/py-mysql:1.0
root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-mysql:1.0

Flask 应用镜像(py-web)#

Terminal window
1)Flask 主程序
root@SH-master py-compose# vim ./data/app.py
from flask import Flask, render_template
import mysql.connector
import os
import sys
app = Flask(__name__)
DB_CONFIG = {
"host": os.getenv("DB_HOST", "localhost"),
"port": int(os.getenv("DB_PORT", 3306)),
"user": os.getenv("DB_USER", "root"),
"password": os.getenv("DB_PASSWORD", ""),
"database": "quote",
"charset": "utf8mb4",
}
def get_random_quote():
conn = mysql.connector.connect(**DB_CONFIG)
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT QUOTE, author FROM quota ORDER BY RAND() LIMIT 1")
return cursor.fetchone()
finally:
conn.close()
def get_all_quotes():
conn = mysql.connector.connect(**DB_CONFIG)
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT QUOTE, author FROM quota ORDER BY ID")
return cursor.fetchall()
finally:
conn.close()
@app.route("/")
def index():
quote = get_random_quote()
if quote is None:
quote = {"QUOTE": "暂无名言", "author": "系统"}
return render_template("index.html", quote=quote)
@app.route("/all")
def all_quotes():
quotes = get_all_quotes()
return render_template("index.html", quotes=quotes)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
Terminal window
2)Jinja2 模板
root@SH-master py-compose# vim ./templates/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>每日名言</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0; padding: 0; min-height: 100vh;
display: flex; align-items: center; justify-content: center;
}
.container {
background: white; padding: 2rem; border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2); max-width: 600px;
width: 90%; text-align: center;
}
.quote { font-size: 1.2rem; line-height: 1.6; color: #333; margin-bottom: 1rem; font-style: italic; }
.author { font-size: 1rem; color: #666; margin-top: 1rem; }
.all-link { margin-top: 1rem; }
.all-link a { color: #667eea; text-decoration: none; }
.error { color: red; }
hr { margin: 1rem 0; border: 0; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="container">
<h1>每日名言</h1>
{% if quote and not quotes %}
<div class="quote">"{{ quote.QUOTE }}"</div>
<div class="author">-- {{ quote.author }}</div>
{% elif quotes %}
<h2>所有名言</h2>
{% for item in quotes %}
<div class="quote">"{{ item.QUOTE }}"</div>
<div class="author">-- {{ item.author }}</div>
<hr>
{% endfor %}
{% else %}
<div class="error">暂无名言数据</div>
{% endif %}
<div class="all-link">
<a href="/">随机名言</a> |
<a href="/all">查看全部</a>
</div>
</div>
</body>
</html>
Terminal window
3)Python 依赖
root@SH-master py-compose# vim ./data/requirements.txt
Flask==2.3.3
mysql-connector-python==8.1.0
gunicorn==21.2.0
Terminal window
4)编写 Dockerfile
root@SH-master py-compose# vim ./dockerfile/flask-1.0
FROM docker.xuanyuan.run/library/python:3.11-slim
LABEL maintainer="jiuzhao"
ENV LANG=C.UTF-8
ENV PYTHONIOENCODING=utf-8
WORKDIR /app
# 创建非 root 用户运行 Flask
RUN groupadd -g 666 www && \
useradd -u 666 -g 666 -M -s /sbin/nologin www
COPY ./data/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
COPY ./data/app.py .
COPY ./templates ./templates
# 统一权限
RUN chown -R www:www /app
EXPOSE 5000
USER www
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
Terminal window
5)构建 + 推送
root@SH-master py-compose# docker build -f ./dockerfile/flask-1.0 -t harbor.shihao.com/kpyun/py-web:1.0 .
Successfully tagged harbor.shihao.com/kpyun/py-web:1.0
root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-web:1.0

Nginx 前端镜像(py-nginx)#

Terminal window
1)Nginx 子配置文件
root@SH-master py-compose# vim ./conf/default.conf
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
proxy_pass http://py-web:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
Terminal window
2)编写 Dockerfile
root@SH-master py-compose# vim ./dockerfile/nginx-1.0
FROM nginx:latest
LABEL maintainer="jiuzhao"
EXPOSE 80
COPY ./conf/default.conf /etc/nginx/conf.d/
Terminal window
3)构建 + 推送
root@SH-master py-compose# docker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/py-nginx:1.0 .
Successfully tagged harbor.shihao.com/kpyun/py-nginx:1.0
root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-nginx:1.0
030365c1a354: Mounted from kpyun/ruoyi-frontend
735e1c628373: Mounted from kpyun/ruoyi-frontend
f612eeda2e8b: Pushed

单机 Compose 验证#

Terminal window
root@SH-master py-compose# vim /root/py-compose/docker-compose.yml
name: pyapp
services:
py-db:
image: harbor.shihao.com/kpyun/py-mysql:1.0
container_name: py-db
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "quote"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "passwd"
volumes:
- py-mysql-data:/var/lib/mysql
networks:
- py-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
py-web:
image: harbor.shihao.com/kpyun/py-web:1.0
container_name: py-web
environment:
DB_HOST: py-db
DB_PORT: "3306"
DB_USER: jiu
DB_PASSWORD: passwd
networks:
- py-net
depends_on:
py-db:
condition: service_healthy
py-nginx:
image: harbor.shihao.com/kpyun/py-nginx:1.0
container_name: py-nginx
ports:
- "5001:80"
networks:
- py-net
depends_on:
- py-web
networks:
py-net:
driver: bridge
volumes:
py-mysql-data:
Terminal window
# 启动测试
root@SH-master py-compose# docker-compose up -d

image-20260516211351767
image-20260516211351767

image-20260516211410205
image-20260516211410205

Terminal window
root@SH-master py-compose# docker-compose ps
# 确认 3 个容器均 Up
浏览器访问 http://10.0.0.90:5001 每日名言页

image-20260516213052029
image-20260516213052029

Terminal window
# 测试通过后停掉
root@SH-master py-compose# docker-compose down -t 0
[+] Running 4/3
Container py-nginx Removed
Container py-web Removed
Container py-db Removed
Network pyapp_py-net Removed

镜像仓库确认#

image-20260516213437892
image-20260516213437892

Terminal window
root@SH-master ~# docker images --format '{{.Repository}}:{{.Tag}}' | grep kpyun
harbor.shihao.com/kpyun/ruoyi-mysql:1.0
harbor.shihao.com/kpyun/ruoyi-backend:1.0
harbor.shihao.com/kpyun/ruoyi-frontend:1.0
harbor.shihao.com/kpyun/redis:7.2.8
harbor.shihao.com/kpyun/py-mysql:1.0
harbor.shihao.com/kpyun/py-web:1.0
harbor.shihao.com/kpyun/py-nginx:1.0
# 共 7 个镜像,全在 Harbor 中,Worker 可直接拉取

phase4-python-build.sh#

#!/bin/sh
# ============================================
# 第四阶段:Python Flask 名言应用镜像构建
# 作者: 久棹
# 用法: 在 master 节点以 root 执行
# dash phase4-python-build.sh
# ============================================
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
# ---------- 1. 项目目录 ----------
step "4.1 创建项目目录"
. /etc/profile.d/env.sh
mkdir -p /root/py-compose/dockerfile /root/py-compose/conf /root/py-compose/data /root/py-compose/templates
echo "目录已创建: /root/py-compose/"
ok "项目目录创建完成"
# ---------- 2. 仓库登录 ----------
step "4.2 登录第三方仓库 + Harbor"
. /etc/profile.d/env.sh
echo "Oldboy123.com" | docker login -u 13353958307 --password-stdin docker.xuanyuan.run
docker login -u admin -p passwd harbor.shihao.com
ok "仓库登录完成"
# ---------- 3. SQL 初始化脚本 ----------
step "4.3 准备 SQL 初始化脚本"
cat > /root/py-compose/data/quote-init.sql << 'EOF'
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS `quote`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'jiu'@'%' IDENTIFIED BY 'passwd';
GRANT ALL PRIVILEGES ON `quote`.* TO 'jiu'@'%';
FLUSH PRIVILEGES;
USE `quote`;
CREATE TABLE IF NOT EXISTS `quota` (
`ID` INT AUTO_INCREMENT PRIMARY KEY,
`QUOTE` TEXT NOT NULL,
`author` VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO `quota` (`QUOTE`, `author`) VALUES
('The only way to do great work is to love what you do.', 'Steve Jobs'),
('生活不止眼前的苟且,还有诗和远方。', '高晓松'),
('人生如逆旅,我亦是行人。', '苏轼'),
('In the middle of difficulty lies opportunity.', 'Albert Einstein'),
('天行健,君子以自强不息。', '《周易》'),
('Stay hungry, stay foolish.', 'Steve Jobs'),
('博观而约取,厚积而薄发。', '苏轼'),
('路漫漫其修远兮,吾将上下而求索。', '屈原'),
('海内存知己,天涯若比邻。', '王勃'),
('Do or do not. There is no try.', 'Yoda'),
('人固有一死,或重于泰山,或轻于鸿毛。', '司马迁'),
('不积跬步,无以至千里。', '荀子'),
('To be or not to be, that is the question.', 'William Shakespeare'),
('山重水复疑无路,柳暗花明又一村。', '陆游'),
('业精于勤,荒于嬉。', '韩愈'),
('Knowledge is power.', 'Francis Bacon'),
('长风破浪会有时,直挂云帆济沧海。', '李白'),
('学而不思则罔,思而不学则殆。', '孔子'),
('All that we see or seem is but a dream within a dream.', 'Edgar Allan Poe'),
('非淡泊无以明志,非宁静无以致远。', '诸葛亮');
EOF
echo "SQL 脚本已生成 (20 条名言)"
ok "SQL 初始化脚本就绪"
# ---------- 4. py-mysql 镜像 ----------
step "4.4 构建 py-mysql 镜像"
# my.cnf(完整版)
cat > /root/py-compose/conf/my.cnf << 'EOF'
[mysqld]
skip-host-cache
skip-name-resolve
datadir=/var/lib/mysql
socket=/var/run/mysqld/mysqld.sock
secure-file-priv=/var/lib/mysql-files
user=mysql
pid-file=/var/run/mysqld/mysqld.pid
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
[client]
socket=/var/run/mysqld/mysqld.sock
default-character-set = utf8mb4
!includedir /etc/mysql/conf.d/
EOF
cat > /root/py-compose/dockerfile/mysql-1.0 << EOF
FROM mysql:8.0.36
LABEL maintainer="jiuzhao"
EXPOSE 3306
COPY ./conf/my.cnf /etc/my.cnf
COPY ./data/quote-init.sql /docker-entrypoint-initdb.d/
EOF
cd /root/py-compose
docker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/py-mysql:1.0 .
docker push harbor.shihao.com/kpyun/py-mysql:1.0
echo "py-mysql:1.0 构建+推送完成"
ok "py-mysql 镜像构建完成"
# ---------- 5. Flask 应用镜像 ----------
step "4.5 构建 py-web (Flask + Gunicorn) 镜像"
# Flask 主程序
cat > /root/py-compose/data/app.py << 'EOF'
from flask import Flask, render_template
import mysql.connector
import os
import sys
app = Flask(__name__)
DB_CONFIG = {
"host": os.getenv("DB_HOST", "localhost"),
"port": int(os.getenv("DB_PORT", 3306)),
"user": os.getenv("DB_USER", "root"),
"password": os.getenv("DB_PASSWORD", ""),
"database": "quote",
"charset": "utf8mb4",
}
def get_random_quote():
conn = mysql.connector.connect(**DB_CONFIG)
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT QUOTE, author FROM quota ORDER BY RAND() LIMIT 1")
return cursor.fetchone()
finally:
conn.close()
def get_all_quotes():
conn = mysql.connector.connect(**DB_CONFIG)
try:
cursor = conn.cursor(dictionary=True)
cursor.execute("SELECT QUOTE, author FROM quota ORDER BY ID")
return cursor.fetchall()
finally:
conn.close()
@app.route("/")
def index():
quote = get_random_quote()
if quote is None:
quote = {"QUOTE": "暂无名言", "author": "系统"}
return render_template("index.html", quote=quote)
@app.route("/all")
def all_quotes():
quotes = get_all_quotes()
return render_template("index.html", quotes=quotes)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
EOF
# Jinja2 模板
cat > /root/py-compose/templates/index.html << 'EOFHTML'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>每日名言</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0; padding: 0; min-height: 100vh;
display: flex; align-items: center; justify-content: center;
}
.container {
background: white; padding: 2rem; border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2); max-width: 600px;
width: 90%; text-align: center;
}
.quote { font-size: 1.2rem; line-height: 1.6; color: #333; margin-bottom: 1rem; font-style: italic; }
.author { font-size: 1rem; color: #666; margin-top: 1rem; }
.all-link { margin-top: 1rem; }
.all-link a { color: #667eea; text-decoration: none; }
.error { color: red; }
hr { margin: 1rem 0; border: 0; border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="container">
<h1>每日名言</h1>
{% if quote and not quotes %}
<div class="quote">"{{ quote.QUOTE }}"</div>
<div class="author">-- {{ quote.author }}</div>
{% elif quotes %}
<h2>所有名言</h2>
{% for item in quotes %}
<div class="quote">"{{ item.QUOTE }}"</div>
<div class="author">-- {{ item.author }}</div>
<hr>
{% endfor %}
{% else %}
<div class="error">暂无名言数据</div>
{% endif %}
<div class="all-link">
<a href="/">随机名言</a> |
<a href="/all">查看全部</a>
</div>
</div>
</body>
</html>
EOFHTML
# requirements.txt
cat > /root/py-compose/data/requirements.txt << EOFREQ
Flask==2.3.3
mysql-connector-python==8.1.0
gunicorn==21.2.0
EOFREQ
# Dockerfile
cat > /root/py-compose/dockerfile/flask-1.0 << 'EOFDF'
FROM docker.xuanyuan.run/library/python:3.11-slim
LABEL maintainer="jiuzhao"
ENV LANG=C.UTF-8
ENV PYTHONIOENCODING=utf-8
WORKDIR /app
# 创建非 root 用户运行 Flask
RUN groupadd -g 666 www && \
useradd -u 666 -g 666 -M -s /sbin/nologin www
COPY ./data/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
COPY ./data/app.py .
COPY ./templates ./templates
# 统一权限
RUN chown -R www:www /app
EXPOSE 5000
USER www
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
EOFDF
cd /root/py-compose
docker build -f ./dockerfile/flask-1.0 -t harbor.shihao.com/kpyun/py-web:1.0 .
docker push harbor.shihao.com/kpyun/py-web:1.0
echo "py-web:1.0 构建+推送完成"
ok "py-web 镜像构建完成"
# ---------- 6. py-nginx 镜像 ----------
step "4.6 构建 py-nginx 镜像"
cat > /root/py-compose/conf/default.conf << 'EOFNG'
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
proxy_pass http://py-web:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
EOFNG
cat > /root/py-compose/dockerfile/nginx-1.0 << EOF
FROM nginx:latest
LABEL maintainer="jiuzhao"
EXPOSE 80
COPY ./conf/default.conf /etc/nginx/conf.d/
EOF
cd /root/py-compose
docker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/py-nginx:1.0 .
docker push harbor.shihao.com/kpyun/py-nginx:1.0
echo "py-nginx:1.0 构建+推送完成"
ok "py-nginx 镜像构建完成"
# ---------- 7. 单机 Compose 验证 ----------
step "4.7 单机 Compose 验证 Flask 名言应用"
cat > /root/py-compose/docker-compose.yml << 'EOFYML'
name: pyapp
services:
py-db:
image: harbor.shihao.com/kpyun/py-mysql:1.0
container_name: py-db
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "quote"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "passwd"
volumes:
- py-mysql-data:/var/lib/mysql
networks:
- py-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 5s
retries: 10
py-web:
image: harbor.shihao.com/kpyun/py-web:1.0
container_name: py-web
environment:
DB_HOST: py-db
DB_PORT: "3306"
DB_USER: jiu
DB_PASSWORD: passwd
networks:
- py-net
depends_on:
py-db:
condition: service_healthy
py-nginx:
image: harbor.shihao.com/kpyun/py-nginx:1.0
container_name: py-nginx
ports:
- "5001:80"
networks:
- py-net
depends_on:
- py-web
networks:
py-net:
driver: bridge
volumes:
py-mysql-data:
EOFYML
cd /root/py-compose
docker-compose up -d
echo "等待服务启动..."
sleep 20
echo ""
echo "=== 验证 ==="
docker-compose ps
echo ""
curl -s http://127.0.0.1:5001 2>&1 | head -20
echo ""
echo "访问 http://10.0.0.90:5001 验证名言展示页"
echo "验证通过后,执行: cd /root/py-compose && docker-compose down -t 0"
ok "单机 Compose 验证部署完成"
echo ""
info "==========================================="
info "请在浏览器访问: ${BOLD}http://10.0.0.90:5001${RESET}"
info "确认名言展示页正常后,执行关闭命令:"
info " ${BOLD}cd /root/py-compose && docker-compose down -t 0${RESET}"
info "==========================================="
ok "第四阶段完成!3个 Python 镜像已推送 Harbor"

第五阶段:Swarm Stack 集群编排#

这是整个项目的核心环节——将前面构建的 Java + Python 两套应用、统一 Nginx、TLS 证书、双层 Overlay 网络通过一条 docker stack deploy 命令一次性编排到 Swarm 集群

架构回顾#

用户浏览器
http :8080 → 302 → https :4433
┌─────────────────────────────────────────────┐
│ 统一 Nginx (TLS 终止, replicas: 1) │
│ Network: frontend · placement: master │
│ mode: host 宿主机 :8080→容器 :80 │
│ 宿主机 :4433→容器 :443 │
│ java.shihao.com → ruoyi-ui:80 │
│ py.shihao.com → py-nginx:80 │
└──────┬───────────────────────┬──────────────┘
│ │
┌──────▼─────────┐ ┌───────▼──────────┐
│ ruoyi-ui │ │ py-nginx │
│ replicas: 2 │ │ replicas: 2 │
│ frontend │ │ frontend │
└──────┬─────────┘ └───────┬──────────┘
│ │
┌──────▼─────────┐ ┌───────▼──────────┐
│ java-web │ │ py-web │
│ replicas: 2 │ │ replicas: 2 │
│ frontend+backend│ │ frontend+backend │
└──┬─────────┬───┘ └────────┬─────────┘
│ │ │
┌──▼──┐ ┌───▼───┐ ┌───▼──┐
│java │ │ redis │ │ py │
│-db │ │server │ │ -db │
│ 1 │ │ 1 │ │ 1 │
└─────┘ └───────┘ └──────┘
└──────────── backend ─────────────┘

端口映射流程#

Nginx 容器内部只能看到自己的端口,不知道宿主机的映射端口。

浏览器访问 http://java.shihao.com:8080
宿主机 :8080 ──mode:host──▶ 容器 :80 (Nginx listen 80)
Nginx 返回 302 跳转
Location: https://java.shihao.com:4433
浏览器重新请求 https://java.shihao.com:4433
宿主机 :4433 ──mode:host──▶ 容器 :443 (Nginx listen 443 ssl)
SSL 解密后按域名分发:
java.shihao.com → ruoyi-ui:80 (若依前端)
py.shihao.com → py-nginx:80 (Flask名言)

关键:容器内 listen 的是 80/443,但返回给浏览器的 Location 头必须写宿主机端口 4433——因为浏览器只认宿主机端口。

创建 Overlay 网络#

Terminal window
root@SH-master ~# docker network create --driver overlay frontend
zyfnqdpkdrgojfgb4wgmh8k0i
root@SH-master ~# docker network create --driver overlay backend
w7ma5m8hn7d6h7hd01kemrtko
root@SH-master ~# docker network ls --filter driver=overlay
NETWORK ID NAME DRIVER "SCOPE"
w7ma5m8hn7d6 backend overlay swarm
zyfnqdpkdrgo frontend overlay swarm
yr8igd2u2etu ingress overlay swarm
"SCOPE 为 swarm 表示集群全局可用"
  • Overlay 网络让分布在不同节点的容器就像在同一个局域网里
    • ——这是 Swarm 跨主机通信的基石

准备统一 Nginx 配置#

  • 统一入口 Nginx 负责:TLS 终止 + 按域名分发流量

  • 使用官方 nginx:latest 镜像,配置和证书通过 Docker Config 注入

Terminal window
root@SH-master ~# vim /root/stack-nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# HTTP → HTTPS 跳转
server {
# 浏览器访问8080 --> 容器的80端口
listen 80;
# 容器里就是 80
server_name java.shihao.com py.shihao.com;
return 302 https://$server_name:4433$request_uri;
# 它返回的 Location 头是给浏览器看的,浏览器只认宿主机端口 4433
# 宿主机端口4433(不是 443,因为 443 是 Harbor 的)
# 浏览器拿到4433端口 --> 映射到容器里面的443
}
# ===== Java 应用 =====
server {
listen 443 ssl;
# 容器端口 80 / 443
server_name java.shihao.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://ruoyi-ui:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ===== Python 应用 =====
server {
listen 443 ssl;
server_name py.shihao.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://py-nginx:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

创建 Docker Configs#

Terminal window
root@SH-master ~# docker config create nginx_conf /root/stack-nginx.conf
root@SH-master ~# docker config create nginx_cert /root/mycat/certs/fullchain02.crt
root@SH-master ~# docker config create nginx_key /root/mycat/private/nginx-server.key
root@SH-master ~# docker config ls
ID NAME CREATED
s78xx nginx_conf 29 seconds ago
sj1xx nginx_cert 34 seconds ago
p75xx nginx_key 25 seconds ago

image-20260516215736865
image-20260516215736865

编写 Swarm Stack 编排文件#

Terminal window
root@SH-master ~# vim /root/swarm-stack.yml
# ============================================
# Swarm Stack — 一键部署 Java + Python 全部应用
# 端口: Nginx 8080(HTTP) + 4433(HTTPS)
# Harbor 已占用 80/443,故应用换用 8080/4433
# ============================================
networks:
frontend:
driver: overlay
backend:
driver: overlay
volumes:
java-mysql-data:
py-mysql-data:
configs:
nginx_conf:
external: true
nginx_cert:
external: true
nginx_key:
external: true
services:
# ==========================================
# ① 统一 Nginx 入口(TLS 终止 + 域名分发)
# ==========================================
nginx:
image: nginx:latest
ports:
- target: 80
published: 8080
protocol: tcp
mode: host
- target: 443
published: 4433
protocol: tcp
mode: host
networks:
- frontend
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
- source: nginx_cert
target: /etc/nginx/ssl/fullchain.pem
- source: nginx_key
target: /etc/nginx/ssl/server.key
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
delay: 5s
# ==========================================
# ② Java 前端(RuoYi-Vue UI)
# ==========================================
ruoyi-ui:
image: harbor.shihao.com/kpyun/ruoyi-frontend:1.0
networks:
- frontend
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ③ Java 后端(Spring Boot)
# ==========================================
java-web:
image: harbor.shihao.com/kpyun/ruoyi-backend:1.0
networks:
- frontend
- backend
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://java-db:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: jiu
SPRING_DATASOURCE_PASSWORD: oldboy123.com
SPRING_REDIS_HOST: redis-server
SPRING_REDIS_PORT: "6379"
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ④ Java 数据库(MySQL + 初始化脚本)
# ==========================================
java-db:
image: harbor.shihao.com/kpyun/ruoyi-mysql:1.0
networks:
- backend
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "ry-vue"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "oldboy123.com"
command:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
- "--default-authentication-plugin=mysql_native_password"
volumes:
- java-mysql-data:/var/lib/mysql
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
# ==========================================
# ⑤ Redis 缓存
# ==========================================
redis-server:
image: harbor.shihao.com/kpyun/redis:7.2.8
networks:
- backend
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
# ==========================================
# ⑥ Python 前端 Nginx
# ==========================================
py-nginx:
image: harbor.shihao.com/kpyun/py-nginx:1.0
networks:
- frontend
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ⑦ Python Flask 后端
# ==========================================
py-web:
image: harbor.shihao.com/kpyun/py-web:1.0
networks:
- frontend
- backend
environment:
DB_HOST: py-db
DB_PORT: "3306"
DB_USER: jiu
DB_PASSWORD: passwd
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ⑧ Python 数据库(MySQL + 名言初始化脚本)
# ==========================================
py-db:
image: harbor.shihao.com/kpyun/py-mysql:1.0
networks:
- backend
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "quote"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "passwd"
volumes:
- py-mysql-data:/var/lib/mysql
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any

一键部署#

Terminal window
# 先停掉单机 Compose(端口冲突)
root@SH-master ~# docker-compose -f /root/ruoyi-compose/docker-compose.yml down -t 0
root@SH-master ~# docker-compose -f /root/py-compose/docker-compose.yml down -t 0
# 部署 Stack
root@SH-master ~# docker stack deploy -c /root/swarm-stack.yml swarm-apps
Creating network swarm-apps_frontend
Creating network swarm-apps_backend
Creating service swarm-apps_nginx
Creating service swarm-apps_ruoyi-ui
Creating service swarm-apps_java-web
Creating service swarm-apps_java-db
Creating service swarm-apps_redis-server
Creating service swarm-apps_py-nginx
Creating service swarm-apps_py-web
Creating service swarm-apps_py-db
# 查看 Stack 状态
root@SH-master ~# docker stack ls
NAME SERVICES ORCHESTRATOR
swarm-apps 8 Swarm

image-20260516220421152
image-20260516220421152

  • 首次部署 Worker 节点需要从 Harbor 拉取镜像,可能需要 1~3 分钟

  • 不要急,等所有 replica 就绪

phase5-stack-deploy.sh#

#!/bin/sh
# ============================================
# 第五阶段:Swarm Stack 集群编排 一键部署
# 作者: 久棹
# 用法: 在 master 节点以 root 执行
# dash phase5-stack-deploy.sh
# ============================================
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
warn() { printf '%b\n' "${YELLOW}[WARN]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
MASTER_IP="10.0.0.90"
SLAVE01_IP="10.0.0.91"
SLAVE02_IP="10.0.0.92"
SSH_USER="test"; SSH_PASS="1"
# SSH 远程执行(仅用于 slave 节点)
ssh_sudo() {
local ip="$1"; local cmd="$2"
local escaped
escaped=$(printf '%s' "$cmd" | sed 's/"/\\"/g')
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; echo ${SSH_PASS} | sudo -S bash -c \"${escaped}\""
}
# ---------- 1. 停掉单机 Compose ----------
step "5.1 停掉单机 Compose (释放端口)"
cd /root/ruoyi-compose && docker-compose down -t 0 2>/dev/null || true
cd /root/py-compose && docker-compose down -t 0 2>/dev/null || true
echo "单机 Compose 已停掉"
ok "单机 Compose 已停掉"
# ---------- 2. 创建 Overlay 网络 ----------
step "5.2 创建 Overlay 网络"
docker network create --driver overlay frontend 2>/dev/null || echo "frontend 已存在"
docker network create --driver overlay backend 2>/dev/null || echo "backend 已存在"
docker network ls --filter driver=overlay
ok "Overlay 网络 frontend + backend 已创建"
# ---------- 3. 生成 Nginx 统一入口配置 ----------
step "5.3 生成 stack-nginx.conf"
cat > /root/stack-nginx.conf << 'EOFNG'
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# HTTP → HTTPS 跳转
server {
# 浏览器访问8080 --> 容器的80端口
listen 80;
# 容器里就是 80
server_name java.shihao.com py.shihao.com;
return 302 https://$server_name:4433$request_uri;
# 它返回的 Location 头是给浏览器看的,浏览器只认宿主机端口 4433
# 宿主机端口4433(不是 443,因为 443 是 Harbor 的)
# 浏览器拿到4433端口 --> 映射到容器里面的443
}
# ===== Java 应用 =====
server {
listen 443 ssl;
# 容器端口 80 / 443
server_name java.shihao.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://ruoyi-ui:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# ===== Python 应用 =====
server {
listen 443 ssl;
server_name py.shihao.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://py-nginx:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
EOFNG
echo "stack-nginx.conf 已生成"
ok "Nginx 统一入口配置已生成"
# ---------- 4. 创建 Docker Configs ----------
step "5.4 创建 Docker Configs"
docker config rm nginx_conf 2>/dev/null || true
docker config rm nginx_cert 2>/dev/null || true
docker config rm nginx_key 2>/dev/null || true
docker config create nginx_conf /root/stack-nginx.conf
docker config create nginx_cert /root/mycat/certs/fullchain02.crt
docker config create nginx_key /root/mycat/private/nginx-server.key
echo "=== Docker Configs ==="
docker config ls
ok "Docker Configs 创建完成"
# ---------- 5. 生成 swarm-stack.yml ----------
step "5.5 生成 swarm-stack.yml"
cat > /root/swarm-stack.yml << 'EOFSTACK'
# ============================================
# Swarm Stack — 一键部署 Java + Python 全部应用
# 端口: Nginx 8080(HTTP) + 4433(HTTPS)
# Harbor 已占用 80/443,故应用换用 8080/4433
# ============================================
networks:
frontend:
driver: overlay
backend:
driver: overlay
volumes:
java-mysql-data:
py-mysql-data:
configs:
nginx_conf:
external: true
nginx_cert:
external: true
nginx_key:
external: true
services:
# ==========================================
# ① 统一 Nginx 入口(TLS 终止 + 域名分发)
# ==========================================
nginx:
image: nginx:latest
ports:
- target: 80
published: 8080
protocol: tcp
mode: host
- target: 443
published: 4433
protocol: tcp
mode: host
networks:
- frontend
configs:
- source: nginx_conf
target: /etc/nginx/nginx.conf
- source: nginx_cert
target: /etc/nginx/ssl/fullchain.pem
- source: nginx_key
target: /etc/nginx/ssl/server.key
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
delay: 5s
# ==========================================
# ② Java 前端(RuoYi-Vue UI)
# ==========================================
ruoyi-ui:
image: harbor.shihao.com/kpyun/ruoyi-frontend:1.0
networks:
- frontend
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ③ Java 后端(Spring Boot)
# ==========================================
java-web:
image: harbor.shihao.com/kpyun/ruoyi-backend:1.0
networks:
- frontend
- backend
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://java-db:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
SPRING_DATASOURCE_USERNAME: jiu
SPRING_DATASOURCE_PASSWORD: oldboy123.com
SPRING_REDIS_HOST: redis-server
SPRING_REDIS_PORT: "6379"
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ④ Java 数据库(MySQL + 初始化脚本)
# ==========================================
java-db:
image: harbor.shihao.com/kpyun/ruoyi-mysql:1.0
networks:
- backend
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "ry-vue"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "oldboy123.com"
command:
- "--character-set-server=utf8mb4"
- "--collation-server=utf8mb4_unicode_ci"
- "--default-authentication-plugin=mysql_native_password"
volumes:
- java-mysql-data:/var/lib/mysql
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
# ==========================================
# ⑤ Redis 缓存
# ==========================================
redis-server:
image: harbor.shihao.com/kpyun/redis:7.2.8
networks:
- backend
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
# ==========================================
# ⑥ Python 前端 Nginx
# ==========================================
py-nginx:
image: harbor.shihao.com/kpyun/py-nginx:1.0
networks:
- frontend
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ⑦ Python Flask 后端
# ==========================================
py-web:
image: harbor.shihao.com/kpyun/py-web:1.0
networks:
- frontend
- backend
environment:
DB_HOST: py-db
DB_PORT: "3306"
DB_USER: jiu
DB_PASSWORD: passwd
deploy:
replicas: 2
restart_policy:
condition: any
# ==========================================
# ⑧ Python 数据库(MySQL + 名言初始化脚本)
# ==========================================
py-db:
image: harbor.shihao.com/kpyun/py-mysql:1.0
networks:
- backend
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "quote"
MYSQL_USER: "jiu"
MYSQL_PASSWORD: "passwd"
volumes:
- py-mysql-data:/var/lib/mysql
deploy:
replicas: 1
placement:
constraints:
- "node.hostname == SH-master"
restart_policy:
condition: any
EOFSTACK
echo "swarm-stack.yml 已生成"
ok "swarm-stack.yml 生成完成"
# ---------- 6. 一键部署 ----------
step "5.6 docker stack deploy 一键部署"
docker stack deploy -c /root/swarm-stack.yml swarm-apps
info "等待所有服务就绪(首次拉取镜像约 1~3 分钟)..."
sleep 30
# 检查服务状态
echo ""
echo "=== 服务清单 ==="
docker stack services swarm-apps --format 'table {{.Name}}\t{{.Replicas}}\t{{.Image}}'
echo ""
ok "8 个服务已部署"
# ---------- 7. 副本分布 ----------
step "5.7 服务副本分布 + Overlay 网络"
echo ""
echo "=== 副本分布 ==="
docker stack ps swarm-apps --filter desired-state=running --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}'
echo ""
echo "=== Overlay 网络 (master) ==="
docker network ls --filter driver=overlay
# 检查 slave 节点的 Overlay 网络
for ip in $SLAVE01_IP $SLAVE02_IP; do
echo ""
info "Overlay 网络 ($ip):"
ssh_sudo "$ip" "docker network ls --filter driver=overlay --format ' {{.Name}} {{.Driver}} {{.Scope}}'"
done
# ---------- 8. 快速功能验证 ----------
step "5.8 快速功能验证"
echo ""
info "测试 Java 应用:"
curl -skI --connect-timeout 10 https://java.shihao.com:4433 2>&1 | head -3 && ok "Java 若依 正常"
echo ""
info "测试 Python 应用:"
curl -skI --connect-timeout 10 https://py.shihao.com:4433 2>&1 | head -3 && ok "Python 名言 正常"
echo ""
info "测试 Harbor:"
curl -skI --connect-timeout 10 https://harbor.shihao.com 2>&1 | head -3 && ok "Harbor 正常"
echo ""
echo -e "${BOLD}${GREEN}==========================================="
echo " 第五阶段完成!Swarm Stack 一键部署成功"
echo "===========================================${RESET}"
echo ""
echo -e "访问地址:"
echo -e " ${CYAN}https://java.shihao.com:4433${RESET} → 若依管理系统"
echo -e " ${CYAN}https://py.shihao.com:4433${RESET} → 每日名言"
echo -e " ${CYAN}https://harbor.shihao.com${RESET} → Harbor 仓库"
echo ""

验证服务状态#

  • 查看全部服务
    • docker stack services swarm-apps

image-20260517184040471
image-20260517184040471


  • 查看副本分布在哪些节点
    • docker stack ps swarm-apps
    • 4433->443/tcp 8080->80/tcp

image-20260517184531130
image-20260517184531130

Terminal window
1)副本分布
root@SH-master ~# docker stack ps swarm-apps
cfcxxx swarm-apps_java-db.1 ruoyi-mysql:1.0 SH-master Running
1bwxxx swarm-apps_java-web.1 ruoyi-backend:1.0 SH-master Running
hzzxxx swarm-apps_java-web.2 ruoyi-backend:1.0 SH-slave01 Running
xwuxxx swarm-apps_nginx.1 nginx:latest SH-slave01 Running
1juxxx swarm-apps_py-db.1 py-mysql:1.0 SH-master Running
u6cxxx swarm-apps_py-nginx.1 py-nginx:1.0 SH-slave02 Running
357xxx swarm-apps_py-nginx.2 py-nginx:1.0 SH-master Running
3jaxxx swarm-apps_py-web.1 py-web:1.0 SH-slave01 Running
vz4xxx swarm-apps_py-web.2 py-web:1.0 SH-slave02 Running
nuixxx swarm-apps_redis-server.1 redis:7.2.8 SH-master Running
pzyxxx swarm-apps_ruoyi-ui.1 ruoyi-frontend:1.0 SH-master Running
6fpxxx swarm-apps_ruoyi-ui.2 ruoyi-frontend:1.0 SH-slave02 Running
2)Overlay网络
root@SH-slave01 ~# docker network ls --filter driver=overlay
NETWORK ID NAME DRIVER SCOPE
yr8igd2u2etu ingress overlay swarm
8llqkvyt4x2q swarm-apps_backend overlay swarm
3104hg0bbobi swarm-apps_frontend overlay swarm
"Worker 上也能看到 Overlay 网络"
root@SH-slave02 ~# docker network ls --filter driver=overlay
NETWORK ID NAME DRIVER SCOPE
yr8igd2u2etu ingress overlay swarm
8llqkvyt4x2q swarm-apps_backend overlay swarm
3104hg0bbobi swarm-apps_frontend overlay swarm

功能测试#

Terminal window
1)HTTPS 访问 Java 应用
root@SH-master ~# curl -sI https://java.shihao.com:4433
HTTP/1.1 200 OK
Server: nginx/1.31.0
Date: Sun, 17 May 2026 11:02:16 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 12909
Connection: keep-alive
Last-Modified: Sat, 16 May 2026 11:49:08 GMT
ETag: "6a0859b4-326d"
Accept-Ranges: bytes
2)HTTPS 访问 Python 名言应用
root@SH-master ~# curl -sI https://py.shihao.com:4433
HTTP/1.1 200 OK
Server: nginx/1.31.0
Date: Sun, 17 May 2026 11:04:45 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1461
Connection: keep-alive
3)浏览器访问
https://harbor.shihao.com:443 --> Harbor 镜像仓库
https://java.shihao.com:4433 --> 若依管理系统登录页
https://py.shihao.com:4433 --> 每日名言展示页
三个域名全部通过 HTTPS 访问,无证书警告

image-20260517193011898
image-20260517193011898

image-20260517193058950
image-20260517193058950

image-20260517193144780
image-20260517193144780

Swarm 集群能力验证#

以下测试验证 Swarm 的四大核心能力:负载均衡、跨节点 Overlay 通信、网络隔离、服务自愈

① 负载均衡#

Terminal window
# 查看 ruoyi-ui 副本分布(2 个副本在不同节点)
root@SH-master ~# docker service ps swarm-apps_ruoyi-ui --filter desired-state=running --format "{{.Name}} {{.Node}}"
swarm-apps_ruoyi-ui.1 SH-slave02
swarm-apps_ruoyi-ui.2 SH-slave01
# Nginx 统一入口中配置了:
# proxy_pass http://ruoyi-ui:80;
# Swarm DNS 将 ruoyi-ui 解析为 2 个副本的虚拟IP
# Nginx 自动 round-robin 轮询分发请求

结论:Nginx 通过 Swarm DNS 实现服务发现 + 负载均衡,请求自动分发到不同节点的 ruoyi-ui 副本

② 跨节点 Overlay 通信#

Terminal window
# 所有节点都能看到 frontend + backend Overlay 网络
1)slav01 & 02 分别执行
root@SH-slave01 ~# docker network ls --filter driver=overlay
NETWORK ID NAME DRIVER SCOPE
8llqkvyt4x2q swarm-apps_backend overlay swarm
3104hg0bbobi swarm-apps_frontend overlay swarm
yr8igd2u2etu ingress overlay swarm
root@SH-slave02 ~# docker network ls --filter driver=overlay
Worker 节点同样能看到 Overlay 网络
# ruoyi-ui 容器 (slave01) curl 访问 java-web (可能在任意节点)
# 跨 Overlay 服务名通信
2)slav01执行
root@SH-slave01 ~# CID=$(docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1)
root@SH-slave01 ~# docker exec $CID curl -sI --connect-timeout 5 http://java-web:8080 | head -1
HTTP/1.1 200
3)master执行
# Nginx 容器 (master) curl 访问 py-nginx (slave01/slave02)
root@SH-master ~# CID=$(docker ps -q --filter name=swarm-apps_nginx | head -1)
root@SH-master ~# docker exec $CID curl -sI --connect-timeout 5 http://py-nginx:80 | head -1
HTTP/1.1 200 OK

结论:Overlay 网络跨 3 个节点工作正常,容器通过服务名直接通信,无需关心对方在哪个节点

③ 数据库网络隔离#

Terminal window
# ruoyi-ui 只在 frontend 网络,java-db 只在 backend 网络
--> ruoyi-ui DNS 无法解析 java-db
1)slave01验证
# 验证:ruoyi-ui 尝试 DNS 解析 java-db
root@SH-slave01 ~# CID=$(docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1)
root@SH-slave01 ~# docker exec $CID getent hosts java-db
(无输出) DNS 解析不到,网络隔离生效
2)slave02验证
# java-web 同时加入 frontend + backend
# → DNS 可以正常解析 java-db
root@SH-slave02 ~# CID=$(docker ps -q --filter name=swarm-apps_java-web | head -1)
root@SH-slave02 ~# docker exec $CID getent hosts java-db
10.0.3.14 java-db

结论frontend / backend 双层 Overlay 实现了数据库网络隔离

  • 前端容器只能访问 frontend → DNS 解析不到 backend 服务
  • 后端容器同时连接 frontend + backend → 可正常解析并访问数据库

④ 服务自愈#

Terminal window
1)slave01执行
# 模拟 ruoyi-ui 容器意外崩溃
root@SH-slave01 ~# CID=$(docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1)
root@SH-slave01 ~# docker stop $CID
657424dae0f2
# 等待 5 秒后检查
2)master执行
root@SH-master ~# docker service ps swarm-apps_ruoyi-ui --filter desired-state=running --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}"
NAME NODE CURRENT STATE
swarm-apps_ruoyi-ui.1 SH-slave02 Running 57 minutes ago
swarm-apps_ruoyi-ui.2 SH-slave01 Running about a minute ago 刚重启
# 副本数自动恢复到 2/2
root@SH-master ~# docker service ls --filter name=swarm-apps_ruoyi-ui --format "{{.Replicas}}"
2/2

结论:容器被手动停止后,Swarm 在 5 秒内自动重新调度启动,副本数恢复到 2/2

  • restart_policy: any + Swarm 调度器保障服务高可用
  • 全部 8 个服务都在 Running,无失败任务
Terminal window
root@SH-master ~# docker stack services swarm-apps --format "table {{.Name}}\t{{.Replicas}}"
NAME REPLICAS
swarm-apps_java-db 1/1
swarm-apps_java-web 2/2
swarm-apps_nginx 1/1
swarm-apps_py-db 1/1
swarm-apps_py-nginx 2/2
swarm-apps_py-web 2/2
swarm-apps_redis-server 1/1
swarm-apps_ruoyi-ui 2/2

phase6-verify.sh#

#!/bin/sh
# ============================================
# 第六阶段:Swarm 集群能力验证
# 作者: 久棹
# 用法: 在 master 节点以 root 执行
# dash phase6-verify.sh
# ============================================
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
CYAN='\033[36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { printf '%b\n' "${CYAN}[INFO]${RESET} $1"; }
ok() { printf '%b\n' "${GREEN}[OK]${RESET} $1"; }
err() { printf '%b\n' "${RED}[ERR]${RESET} $1"; }
step() { printf '\n%b\n' "${BOLD}${YELLOW}==== $1 ====${RESET}\n"; }
MASTER_IP="10.0.0.90"
SLAVE01_IP="10.0.0.91"
SLAVE02_IP="10.0.0.92"
SSH_USER="test"; SSH_PASS="1"
# SSH 远程执行(仅用于 slave 节点)
ssh_sudo() {
local ip="$1"; local cmd="$2"
local escaped
escaped=$(printf '%s' "$cmd" | sed 's/"/\\"/g')
sshpass -p "$SSH_PASS" ssh -o StrictHostKeyChecking=no ${SSH_USER}@${ip} \
"export TERM=dumb; echo ${SSH_PASS} | sudo -S bash -c \"${escaped}\""
}
# ---------- 1. 基本访问验证 ----------
step "6.1 基本 HTTPS 访问验证"
echo ""
echo -e "${YELLOW}--- Java 若依 ---${RESET}"
curl -skI https://java.shihao.com:4433 2>&1 | head -5
echo ""
echo -e "${YELLOW}--- Python 名言 ---${RESET}"
curl -skI https://py.shihao.com:4433 2>&1 | head -5
echo ""
echo -e "${YELLOW}--- Harbor 仓库 ---${RESET}"
curl -skI https://harbor.shihao.com 2>&1 | head -5
ok "基本访问全部 200 OK"
# ---------- 2. 集群状态 ----------
step "6.2 集群节点状态"
echo "=== Swarm 节点 ==="
docker node ls
echo ""
echo "=== 服务清单 ==="
docker stack services swarm-apps --format 'table {{.Name}}\t{{.Replicas}}\t{{.Image}}'
echo ""
echo "=== 副本分布 ==="
docker stack ps swarm-apps --filter desired-state=running --format 'table {{.Name}}\t{{.Node}}'
ok "集群状态正常"
# ---------- 3. 负载均衡验证 ----------
step "6.3 负载均衡验证"
echo -e "${YELLOW}ruoyi-ui 副本分布:${RESET}"
docker service ps swarm-apps_ruoyi-ui --filter desired-state=running --format ' {{.Name}} → {{.Node}}'
echo ""
info "Nginx 通过 Swarm DNS 将 proxy_pass http://ruoyi-ui:80 自动 round-robin"
info "分发到 2 个副本(位于不同节点),实现负载均衡"
ok "负载均衡: ruoyi-ui ×2 副本 + Swarm DNS 轮询"
# ---------- 4. 跨节点 Overlay 通信 ----------
step "6.4 跨节点 Overlay 通信验证"
echo -e "${YELLOW}Overlay 网络 (slave01):${RESET}"
ssh_sudo "$SLAVE01_IP" "docker network ls --filter driver=overlay --format ' {{.Name}} {{.Driver}} {{.Scope}}'"
echo ""
info "测试: ruoyi-ui(slave01) --Overlay--> java-web(任意节点)"
CID=$(ssh_sudo "$SLAVE01_IP" "docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1")
if [ -n "$CID" ]; then
RESULT=$(ssh_sudo "$SLAVE01_IP" "docker exec $CID curl -sI --connect-timeout 5 http://java-web:8080 2>&1 | head -1")
echo -e " ruoyi-ui curl java-web:8080 → ${GREEN}${RESULT}${RESET}"
fi
echo ""
info "测试: nginx(master) --Overlay--> py-nginx(slave)"
CID2=$(docker ps -q --filter name=swarm-apps_nginx | head -1)
if [ -n "$CID2" ]; then
RESULT2=$(docker exec $CID2 curl -sI --connect-timeout 5 http://py-nginx:80 2>&1 | head -1)
echo -e " nginx curl py-nginx:80 → ${GREEN}${RESULT2}${RESET}"
fi
ok "跨节点 Overlay 通信正常"
# ---------- 5. 数据库网络隔离 ----------
step "6.5 数据库网络隔离验证"
echo -e "${YELLOW}测试: ruoyi-ui(frontend only) → java-db(backend only)${RESET}"
CID3=$(ssh_sudo "$SLAVE01_IP" "docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1")
if [ -n "$CID3" ]; then
RESULT3=$(ssh_sudo "$SLAVE01_IP" "docker exec $CID3 getent hosts java-db 2>&1")
if [ -z "$RESULT3" ]; then
echo -e " getent hosts java-db → ${GREEN}(无结果 - 隔离生效)${RESET}"
else
echo -e " getent hosts java-db → ${RED}${RESULT3} (隔离失败!)${RESET}"
fi
fi
echo ""
echo -e "${YELLOW}测试: java-web(frontend+backend) → java-db(backend)${RESET}"
CID4=$(ssh_sudo "$SLAVE02_IP" "docker ps -q --filter name=swarm-apps_java-web | head -1")
if [ -n "$CID4" ]; then
RESULT4=$(ssh_sudo "$SLAVE02_IP" "docker exec $CID4 getent hosts java-db 2>&1")
echo -e " getent hosts java-db → ${GREEN}${RESULT4} (正常解析)${RESET}"
fi
ok "DB 网络隔离: frontend 容器无法解析 backend 服务"
# ---------- 6. 服务自愈 ----------
step "6.6 服务自愈验证"
echo -e "${YELLOW}模拟: 强制停止 ruoyi-ui 容器${RESET}"
VICTIM=$(ssh_sudo "$SLAVE01_IP" "docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1")
echo -e " 停止容器: ${VICTIM}"
if [ -n "$VICTIM" ]; then
ssh_sudo "$SLAVE01_IP" "docker stop $VICTIM" > /dev/null 2>&1
info "等待 Swarm 自动恢复 (5 秒)..."
sleep 5
echo ""
docker service ps swarm-apps_ruoyi-ui --filter desired-state=running --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}'
echo ""
REPLICAS=$(docker service ls --filter name=swarm-apps_ruoyi-ui --format '{{.Replicas}}')
echo -e "ruoyi-ui 副本数: ${GREEN}${REPLICAS}${RESET} (应为 2/2)"
fi
ok "服务自愈: 容器崩溃后 5 秒内自动恢复"
# ---------- 7. 总结 ----------
step "6.7 验证总结"
echo ""
printf "┌─────────────────────────────────────────────────┐\n"
printf "│ %-47s │\n" " 测试项"
printf "├─────────────────────────────────────────────────┤\n"
printf "│ %-47s │\n" " 集群节点状态 3/3 Ready ✅"
printf "│ %-47s │\n" " 基本 HTTPS 访问 3 域名 200 ✅"
printf "│ %-47s │\n" " 负载均衡 ruoyi-ui ×2 ✅"
printf "│ %-47s │\n" " 跨节点 Overlay 通信 跨3节点通 ✅"
printf "│ %-47s │\n" " 数据库网络隔离 双层 overlay ✅"
printf "│ %-47s │\n" " 服务自愈 5s 自动恢复 ✅"
printf "└─────────────────────────────────────────────────┘\n"
echo ""
echo -e "${BOLD}${GREEN}==========================================="
echo " 全部验证通过!Swarm 集群运行正常"
echo "===========================================${RESET}"
echo ""

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Docker结课实践考核
https://www.kpyun.fun/posts/docker/docker09/
作者
久棹
发布于
2026-03-10
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
久棹
只要胆子大,天天寒暑假!
公告
欢迎来到久棹的技术小站!本站专注 Linux 运维学习笔记分享,如有问题欢迎交流探讨 🎉
分类
标签
站点统计
文章
98
分类
11
标签
203
总字数
244,453
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录