梦泽烂笔纪 用烂笔头,记浮生梦

在宝塔面板上通过Docker模块和容器编排搭建McLogs-Next

前言

McLogs-Next是什么?

McLogs-Next-API 是一个由梦泽主导开发的现代化 Minecraft 日志分析与分享平台,基于业界成熟的 @aternos/mclogs 底层架构进行深度重构与增强。该项目诞生于 Minecraft 服务器运维的实际需求——面对动辄数千行的复杂日志,传统的手动排查方式往往需要 15-30 分钟才能定位问题,而 McLogs-Next-API 通过智能化技术将这一过程缩短至秒级。
本软件专注于解决三大核心痛点:快速错误定位、智能日志分析与便捷日志分享。相较于传统的 @aternos/mclogs 方案,McLogs-Next-API 在完全兼容其所有基础功能——包括语法高亮显示、行号标记、多存储后端支持(MongoDB、Redis、文件系统)——的同时,引入了AI 驱动的深度日志分析引擎,能够自动识别内存溢出、插件冲突、配置错误等常见故障模式,并提供针对性的修复建议。
项目采用无头 API(Headless API)架构设计,仅通过 RESTful API 接口与自动化文档(APIDocs)对外提供服务,彻底解耦前端展示与后端逻辑。这种设计带来了显著的工程优势。

为什么是Docker Compose?

我们使用Docker Compose设计这款软件也是看中了Docker Compose的便捷性:
NingZeStudio/McLogs-Next-API 采用现代化的技术栈构建,其核心架构包含多个关键组件:MongoDB 作为数据库存储支持百万级日志秒级检索,Redis 提供内存加速以提升API性能,同时还集成了大模型进行智能日志分析。这种多服务架构天然需要容器编排工具来协调各个组件的启动顺序、网络通信和数据共享。Docker Compose 通过单一的 docker-compose.yml 文件定义这些服务之间的依赖关系,确保数据库、缓存、前端和后端API能够按照正确的顺序启动并自动连接,避免了手动配置每个容器网络端口的繁琐过程,实现了"一键部署完整环境"的便捷性。

搭建后端Headless API

准备环境

在正式开始之前,你需要一台可以公网访问的服务器,如果你的运维能力不够强的话这边推荐安装一个面板,这里推荐宝塔面板,本教程也会基于宝塔面板和宝塔Docker模块展开。宝塔官网:https://bt.cn/,运行如下命令安装宝塔:

if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh ed8484bec

安装完成之后你就会看到类似如下反馈:
1000287756.jpg
通过外网IPv4地址、访问宝塔面板,再输入用户名和密码登录之后,绑定宝塔账号就可以开始用了。

开始搭建

接着就是下载程序本身,前往Github下载:https://github.com/NingZeStudio/McLogs-Next-API
下载好压缩包之后,访问宝塔面板,选择LNMP环境安装,完成之后再点击侧栏的文件管理,按照图片上的路径,依次点击www>wwwroot,来到wwwroot目录,在这里新建一个LogShare目录,当然这个名字随便,进入这个目录之后,按照图片点击上传按钮上传刚刚下载的压缩包:
#准备环境 P2

上传完成解压之后,在文件右侧更多按钮选择解压,解压完成之后,顶部工具栏选择终端,运行如下命令安装Composer依赖:

# 如果没有安装Composer
apt install composer   # Ubuntu / Debian
yum install composer   # RedHat / CentOS

# 安装依赖
composer install --ignore-platform-reqs

修改配置文件

解压完成之后,进入www/wwwroot/你的文件夹名字/docker文件夹,找到并打开mclogs.conf配置文件,内容如下:

server {
    listen 9300;
    sendfile off;
    server_name api.mclogs.lemwood.icu mc.logshare.cn;
    root /web/mclogs/api/public;
    error_log /var/log/nginx/api-mclogs-error.log error;
    client_max_body_size 210m;
    location / {
        try_files $uri /index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php-fpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
        include fastcgi_params;
    }
}

需要修改的部分是server_name的值,改成你的API地址即可,就比如我的API地址是api.logshare.cn,我就写:

server {
    # nore...
    server_name api.logshare.cn;
    # more...
}

接着进入www/wwwroot/你的文件夹名字/core/config/文件夹,里面是需要修改的配置文件,其实只需要修改urls.phpai.php,这是urls.php文件代码:

<?php

$config = [

    /**
     * The base URL for the front end
     *
     * Should not end with a slash
     */
    'baseUrl' => 'https://mclogs.lemwood.icu',

    /**
     * The base URL for the API
     *
     * Should not end with a slash
     */
    'apiBaseUrl' => 'https://api.mclogs.lemwood.icu',

];

这个配置文件配的的是API返回的地址和API地址,把baseUrl改成你的项目前端地址,后面会教你怎么部署前端,apiBaseUrl改成你在/docker/mclogs.conf中设置的域名。

