Docker结课实践考核
《Docker 容器技术》结课实践考核报告
[TOC]
第一阶段:环境准备
系统优化
'开启模板机后'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\]\\$ 'EOFsource ~/.bashrc
# 更换软件源sed -i '/^URI/c\URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/' /etc/apt/sources.list.d/ubuntu.sourcesapt-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 -rfcat > /etc/update-motd.d/99-local << 'SCRIPT'#!/bin/bashcat << 'EOF' (@) * (@) * (@) : * (@) * (@) * .; (@) * (@) * (@) * (@) * ; * ; (@) * ; * : ;\ \ \ \| / / /; \\ \ Y/ / / `_\ |/ _' ' / \\Y// \ ( ,-}={-, ) \_//((\_/ //))(\ (/ )) (/EOFSCRIPTchmod +x 99-local# 赋权 --> 返回目录cd -
# ssh连接优化cat >>/etc/ssh/sshd_config<<EOFUseDNS no# 相当于网络命令的-n选项,这个就是说不解析为主机名,直接成IP地址.GSSAPIAuthentication no# 关闭GSS认证.EOFsystemctl restart ssh.service
# 调整时区timedatectl set-timezone Asia/Shanghai
# 类似于SElinuxsystemctl disable --now apparmor &> /dev/null
# 关闭防火墙systemctl disable --now ufw &> /dev/null
# 安装Iptables持久化工具# 默认有弹窗# 防止 debconf 交互export DEBIAN_FRONTEND=noninteractivedebconf-set-selections << EOFiptables-persistent iptables-persistent/autosave_v4 boolean trueiptables-persistent iptables-persistent/autosave_v6 boolean trueEOF# 安装(不会卡住)apt-get install -y iptables-persistent# 开机自启# systemctl enable --now iptables.servicesystemctl daemon-reload
# ssh允许root登录echo 'PermitRootLogin yes' >> /etc/ssh/sshd_configecho 'root:1' | sudo chpasswdsystemctl restart ssh
# 文件描述符# 分开软硬限制cat >> /etc/security/limits.conf << 'EOF'* soft nofile 65535* hard nofile 65535root soft nofile 65535root hard nofile 65535EOF# Ubuntu 的 PAM 默认没有启用 pam_limits.soecho "session required pam_limits.so" >> /etc/pam.d/common-sessionecho "session required pam_limits.so" >> /etc/pam.d/common-session-noninteractive# 手动添加# Systemd 服务的限制 --> 有些服务不受 limits.conf 直接控制echo "DefaultLimitNOFILE=65535" >> /etc/systemd/system.confecho "DefaultLimitNOFILE=65535" >> /etc/systemd/user.confreboot# 重启系统生效主机名 & IP
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.yamlsed -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 "- 以上两条严格意义来讲不算脚本
- 都是用基础命名堆上去的
- 连循环, 判断, 函数都没有
'模版机只需要这两个脚本即可'root@ubuntu:/server/script# tree ./Command 'tree' not foundroot@ubuntu:/server/script# apt install treeroot@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.shresize2fs 1.47.0 (5-Feb-2023)
3)打快照'这个以后就是我们的模板机'
创建链接克隆

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

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

Docker 环境安装
#!/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-releaseOS_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 EngineDocumentation=https://docs.docker.comAfter=network-online.target firewalld.serviceWants=network-online.target
[Service]Type=notifyExecStart=/usr/bin/dockerdExecReload=/bin/kill -s HUP \$MAINPIDTimeoutSec=0RestartSec=2Restart=alwaysStartLimitBurst=3StartLimitInterval=60sLimitNOFILE=infinityLimitNPROC=infinityLimitCORE=infinityTasksMax=infinityDelegate=yesKillMode=processOOMScoreAdjust=-500
[Install]WantedBy=multi-user.targetEOF 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 不存在,跳过软链接创建。" fielse 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 $11)上传脚本和文件root@SH-master tmp# rz....root@SH-master tmp# lsautoinstall-docker.zip
2)解压root@SH-master tmp# unzip autoinstall-docker.zip -d /root/root@SH-master tmp# cd /rootroot@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.shroot@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.jsonsystemd 服务文件已生成: /usr/lib/systemd/system/docker.serviceDocker 服务已启动并设置为开机自启
========================================= 安装完成!版本信息如下:=========================================
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 ~# lsdownload install-docker.shroot@SH-master ~# tar zcf hh.tar.gz download/ install-docker.shroot@SH-master ~# lsdownload hh.tar.gz install-docker.shroot@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:00root@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:00root@SH-slave01 ~# ls /home/test/hh.tar.gzroot@SH-slave01 ~# cd /home/test/root@SH-slave01 test# tar xf hh.tar.gzroot@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集群初始化
1)hosts解析root@SH-master ~# cat >> /etc/hosts <<EOF10.0.0.90 SH-master harbor.shihao.com10.0.0.91 SH-slave0110.0.0.92 SH-slave0210.0.0.90 java.shihao.com py.shihao.comEOF'harbor.shihao.com' --> 为后面域名解析做准备'java.shihao.com py.shihao.com' --> 为后面两个应用做准备
root@SH-master ~# ping -W2 -c2 SH-masterPING 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 ms64 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 1046msrtt min/avg/max/mdev = 0.027/0.030/0.034/0.003 msroot@SH-master ~# ping -W2 -c2 SH-slave01PING 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 ms64 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 1023msrtt min/avg/max/mdev = 0.225/0.300/0.376/0.075 msroot@SH-master ~# ping -W2 -c2 SH-slave02PING 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 ms64 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 1008msrtt 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:2377Swarm 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:2377root@SH-slave01 test# docker swarm join --token SWMTKN-1-4e1jktyl6rivm40dea50sbkvjc74bscfzxhh7906k3io941i5s-0oollg7t9xuwsrnqxl5jdikja 10.0.0.90:2377This node joined a swarm as a worker.root@SH-slave02 test# docker swarm join --token SWMTKN-1-4e1jktyl6rivm40dea50sbkvjc74bscfzxhh7906k3io941i5s-0oollg7t9xuwsrnqxl5jdikja 10.0.0.90:2377This node joined a swarm as a worker.'看到 "joined a swarm as a worker" 说明加入成功'
3)Manager 节点验证集群状态root@SH-master ~# docker node lsID HOSTNAME STATUS STATUS VERSIONqhnxxx * SH-master Ready Active "Leader" 29.1.45uvxxx SH-slave01 Ready Active 29.1.4x85xxx 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 配置完成" fidone
# ---------- 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-tokenif [ -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 加入失败" fidone
# ---------- 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)
- GitHub 地址:https://github.com/goharbor/harbor

