跳转到内容

第三方1Panel应用商店不完全指北

更新于: 2025-08-29
LiuShen
9 分钟
3,416 字
PV --
UV --

这里是清羽AI,这篇文章介绍了1Panel应用商店的原理和使用方法,包括第三方应用商店的维护和自动更新工作流。文章首先描述了作者近期的生活状态和对博客更新的思考,然后详细介绍了1Panel作为新一代Linux服务器运维管理面板的特点,特别是其应用商店的功能。文章解释了如何通过维护一个本地应用商店来简化应用管理,并详细说明了应用目录的结构和应用更新的实现方式。最后,文章深入探讨了Renovate在自动更新中的应用,包括配置文件的作用和更新版本的触发机制,为读者提供了全面的应用商店管理和更新指南。

碎碎念

最近回了趟老家,结果发现老家的网络环境简直能把人逼疯,什么更新文章、写点小东西,统统没法搞,甚至聊天都得我弟的手机开热点,可能是手机硬件不行吧,那干脆就顺理成章地不写了,反正也没人追更。回来之后呢,更惨,直接进入了“开摆正道”,那种明知道该动手写点啥,却每天都在“明天一定”的状态,摆到最后竟然还摆出点小爽感——哎呀,真舒服,太舒服了,舒服到根本不想写。

但人不能一直摆下去,毕竟博客摆太久,自己看着也会心虚。再加上这段时间正好有朋友问我:我的应用商店是怎么实现自动更新的,既然有人抛话题,那我就顺势一接,正大光明地写一篇“看似干货,其实就是更新”的文章。反正更新嘛,不管怎么写,只要发出来,咕咕就算结束,万事大吉!

下个月月中,就该去工作咯,希望后面的路途一帆风顺!

PS: 八月二十九是我的生日哦!

介绍

先说说 1Panel。它也算是这两年最火的新一代Linux服务器运维管理面板了吧,主打一个“现代化 + 简洁好用”。不像某Python面板那样一股子历史包袱,1Panel直接走的就是容器化路线,基于Docker来管理应用,UI也比较清爽,该有的功能都安排得明明白白:建站、运维、监控、备份,一套搞定,比较适合既想偷懒又想装专业的用户。

1Panel的应用商店就是它的灵魂之一。简单来说,你可以把它理解成“手机应用商店”在服务器上的翻版:点一点就能把一个完整的服务(比如WordPressHaloRedis之类)拉起来,免去自己维护并更新的麻烦。官方应用商店本身已经挺丰富了,但嘛,跟GitHub这个无底洞比还是差点意思,经常会有一些项目你得自己手动折腾,想加进商店还得自己维护,这就给了像我这种强迫症患者“二次创作”的机会。

第三方应用商店有很多比较著名,比如下面,维护了海量的实用应用,基本上覆盖了全部应用,杂七杂八,你能想到的都有,但是这是他们的优点,也是他们的缺点,如果添加单独一个,会跟不上更新,如果全部添加,会非常冗余,甚至有很多重复应用。

站外引用 · 引用站外地址,不保证站点的可用性和安全性1Panel 应用商店的非官方应用适配库github.com@okxlin

为了更加直观,且不再冗余,自己维护一个应用商店也就有点用了起来。

仓库介绍

1Panel的资源库默认位置在/opt/1panel,其中我们安装过的所有应用在/opt/1panel/app中,这里涵盖了绝大部分资源,而我们的应用商店,则维护在/opt/1panel/resource/apps中,在其中有一个local文件夹,在其中添加同等格式的应用文件夹,则会被自动解析在应用商店的本地应用中,所以三方仓库的原理就是,将apps中的所有文件夹放在local文件夹中,定时刷新缓存,系统检测到缓存后,就会反馈到仓库中,最终实现推送更新。

我们可以看看okxlin提供的安装第三方应用的命令行:

