先说说 1Panel。它也算是这两年最火的新一代Linux服务器运维管理面板了吧,主打一个“现代化 + 简洁好用”。不像某Python面板那样一股子历史包袱,1Panel直接走的就是容器化路线,基于Docker来管理应用,UI也比较清爽,该有的功能都安排得明明白白:建站、运维、监控、备份,一套搞定,比较适合既想偷懒又想装专业的用户。
而1Panel的应用商店就是它的灵魂之一。简单来说,你可以把它理解成“手机应用商店”在服务器上的翻版:点一点就能把一个完整的服务(比如WordPress、Halo、Redis之类)拉起来,免去自己维护并更新的麻烦。官方应用商店本身已经挺丰富了,但嘛,跟GitHub这个无底洞比还是差点意思,经常会有一些项目你得自己手动折腾,想加进商店还得自己维护,这就给了像我这种强迫症患者“二次创作”的机会。
第三方应用商店有很多比较著名,比如下面,维护了海量的实用应用,基本上覆盖了全部应用,杂七杂八,你能想到的都有,但是这是他们的优点,也是他们的缺点,如果添加单独一个,会跟不上更新,如果全部添加,会非常冗余,甚至有很多重复应用。
为了更加直观,且不再冗余,自己维护一个应用商店也就有点用了起来。
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,用于展示图标:

除此之外,在一级目录下,有一个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可以说是1Panel自动更新的核心,首先克隆一个仓库,这里推荐克隆窝修改后的appstore应用,支持的功能和完整度会稍微高一些:
复刻完成后,添加应用,尝试打开Renovate,添加你个人的仓库,如果不出意外,会自动产生一个issue,用于实时观测应用状态:

无需关闭该issue,他会自动打开的QAQ,别问我怎么知道的。
应用安装好后,可以自行配置一下根目录中的配置文件,当然也可以保持默认,除非有部分应用超出范围。比如,第三方源。
打开下面站点,可以看到其中我添加了了一些三方源比如codeberg.org,这里我建议除了docker hub源,其余都按照规则添加进来,比如ghcr,k8s。
除了第一部分的源配置,下面我限定了更新的范围,比如不更新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文件中的镜像版本为最新。

看第二部分配置文件部分,我匹配了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实现创建分支并提交修改,打开PR,action实现修改文件夹,最终检测PR编号,自动合并并删除多余分支。
由于我们所使用的镜像需要符合docker hub的v2 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:
最终也是基本实现功能,并且保护了私有镜像站不会暴露。
至此,整个流程就算是跑通啦!应用商店的首次维护需要我们手动生成相关的元信息,但后续更新就简单多了:直接交给 action 去跑,再配合一个定时任务,就能实现全自动更新,真正做到“无人参与”。前端点击一下更新,应用就能在商店里展示出来,不仅美观,还方便备份和维护,算是省心又好用。大家有兴趣的话也欢迎试试!
时间过得飞快,转眼暑假就结束了,又要开工了。以前最讨厌的九月一日,如今反倒没什么感觉——毕竟已经没有开学可怕的事了(笑),而是走上了职场的新阶段。希望接下来的日子一切顺利吧!
还有还有,今天是俺的生日!🎂