harbor: 🦄 基于官方的"docker registry"进行二次开发 🦄 是一个适合企业级使用的镜像仓库🔖第三方仓库: --> 轩辕镜像站(docker.xuanyuan.run)'我们的镜像全部都是从👆拉取的'
1)第三方镜像仓库配置root@SH-master ~# echo 镜像密码 | docker login -u 镜像账户 --password-stdin docker.xuanyuan.runhttps://docs.docker.com/go/credential-store/"Login Succeeded"
2)上传解压root@SH-master ~# cd /tmproot@SH-master tmp# rz......root@SH-master tmp# ls harbor-offline-installer-v2.14.3.tgzharbor-offline-installer-v2.14.3.tgzroot@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# lscommon.sh harbor.v2.14.3.tar.gz harbor.yml.tmpl install.sh LICENSE prepareroot@SH-master harbor# cp harbor.yml{.tmpl,}# 把备份文件复制出来"把后缀tmpl去掉"root@SH-master harbor# lscommon.sh harbor.v2.14.3.tar.gz harbor.yml harbor.yml.tmpl install.sh LICENSE prepareroot@SH-master harbor# vim harbor.ymlhostname: 10.0.0.90http: port: 80# 后面再启用HTTPSharbor_admin_password: passwd# 修改登录密码database: password: root123 max_idle_conns: 100 max_open_conns: 900 conn_max_lifetime: 5m conn_max_idle_time: 0data_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: 5m0sjobservice: max_job_workers: 10 max_job_duration_hours: 24 job_loggers: - STD_OUTPUT - FILE logger_sweeper_duration: 1notification: webhook_job_max_retry: 3 webhook_job_http_client_timeout: 3log: level: info local: rotate_count: 50 rotate_size: 200M location: /var/log/harbor_version: 2.15.0proxy: http_proxy: https_proxy: no_proxy: components: - core - jobservice - trivyupload_purging: enabled: true age: 168h interval: 24h dryrun: falsecache: enabled: false expire_hours: 244)安装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 lsNAME STATUS CONFIG FILESharbor running(9) /usr/local/harbor/docker-compose.yml===========================http://10.0.0.90/✅️用户名: admin✅️密码: passwd
新建项目


1)拉镜像&打标签root@SH-master harbor# docker pull docker.xuanyuan.run/mysql:8.0.368.0.36: Pulling from mysqlStatus: Downloaded newer image for docker.xuanyuan.run/mysql:8.0.36root@SH-master harbor# docker tag docker.xuanyuan.run/mysql:8.0.36 10.0.0.90/kpyun/mysql:8.0.36root@SH-master harbor# docker images10.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.36The push refers to repository [10.0.0.90/kpyun/mysql]dial tcp 10.0.0.90:443: connect: connection refused'没有配置证书,所以有问题'HTTPS证书
root@SH-master ~# mkdir mycat && cd mycatroot@SH-master mycat# mkdir private certsroot@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 oksubject=C = CN, ST = Beijing, L = Beijing, O = MyIntermediate, CN = MyIntermediate
3)生成“服务器证书”# 创建扩展配置文件# 为了支持域名,必须用配置文件,不能直接写在命令里root@SH-master mycat# cat > server01.ext <<EOFauthorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names
[alt_names]DNS.1 = harbor.shihao.comIP.1 = 10.0.0.90IP.2 = 127.0.0.1EOFroot@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 oksubject=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 <<EOFauthorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names
[alt_names]DNS.1 = java.shihao.comDNS.2 = py.shihao.comIP.1 = 10.0.0.90IP.2 = 10.0.0.91IP.3 = 10.0.0.92EOF
# 生成 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 根证书到系统信任链
'三台服务器都要做:把根证书导入系统信任'
1)Ubuntu 添加自定义 CAroot@SH-master mycat# cp certs/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crtroot@SH-master mycat# update-ca-certificatesUpdating 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.crtroot@SH-slave01 ~# update-ca-certificates=========================================root@SH-slave02 ~# cp /tmp/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crtroot@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 bundleroot@SH-master mycat# openssl verify -CAfile certs/ca-bundle.crt certs/server.crtcerts/server.crt: OK ✅root@SH-master mycat# openssl verify -CAfile certs/ca-bundle.crt certs/nginx-server.crtcerts/nginx-server.crt: OK ✅配置 Harbor 启用 HTTPS
1)编辑 Harbor 配置,启用 HTTPSroot@SH-master mycat# cd /usr/local/harbor/root@SH-master harbor# vim harbor.ymlhostname: harbor.shihao.com # 改为域名# http 段注释掉(或删除)# http:# port: 80https: # 新增 HTTPS 段 port: 443 # Swarm Nginx 的 443 冲突 <-- 后面应用调接口 certificate: /root/mycat/certs/fullchain01.crt private_key: /root/mycat/private/server.keyharbor_admin_password: passwddatabase: password: root123 # ...其余配置不变2)重新部署 Harbor(install.sh 会自动重建容器)root@SH-master harbor# ./install.sh# 输出省略...✔ ----Harbor has been installed and started successfully.----
3)验证 HTTPSroot@SH-master harbor# docker-compose psNAME ... PORTSnginx ... 0.0.0.0:443->8443/tcp# 443 端口已监听root@SH-master harbor# grep harbor /etc/hosts10.0.0.90 SH-master harbor.shihao.comroot@SH-master harbor# curl -sI https://harbor.shihao.com/HTTP/1.1 200 OK ✅ 返回正常,证书链路可信Server: nginx配置所有节点信任 Harbor
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"]}2)MASTER需要重启root@SH-master harbor# docker-compose down -t 0'停止compose集群'root@SH-master ~# systemctl restart dockerroot@SH-master harbor# docker-compose up -d# 重新启动
3)slaver也要重启'得是修改了daemon.json文件后'root@SH-slave01 ~# systemctl restart dockerroot@SH-slave02 ~# systemctl restart docker
4)推送测试root@SH-master harbor# docker login -u admin -p passwd harbor.shihao.comLogin Succeeded ✅️root@SH-master ~# docker tag docker.xuanyuan.run/mysql:8.0.36 harbor.shihao.com/kpyun/mysql:8.0.36root@SH-master ~# docker push harbor.shihao.com/kpyun/mysql:8.0.36The push refers to repository [harbor.shihao.com/kpyun/mysql]d3f5c7b8a9e1: Pushed...8.0.36: digest: sha256:xxxx size: xxx'推送成功✅'
Windows物理机
1)Windows --> hosts 解析'你自己的 Windows 浏览器端也配一下 hosts'10.0.0.90 harbor.shihao.com10.0.0.90 java.shihao.com10.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/certsroot@SH-master certs# sz rootCA.crt



