搭建spug

安装docker

yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install docker-ce docker-compose-plugin
systemctl enable docker
systemctl start docker

创建docker-compose.yml

要让spug容器内能执行docker命令,要设置让容器包含宿主机docker命令

vi docker-compose.yml
version: "3.3"
services:
  db:
    image: mariadb:10.8.2
    container_name: spug-db
    restart: always
    command: --port 3306 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - /data/spug/mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_ROOT_PASSWORD=spug.cc
  spug:
    image: openspug/spug-service
    container_name: spug
    privileged: true # 特权模式
    restart: always
    volumes:
      - /data/spug:/data/spug
      - /data/repos:/data/repos
      - /var/run/docker.sock:/var/run/docker.sock  # 挂载docker套接字,容器内能运行docker命令
      - /usr/bin/docker:/usr/bin/docker # 挂载docker客户端工具,容器内能运行docker命令
      - /data/scripts:/scripts
    ports:
      # 如果80端口被占用可替换为其他端口,例如: - "8000:80"
      - "8080:80"
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock # 设置环境变量,确保Spug容器知道如何连接到 Docker守护进程
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_HOST=spug-db
      - MYSQL_PORT=3306
    depends_on:
      - db

以上配置会启动两个容器,分别是spug和mysql容器。
如果自己有其他数据库,可在docker-compose.yml里指定已有的数据库,不需要再启动数据库容器了

version: "3.3"
services:
  spug:
    image: openspug/spug-service
    container_name: ops
    privileged: true
    restart: always
    volumes:
      - /data/spug/service:/data/spug
      - /data/spug/repos:/data/repos
      - /var/run/docker.sock:/var/run/docker.sock  # 挂载docker套接字,容器内能运行docker命令
      - /usr/bin/docker:/usr/bin/docker # 挂载docker客户端工具,容器内能运行docker命令
      - /data/scripts:/scripts
    ports:
      - "8080:80"  # 映射端口
    environment:
      - MYSQL_DATABASE=ops  # 数据库名
      - MYSQL_USER=yunwei  # 用户名
      - MYSQL_PASSWORD=DrIVy9O7kzuS46  # 密码
      - MYSQL_HOST=1.1.1.1  # MySQL 数据库地址(IP 或域名)
      - MYSQL_PORT=3306  # MySQL 默认端口

启动容器

docker compose up -d

初始化

以下操作会创建一个用户名为ops密码为123456的管理员账户,可自行替换管理员账户/密码。

docker exec ops init_spug ops 123456

访问测试

在浏览器中输入 http://localhost:8080 访问

版本升级

你可以在 系统管理/系统设置/关于 中查看当前运行的 Spug 版本,可以在更新日志查看当前最新版本,如果需要升级 Spug 请参考 版本升级文档

# 默认更新到最新版本
# spug 是容器名称,也可以替换为自己的容器ID
docker exec -i spug python3 /data/spug/spug_api/manage.py update 
# 更新完成后重启容器
docker restart spug

配置主机

主机管理里可以添加要管理的主机,且spug支持web端远程管理主机
file
下图为添加主机界面:
file

添加环境

添加环境,之后创建发布应用的时候便于分类管理
file

应用管理

spug的功能是让开发通过该平台发布应用到远程主机
所以着重讲如何配置应用
应用发布发布配置里添加应用
file
再在刚新建的应用上点击新建发布常规发布

  • 并行发布:可同时发布到多台主机,发布速度快,但是如果有报错可能导致多台主机的服务都不可用
  • 串行发布:一台台发布。如果有报错会停止,但是发布速度慢。
  • 在配置应用的时候,填写仓库地址"http://[username]:[password]@git.xxx.com:9090/[servername].git"

file

file

file

发布应用

上一步已经配置了应用的相关流水
当用户要发布版本的时候,点击应用发布发布申请
file
点击发布后,状态变为待审核
file
等审核人员审核通过后,状态变成待发布
此时发布者可以选择发布。发布有两种方式,分别是补偿和全量
file
点击发布后,弹出发布日志
file
在目标主机查看,发现代码已拉取到目标主机指定目录
file

服务器监控

进程和端口监控

在spug可以监控服务器的进程、端口存活,还可以设置自定义脚本
file
file
端口和进程的监控这里就不说明了
这里只说明监控服务器性能

自定义脚本

监控服务器性能

监控服务器的CPU、内存、磁盘、IO、负载