git clone -b localApps https://ghp.ci/https://github.com/okxlin/appstore /opt/1panel/resource/apps/local/appstore-localApps
cp -rf /opt/1panel/resource/apps/local/appstore-localApps/apps/* /opt/1panel/resource/apps/local/
rm -rf /opt/1panel/resource/apps/local/appstore-localApps

其实就是实现了我们上面说的那些内容。

应用目录

我们再进入应用目录,以AllinSSL为例,目录如下:

. 📂 allinssl
└── 📂 1.0.7/
│ ├── 📄 data.yml
│ ├── 📄 docker-compose.yml
├── 📄 README.md
├── 📄 data.yml
└── 📄 logo.png

其中的readme.md,很明显,是说明文档,展示在安装的首页,还有logo.png,用于展示图标:

AllinSSL

除此之外,在一级目录下,有一个data.yml文件内容如下:

name: AllinSSL
tags:
  - SSL
  - 证书管理
  - 自动化运维
  - DevOps
  - 安全
title: SSL证书全流程管理工具,一站式证书生命周期解决方案
description: 一站式SSL证书生命周期管理工具,支持多家CA和多平台自动化部署,提供安全入口保护和证书状态监控。
additionalProperties:
  key: allinssl
  name: AllinSSL
  tags:
    - Tool
    - DevOps
  shortDescZh: 一站式SSL证书生命周期管理解决方案,支持多家CA与多平台自动化运维
  shortDescEn: One-stop SSL certificate lifecycle management tool with multi-CA and platform support
  type: website
  crossVersionUpdate: true
  limit: 0
  website: https://github.com/allinssl/allinssl
  github: https://github.com/allinssl/allinssl
  document: https://github.com/allinssl/allinssl
  description:
    en: One-stop SSL certificate lifecycle management tool supporting multiple CAs and platforms, with automated issuance, renewal, deployment, and monitoring.
    zh: 一站式SSL证书生命周期管理工具,支持多家证书颁发机构和多平台自动化部署,提供证书申请、续期、监控等功能。
    zh-Hant: 一站式SSL憑證生命週期管理工具,支援多家憑證頒發機構及多平台自動化部署,提供憑證申請、續期、監控等功能。
    ja: 複数のCAとプラットフォームに対応したワンストップSSL証明書ライフサイクル管理ツール。自動発行、更新、展開、監視を提供。
    ms: Alat pengurusan kitar hayat sijil SSL sehenti yang menyokong pelbagai CA dan platform, dengan pengeluaran, pembaharuan, penyebaran, dan pemantauan automatik.
    pt-br: Ferramenta de gerenciamento de ciclo de vida de certificado SSL tudo-em-um, suportando múltiplas CAs e plataformas, com emissão, renovação, implantação e monitoramento automatizados.
    ru: Универсальный инструмент управления жизненным циклом SSL-сертификатов с поддержкой множества центров сертификации и платформ, автоматическим выпуском, обновлением, развертыванием и мониторингом.
    ko: 여러 CA 및 플랫폼을 지원하는 원스톱 SSL 인증서 수명 주기 관리 도구로 자동 발급, 갱신, 배포 및 모니터링을 제공합니다.
  architectures:
    - amd64
    - arm64

需要注意其中的key,这个值对应着文件夹名称,不容有错,其他的可以象征性的填写一下,tags标签有几个固定的值,如果写了其他的会不显示,但是不会报错,剩下的,建议gpt生成一下嘻嘻。

在一级目录下,还有一个以版本号命名的文件夹,这个文件夹名称就是我们安装时选择的版本号,一般文件夹内部的docker-compose.yml文件中的版本号需要和文件夹名称对应,非必要不要写latest

不能写latest的原因

这个涉及下一部分,应用商店不单单是维护一个仓库即可,如果应用数量较多,手动更新会非常费神,所以需要自动检测到更新,而latest标签的镜像始终指向最新的哈希值,所以无法检测到更新,导致应用没法推送更新,哪怕应用发布了新的应用。

在版本号文件下还有一个data.yml,这个和上面的根目录不同,根目录的data.yml维护的是应用元信息,而版本号下面的data,yml文件则维护的是安装字段信息,如下:

安装字段信息

1Panel会根据这个字段,在目录下创建.env文件,而目录下的docker-compose.yml中的信息也是使用的环境变量,在启动的时候会自动读取.env中维护的信息,从而实现安装,这就是整个安装的过程。

应用更新

这里更新使用的是renovate检测,该组件会定时检测更新,如果有更新则提交PR

这里下一章节讲解,我们先讲解一下1Panel中是怎么实现推送更新的。首先,应用中的文件夹更新,系统会根据版本号大小判断到,当前应用是否有更新,注意这里判断的是文件夹名称,而不是docker-compose.yml中的版本号。

当检测到更新后,系统会提示,更新,首先备份整个目录,由于在1Panel应用商店中,通常会将数据挂载到./data目录下,所以也不用担心。然后将新文件覆盖进来,由于数据文件夹中原始是没有文件的,所以这部分文件不需要担心覆盖。

覆盖完成后,系统会执行docker compose up -d命令,如果一切正常,最终则会正确更新,如果更新出现问题,也会自动回退。

升级前备份应用

至于在上面设置页面的自定义仓库,其实就是给正常仓库的apps文件夹打包为tar.gz压缩包,个人感觉没必要替换掉所有的应用商店,如果有这部分需求可以看以下视频自行学习,这里不再讲解。

站外引用 · 引用站外地址,不保证站点的可用性和安全性发现了个有手就行的服务器面板工具|使用1Panel自建应用商店!凌霞实验室

所以难点就集中在怎么自动更新应用啦!下面我们就来讲解一下1Panel工作流中的一些原理!

更新工作流

Renovate

安装应用

Renovate可以说是1Panel自动更新的核心,首先克隆一个仓库,这里推荐克隆窝修改后的appstore应用,支持的功能和完整度会稍微高一些:

站外引用 · 引用站外地址,不保证站点的可用性和安全性🌭清羽飞扬自建非官方第三方1Panel应用仓库github.com@willow-god

复刻完成后,添加应用,尝试打开Renovate,添加你个人的仓库,如果不出意外,会自动产生一个issue,用于实时观测应用状态:

实时状态

无需关闭该issue,他会自动打开的QAQ,别问我怎么知道的。

配置文件

应用安装好后,可以自行配置一下根目录中的配置文件,当然也可以保持默认,除非有部分应用超出范围。比如,第三方源。

站外引用 · 引用站外地址,不保证站点的可用性和安全性Revovate.jsongithub.com@willow-god

打开下面站点,可以看到其中我添加了了一些三方源比如codeberg.org,这里我建议除了docker hub源,其余都按照规则添加进来,比如ghcrk8s

除了第一部分的源配置,下面我限定了更新的范围,比如不更新action,以稳定运行,不更新部分已经停更的应用,指定更新特殊版本号的应用,比如牢Umami,其余的你们自己看咯,完整的配置文件如下:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:base"],
  "gitIgnoredAuthors": ["githubaction@githubaction.com"],
  "rebaseWhen": "never",
  "prCreation": "immediate",

  "hostRules":
    [
      {
        "hostType": "docker",
        "matchHost": "codeberg.org",
        "registryUrls": ["https://codeberg.org"],
      },
      {
        "hostType": "docker",
        "matchHost": "code.forgejo.org",
        "registryUrls": ["https://code.forgejo.org"],
      },
    ],

  "packageRules":
    [
      { "matchManagers": ["github-actions"], "enabled": false },
      {
        "matchDatasources": ["docker"],
        "matchFileNames": ["apps/meting-api/*/docker-compose.yml"],
        "enabled": false,
      },
      {
        "matchDatasources": ["docker"],
        "matchFileNames": ["apps/chatnio/*/docker-compose.yml"],
        "enabled": false,
      },
      {
        "matchDatasources": ["docker"],
        "matchFileNames": ["apps/*/*/docker-compose.yml"],
        "versioning": "semver",
      },
      {
        "matchDatasources": ["docker"],
        "matchPackageNames": ["ghcr.io/umami-software/umami"],
        "versionCompatibility": "^(?<compatibility>.*)-(?<version>.*)$",
        "versioning": "semver",
      },
    ],
}