拉取镜像测试
1)slave01root@SH-slave01 test# docker pull harbor.shihao.com/kpyun/mysql:8.0.368.0.36: Pulling from kpyun/mysqlcf55ff1c80af: Pull completec38d8660e1fa: Pull complete......Digest: sha256:65cxxx....Status: Downloaded newer image for harbor.shihao.com/kpyun/mysql:8.0.36root@SH-slave01 test# docker imagesharbor.shihao.com/kpyun/mysql:8.0.36 65ce08897519 824MB
2)slave02root@SH-slave02 test# docker pull harbor.shihao.com/kpyun/mysql:8.0.36Status: Downloaded newer image for harbor.shihao.com/kpyun/mysql:8.0.36root@SH-slave02 test# docker imagesharbor.shihao.com/kpyun/mysql:8.0.36 65ce08897519 824MBphase2-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 1fi
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/certsmkdir -p /root/mycat/privatechmod 700 /root/mycat/private
# 根证书openssl genrsa -out /root/mycat/private/rootCA.key 2048 2>/dev/nullopenssl 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/nullopenssl 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 << EOFbasicConstraints=CA:TRUEkeyUsage=critical,keyCertSign,cRLSignEOF
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 << EOFauthorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names[alt_names]DNS.1 = harbor.shihao.comIP.1 = 10.0.0.90IP.2 = 127.0.0.1EOF
openssl genrsa -out /root/mycat/private/server.key 2048 2>/dev/nullopenssl 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/nullopenssl 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/nullcat /root/mycat/certs/server.crt /root/mycat/certs/intermediate.crt > /root/mycat/certs/fullchain01.crt
# Nginx 应用证书cat > /tmp/server02.ext << EOFauthorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentextendedKeyUsage = serverAuthsubjectAltName = @alt_names[alt_names]DNS.1 = java.shihao.comDNS.2 = py.shihao.comIP.1 = 10.0.0.90IP.2 = 10.0.0.91IP.3 = 10.0.0.92EOF
openssl genrsa -out /root/mycat/private/nginx-server.key 2048 2>/dev/nullopenssl 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/nullopenssl 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/nullcat /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.crtopenssl verify -CAfile /tmp/ca-bundle.crt /root/mycat/certs/server.crt 2>/dev/nullopenssl 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.ymlsed -i 's/^ port: 80/# port: 80/' harbor.yml
# 启用 https 段并配置证书路径sed -i 's/^#https:/https:/' harbor.ymlsed -i 's/^# port: 443/ port: 443/' harbor.ymlsed -i 's|^#\{0,1\} certificate:.*| certificate: /root/mycat/certs/fullchain01.crt|' harbor.ymlsed -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.ymlsed -i "s|data_volume: .*|data_volume: /var/lib/harbor|" harbor.yml
# 直接安装(install.sh 内置镜像导入 + prepare + compose up)./install.shok "Harbor HTTPS 安装完成 (443)"
# ---------- 4. 三节点导入根证书 ----------step "2.4 三节点导入根证书 + Docker 信任 Harbor"
# Master 自己导入cp /root/mycat/certs/rootCA.crt /usr/local/share/ca-certificates/my-root-ca.crtupdate-ca-certificates --fresh 2>/dev/nullok "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 → 启 Harborcd /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 dockerok "$MASTER_IP Docker 已重启"
cd /usr/local/harbor && docker-compose up -dok "Harbor 已重新启动"
# Slave 节点:写 daemon.json → 重启 Dockerfor 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)