#!/bin/bash

# 检查内存使用率
mem_used=$(free | grep Mem | awk '{print $3/$2 * 100.0}' | bc -l)
mem_threshold=80
mem_alert=$(echo "$mem_used > $mem_threshold" | bc -l)

if (( $(echo "$mem_alert" | bc -l) )); then
    echo "内存使用超过 ${mem_threshold}%!!"
    exit 1
fi

# 检查CPU使用率
cpu_used=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
cpu_threshold=80
cpu_alert=$(echo "$cpu_used > $cpu_threshold" | bc -l)

if (( $(echo "$cpu_alert" | bc -l) )); then
    echo "CPU使用超过 ${cpu_threshold}%!!"
    exit 1
fi

# 检查磁盘使用率
disk_used=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
disk_threshold=90
disk_alert=$(echo "$disk_used > $disk_threshold" | bc -l)

if (( $(echo "$disk_alert" | bc -l) )); then
    echo "磁盘使用超过 ${disk_threshold}%!!"
    exit 1
fi

# 检查inode使用情况
inode_used=$(df -i / | grep / | awk '{ print $3 }')
inode_total=$(df -i / | grep / | awk '{ print $2 }')
inode_percentage=$(echo "$inode_used / $inode_total * 100" | bc -l)
inode_threshold=90
inode_alert=$(echo "$inode_percentage > $inode_threshold" | bc -l)

if (( $(echo "$inode_alert" | bc -l) )); then
    echo "inode使用超过 ${inode_threshold}%!!"
    exit 1
fi

# 检查IO情况
io_stats=$(iostat -c | awk 'NR==4 {print $1}')
io_threshold=5
io_alert=$(echo "$io_stats > $io_threshold" | bc -l)

if (( $(echo "$io_alert" | bc -l) )); then
    echo "IO使用超过 ${io_threshold}ms!!"
    exit 1
fi

# 检查系统负载
load_avg=$(cat /proc/loadavg | awk '{print $1}')
load_threshold=2.0
load_alert=$(echo "$load_avg > $load_threshold" | bc -l)

if (( $(echo "$load_alert" | bc -l) )); then
    echo "系统负载超过 ${load_threshold}!!"
    exit 1
fi

# 输出当前使用情况,保留两位小数
printf "内存使用率: %.2f%%.\n" "$mem_used"
printf "CPU使用率: %.2f%%.\n" "$cpu_used"
printf "磁盘使用率: %.2f%%.\n" "$disk_used"
printf "inode使用率: %.2f%%.\n" "$inode_percentage"
printf "IO延迟: %.2fms.\n" "$io_stats"
printf "系统负载: %.2f.\n" "$load_avg"

在设置脚本后,可先点执行测试,若弹窗是警告图标,则表示会触发告警。
file
当超过脚本设置的阈值后,会触发告警,在报警历史里可以查看到告警。
file
如果配置了邮箱或电话钉钉等告警,也会相应触发
file

监控数据库值

监控数据库中某个表的 static 值,持续监测数据库中的变动。当出现指定值,则告警。已告警的不会反复告,为了防止大量扫表,默认每次探测1小时内的值
本地维护两个文件:
alert_log.txt:记录所有告警信息,包括时间、静态值和对应的 ID。
alert_track.txt:跟踪已告警的时间戳,以避免重复告警。

#!/bin/bash

# 数据库连接配置
REMOTE_DB_HOST="165.██.███.244"
REMOTE_DB_USER="casdoor"
REMOTE_DB_PASSWORD="█████"
REMOTE_DB_NAME="█████"
REMOTE_TABLE_NAME="testtable"

# 本地告警记录文件
ALERT_LOG_FILE="alert_log.txt"
ALERT_TRACK_FILE="alert_track.txt"

# 设定要监控的 static 值和时间窗口(单位:分钟)
TARGET_STATIC_VALUE=0  # 可以设置为 0 或 1
TIME_WINDOW_MINUTES=60  # 监控过去60分钟的数据

# 创建告警跟踪文件(如果不存在)
touch "$ALERT_TRACK_FILE"