然后再打开ai.php文件,这个文件是配置那个AI分析接口的,主要是配置apikey和模型,我们使用的是Google得生成式模型接口,也就是Google AiStudioGemini,先去这里获取APIKey:https://aistudio.google.com/app/apikey,是AIza...开头的那个,然后这里给出ai.php的完整代码:

<?php

return [
    'gemini_api_key' => '这里写你申请到的APIKey',
    'model' => '这里写你的模型'
];

拉取和构建镜像

由于宝塔面板自带的容器编排存在一些问题:_当我使用新建容器编排的时候选择我的Docker Cmpose文件,它并不支持Yaml中的Build配置,无法触发构建镜像,且拉取镜像之后无法自动建立容器,疑似兼容性欠佳_;所以我们需要使用文件管理自带的终端使用Docker Compose命令来部署Compose项目,之后依旧可以使用宝塔Docker模块来管理。

点击侧栏的Docker,安装宝塔Docker模块,之后来到www/wwwroot/你刚刚创建的文件夹/docker目录,同样点击终端按钮,运行如下命令拉取容器编排:

docker-compose up -d

如果有问题的话你会成功看到如下效果:
a8528a3d4f93263d.jpg
1129076826aed13a.jpg

如果你的截图和我类似,那么恭喜你,完成了80%,接下来在宝塔侧栏点击安全,放行9300端口,这个时候你就可以在内网访问api端点了,接下来开始反向代理。

反向代理

虽然现在的API已经可以在内网访问了,但是前端没办法通过内网访问你的API呀!还知道最开始我叫你们安装LNMP环境了吗?这个时候Nginx就派上用场了,按照图片来,找到侧边栏的Docker,点击并进入,顶部TAB栏选择网站,点进去之后,再点击创建按钮。
287bfafe53287968.jpg
接着,我们点击新建按钮,然后按照图片选择反代容器,填写好你刚刚在配置文件中写的域名之后,反代容器选择mclogs-nginx,端口会自动获取,不用管他,确认即可。
0919b6d8d7c3e30f.jpg
然后去你的域名服务商,解析一下域名:
c99bbe044762df23.jpg
解析完成之后,就该申请SSL了,如图所示:
9083be57f0b951b3.jpg
c2b6a1c3967f12f5.jpg

好的那么到了这里,整个McLogs-Next-API的部署也是完成了,可以通过你的域名访问API了。

部署官方Web UI实现

我们给McLogs-Next Headless API使用Vue3开发了一套Web UI实现,使用的技术栈是Vue3、TypeScript和TailwindCSS,仓库地址是:https://github.com/NingZeStudio/McLogs-Next-UI,这个前端部署起来很简单。

修改配置和构建

先克隆仓库到本地,运行如下命令:

git clone https://github.com/NingZeStudio/McLogs-Next-UI.git

克隆仓库之后,使用npm install安装所需要的依赖,和其他项目一样,使用npm run dev开启开发服务器。

npm是Node.js官方开发的一个JavaScript包管理器,使用npm之前你需要先安装nodejs,前往官网下载:https://nodejs.org
Linux可以执行如下命令安装nvmnodejs以及npm

# 下载并安装 nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash

# 代替重启 shell
\. "$HOME/.nvm/nvm.sh"

# 下载并安装 Node.js:
nvm install 24

# 验证 Node.js 版本:
node -v # Should print "v24.13.1".

# 验证 npm 版本:
npm -v # Should print "11.8.0".

Windows使用如下指令安装:

# 下载并安装 Chocolatey:
powershell -c "irm https://community.chocolatey.org/install.ps1|iex"

# 下载并安装 Node.js:
choco install nodejs --version="24.13.1"

# 验证 Node.js 版本:
node -v # Should print "v24.13.1".

# 验证 npm 版本:
npm -v # Should print "11.8.0".

我们需要编辑的文件并不多,主要就是几个TypeScript文件,分别是sharedUtils.tsapi.ts,首先是api.ts

import axios from 'axios'
const baseURL = 'https://api.logshare.cn'

export const apiClient = axios.create({
  baseURL: baseURL
})

export const getApiUrl = (endpoint: string) => {
    const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint
    return `${baseURL}/${cleanEndpoint}`
}

我们需要做的是把const baseURL = 'https://api.logshare.cn'中的https://api.logshare.cn改成你的后端域名,保存后打开sharedUtils.ts

// Shared utilities for both ApiDocsView and LogView components

// Common constants
export const API_BASE_URL = 'https://api.logshare.cn/'
export const FRONTEND_BASE_URL = 'https://logshare.cn/'

// Common API endpoints
export const API_ENDPOINTS = {
  LOG: '/1/log',
  ANALYSE: '/1/analyse',
  INSIGHTS: '/1/insights/',
  RAW: '/1/raw/',
  AI_ANALYSIS: '/1/ai-analysis/',
  LIMITS: '/1/limits',
  DELETE: '/1/delete/',
  RATE_ERROR: '/1/errors/rate'
}