==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 即可
环境准备
1)克隆(下载)代码root@SH-master ~# git clone https://gitee.com/y_project/RuoYi-Vue.gitCloning into 'RuoYi-Vue'...remote: Enumerating objects: 21949, done.'下载的时候,也会给你创建一个目录'root@SH-master ~# lsRuoYi-Vueroot@SH-master ~# ls RuoYi-Vue/bin LICENSE README.md ruoyi-common ruoyi-generator.....
2)安装Jdk + Maven

3)上传解压root@SH-master ~# cd /tmproot@SH-master tmp# rz.........root@SH-master tmp# ls | egrep "maven|jdk|node"apache-maven-3.9.15-bin.tar.gzjdk-17.0.12_linux-x64_bin.tar.gznode-v24.15.0-linux-x64.tar.xzroot@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# cdroot@SH-master ~# vim /etc/profile.d/env.sh#!/bin/bash# javaexport JAVA_HOME=/usr/local/jdk-17.0.12export PATH=$PATH:$JAVA_HOME/bin
# mavenexport MAVEN_HOME=/usr/local/apache-maven-3.9.15export PATH=$PATH:$MAVEN_HOME/bin
# node.jsexport NODEJS_HOME=/usr/local/node-v24.15.0-linux-x64export PATH=$PATH:$NODEJS_HOME/binroot@SH-master ~# source /etc/profile.d/env.sh
5)检查测试root@SH-master ~# java --versionjava 17.0.12 2024-07-16 LTSJava(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 -vApache Maven 3.9.15 (98b2cdbfdb5f1ac8781f537ea9acccaed7922349)Maven home: /usr/local/apache-maven-3.9.15Java version: 17.0.12, vendor: Oracle Corporation, runtime: /usr/local/jdk-17.0.12Default locale: en_US, platform encoding: UTF-8OS name: "linux", version: "6.8.0-31-generic", arch: "amd64", family: "unix"root@SH-master ~# node -vv24.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-minimalStatus: Downloaded newer image for docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimalroot@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/nginxUsing default tag: latestroot@SH-master ~# docker tag docker.xuanyuan.run/nginx:latest nginx:latest=========================="Redis 镜像(直接拉取 + 推送)"root@SH-master ~# docker pull docker.xuanyuan.run/redis:7.2.8root@SH-master ~# docker tag docker.xuanyuan.run/redis:7.2.8 harbor.shihao.com/kpyun/redis:7.2.8root@SH-master ~# docker push harbor.shihao.com/kpyun/redis:7.2.8==========================root@SH-master ~# docker imageseclipse-temurin:21-jre-ubi9-minimal 0b68af521c74 492MBnginx:latest 06aa3d7be10b 240MB数据库镜像(ruoyi-mysql)
1)准备 SQL 初始化文件(从 RuoYi 源码目录)root@SH-master ~# cd /root/RuoYi-Vueroot@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-cacheskip-name-resolvedatadir=/var/lib/mysqlsocket=/var/run/mysqld/mysqld.socksecure-file-priv=/var/lib/mysql-filesuser=mysqlpid-file=/var/run/mysqld/mysqld.pidcharacter-set-server = utf8mb4collation-server = utf8mb4_unicode_ci
[mysql]default-character-set = utf8mb4
[client]socket=/var/run/mysqld/mysqld.sockdefault-character-set = utf8mb4
!includedir /etc/mysql/conf.d/EOF
3)编写 Dockerfileroot@SH-master RuoYi-Vue# vim /root/ruoyi-compose/dockerfile/mysql-1.0FROM mysql:8.0.36LABEL maintainer="jiuzhao"EXPOSE 3306COPY ./conf/my.cnf /etc/my.cnfCOPY ./data/quartz.sql /docker-entrypoint-initdb.d/COPY ./data/ry_20260417.sql /docker-entrypoint-initdb.d/4)构建 + 推送root@SH-master RuoYi-Vue# cd /root/ruoyi-composeroot@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.0root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-mysql:1.058a83c1f4ebd: PushedJava 后端镜像(ruoyi-backend)
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# 数据库的用户名和密码# Redis 地址root@SH-master RuoYi-Vue# vim ./ruoyi-admin/src/main/resources/application.yml# redis 配置redis: host: redis-server port: 63792)Maven 打包root@SH-master RuoYi-Vue# mvn clean package -Dmaven.test.skip=true[INFO] BUILD SUCCESS
root@SH-master RuoYi-Vue# ls ruoyi-admin/target/ruoyi-admin.jarruoyi-admin/target/ruoyi-admin.jarroot@SH-master RuoYi-Vue# cp ruoyi-admin/target/ruoyi-admin.jar /root/ruoyi-compose/data/
3)编写 Dockerfileroot@SH-master RuoYi-Vue# vim /root/ruoyi-compose/dockerfile/jre-1.0FROM eclipse-temurin:21-jre-ubi9-minimalLABEL maintainer="jiuzhao"EXPOSE 8080COPY ./data/ruoyi-admin.jar /CMD ["java", "-jar", "/ruoyi-admin.jar"]4)构建 + 推送root@SH-master RuoYi-Vue# cd /root/ruoyi-composeroot@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.0root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-backend:1.08379cb2e116b: Pushedc06b8ad3393f: Pushed53f074f73cee: Pushed1.0: digest: sha256:d77exxx......前端镜像(ruoyi-frontend)
1)修改后端接口地址"容器化后指向 Swarm 服务名 java-web"root@SH-master ruoyi-compose# cd /root/RuoYi-Vue/ruoyi-uiroot@SH-master ruoyi-ui# grep "baseUrl" vue.config.jsconst baseUrl = 'http://localhost:8080' // 后端接口root@SH-master ruoyi-ui# sed -i "s#localhost:8080#java-web:8080#g" vue.config.jsroot@SH-master ruoyi-ui# npm install --registry=https://registry.npmmirror.com

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 stylesroot@SH-master ruoyi-ui# cp -r dist/ /root/ruoyi-compose/data/
2)编写 Nginx 子配置文件root@SH-master ruoyi-ui# vim /root/ruoyi-compose/conf/default.confserver { 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; }}3)编写 Dockerfileroot@SH-master ruoyi-ui# vim /root/ruoyi-compose/dockerfile/nginx-1.0FROM nginx:latestLABEL maintainer="jiuzhao"EXPOSE 80COPY ./conf/default.conf /etc/nginx/conf.d/COPY ./data/dist /usr/share/nginx/htmlRUN chown -R nginx:nginx /usr/share/nginx/html4)构建 + 推送root@SH-master ruoyi-ui# cd /root/ruoyi-composeroot@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.0root@SH-master ruoyi-compose# docker push harbor.shihao.com/kpyun/ruoyi-frontend:1.0单机 Compose 验证(推 Swarm 前先跑通)
root@SH-master ruoyi-compose# vim /root/ruoyi-compose/docker-compose.ymlname: ruoyiservices: 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:# 启动测试root@SH-master ruoyi-compose# docker-compose up -d

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