while true; do
    # 获取当前时间和过去指定分钟的时间
    CURRENT_TIME=$(date '+%Y-%m-%d %H:%M:%S')
    PAST_TIME=$(date -d "-$TIME_WINDOW_MINUTES minutes" '+%Y-%m-%d %H:%M:%S')

    # 查询过去指定分钟内的 static 值、created_at 和 id
    RESULT=$(mysql -h "$REMOTE_DB_HOST" -u "$REMOTE_DB_USER" -p"$REMOTE_DB_PASSWORD" -D "$REMOTE_DB_NAME" -se "SELECT id, static, created_at FROM $REMOTE_TABLE_NAME WHERE created_at >= '$PAST_TIME' ORDER BY created_at DESC;")

    # 检查是否找到任何记录
    if [[ -z "$RESULT" ]]; then
        echo "$CURRENT_TIME - 未找到 static=$TARGET_STATIC_VALUE,扫描行数: 0"
        sleep 10  # 每10秒检查一次
        continue
    fi

    # 加载已告警的时间戳
    declare -A ALERTED_TIMESTAMPS
    while IFS= read -r line; do
        # 确保行不为空
        if [[ -n "$line" ]]; then
            ALERTED_TIMESTAMPS["$line"]=1
        fi
    done < "$ALERT_TRACK_FILE"

    # 计算扫描的行数
    LINE_COUNT=$(echo "$RESULT" | wc -l)

    # 标志:是否发现新的告警
    NEW_ALERT_FOUND=0

    # 遍历查询结果并检查告警
    while read -r line; do
        ID=$(echo "$line" | awk '{print $1}')  # 提取 ID
        STATIC_VALUE=$(echo "$line" | awk '{print $2}')  # 提取 static 值
        CREATED_AT=$(echo "$line" | awk '{print substr($0, index($0,$3))}')  # 提取完整的时间戳

        # 判断 static 值
        if [[ "$STATIC_VALUE" == "$TARGET_STATIC_VALUE" ]]; then
            # 检查时间戳是否已告警
            if [[ -z "${ALERTED_TIMESTAMPS[$CREATED_AT]}" ]]; then
                # 记录告警
                echo "$CURRENT_TIME - 告警: ID=$ID, static=$STATIC_VALUE,时间: $CREATED_AT"
                echo "$CURRENT_TIME - 告警: ID=$ID, static=$STATIC_VALUE,时间: $CREATED_AT" >> "$ALERT_LOG_FILE"

                # 将时间戳记录到跟踪文件
                echo "$CREATED_AT" >> "$ALERT_TRACK_FILE"

                # 设置标志,表示发现新的告警
                NEW_ALERT_FOUND=1
            fi
        fi
    done <<< "$RESULT"  # 使用 here-string 来遍历查询结果

    # 输出扫描行数
    # echo "$CURRENT_TIME - 扫描行数: $LINE_COUNT"

    # 检查是否发现新的告警
    if [[ "$NEW_ALERT_FOUND" -eq 0 ]]; then
        echo "$CURRENT_TIME - 无新增 static=$TARGET_STATIC_VALUE,扫描行数: $LINE_COUNT"
    fi

    sleep 10  # 每10秒检查一次
done

exit 0  # 正常情况

脚本输出结果:
file
可将脚本写入spug的自定义脚本中触发

告警系统接入Telegram

将告警接入Telegram,让告警消息通过telegram发出。

创建机器人

打开 Telegram,搜索 @BotFather。
发送 /newbot 命令,按照提示创建一个新的 Bot。
记下 Bot 的 Token,稍后会用到。

获取 Chat ID

在 Telegram 中找到创建的 Bot,发送一条消息。
接着使用以下 URL 获取您的 Chat ID:

https://api.telegram.org/bot<YourBOTToken>/getUpdates
# 替换 <YourBOTToken> 为您在第一步中获得的 Token。找到您发送消息后的 chat 对象中的 id。

得到的返回如下:
file

监控邮箱内容

需求:当邮箱收到来自告警邮箱发来的消息时,则转发到telegrambot里
前提是先安装了python3
安装pip3

yum install python3-pip

邮箱开启IMAP

以gmail为例,在邮箱设置页面–启用IMAP
谷歌账号界面点安全性–启用两步验证
创建和管理应用专用密码界面创建一个密码,记住这个密码,一会配置邮箱的。

部署服务

安装依赖

pip3 install python-telegram-bot==13.7
pip3 install configparser
pip3 install argparse
pip3 install imaplib2

监控代码