按道理默认的够用了,但是万一你们有抽象的要求呢嘻嘻。

作用

Renovate会不定时开始检测,具体看其队列中的检测任务的时间,如果检测到更新,则会自动创建新分支,修改版本号后提交pr,修改docker-compose文件中的镜像版本为最新。

Renovate提交的pr信息

看第二部分配置文件部分,我匹配了apps文件夹下所有的镜像文件,做到不遗漏更新,但是根据第一部分的讲解,仅仅更新docker-compose文件无法推送更新,推送更新主要依赖于文件夹的版本号实现更新,这部分是renovate机器人无法做到的~

那就继续看第二部分!

更新版本

触发机制

在我们仓库的action工作流中,除了Renovate工作流触发器,还有第一个工作流,这个工作流才是整个系统的核心。

所有工作流

工作流内容如下:

name: Update app version in Renovate Branches

on:
  push:
    branches: ["renovate/*"]
  workflow_dispatch:
    inputs:
      manual-trigger:
        description: "Manually trigger Renovate"
        default: ""

jobs:
  update-app-version:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Configure git
        run: |
          git config --local user.email "githubaction@githubaction.com"
          git config --local user.name "github-action update-app-version"

      - name: Get list of updated files by the last commit
        id: updated-files
        run: |
          echo "files=$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} | tr '\n' ' ')" >> $GITHUB_OUTPUT

      - name: Run renovate-app-version.sh on updated files
        id: rename
        run: |
          set -e
          chmod +x .github/workflows/renovate-app-version.sh

          files="${{ steps.updated-files.outputs.files }}"
          declare -a changed_apps=()

          echo "Updated files: $files"

          for file in $files; do
            if [[ $file == *"docker-compose.yml"* ]]; then
              echo "Processing file: $file"

              app_name=$(echo $file | cut -d'/' -f 2)
              old_version=$(echo $file | cut -d'/' -f 3)
              echo "App name: $app_name, old version: $old_version"

              # 获取所有服务名
              services=$(yq '.services | keys | .[]' "$file")
              service=""
              image_line=""

              for s in $services; do
                # 通过awk获取服务下的image行(包含注释)
                image_line=$(awk "/services:/{flag=0} /^\s*$s:/{flag=1} flag && /^\s*image:/{print; exit}" "$file")
                echo "Service $s image line: $image_line"
                if [[ "$image_line" != *"[ignore]"* ]]; then
                  service="$s"
                  break
                else
                  echo "Skipping service $s due to [ignore]"
                fi
              done

              if [[ -z "$service" ]]; then
                echo "No valid service found in $file, skipping..."
                continue
              fi

              # 提取image纯字符串,去除注释和多余空格
              image=$(echo "$image_line" | sed -E 's/^\s*image:\s*([^ #]+).*/\1/')
              echo "Selected service: $service"
              echo "Extracted image: $image"

              if [[ "$image" == *":"* ]]; then
                new_version=$(cut -d ":" -f2- <<< "$image")
                trimmed_version=${new_version/#"v"/}
                echo "Parsed new version: $trimmed_version"
              else
                trimmed_version=""
                echo "No version tag found in image."
              fi

              changed_apps+=("${app_name}:${old_version}:${trimmed_version}")
              echo "Calling renovate-app-version.sh with: $app_name, $old_version, $trimmed_version"
              .github/workflows/renovate-app-version.sh "$app_name" "$old_version" "$trimmed_version"
            fi
          done

          echo "All changed apps: ${changed_apps[*]}"
          echo "apps=$(IFS=, ; echo "${changed_apps[*]}")" >> $GITHUB_OUTPUT

      - name: Commit & Push Changes
        run: |
          set -e
          IFS=',' read -r -a apps <<< "${{ steps.rename.outputs.apps }}"
          for item in "${apps[@]}"; do
            app_name=$(cut -d':' -f1 <<< "$item")
            old_version=$(cut -d':' -f2 <<< "$item")
            new_version=$(cut -d':' -f3 <<< "$item")

            if [[ -n "$app_name" && -n "$new_version" ]]; then
              git add "apps/$app_name/*"
              git commit -m "📈将应用 $app_name 的版本从 $old_version 升级到 $new_version [skip ci]" --no-verify || echo "无内容可提交"
            fi
          done

          git push || echo "无内容可推送"

      - name: Force merge PR after version bump
        if: github.ref_name != 'main'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -e
          branch_name=$(git rev-parse --abbrev-ref HEAD)
          echo "Current branch: $branch_name"

          # 获取 PR 编号
          pr_number=$(gh pr list --state open --head "$branch_name" --json number -q '.[0].number')
          if [ -z "$pr_number" ]; then
            echo "No PR found for branch $branch_name"
            exit 0
          fi

          echo "Found PR #$pr_number, force merging..."

          # 强制合并,不管 mergeable 状态
          gh pr merge "$pr_number" --merge --delete-branch --admin

可以看到触发方式中有,在分支renovate/*触发推送,则会进入到该工作流,恰好,在上一部分renovate中,自动更新创建的分支也是以这个为开头的,所以当renovate更新后,我们可以抓取到更新并触发该工作流。

更新文件夹

renovate机器人更新时,会在提交信息中给出一个规范信息,从xxx版本更新到了yyy版本都有记录,我们可以从该记录中提取到旧版本信息和新版本信息,再执行renovate-app-version.sh脚本,该脚本经过我大量简化,功能仅为输入应用名称,旧版本,新版本,即可实现文件夹的重命名。

具体提取版本号的过程,你们可以自行研究一下,这里不再细讲,能用即可。

自动合并

原版appstore到这里就结束了,而我实现的新版则会自动合并符合要求的更新PR,实现全自动化,由于我们的触发器是Push触发器,我们无法直接获取到PR的编号,所以这里我使用github API,检测PR编号,并自动强制合并。

最终实现的效果如下:

实现更新版本全流程

首先,renovate实现创建分支并提交修改,打开PRaction实现修改文件夹,最终检测PR编号,自动合并并删除多余分支。

镜像

由于我们所使用的镜像需要符合docker hubv2 API规范,才能正常通过renovate更新并检测,普通源倒是很多,但是譬如ghcr这种的镜像源,比较稳定的非常有限,ghcr.nju.edu.cn是南京大学官方维护的镜像,稳定,但是很遗憾,经过测试,无法直接作为镜像源添加在列表中,无法支持检测更新的功能。

但是嘛,我总不能每次安装手动改一次镜像地址吧,作为一个彻头彻尾的懒蛋,我是不能接受的,所以我写了一个脚本,用来替换相关的镜像源。

设计

起初我想通过直接维护一个允许api的镜像源,后面发现成本较高,并且暴露在公网,容易被滥用,毕竟反向代理这些被墙的站点,风险是众所周知的,所以我选择了将配置写在服务器本地,提供脚本实现替换并安装。

脚本

首先,更新本地应用的脚本设计如下:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

GIT_REPO="https://cnb.cool/Liiiu/appstore"
TMP_DIR="/opt/1panel/resource/apps/local/appstore-localApps"
LOCAL_APPS_DIR="/opt/1panel/resource/apps/local"

trap 'rm -rf "$TMP_DIR"' EXIT

echo "📥 Cloning appstore repo..."
[ -d "$TMP_DIR" ] && rm -rf "$TMP_DIR"
git clone "$GIT_REPO" "$TMP_DIR"

echo "🔄 Mirroring apps..."
cd "$TMP_DIR"
if [[ -f ./mirror.sh ]]; then
    chmod +x ./mirror.sh
    ./mirror.sh
else
    echo "⚠️ mirror.sh not found, skipping mirroring"
fi
cd -

mkdir -p "$LOCAL_APPS_DIR"

for app_path in "$TMP_DIR/apps/"*; do
    [ -d "$app_path" ] || continue
    app_name=$(basename "$app_path")
    local_app_path="$LOCAL_APPS_DIR/$app_name"

    echo "🔁 Updating app: $app_name"
    [ -d "$local_app_path" ] && rm -rf "$local_app_path"
    cp -r "$app_path" "$local_app_path"
done

echo "✅ Sync completed."

在其中,会执行一个Mirror.sh脚本,该脚本实现的功能为,首先从本地找到配置文件,地址为/opt/mirror-config.env,内容示例如下:

# ====== GHCR (GitHub Container Registry) ======
# 是否经常被墙:是
GHCR_ENABLE=true
GHCR_MIRROR=ghcr.io.mirror

# ====== Quay.io (RedHat/Community images) ======
# 是否经常被墙:是
QUAY_ENABLE=false
QUAY_MIRROR=quay.io.mirror

# ====== GCR (Google Container Registry) ======
# 是否经常被墙:是
GCR_ENABLE=false
GCR_MIRROR=gcr.io.mirror

# ====== k8s.gcr.io (旧 Kubernetes 镜像仓库) ======
# 是否经常被墙:是
K8S_GCR_ENABLE=false
K8S_GCR_MIRROR=k8s.gcr.io.mirror

# ====== registry.k8s.io (新 Kubernetes 镜像仓库) ======
# 是否经常被墙:是
K8S_REG_ENABLE=false
K8S_REG_MIRROR=registry.k8s.io.mirror

在项目的根目录中,mirror.sh执行后,首先会检测本地的该路径的配置文件,如果存在,则会读取其中的配置,选择是否替换镜像和镜像地址,如果存在文件,且设置为true,则会按照根目录中,维护的.env文件,分辨哪些项目是对应的镜像,并检索目录进行替换。

最终拉取下来后,呈现在应用商店的即为镜像站点,并且由于版本检测并不在本地进行,所以只要可以拉取即可,是否支持api并不重要。

具体的文档也可以看到github

站外引用 · 引用站外地址,不保证站点的可用性和安全性🌭清羽飞扬自建非官方第三方1Panel应用仓库github.com@willow-god

最终也是基本实现功能,并且保护了私有镜像站不会暴露。

总结

至此,整个流程就算是跑通啦!应用商店的首次维护需要我们手动生成相关的元信息,但后续更新就简单多了:直接交给 action 去跑,再配合一个定时任务,就能实现全自动更新,真正做到“无人参与”。前端点击一下更新,应用就能在商店里展示出来,不仅美观,还方便备份和维护,算是省心又好用。大家有兴趣的话也欢迎试试!

时间过得飞快,转眼暑假就结束了,又要开工了。以前最讨厌的九月一日,如今反倒没什么感觉——毕竟已经没有开学可怕的事了(笑),而是走上了职场的新阶段。希望接下来的日子一切顺利吧!

还有还有,今天是俺的生日!🎂

每日一图

图片来自哲风壁纸

生日快乐!

Previous
血证未泯,山河已立
Next
循一缕风,入山偷得夏日凉