# 测试通过后停掉root@SH-master ruoyi-compose# docker-compose down -t 0
浏览器访问: --> '登录Harbor仓库'https://harbor.shihao.com/harbor/projects/2/repositories# 查看镜像
四个 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
# JDKif [ -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 1fi
# Mavenif [ -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 1fi
# Node.jsif [ -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 1fi
# 环境变量cat > /etc/profile.d/env.sh << 'EOF'#!/bin/bash# javaexport JAVA_HOME=/usr/local/jdk-17.0.12export PATH=$PATH:$JAVA_HOME/bin
# mavenexport MAVEN_HOME=/usr/local/apache-maven-3.9.15export PATH=$PATH:$MAVEN_HOME/bin
# node.jsexport NODEJS_HOME=/usr/local/node-v24.15.0-linux-x64export PATH=$PATH:$NODEJS_HOME/binEOF
. /etc/profile.d/env.shecho "环境变量已配置"java --version 2>&1 | head -1mvn -v 2>&1 | head -1node -v 2>&1ok "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 /rootif [ -d /root/RuoYi-Vue ]; then echo "RuoYi-Vue 目录已存在,跳过 clone"else git clone https://gitee.com/y_project/RuoYi-Vue.git echo "RuoYi-Vue 克隆完成"fiok "RuoYi-Vue 源码就绪,项目目录已创建"
# ---------- 3. 拉取基础镜像 ----------step "3.3 拉取基础镜像"
. /etc/profile.d/env.sh
# 登录第三方仓库echo "Oldboy123.com" | docker login -u 13353958307 --password-stdin docker.xuanyuan.run
# 登录 Harbordocker login -u admin -p passwd harbor.shihao.com
# 拉取基础镜像docker pull docker.xuanyuan.run/mysql:8.0.36docker tag docker.xuanyuan.run/mysql:8.0.36 mysql:8.0.36
docker pull docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimaldocker tag docker.xuanyuan.run/eclipse-temurin:21-jre-ubi9-minimal eclipse-temurin:21-jre-ubi9-minimal
docker pull docker.xuanyuan.run/nginx:latestdocker tag docker.xuanyuan.run/nginx:latest nginx:latest
docker pull docker.xuanyuan.run/redis:7.2.8docker tag docker.xuanyuan.run/redis:7.2.8 harbor.shihao.com/kpyun/redis:7.2.8docker 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-cacheskip-name-resolvedatadir=/var/lib/mysqlsocket=/var/run/mysqld/mysqld.socksecure-file-priv=/var/lib/mysql-filesuser=mysqlpid-file=/var/run/mysqld/mysqld.pidcharacter-set-server = utf8mb4collation-server = utf8mb4_unicode_ci
[mysql]default-character-set = utf8mb4
[client]socket=/var/run/mysqld/mysqld.sockdefault-character-set = utf8mb4
!includedir /etc/mysql/conf.d/EOF
# Dockerfilecat > /root/ruoyi-compose/dockerfile/mysql-1.0 << EOFFROM mysql:8.0.36LABEL maintainer="jiuzhao"EXPOSE 3306COPY ./conf/my.cnf /etc/my.cnfCOPY ./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/nullcp /root/RuoYi-Vue/sql/ry_20260417.sql /root/ruoyi-compose/data/ 2>/dev/null
cd /root/ruoyi-composedocker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/ruoyi-mysql:1.0 .docker push harbor.shihao.com/kpyun/ruoyi-mysql:1.0echo "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 || truesed -i "s/username: root/username: jiu/" \ ./ruoyi-admin/src/main/resources/application-druid.yml 2>/dev/null || truesed -i "s/password: password/password: oldboy123.com/" \ ./ruoyi-admin/src/main/resources/application-druid.yml 2>/dev/null || truesed -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 -qcp ruoyi-admin/target/ruoyi-admin.jar /root/ruoyi-compose/data/
# Dockerfilecat > /root/ruoyi-compose/dockerfile/jre-1.0 << EOFFROM eclipse-temurin:21-jre-ubi9-minimalLABEL maintainer="jiuzhao"EXPOSE 8080COPY ./data/ruoyi-admin.jar /CMD ["java", "-jar", "/ruoyi-admin.jar"]EOF
cd /root/ruoyi-composedocker build -f ./dockerfile/jre-1.0 -t harbor.shihao.com/kpyun/ruoyi-backend:1.0 .docker push harbor.shihao.com/kpyun/ruoyi-backend:1.0echo "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 --silentnpm run build:prod
# 复制 distcp -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
# Dockerfilecat > /root/ruoyi-compose/dockerfile/nginx-1.0 << EOFFROM nginx:latestLABEL maintainer="jiuzhao"EXPOSE 80COPY ./conf/default.conf /etc/nginx/conf.d/COPY ./data/dist /usr/share/nginx/htmlRUN chown -R nginx:nginx /usr/share/nginx/htmlEOF
cd /root/ruoyi-composedocker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/ruoyi-frontend:1.0 .docker push harbor.shihao.com/kpyun/ruoyi-frontend:1.0echo "ruoyi-frontend:1.0 构建+推送完成"ok "ruoyi-frontend 镜像构建完成"
# ---------- 7. 单机 Compose 验证 ----------step "3.7 单机 Compose 验证若依4件套"
cat > /root/ruoyi-compose/docker-compose.yml << 'EOFYML'name: ruoyiservices: 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-composedocker-compose up -d
echo "等待服务启动..."sleep 30
echo ""echo "=== 验证 ==="docker-compose psecho ""curl -sI http://127.0.0.1:8088 2>&1 | head -5echo ""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 反向代理)
项目目录结构
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)
1)准备 SQL 初始化文件root@SH-master ~# vim /root/py-compose/data/quote-init.sqlSET 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'),('非淡泊无以明志,非宁静无以致远。', '诸葛亮');2)准备 my.cnfroot@SH-master ~# vim /root/py-compose/conf/my.cnf[mysqld]skip-host-cacheskip-name-resolvedatadir=/var/lib/mysqlsocket=/var/run/mysqld/mysqld.socksecure-file-priv=/var/lib/mysql-filesuser=mysqlpid-file=/var/run/mysqld/mysqld.pidcharacter-set-server = utf8mb4collation-server = utf8mb4_unicode_ci
[mysql]default-character-set = utf8mb4
[client]socket=/var/run/mysqld/mysqld.sockdefault-character-set = utf8mb4
!includedir /etc/mysql/conf.d/3)编写 Dockerfileroot@SH-master ~# vim /root/py-compose/dockerfile/mysql-1.0FROM mysql:8.0.36LABEL maintainer="jiuzhao"EXPOSE 3306COPY ./conf/my.cnf /etc/my.cnfCOPY ./data/quote-init.sql /docker-entrypoint-initdb.d/4)构建 + 推送root@SH-master ~# cd /root/py-composeroot@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.0root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-mysql:1.0Flask 应用镜像(py-web)
1)Flask 主程序root@SH-master py-compose# vim ./data/app.pyfrom flask import Flask, render_templateimport mysql.connectorimport osimport 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)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>3)Python 依赖root@SH-master py-compose# vim ./data/requirements.txtFlask==2.3.3mysql-connector-python==8.1.0gunicorn==21.2.04)编写 Dockerfileroot@SH-master py-compose# vim ./dockerfile/flask-1.0FROM docker.xuanyuan.run/library/python:3.11-slimLABEL maintainer="jiuzhao"ENV LANG=C.UTF-8ENV PYTHONIOENCODING=utf-8WORKDIR /app
# 创建非 root 用户运行 FlaskRUN 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 5000USER wwwCMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]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.0root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-web:1.0Nginx 前端镜像(py-nginx)
1)Nginx 子配置文件root@SH-master py-compose# vim ./conf/default.confserver { 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; }}2)编写 Dockerfileroot@SH-master py-compose# vim ./dockerfile/nginx-1.0FROM nginx:latestLABEL maintainer="jiuzhao"EXPOSE 80COPY ./conf/default.conf /etc/nginx/conf.d/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.0root@SH-master py-compose# docker push harbor.shihao.com/kpyun/py-nginx:1.0030365c1a354: Mounted from kpyun/ruoyi-frontend735e1c628373: Mounted from kpyun/ruoyi-frontendf612eeda2e8b: Pushed单机 Compose 验证
root@SH-master py-compose# vim /root/py-compose/docker-compose.ymlname: pyappservices: 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:# 启动测试root@SH-master py-compose# docker-compose up -d