mkdir /opt/mailToTg && cd /opt/mailToTg
# ln -sT /opt/mailToTg/mailToTg.py /usr/local/bin/mailToTg
touch mailToTg.py chmod +x mailToTg.py
vim mailToTg.py
import imaplib
import logging
import email
import re
import requests
import os
import time
import argparse
import configparser
from email.header import decode_header
from logging.handlers import RotatingFileHandler
import sqlite3

class MailToTelegramForwarder:
    def __init__(self, config):
        self.config = config
        self.mailbox = None
        self.last_uid = None
        self.allowed_senders = self.config['default'].get('allowed_senders', '').split(',')
        self.check_interval = int(self.config['default'].get('check_interval', 10))
        self.db = self.setup_database()

    def setup_database(self):
        """设置数据库以存储最后处理的邮件 UID。"""
        conn = sqlite3.connect('mail_forwarder.db')
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS last_uid (
                uid TEXT PRIMARY KEY
            )
        ''')
        conn.commit()
        return conn

    def connect(self):
        """连接到 IMAP 邮箱。"""
        try:
            self.mailbox = imaplib.IMAP4_SSL(self.config['default']['imap_host'])
            self.mailbox.login(self.config['default']['imap_user'], self.config['default']['imap_password'])
            logging.info("Logged in successfully to IMAP server.")
        except Exception as e:
            logging.error(f"Failed to connect to IMAP server: {e}")
            raise

    def search_mails(self):
        """搜索远程 IMAP 邮箱并返回邮件数据。"""
        logging.info("Searching for new mails...")
        self.last_uid = self.get_last_uid()

        try:
            self.mailbox.select("INBOX")  # 选择收件箱
        except Exception as e:
            logging.error(f"Failed to select mailbox: {e}")
            return []

        search_string = self.config['default'].get('imap_search', "(UNSEEN)")
        search_string = re.sub(r'\${lastUID}', str(self.last_uid), search_string, flags=re.IGNORECASE)

        logging.info(f"Using search string: {search_string}")

        try:
            rv, data = self.mailbox.uid('search', None, search_string)
            if rv != 'OK':
                logging.info("No messages found!")
                return []

            email_uids = data[0].split()
            if not email_uids:
                logging.info("No new emails found.")
                return []

            mails = []
            for email_uid in email_uids:
                rv, email_data = self.mailbox.uid('fetch', email_uid, '(RFC822)')
                if rv != 'OK':
                    logging.warning(f"Failed to fetch email with UID {email_uid}")
                    continue

                raw_email = email_data[0][1]
                msg = email.message_from_bytes(raw_email)

                if not self.is_allowed_sender(msg["From"]):
                    logging.info(f"Ignoring email from: {msg['From']}")
                    continue

                mail_data = self.parse_email(msg)
                mails.append((email_uid.decode(), mail_data))

            return mails
        except Exception as e:
            logging.error(f"Error during IMAP search: {e}")
            self.disconnect()
            return []

    def is_allowed_sender(self, sender):
        """检查发件人是否在允许的发件人列表中。"""
        return any(allowed_sender.strip() in sender for allowed_sender in self.allowed_senders)

    def parse_email(self, msg):
        """解析邮件内容并返回所需的信息。"""
        subject, encoding = decode_header(msg["Subject"])[0]
        if isinstance(subject, bytes):
            subject = subject.decode(encoding if encoding else 'utf-8')

        from_email = self.decode_header_field(msg["From"])
        date = msg["Date"]
        body = self.extract_body(msg)
        logging.debug("Extracted body: %s", body)  # 输出提取的正文
        body = self.filter_body(body)  # 在此处过滤邮件正文

        mail_data = {
            "body": body  # 仅保留过滤后的邮件正文
        }

        return mail_data

    def decode_header_field(self, header_value):
        """解码邮件头字段。"""
        decoded_fragments = decode_header(header_value)
        decoded_string = ''

        for fragment, encoding in decoded_fragments:
            if isinstance(fragment, bytes):
                decoded_string += fragment.decode(encoding if encoding else 'utf-8')
            else:
                decoded_string += fragment

        return decoded_string

    def extract_body(self, msg):
        """提取邮件正文,支持多种邮件格式。"""
        if msg.is_multipart():
            for part in msg.walk():
                content_type = part.get_content_type()
                content_disposition = str(part.get("Content-Disposition"))

                if content_type == "text/plain" and "attachment" not in content_disposition:
                    body = part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8', errors='ignore')
                    return body
                elif content_type == "text/html":
                    body = part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8', errors='ignore')
                    return body
        else:
            body = msg.get_payload(decode=True).decode(msg.get_content_charset() or 'utf-8', errors='ignore')
            return body
        return ""

    def filter_body(self, body):
        """根据配置文件中的规则过滤邮件正文。"""
        # 从 REPLACEMENTS 节中读取配置
        remove_lines = self.config['replacements'].get('remove_lines', '').strip().split(',')
        replace_lines = self.config['replacements'].get('replace_lines', '').strip().split(',')

        logging.debug("Loaded remove_lines: %s", remove_lines)
        logging.debug("Loaded replace_lines: %s", replace_lines)

        for line in remove_lines:
            line = line.strip()
            if line:  # 确保行不为空
                body = body.replace(line, '')

        for replacement in replace_lines:
            if ':' in replacement:
                old, new = map(str.strip, replacement.split(':', 1))
                body = body.replace(old, new)

        logging.debug("Filtered body: %s", body)  # 输出过滤后的结果
        return body.strip()

    def disconnect(self):
        """断开与 IMAP 邮箱的连接。"""
        if self.mailbox:
            self.mailbox.close()
            self.mailbox.logout()
            logging.info("Disconnected from IMAP server.")

    def get_last_uid(self):
        """获取最后处理过的邮件 UID。"""
        cursor = self.db.cursor()
        cursor.execute("SELECT uid FROM last_uid")
        row = cursor.fetchone()
        return row[0] if row else ""

    def save_last_uid(self, uid):
        """保存最后处理过的邮件 UID。"""
        cursor = self.db.cursor()
        cursor.execute("REPLACE INTO last_uid (uid) VALUES (?)", (uid,))
        self.db.commit()

    def forward_to_telegram(self, mail_data):
        """将邮件内容转发到 Telegram bot。"""
        telegram_url = f"https://api.telegram.org/bot{self.config['default']['telegram_token']}/sendMessage"
        body = mail_data['body'][:4000]  # 只使用邮件正文
        message = body  # 直接使用正文内容

        payload = {
            "chat_id": self.config['default']['telegram_chat_id'],
            "text": message,
            "parse_mode": "Markdown"
        }

        for attempt in range(3):  # Retry mechanism
            try:
                response = requests.post(telegram_url, data=payload)
                response.raise_for_status()
                logging.info(f"Message sent to Telegram: {response.text}")
                return True
            except requests.exceptions.HTTPError as http_err:
                logging.error(f"HTTP error occurred: {http_err} - Response: {response.text}")
            except requests.exceptions.RequestException as e:
                logging.error(f"Error sending message to Telegram: {e}")

            time.sleep(2)  # Wait before retrying

        return False

    def run(self):
        """启动邮件转发任务,连接邮箱并定期检查未读邮件,转发到 Telegram。"""
        self.connect()

        while True:
            try:
                mails = self.search_mails()
                if mails:
                    for email_uid, mail in mails:
                        success = self.forward_to_telegram(mail)
                        if success:
                            self.save_last_uid(email_uid)
                else:
                    logging.info("No new mails to forward.")
            except Exception as e:
                logging.error(f"An error occurred in the run loop: {e}")
                self.disconnect()
                time.sleep(5)  # 等待片刻再重试
            time.sleep(self.check_interval)

def load_config(config_path):
    """从指定的文件路径加载配置。"""
    parser = configparser.ConfigParser()
    parser.read(config_path, encoding='utf-8')

    if "DEFAULT" not in parser:
        raise ValueError("Configuration file is missing the DEFAULT section.")
    if "REPLACEMENTS" not in parser:
        raise ValueError("Configuration file is missing the REPLACEMENTS section.")

    return {
        "default": parser["DEFAULT"],       # 返回 DEFAULT 节的内容
        "replacements": parser["REPLACEMENTS"]  # 返回 REPLACEMENTS 节的内容
    }

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Mail to Telegram Forwarder")
    parser.add_argument("--config", required=True, help="Path to the configuration file")
    args = parser.parse_args()

    # 设置日志
    handler = RotatingFileHandler('mail_to_telegram.log', maxBytes=5*1024*1024, backupCount=5)
    logging.basicConfig(handlers=[handler], level=logging.DEBUG,
                        format='%(asctime)s - %(levelname)s - %(message)s')

    # 加载配置
    config_file = args.config
    config = load_config(config_file)

    # 创建并运行邮件转发器
    forwarder = MailToTelegramForwarder(config)
    forwarder.run()

编辑配置文件

[DEFAULT]
# imap服务器、账号、密码
imap_host = imap.gmail.com
imap_user = ████@gmail.com
imap_password = v████whkcwyqkm
# imap搜索字符串,默认为未读邮件
imap_search = (UNSEEN)
# telegram机器人令牌
telegram_token = 8049819████████████A0imimPripiy8YMh4
# telegram聊天ID
telegram_chat_id = 11████6500
# 邮件检查间隔(秒)
check_interval = 10
# 转发的收件人列表
allowed_senders = 20████36@qq.com, ████44@qq.com, jus████@foxmail.com

[REPLACEMENTS]
# 要从转发的邮件正文中移除的文本。格式为(文本,文本2)
remove_lines = ,自动发送,请勿回复。
# 要替换的文本。格式为(旧文本:新文本)
replace_lines = 故障持续时间:故障持续

启动服务

# 启动服务
cd /opt/mailToTg && nohup python3 mailToTg.py --config conf.ini &
# 日志文件
在mail_to_telegram.log

用户与权限管理

待补充

实例

需求

为了满足使用 Spug 发布在 Docker 中启动的 Java 应用这一需求,需要完成一系列操作。具体为:利用 Spug 从 Git 仓库下载代码,接着对代码进行编译和打包,然后将打包好的内容传输到后端服务器,之后在后端服务器编译 Docker 镜像,最后基于该镜像启动 Docker 容器。

分析

为了实现对 Java 项目的自动化编译、测试以及打包,安装 “maven” 是必要的。具体的操作流程如下:当 spug 成功拉取 git 代码后,需借助宿主机创建一个 maven 容器来执行编译任务。然而,要在容器内使宿主机运行相关命令,关键在于挂载 “Docker socket”,通过这种方式,容器才能获得对宿主机 Docker 的控制权。但在此之前,容器中必须预先安装 docker 客户端,因为只有这样,容器才能够执行 docker 命令。所以,我们首先要基于 spug 镜像,提前安装好 docker 客户端,并生成新的镜像文件。完成这一步骤后,后续启动容器时,就可以直接使用相关功能,顺利推进 Java 项目的自动化流程了。

重新编译spug镜像

Dockerfile内容如下

FROM openspug/spug-service
# 安装必要的工具
RUN apt-get update && \
    apt-get install -y \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    software-properties-common
# 添加 Docker 的官方 GPG 密钥
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
# 添加 Docker 的稳定版本库
RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# 安装 Docker 客户端
RUN apt-get update && \
    apt-get install -y docker-ce-cli
# 设置工作目录(可选)
WORKDIR /scripts
# 挂载点(可选)
VOLUME ["/scripts"]

编译镜像

docker build -t my-openspug-docker .

修改docker-compose.yml

version: "3.3"
services:
  db:
    image: mariadb:10.8.2
    container_name: spug-db
    restart: always
    command: --port 3306 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - /data/spug/mysql:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_ROOT_PASSWORD=spug.cc
  spug:
    image: openspug/spug-service
    container_name: spug
    privileged: true # 特权模式
    restart: always
    volumes:
      - /data/spug:/data/spug
      - /data/repos:/data/repos
      - /var/run/docker.sock:/var/run/docker.sock  # 挂载docker套接字,容器内能运行docker命令
      - /usr/bin/docker:/usr/bin/docker # 挂载docker客户端工具,容器内能运行docker命令
      - /data/scripts:/scripts
    ports:
      # 如果80端口被占用可替换为其他端口,例如: - "8000:80"
      - "8080:80"
    environment:
      - DOCKER_HOST=unix:///var/run/docker.sock # 设置环境变量,确保Spug容器知道如何连接到 Docker守护进程
      - MYSQL_DATABASE=spug
      - MYSQL_USER=spug
      - MYSQL_PASSWORD=spug.cc
      - MYSQL_HOST=spug-db
      - MYSQL_PORT=3306
    depends_on:
      - db

运行容器

docker-compose up -d

现在就可以实现在spug容器中,让宿主机创建其他容器了

配置方法一:常规发布

(注:以下流程发布的有问题,会让并非的代码并非最新版。这可能和spug的bug有关,导致编译后的代码并未传到目标机器。以下流程仅供参考,生效流程见后续自定义发布脚本)
在配置发布时有以下几个阶段:

  • 代码检出前执行:是在 Spug 服务器上,在拉取代码前,进行环境配置等工作;
  • 代码检出后执行,是在拉取代码后,进行依赖安装、编译和构建等工作;
  • 应用发布前执行,是在目标主机上,进行旧应用的停止、运行环境配置等工作;
  • 应用发布后执行,是在把项目发布到目标主机后,进行应用的启动等。
    接下来是配置:
    代码检出前执行

    echo "【代码检出前】开始"
    echo "当前在$(pwd)"
    echo "当前公网IP: $(curl -s ifconfig.me)"
    echo "本次发布应用的源码目录(容器内目录)$SPUG_REPOS_DIR/$SPUG_DEPLOY_ID"
    ls -tl $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID
    if ! docker run --rm --name ${SPUG_APP_NAME}_mvn_$(date +%Y%m%d%H%M)  -v "$SPUG_REPOS_DIR/$SPUG_DEPLOY_ID:/app"   -w /app  maven:3.9.8-amazoncorretto-17 mvn clean package -Dmaven.test.skip=true -U; then
    echo "Maven 打包失败"
    exit 1
    fi
    echo "当前在$(pwd)"
    echo "【代码检出前】结束"

    代码检出后执行

    echo "【代码检出后】开始"
    echo "当前在$(pwd)"
    echo "查看$SPUG_REPOS_DIR/$SPUG_DEPLOY_ID/target/下jar文件"
    ls -lht $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID/target/*.jar
    echo "查看jar文件md5sum(发布后进容器查看是否和以下一致)"
    md5sum $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID/target/*.jar
    if ls $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID/target/*.jar 1> /dev/null 2>&1; then
    cp $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID/target/*.jar ./
    else
    echo "没有找到 jar 文件"
    exit 1
    fi
    echo "当前公网IP: $(curl -s ifconfig.me)"
    echo "查看本次编译目录$SPUG_REPOS_DIR/$SPUG_DEPLOY_ID"
    ls -lt $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID
    # echo "删除编译目录,防止下次发布代码非最新"
    # rm -rf $SPUG_REPOS_DIR/$SPUG_DEPLOY_ID
    echo "【代码检出后】结束"

    应用发布前执行

    
    echo "【应用发布前】开始"
    echo "当前公网IP: $(curl -s ifconfig.me)"
    echo "目标主机部署路径是:$SPUG_DST_DIR"
    echo "代码检出路径:$SPUG_REPOS_DIR/$SPUG_DEPLOY_ID"
    echo "当前显示的项目的真实地址是$(readlink -f /data/aladdin/$SPUG_APP_NAME)"
    echo "显示当前项目下的jar包的md5"
    md5sum $(readlink -f /data/aladdin/$SPUG_APP_NAME/aladdin*.jar)

echo "【应用发布前】结束"

应用发布后执行:
```bash
echo "【应用发布后】开始"
echo "当前显示的项目的真实地址是$(readlink -f /data/aladdin/$SPUG_APP_NAME)"
echo "显示当前项目下的jar包的md5"
md5sum $(readlink -f /data/aladdin/$SPUG_APP_NAME/aladdin*.jar)
echo "正在准备Dockerfile"
cat << EOF >Dockerfile
# VERSION 1.0.0
# Author: robin
# 2025
# 打包jar采用maven镜像
# 运行jar采用jdk基础镜像
FROM amazoncorretto:17.0.12
# 设置工作目录在镜像的 /app 目录下
WORKDIR /app
# 将jar包添加到容器中
# COPY *.jar /app/
COPY aladdin-be-*.jar /app/aladdin-be-1.0-SNAPSHOT.jar 
# 运行jar包
ENTRYPOINT ["java","-jar","aladdin-be-1.0-SNAPSHOT.jar"]
EOF
echo "当前在$(pwd)"
echo "构建镜像"
if ! docker build -t aladdin-be:v1.0 -f Dockerfile .; then
    echo "Docker 镜像构建失败"
    exit 1
fi
echo "进入目标主机部署路径 $SPUG_DST_DIR"
cd $(readlink -f /data/aladdin/$SPUG_APP_NAME)
echo "当前在$(pwd)"
# 设置当前发布环境
sed -i.bak 's/-local}/-test}/' tools/start.sh
sed -i.bak 's/600m/1g/' tools/start.sh
chmod +x tools/*.sh
echo "正在重启服务"
sh tools/stop.sh
sh tools/start.sh
# 打印启动日志到文件
docker logs $SPUG_APP_NAME > $SPUG_APP_NAME.log 2>&1 &
echo "日志已输出到 $SPUG_APP_NAME.log"
# 检查服务是否启动成功
if docker ps -a | grep -q $SPUG_APP_NAME; then
    echo "服务已成功启动"
else
    echo "服务启动失败"
    exit 1
fi
echo "【应用发布后】结束"

配置方法二:自定义发布

以下流程生效
本地动作1:拉取代码

echo "当前在$(pwd)"
rm -rf /data/repos/$SPUG_APP_NAME
cd /data/repos/
echo "当前在$(pwd)"
# 下载代码 SPUG_RELEASE为发布的分支,在发布时手动定义。分支可访问git仓库,在tree后查看
git clone http://liuhong:UA3PSokE@git.pgypay.com:9090/wallet/aladdin-be.git \
&& cd aladdin-be && git checkout "$SPUG_RELEASE"
echo "当前在$(pwd)"
echo "查看拉取的代码"
ls -lhta

本地动作2:编译代码

if ! docker run --rm --name ${SPUG_APP_NAME}_mvn_$(date +%Y%m%d%H%M)  -v "/data/repos/$SPUG_APP_NAME:/app"  -w /app  maven:3.9.8-amazoncorretto-17 mvn clean package -Dmaven.test.skip=true -U; then
    echo "Maven 打包失败"
    exit 1
fi
echo "当前在$(pwd)"
echo "查看 JAR 文件信息:"
JAR_PATH=$(find "/data/repos/$SPUG_APP_NAME/target/" -name "*.jar" -print -quit)
if [ -z "$JAR_PATH" ]; then
    echo "未找到 JAR 文件。"
else
    echo "JAR 路径: $JAR_PATH"
    JAR_MD5=$(md5sum "$JAR_PATH" | awk '{print $1}')
    echo "JAR MD5: $JAR_MD5"
fi
# 将编译后的文件拷贝到根目录
cp /data/repos/$SPUG_APP_NAME/target/*.jar  /data/repos/$SPUG_APP_NAME/
ls -lhta /data/repos/$SPUG_APP_NAME/*.jar

目标主机动作1:检查环境

echo "当前在$(pwd)"
mkdir -p /data/project/$SPUG_APP_NAME
cd /data/project/$SPUG_APP_NAME
echo "当前在$(pwd)"
echo "删除jar包"
rm -rf /data/project/$SPUG_APP_NAME/aladdin*.jar

目标主机动作2:数据传输

本地路径:/data/repos/$SPUG_APP_NAME
过滤规则:target
目标路径:/data/project/$SPUG_APP_NAME

目标主机动作3:数据传输后检查文件

echo "当前在$(pwd)"
cd /data/project/$SPUG_APP_NAME
if find . -maxdepth 1 -name 'aladdin*.jar' -print -quit | grep -q .; then
    md5sum aladdin*.jar
else
    echo "文件不存在"
    exit 1
fi

目标主机动作4:编译启动镜像

echo "当前在$(pwd)"
cd /data/project/$SPUG_APP_NAME
echo "当前在$(pwd)"

echo "正在准备 Dockerfile"
cat << EOF > Dockerfile
FROM amazoncorretto:17.0.12
WORKDIR /app
COPY *.jar /app/aladdin-be-1.0-SNAPSHOT.jar
ENTRYPOINT ["java", "-jar", "aladdin-be-1.0-SNAPSHOT.jar"]
EOF

echo "构建 Docker 镜像"
if ! docker build -t aladdin-be:v1.0 -f Dockerfile .; then
    echo "Docker 镜像构建失败"
    exit 1
fi

# 设置当前发布环境
sed -i.bak 's/-local}/-test}/' tools/start.sh
sed -i.bak 's/600m/1g/' tools/start.sh
chmod +x tools/*.sh
echo "正在重启服务"
sh tools/stop.sh
sh tools/start.sh

# 打印启动日志到文件
docker logs $SPUG_APP_NAME > $SPUG_APP_NAME.log 2>&1 &
echo "日志已输出到 $SPUG_APP_NAME.log"

# 检查服务是否启动成功
if docker ps -a | grep -q $SPUG_APP_NAME; then
    echo "服务已成功启动"
else
    echo "服务启动失败"
    exit 1
fi

echo "【应用发布后】结束"
版权所有,转载注明来源