// Common HTTP methods
export const HTTP_METHODS = {
  GET: 'GET',
  POST: 'POST',
  PUT: 'PUT',
  DELETE: 'DELETE',
  PATCH: 'PATCH'
}

// Common response types
export interface ApiResponse {
  success: boolean
  error?: string
}

// Common log levels
export const LOG_LEVELS = {
  ERROR: 'error',
  WARNING: 'warning',
  INFO: 'info',
  DEBUG: 'debug',
  CRITICAL: 'critical',
  EMERGENCY: 'emergency'
}

// Utility function to get API URL
export const getApiUrl = (endpoint: string): string => {
  return `${API_BASE_URL}${endpoint}`
}

// Utility function to get frontend URL
export const getFrontendUrl = (path: string): string => {
  return `${FRONTEND_BASE_URL}${path}`
}

// Utility function to validate log ID format
export const isValidLogId = (id: string): boolean => {
  // Assuming log IDs are alphanumeric with certain length
  const logIdRegex = /^[a-zA-Z0-9-_]+$/;
  return logIdRegex.test(id) && id.length >= 3 && id.length <= 50
}

// Utility function to format bytes to human readable format
export const formatBytes = (bytes: number, decimals: number = 2): string => {
  if (bytes === 0) return '0 Bytes'

  const k = 1024
  const dm = decimals < 0 ? 0 : decimals
  const sizes = ['Bytes', 'KB', 'MB', 'GB']

  const i = Math.floor(Math.log(bytes) / Math.log(k))

  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}

// Utility function to truncate text
export const truncateText = (text: string, maxLength: number, suffix: string = '...'): string => {
  if (text.length <= maxLength) return text
  return text.substring(0, maxLength) + suffix
}

// Utility function to debounce function calls
export const debounce = <T extends (...args: any[]) => any>(
  func: T,
  delay: number
): ((...args: Parameters<T>) => void) => {
  let timeoutId: ReturnType<typeof setTimeout> | null = null
  
  return (...args: Parameters<T>): void => {
    if (timeoutId) clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func(...args), delay)
  }
}

// Utility function to throttle function calls
export const throttle = <T extends (...args: any[]) => any>(
  func: T,
  limit: number
): ((...args: Parameters<T>) => void) => {
  let inThrottle: boolean
  
  return (...args: Parameters<T>): void => {
    if (!inThrottle) {
      func(...args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// Utility function to copy text to clipboard
export const copyToClipboard = async (text: string): Promise<boolean> => {
  try {
    if (navigator.clipboard && window.isSecureContext) {
      await navigator.clipboard.writeText(text)
      return true
    } else {
      // Fallback for older browsers or insecure contexts
      const textArea = document.createElement('textarea')
      textArea.value = text
      document.body.appendChild(textArea)
      textArea.focus()
      textArea.select()
      
      const successful = document.execCommand('copy')
      document.body.removeChild(textArea)
      return successful
    }
  } catch (error) {
    console.error('Failed to copy text to clipboard:', error)
    return false
  }
}

// Utility function to download content as file
export const downloadFile = (content: string, filename: string, contentType: string = 'text/plain'): void => {
  const blob = new Blob([content], { type: contentType })
  const url = URL.createObjectURL(blob)
  
  const link = document.createElement('a')
  link.href = url
  link.download = filename
  link.style.display = 'none'
  
  document.body.appendChild(link)
  link.click()
  
  // Clean up
  document.body.removeChild(link)
  URL.revokeObjectURL(url)
}

// Utility function to detect if running in a mobile environment
export const isMobile = (): boolean => {
  return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}

// Utility function to get current timestamp
export const getCurrentTimestamp = (): number => {
  return Math.floor(Date.now() / 1000)
}

// Utility function to format timestamp to readable date
export const formatDate = (timestamp: number): string => {
  const date = new Date(timestamp * 1000)
  return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}

我们需要修改的地方是这两块,也是改成自己的域名和自己的后端域名:

export const API_BASE_URL = 'https://api.logshare.cn/'
export const FRONTEND_BASE_URL = 'https://logshare.cn/'

在这两个配置文件修改完成之后,你可以尝试修改前端代码来实现自己的想法,接着运行:

npm run build   # 开始构建

构建完成之后,构建产物在dist目录,把这个目录的文件全部上传到服务器之后,新建一个静态网站就大功告成了!

新建静态网站

重新打开宝塔面板,侧栏找到网站点击并进入,TAB栏选择HTML项目,然后点击添加项目,在新弹出的弹窗里面,输入绑定域名也就是你准备的前端域名,根目录可以默认,点击确定,注意这个根目录要记住,后面要用。
8c70d29a0dcedb4a.jpg
57eab3f641df3f25.jpg
然后在在侧栏点击文件管理,进入刚刚网站的根目录,接着在本地把刚刚的dist目录打包成压缩包,上传到根目录之后,再按照上面的方法申请SSL之后,网站就可以正常访问了。
7fd2ac24bb2e07da.jpg

By MengZe2 On
此页面评论区已关闭