root@SH-master py-compose# docker-compose ps# 确认 3 个容器均 Up
浏览器访问 http://10.0.0.90:5001 → 每日名言页 ✅
# 测试通过后停掉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镜像仓库确认

root@SH-master ~# docker images --format '{{.Repository}}:{{.Tag}}' | grep kpyunharbor.shihao.com/kpyun/ruoyi-mysql:1.0harbor.shihao.com/kpyun/ruoyi-backend:1.0harbor.shihao.com/kpyun/ruoyi-frontend:1.0harbor.shihao.com/kpyun/redis:7.2.8harbor.shihao.com/kpyun/py-mysql:1.0harbor.shihao.com/kpyun/py-web:1.0harbor.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.shmkdir -p /root/py-compose/dockerfile /root/py-compose/conf /root/py-compose/data /root/py-compose/templatesecho "目录已创建: /root/py-compose/"ok "项目目录创建完成"
# ---------- 2. 仓库登录 ----------step "4.2 登录第三方仓库 + Harbor"
. /etc/profile.d/env.shecho "Oldboy123.com" | docker login -u 13353958307 --password-stdin docker.xuanyuan.rundocker login -u admin -p passwd harbor.shihao.comok "仓库登录完成"
# ---------- 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'),('非淡泊无以明志,非宁静无以致远。', '诸葛亮');EOFecho "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-cacheskip-name-resolvedatadir=/var/lib/mysqlsocket=/var/run/mysqld/mysqld.socksecure-file-priv=/var/lib/mysql-filesuser=mysqlpid-file=/var/run/mysqld/mysqld.pidcharacter-set-server = utf8mb4collation-server = utf8mb4_unicode_ci
[mysql]default-character-set = utf8mb4
[client]socket=/var/run/mysqld/mysqld.sockdefault-character-set = utf8mb4
!includedir /etc/mysql/conf.d/EOF
cat > /root/py-compose/dockerfile/mysql-1.0 << EOFFROM mysql:8.0.36LABEL maintainer="jiuzhao"EXPOSE 3306COPY ./conf/my.cnf /etc/my.cnfCOPY ./data/quote-init.sql /docker-entrypoint-initdb.d/EOF
cd /root/py-composedocker build -f ./dockerfile/mysql-1.0 -t harbor.shihao.com/kpyun/py-mysql:1.0 .docker push harbor.shihao.com/kpyun/py-mysql:1.0echo "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_templateimport mysql.connectorimport osimport 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.txtcat > /root/py-compose/data/requirements.txt << EOFREQFlask==2.3.3mysql-connector-python==8.1.0gunicorn==21.2.0EOFREQ
# Dockerfilecat > /root/py-compose/dockerfile/flask-1.0 << 'EOFDF'FROM docker.xuanyuan.run/library/python:3.11-slimLABEL maintainer="jiuzhao"ENV LANG=C.UTF-8ENV PYTHONIOENCODING=utf-8WORKDIR /app
# 创建非 root 用户运行 FlaskRUN 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 5000USER wwwCMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]EOFDF
cd /root/py-composedocker build -f ./dockerfile/flask-1.0 -t harbor.shihao.com/kpyun/py-web:1.0 .docker push harbor.shihao.com/kpyun/py-web:1.0echo "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 << EOFFROM nginx:latestLABEL maintainer="jiuzhao"EXPOSE 80COPY ./conf/default.conf /etc/nginx/conf.d/EOF
cd /root/py-composedocker build -f ./dockerfile/nginx-1.0 -t harbor.shihao.com/kpyun/py-nginx:1.0 .docker push harbor.shihao.com/kpyun/py-nginx:1.0echo "py-nginx:1.0 构建+推送完成"ok "py-nginx 镜像构建完成"
# ---------- 7. 单机 Compose 验证 ----------step "4.7 单机 Compose 验证 Flask 名言应用"
cat > /root/py-compose/docker-compose.yml << 'EOFYML'name: pyappservices: 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-composedocker-compose up -d
echo "等待服务启动..."sleep 20
echo ""echo "=== 验证 ==="docker-compose psecho ""curl -s http://127.0.0.1:5001 2>&1 | head -20echo ""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 网络
root@SH-master ~# docker network create --driver overlay frontendzyfnqdpkdrgojfgb4wgmh8k0iroot@SH-master ~# docker network create --driver overlay backendw7ma5m8hn7d6h7hd01kemrtko
root@SH-master ~# docker network ls --filter driver=overlayNETWORK ID NAME DRIVER "SCOPE"w7ma5m8hn7d6 backend overlay swarmzyfnqdpkdrgo frontend overlay swarmyr8igd2u2etu ingress overlay swarm"SCOPE 为 swarm 表示集群全局可用"
- Overlay 网络让分布在不同节点的容器就像在同一个局域网里
- ——这是 Swarm 跨主机通信的基石
准备统一 Nginx 配置
统一入口 Nginx 负责:TLS 终止 + 按域名分发流量
使用官方
nginx:latest镜像,配置和证书通过 Docker Config 注入
root@SH-master ~# vim /root/stack-nginx.confworker_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
root@SH-master ~# docker config create nginx_conf /root/stack-nginx.confroot@SH-master ~# docker config create nginx_cert /root/mycat/certs/fullchain02.crtroot@SH-master ~# docker config create nginx_key /root/mycat/private/nginx-server.key
root@SH-master ~# docker config lsID NAME CREATEDs78xx nginx_conf 29 seconds agosj1xx nginx_cert 34 seconds agop75xx nginx_key 25 seconds ago
编写 Swarm Stack 编排文件
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一键部署
# 先停掉单机 Compose(端口冲突)root@SH-master ~# docker-compose -f /root/ruoyi-compose/docker-compose.yml down -t 0root@SH-master ~# docker-compose -f /root/py-compose/docker-compose.yml down -t 0
# 部署 Stackroot@SH-master ~# docker stack deploy -c /root/swarm-stack.yml swarm-appsCreating network swarm-apps_frontendCreating network swarm-apps_backendCreating service swarm-apps_nginxCreating service swarm-apps_ruoyi-uiCreating service swarm-apps_java-webCreating service swarm-apps_java-dbCreating service swarm-apps_redis-serverCreating service swarm-apps_py-nginxCreating service swarm-apps_py-webCreating service swarm-apps_py-db
# 查看 Stack 状态root@SH-master ~# docker stack lsNAME SERVICES ORCHESTRATORswarm-apps 8 Swarm
首次部署 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 || truecd /root/py-compose && docker-compose down -t 0 2>/dev/null || trueecho "单机 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=overlayok "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 || truedocker config rm nginx_cert 2>/dev/null || truedocker config rm nginx_key 2>/dev/null || true
docker config create nginx_conf /root/stack-nginx.confdocker config create nginx_cert /root/mycat/certs/fullchain02.crtdocker config create nginx_key /root/mycat/private/nginx-server.key
echo "=== Docker Configs ==="docker config lsok "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: anyEOFSTACK
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

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

1)副本分布root@SH-master ~# docker stack ps swarm-appscfcxxx swarm-apps_java-db.1 ruoyi-mysql:1.0 SH-master Running1bwxxx swarm-apps_java-web.1 ruoyi-backend:1.0 SH-master Runninghzzxxx swarm-apps_java-web.2 ruoyi-backend:1.0 SH-slave01 Runningxwuxxx swarm-apps_nginx.1 nginx:latest SH-slave01 Running1juxxx swarm-apps_py-db.1 py-mysql:1.0 SH-master Runningu6cxxx swarm-apps_py-nginx.1 py-nginx:1.0 SH-slave02 Running357xxx swarm-apps_py-nginx.2 py-nginx:1.0 SH-master Running3jaxxx swarm-apps_py-web.1 py-web:1.0 SH-slave01 Runningvz4xxx swarm-apps_py-web.2 py-web:1.0 SH-slave02 Runningnuixxx swarm-apps_redis-server.1 redis:7.2.8 SH-master Runningpzyxxx swarm-apps_ruoyi-ui.1 ruoyi-frontend:1.0 SH-master Running6fpxxx swarm-apps_ruoyi-ui.2 ruoyi-frontend:1.0 SH-slave02 Running
2)Overlay网络root@SH-slave01 ~# docker network ls --filter driver=overlayNETWORK ID NAME DRIVER SCOPEyr8igd2u2etu ingress overlay swarm8llqkvyt4x2q swarm-apps_backend overlay swarm3104hg0bbobi swarm-apps_frontend overlay swarm✅ "Worker 上也能看到 Overlay 网络"root@SH-slave02 ~# docker network ls --filter driver=overlayNETWORK ID NAME DRIVER SCOPEyr8igd2u2etu ingress overlay swarm8llqkvyt4x2q swarm-apps_backend overlay swarm3104hg0bbobi swarm-apps_frontend overlay swarm功能测试
1)HTTPS 访问 Java 应用root@SH-master ~# curl -sI https://java.shihao.com:4433HTTP/1.1 200 OKServer: nginx/1.31.0Date: Sun, 17 May 2026 11:02:16 GMTContent-Type: text/html; charset=utf-8Content-Length: 12909Connection: keep-aliveLast-Modified: Sat, 16 May 2026 11:49:08 GMTETag: "6a0859b4-326d"Accept-Ranges: bytes
2)HTTPS 访问 Python 名言应用root@SH-master ~# curl -sI https://py.shihao.com:4433HTTP/1.1 200 OKServer: nginx/1.31.0Date: Sun, 17 May 2026 11:04:45 GMTContent-Type: text/html; charset=utf-8Content-Length: 1461Connection: keep-alive
3)浏览器访问https://harbor.shihao.com:443 --> Harbor 镜像仓库https://java.shihao.com:4433 --> 若依管理系统登录页https://py.shihao.com:4433 --> 每日名言展示页✅ 三个域名全部通过 HTTPS 访问,无证书警告


Swarm 集群能力验证
以下测试验证 Swarm 的四大核心能力:负载均衡、跨节点 Overlay 通信、网络隔离、服务自愈
① 负载均衡
# 查看 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-slave02swarm-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 通信
# 所有节点都能看到 frontend + backend Overlay 网络1)slav01 & 02 分别执行root@SH-slave01 ~# docker network ls --filter driver=overlayNETWORK ID NAME DRIVER SCOPE8llqkvyt4x2q swarm-apps_backend overlay swarm3104hg0bbobi swarm-apps_frontend overlay swarmyr8igd2u2etu ingress overlay swarmroot@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 -1HTTP/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 -1HTTP/1.1 200 OK ✅结论:Overlay 网络跨 3 个节点工作正常,容器通过服务名直接通信,无需关心对方在哪个节点
③ 数据库网络隔离
# ruoyi-ui 只在 frontend 网络,java-db 只在 backend 网络--> ruoyi-ui 的 DNS 无法解析 java-db
1)slave01验证# 验证:ruoyi-ui 尝试 DNS 解析 java-dbroot@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-dbroot@SH-slave02 ~# CID=$(docker ps -q --filter name=swarm-apps_java-web | head -1)root@SH-slave02 ~# docker exec $CID getent hosts java-db10.0.3.14 java-db ✅结论:
frontend/backend双层 Overlay 实现了数据库网络隔离
- 前端容器只能访问 frontend → DNS 解析不到 backend 服务
- 后端容器同时连接 frontend + backend → 可正常解析并访问数据库
④ 服务自愈
1)slave01执行# 模拟 ruoyi-ui 容器意外崩溃root@SH-slave01 ~# CID=$(docker ps -q --filter name=swarm-apps_ruoyi-ui | head -1)root@SH-slave01 ~# docker stop $CID657424dae0f2# 等待 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 STATEswarm-apps_ruoyi-ui.1 SH-slave02 Running 57 minutes agoswarm-apps_ruoyi-ui.2 SH-slave01 Running about a minute ago ← 刚重启
# 副本数自动恢复到 2/2root@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,无失败任务
root@SH-master ~# docker stack services swarm-apps --format "table {{.Name}}\t{{.Replicas}}"NAME REPLICASswarm-apps_java-db 1/1swarm-apps_java-web 2/2swarm-apps_nginx 1/1swarm-apps_py-db 1/1swarm-apps_py-nginx 2/2swarm-apps_py-web 2/2swarm-apps_redis-server 1/1swarm-apps_ruoyi-ui 2/2phase6-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 -5echo ""echo -e "${YELLOW}--- Python 名言 ---${RESET}"curl -skI https://py.shihao.com:4433 2>&1 | head -5echo ""echo -e "${YELLOW}--- Harbor 仓库 ---${RESET}"curl -skI https://harbor.shihao.com 2>&1 | head -5ok "基本访问全部 200 OK"
# ---------- 2. 集群状态 ----------step "6.2 集群节点状态"
echo "=== Swarm 节点 ==="docker node lsecho ""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}"fiok "跨节点 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}" fifi
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}"fiok "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)"fiok "服务自愈: 容器崩溃后 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 ""文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!




