6598 字
33 分钟
Kener 自部署与美化
rajnandan1kener

我的示例站点

TL;DR#

  • Kener 是开源(MIT)状态页系统,用 Docker Compose 可以在 10 分钟内跑起来,最少只需要 3 个变量:KENER_SECRET_KEY、ORIGIN、REDIS_URL。
  • 想省事就用 SQLite;想要生产级稳定就换 PostgreSQL/MySQL + Redis(本文走这条)。
  • 美化不需要改源码:Manage → Site Configurations 管品牌与导航,Manage → Customizations 管配色 / 字体 / 页脚 / 公告 / Custom CSS,外加 static/ 自定义样式表这条进阶路径。
  • 部署阶段 90% 的坑集中在三处:ORIGIN 写错、KENER_BASE_PATH 与反向代理路径不一致、监控没有分配到任何 Page(于是公开页面永远空空如也)。
  • 文章最后附「上线前检查清单」与「踩坑清单(按概率排序)」,可直接当发布 checklist 用。

1. 为什么选 Kener#

1.1 三条选型路线#

需要对外暴露服务可用性时,市面上有三条路:

方案代表适合
SaaSAtlassian Statuspage、Instatus、Better Stack不想运维、有预算
纯监控面板(状态页是附属品)Uptime Kuma、Gatus监控优先,状态页次之
独立状态页(监控内建)Kener、Upptime、Cachet想要「监控 + 状态页 + 事件管理」一体,且能自己运维

Kener 属于最后一类,而且是目前少数把监控、状态页、事件(Incident)管理、维护窗口、订阅通知、嵌入组件全部做进同一个自托管包里、且默认 UI 就比较精致的开源项目。官方一句话定位:

Kener 不是要替代 Datadog 或 Atlassian,而是用最少力气,搭出一个好看、好用、可定制的状态页。

1.2 它把哪几件事做进了一个包里#

做自托管监控时,通常会被拆成两件事:采集(谁挂了、延迟多少)和对外沟通(用户看到的是什么)。Kener 的价值在于它专注第二件事,同时把第一件事也做进内置能力里:

能力说明
多类型监控API/网站、Ping、TCP、DNS、SSL 证书到期、SQL、gRPC、Heartbeat(任务上报)、GameDig(游戏服)、Prometheus(PromQL 阈值)、Docker 容器与守护进程、Group(多个监控加权聚合)等 11 种
事件沟通事件(Incidents)与维护窗口(Maintenances),维护窗口支持 RRULE 周期规则,事件有完整生命周期(Investigating → Identified → Monitoring → Resolved),支持从告警自动生成事件
告警告警规则 + 阈值/严重级别 + 触发路由,支持 Webhook / Discord / Slack / Email,消息模板用 Mustache 变量
订阅用户可邮件订阅事件与维护通知,配 RSS 2.0 输出;支持 CAPTCHA 防刷
多页面一份实例可以挂多个状态页(按项目/客户拆分),可控制页面可见性、页面级导航与事件范围
企业特性OIDC 单点登录、用户角色与邀请、i18n 多语言与时区、Analytics 接入
可定制页脚 HTML、明暗双套状态色、字体、公告条、Custom CSS、页面排序、SEO/社交预览
数据自由SQLite(默认零配置)/ PostgreSQL / MySQL 三选一,无订阅者数量与监控数量上限

一句话:它既是监控系统,也是可以直接发给客户看的门面。这也是后面「美化」值得花时间的原因——状态页本身就是产品的一部分。


2. 技术栈与架构速览#

部署前先知道它跑起来需要什么,能少踩坑。

  • 前端:SvelteKit(Svelte 5,TypeScript-first)+ shadcn-svelte + Tailwind CSS v4 —— 这就是「默认就好看」的来源。
  • 运行时:Node.js(要求 ≥ 24.14,生产镜像内是 Node 24)。
  • 任务队列 / 调度:BullMQ + Redis(监控检查、定时维护、每日清理都走队列)—— 这也是 REDIS_URL 必填、Redis 是硬依赖的原因。
  • 数据库:Knex.js 抽象,默认 SQLite,可选 PostgreSQL / MySQL;迁移与 seed 启动时自动跑。
  • 双进程模型:
    • 开发态 npm run dev = SvelteKit 开发服务器 + 一个 cron 调度器并行;
    • 生产态单一入口 scripts/main.ts:Express 服务 + SvelteKit handler + 启动时自动跑迁移与 seed + 调度器。
  • 生产镜像:多阶段构建,Alpine / Debian 两个变体,非 root(node 用户)运行,内置 /healthcheck 健康检查,并为 ping 授予 NET_RAW 能力(这样 ICMP 监控才能工作)。

一句话记忆:Kener 容器 + 一个 Redis,就是一条最小部署。


3.1 准备#

  • 一台装了 Docker Engine 与 Compose v2 的服务器(1 vCPU / 1GB 内存起步即可支撑约百来个监控)
  • 一个(子)域名,例如 status.example.com
  • 已把域名解析到服务器

官方给出的最小规格参考:应用实例最低 1 vCPU / 512MB,推荐 1 vCPU / 1GB;PostgreSQL/MySQL 最低 1 vCPU / 512MB,推荐 1~2GB;Redis 最低 128MB,推荐 256MB。

3.2 目录与 .env#

Terminal window
mkdir -p /opt/kener && cd /opt/kener

.env

.env
# ---------- 必填三项 ----------
# 生成:openssl rand -base64 32
KENER_SECRET_KEY=换成一段足够长的随机字符串
# 公网访问地址:带协议、不带结尾斜杠、不要带子路径
ORIGIN=https://status.example.com
REDIS_URL=redis://redis:6379
# ---------- 数据库 ----------
DATABASE_URL=postgresql://kener:换成强密码@postgres:5432/kener
# ---------- 常用可选项 ----------
PORT=3000
TZ=UTC
BODY_SIZE_LIMIT=10M
# ---------- 邮件(二选一,同时配置时 SMTP 优先) ----------
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=mailer-user
SMTP_PASS=邮件服务商给的授权码
SMTP_FROM_EMAIL=status@example.com
# SMTP_SECURE: 1 = implicit TLS (usually port 465)
# SMTP_SECURE: 0 = STARTTLS (usually port 587 / 25)
SMTP_SECURE=0

三个必填项各自的「写错的后果」值得先记住:

  • KENER_SECRET_KEY:JWT 签名与敏感数据加密用。不设置会回落到默认 key,生产环境等同于把会话送人,日志里也会刷警告。
  • ORIGIN:SvelteKit 生产构建用它做 CSRF 校验。缺失或写成 http://localhost:3000 时,后台的登录、保存设置等所有表单提交都会被拒,报 Cross-site POST form submissions are forbidden。
  • REDIS_URL:Redis 是必需依赖,没有它 Kener 起不来(队列、缓存、调度都靠它)。

KENER_SECRET_KEY 生成方式:

任选其一
openssl rand -base64 32
# 或者
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

3.3 compose 文件(应用 + Postgres + Redis)#

docker-compose.yml
name: kener
services:
kener:
image: rajnandan1/kener:latest # Alpine 变体:rajnandan1/kener:alpine
env_file:
- .env
ports:
- "127.0.0.1:3000:3000" # 只对本机暴露,走反向代理对外
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
postgres:
image: postgres:16
environment:
POSTGRES_DB: kener
POSTGRES_USER: kener
POSTGRES_PASSWORD: 换成强密码 # 与 DATABASE_URL 中的保持一致
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kener -d kener"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
pg_data:
redis_data:

要点:

  • 用 127.0.0.1:3000:3000 而不是 3000:3000,避免应用端口直接暴露到公网。
  • 同一 compose 网络里,主机名就是服务名(postgres、redis),所以 DATABASE_URL 和 REDIS_URL 里写服务名即可。
  • depends_on 配 condition: service_healthy 能少一类「数据库还没起来,应用先崩」的启动顺序问题。

如果你的团队已有托管数据库/Redis,把 postgres/redis 两个服务删掉,.env 里的连接串换成托管地址或 rediss://(TLS)即可,应用部分完全不用动。

3.4 启动与验收#

启动并查看日志
docker compose up -d
docker compose logs -f kener

看到应用监听日志后,验证健康检查端点:

健康检查
curl -fsS https://status.example.com/healthcheck
# {"status":"ok","db":true,"redis":true}

健康检查的语义要记清楚:

  • 默认 /healthcheck 永远返回 HTTP 200,status 会是 degraded 当数据库或 Redis 不可达。这是故意的——避免「数据库挂了 → 容器健康检查失败 → 容器反复重启 → 整段故障期一直抖动」。
  • 需要让编排系统(负载均衡就绪探针、告警)对依赖故障做出反应时,才用 ?strict=1,此时任一组件不可用会返回 503。
  • 但不要把 ?strict=1 接到「失败就重启」的健康检查上(Docker HEALTHCHECK、Railway 之类),重启治不好一个死的数据库,只会把故障期拉成无限重启循环。

3.5 不装 Docker 的裸机跑法(附)#

如果就是不想装 Docker,要求 Node ≥ 24.14 + 一个 Redis:

裸机最小步骤
git clone https://github.com/rajnandan1/kener.git && cd kener
npm install
# 起一个 Redis
docker run -d --name kener-redis -p 6379:6379 redis:7-alpine # 或裸机装 redis
npm run build
npm run start

.env 至少包含:

.env 最小集
KENER_SECRET_KEY=<random>
ORIGIN=http://localhost:3000
REDIS_URL=redis://localhost:6379
PORT=3000

仓库还带了 docker-compose.dev.yml,从本地 Dockerfile 现场构建,适合开发 / 改代码时调试:

本地源码构建
# 纯本地构建
docker compose -f docker-compose.dev.yml up -d --build
# 或:保留生产基础配置,只把 Kener 换成本地构建
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

构建参数(--build-arg):VARIANT=alpine|debian、WITH_DOCS=true|false、KENER_BASE_PATH=。

3.6 从「服务起来了」到「页面能看了」#

这一步是很多教程漏掉的:新建的监控如果不分配到至少一个 Page,就不会出现在公开状态页上。最小可用流程:

最小可用流程

  1. 进后台建管理员

    访问 /manage,首次进入会引导你创建管理员账号(密码用 bcrypt 存)。

  2. 站点信息

    Manage → Site Configurations,填 siteName、siteURL、logo、favicon、导航菜单。

  3. 建监控

    Manage → Monitors → New Monitor,设置 Tag(唯一 id,如 homepage)、Name(展示名)、Monitor Type(先用 API/网站监控)。

  4. 配检查

    填目标 URL、检查间隔或 cron 表达式;有条件的把「宽限期/确认阈值」调成连续 N 次失败才判定状态变化,避免瞬断被当成故障。

  5. 分配到页面

    打开该监控的 Page Visibility,勾到目标页面上。这一步不做,前面全白干。

  6. 验证

    点监控的 View,确认开始采集并在页面上显示。

顺手建议一起配掉的东西:

  • 邮件:不配邮件则订阅、邀请、找回都不可用。SMTP 与 Resend 同时配置时 SMTP 优先;SMTP_SECURE=1 是隐式 TLS(一般配 465),SMTP_SECURE=0 是 STARTTLS(一般配 587/25),把 587 错配成 1 会直接连接超时。
  • 告警通道:Webhook / Discord / Slack / Email,先在测试按钮上把链路打通再上生产。
  • 数据保留策略:Manage → Site Configurations → Data Retention Policy,默认清理任务每天 UTC 午夜跑一次,retentionDays 决定监控数据留多久。不设上限的话,monitoring_data 会稳定变大。

3.7 数据库侧的两个「长期运行」注意点#

PostgreSQL:Kener 的迁移会把 monitoring_data 的 autovacuum 阈值调低(约 1% 死行即 vacuum、0.5% 变更即 analyze),让每日清理后能尽快异步维护,避免膨胀拖慢每次页面加载。如果你是升级上来的老实例且 monitoring_data 已经很大,迁移后手动跑一次:

手动 vacuum
VACUUM (ANALYZE) monitoring_data;

连接池:Kener 用两个独立连接池——Web 池(DATABASE_POOL_MAX,默认 10)服务 HTTP 请求,Worker 池(DATABASE_WORKER_POOL_MAX,默认 5)跑后台任务。好处是后台任务爆发只能吃满 Worker 池,打不垮页面加载。扩容前先算一笔账:

池预算公式
副本数 × (DATABASE_POOL_MAX + DATABASE_WORKER_POOL_MAX) < 数据库 max_connections

小规格托管 Postgres(常见上限 20~25 连接)请保持默认或降低。页面加载报 KnexTimeoutError 就调大前者,后台检查/告警报同一错误就调大后者。

MySQL:务必让服务端时区是 UTC(如 --default-time-zone=+00:00)。Kener 内部按 UTC 读写 created_at/updated_at,会话时区不是 UTC 会让时间整体偏移。注意 TZ 环境变量一般保持 UTC,不要为了「本地时间好看」去改它——那会引入时间戳比较不一致的问题,用户侧时区在界面里配。

3.8 备份、升级、日常维护#

日常维护命令
# 数据库备份(Postgres)
docker compose exec -T postgres pg_dump -U kener kener | gzip > kener-$(date +%F).sql.gz
# 只读探针(外部监控 Kener 自己)
curl -fsS https://status.example.com/healthcheck?strict=1
# 升级
docker compose pull && docker compose up -d
  • docker compose pull 前先备份:Kener 启动时会自动跑迁移,跨大版本(例如 v3 → v4)不做数据自动迁移,需要按官方迁移说明走。
  • 换数据库(SQLite → Postgres)不会自动搬数据,要自己导出导入。
  • SQLite 用户:数据库是单文件,直接备份 kener.db 即可;Redis 数据是队列态,一般不用特别备份。
  • 建议给状态页本身也配一个外部监控(Uptime Kuma / 云拨测都行):状态页自己挂了,是最难被发现的故障。

4. 美化实录(不写源码,先把面板用满)#

Kener 的视觉定制分两层:面板层(Manage → Customizations / Manage → Site Configurations,改完即时生效,升级不丢)和 代码层(static/ 静态文件 + src/app.html,需要重新构建镜像)。建议先用满面板层,只有需要注入 JS 或做全局替换时才动代码层。

4.1 品牌与导航(花 10 分钟,收益最大)#

在 Manage → Site Configurations:

项目存储键效果
站点名 / URL / LogositeName、siteURL、logo顶部导航品牌区与页面标题
Faviconfavicon<head> 中的页面图标
导航菜单nav顶部导航链接
监控子菜单subMenuOptions控制监控详情页是否显示徽章/嵌入的分享入口
全局页面可见性globalPageVisibilitySettings页面切换器与页面级导航/事件范围;forceExclusivity 可做页面级隔离(多品牌/多业务线共用一个实例)
社交预览与 SEOmetaSiteTitle、metaSiteDescription、socialPreviewImage全站默认 <title>、og:title、<meta description>、og:description、og:image

几个容易被忽略的细节:

  • Logo 建议用透明底 SVG/PNG,深浅两种主题下都不会出现白框。
  • v4 起 Logo / Favicon 图片直接入库(不再走 v3 的 /uploads/ 文件路径),支持 HEIC/HEIF(iPhone 截图可直传,服务端自动转 JPEG)。
  • socialPreviewImage 用 1200×630,微信/Slack/Twitter 抓取的卡片才不会被裁掉关键信息。
  • 页面多起来后打开「自定义页面排序」(pageOrderingSettings),切换器里 Pages 的顺序就由你决定;关闭排序只是恢复默认展示,已保存的顺序不会丢。

4.2 配色:明暗两套状态色#

Manage → Customizations → Colors 提供浅色(colors)与深色(colorsDark)两组状态/强调色,运行时被注入为公共布局 <head> 里的 CSS 变量。所以改色本质是定义一套能同时活在两种主题下的色板。改完公开页、嵌入组件、邮件模板里的状态徽章都会跟着变(状态色会以 CSS 变量 + 模板变量 {{colors_up}} 等下发)。

推荐一套低饱和、偏「工程感」的配色思路(把 # 开头的值填进面板对应字段即可):

状态浅色主题深色主题用途
UP / 正常#16A34A#22C55E正常运行
DEGRADED / 降级#D97706#FBBF24性能下降、部分异常
DOWN / 故障#DC2626#F87171不可用
MAINTENANCE / 维护#2563EB#60A5FA计划内维护
强调色(链接/按钮)#4F46E5#818CF8品牌色

调色三条经验:

  1. 深色主题不要用纯饱和色(#FF0000 这种),在暗底上会「发光刺眼」,加一点亮度、降一点饱和更耐看。
  2. 浅色主题下色块要保证白底可读,别用低于 3:1 对比度的浅黄/浅绿。
  3. 状态色只承担「状态」语义,品牌色单独留给链接与按钮,否则页面上会到处是「红色按钮」而失去警示作用。

4.3 字体:从 CDN 到自托管#

走 CDN 最省事(Manage → Customizations → Font):

  1. Font CSS URL 填样式表地址,例如 https://fonts.bunny.net/css?family=lato:400,700&display=swap
  2. Font Family Name 填字体名,例如 Lato
  3. 保存

自托管字体则必须注意 v4 的一个变化:Kener v4 不再从本地 /uploads/ 提供文件,v3 时代把字体丢进 /uploads/ 再引用的做法已经失效。正确做法是把字体文件放到可公网访问的 CDN/静态服务器,然后清空面板里的 Font 配置,改用 Custom CSS 写 @font-face:

@font-face 示例
@font-face {
font-family: "CustomFont";
src:
url("https://cdn.example.com/fonts/CustomFont.woff2") format("woff2"),
url("https://cdn.example.com/fonts/CustomFont.woff") format("woff");
font-weight: normal;
font-style: normal;
font-display: swap; /* 字体没到之前先用系统字体渲染,避免白屏 */
}
:root {
--font-family: "CustomFont", system-ui, -apple-system, "Segoe UI", sans-serif;
}

最后在 DevTools 的 Computed 面板确认某个文本节点实际应用的 font-family 生效。

4.4 公告条:把「计划维护」提前告诉用户#

Customizations → Announcement:标题与正文都填写时,站点顶部才会出现公告条。适合放:

  • 即将进行的维护窗口(时间 + 影响范围 + 预计时长)
  • 已知问题与临时绕行方案
  • 新监控上线/页面结构调整说明

建议约定一个写法:【计划维护】3 月 12 日 02:00–04:00 UTC,API 只读。带时间与影响面的公告,能显著减少故障期间的支持工单。

4.5 主题模式与切换#

Customizations → Theme:

  • theme:默认模式,light / dark / system。对内团队用的状态页建议 system;
  • themeToggle:是否允许用户自行切换。对外页面建议开启,暗色环境(值班大屏、夜间)体验差别很大。

v4.1.2 起「首次访问就尊重你设的默认主题」(之前是切换之后才生效)。

4.6 国际化与时区(面向多语言访客必做)#

Kener 自带 24 种语言(含简体中文 zh-CN、zh-TW、日语、韩语、俄语等),在 Manage → Internationalization 配三件事:

  1. Languages:勾选要启用的语言、设默认语言(只勾中的会出现在访客的语言选择器里);
  2. Timezone:是否允许访客手动切时区(OFF 时自动按浏览器时区);
  3. Date & Time Format:基于 date-fns 的 token,三个独立格式串——
    • PPp(日期+时间,用于事件/维护时间戳)
    • PP(仅日期,用于日期标题/可用率历史)
    • p / HH:mm(仅时间,用于监控 tick 提示)

大写字母开头的 token(PP、PPp)是「感知语言」的,会自动适配访客语言;yyyy-MM-dd 这类固定 token 则永远一致。对面向海外或中文访客的状态页非常友好。

4.7 Custom CSS 配方集(直接抄)#

Custom CSS 会被注入为公共布局 <head> 里的 <style> 块,只写裸 CSS,不要再包 <style> 标签。

(1)全局圆角风格(官方文档给的方角示例,做「科技感/终端风」很好用):

全局方角
.rounded-3xl,
.rounded-2xl,
.rounded-xl,
.rounded-md,
.rounded-btn,
.rounded-full {
border-radius: 0 !important;
}

(2)卡片玻璃拟态 + 悬停微交互(暗色主题下尤其好看):

卡片玻璃拟态
/* 卡片:半透明 + 背景模糊 + 细边框 */
[class*="rounded-2xl"],
[class*="rounded-3xl"] {
backdrop-filter: blur(10px);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition:
transform 0.18s ease,
box-shadow 0.18s ease;
}
[class*="rounded-2xl"]:hover,
[class*="rounded-3xl"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.1);
}
/* 深色模式下调低边框亮度 */
@media (prefers-color-scheme: dark) {
[class*="rounded-2xl"],
[class*="rounded-3xl"] {
border-color: rgba(148, 163, 184, 0.14);
box-shadow: none;
}
}

(3)顶部渐变氛围带(让页面第一屏不那么「表格化」):

顶部渐变氛围带
body::before {
content: "";
position: fixed;
inset: 0 0 auto 0;
height: 220px;
pointer-events: none;
z-index: -1;
background: radial-gradient(
60% 100% at 50% 0%,
rgba(79, 70, 229, 0.14) 0%,
rgba(79, 70, 229, 0) 70%
);
}

(4)历史可用率条更紧凑(监控多的时候一屏能多放几行):把每天的小方块高度压小、间距收窄——具体选择器随版本变化,用 DevTools 选中那些小方块(通常是带 w- / h- 工具类的 div 或 span)后自定义即可,例如:

可用率条紧凑化(示意)
/* 示意:用你的实际选择器替换 .uptime-bar > * */
.uptime-bar > * {
border-radius: 2px;
margin-right: 1px;
}

(5)打印友好(值班同学把故障页面打出来/导 PDF 时):

打印友好
@media print {
nav,
footer,
.announcement {
display: none !important;
}
body {
background: #fff;
}
}

4.8 页脚:放品牌与合规信息#

Customizations → Footer 的内容会直接作为 HTML 渲染,所以保持最小且可信(不要把不可控的第三方 HTML 贴进来):

页脚示例
<div
style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;font-size:13px"
>
<span>© 2026 Example Inc.</span>
<a href="https://example.com">主站</a>
<a href="/rss">RSS</a>
<a href="mailto:support@example.com">联系我们</a>
<span style="opacity:.6">状态数据每分钟更新 · 时区 UTC</span>
</div>

页脚是放「状态页说明」的好位置:数据刷新频率、时区、免责口径、以及一个能联系到人的入口。

4.9 进阶:static/ 自定义 JS/CSS(需要构建)#

只有在需要注入脚本或做全局替换(例如接入统计脚本、加自定义键盘快捷键)时才走这条路:

  1. 把文件放进 static/(如 static/custom.css、static/custom.js);
  2. 编辑 src/app.html,在 <head> 加样式表、在 </body> 前加脚本:
src/app.html(根路径部署)
<link rel="stylesheet" href="/custom.css" />
<script src="/custom.js"></script>
  1. 子路径部署要带上 base path:
src/app.html(子路径部署)
<link rel="stylesheet" href="/status/custom.css" />
<script src="/status/custom.js"></script>
  1. 重新构建并部署镜像,然后用 DevTools 确认资源没有 404。

4.10 嵌入到你自己的官网 / 文档站#

Kener 提供可嵌入的状态组件(badge、状态条、延迟图表),复制一段脚本粘进你的官网,访客就能直接看到你的服务状态,还能把自定义状态色带进嵌入件里(v4.1.5 修过嵌入件读不到自定义 UP/DOWN 颜色的 bug)。对「状态页只是门面之一,主战场在官网/文档站」的团队很有用。


4.11 我的完整自定义css#

我的完整美化css
/* =========================================================
Kener Custom CSS · 连体分段版
Manage → Customizations → Custom CSS(勿包 <style>)
Font CSS URL 留空;Font Family Name 可留 lxgw-wenkai 或清空
========================================================= */
/* ---------- 0. 可调参数 ---------- */
body {
--k-font: "lxgw-wenkai", "LXGW WenKai Mono", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
--k-mono: "lxgw-wenkai", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
--k-radius-card: 14px;
--k-radius-pill: 999px;
--k-lh: 1.75;
--k-tracking: 0.015em;
--k-glass-blur: 18px;
--k-glass-tint: 82%;
}
/* ---------- 1. 字体加载与应用 ---------- */
@font-face {
font-family: "lxgw-wenkai";
src: url("https://cdn.jsdelivr.net/gh/nulijiazaizhong/fonts/lxgw-wenkai/LXGWWenKaiMono-Regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
html {
font-family: var(--k-font);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
--font-family: var(--k-font);
font-family: var(--k-font);
line-height: var(--k-lh);
letter-spacing: var(--k-tracking);
}
:where(h1, h2, h3, h4, h5, h6) {
letter-spacing: 0.02em;
font-weight: 600;
line-height: 1.35;
text-wrap: balance;
}
:where(p, li, dd) {
text-wrap: pretty;
}
:where(code, pre, kbd, samp, .tabular-nums, table) {
font-variant-numeric: tabular-nums;
}
:where(code, pre, kbd, samp) {
font-family: var(--k-mono);
font-size: 0.92em;
}
/* ---------- 2. 卡片圆角 ---------- */
:where(.kener-card, .rounded-3xl, .rounded-2xl, .rounded-xl, [data-slot="card"]) {
border-radius: var(--k-radius-card);
box-shadow: none;
border: 1px solid color-mix(in oklab, var(--border) 90%, transparent);
background-color: var(--background);
}
/* 普通胶囊(不进 button-group 的按钮) */
:where(.rounded-btn, .rounded-full) {
border-radius: var(--k-radius-pill);
}
/* ---------- 3. 卡片玻璃拟态 + 悬停微交互 ---------- */
/*
注意:本节优先级(0-1-0)高于上面第 2 节的 :where()(0-0-0),
会覆盖 border / box-shadow / background-color。
backdrop-filter 需要背景半透明才看得出效果,因此这里把
卡片底色改成 82% 不透明度的 var(--background)。
*/
[class*="rounded-2xl"],
[class*="rounded-3xl"] {
background-color: color-mix(in oklab, var(--background) 82%, transparent);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid rgba(148, 163, 184, 0.18);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
}
[class*="rounded-2xl"]:hover,
[class*="rounded-3xl"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.10);
}
/* 深色模式下调低边框亮度(系统偏好 + .dark class 双兼容) */
@media (prefers-color-scheme: dark) {
[class*="rounded-2xl"],
[class*="rounded-3xl"] {
border-color: rgba(148, 163, 184, 0.14);
box-shadow: none;
}
[class*="rounded-2xl"]:hover,
[class*="rounded-3xl"]:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
}
.dark [class*="rounded-2xl"],
.dark [class*="rounded-3xl"] {
border-color: rgba(148, 163, 184, 0.14);
box-shadow: none;
}
.dark [class*="rounded-2xl"]:hover,
.dark [class*="rounded-3xl"]:hover {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
/* 顶栏那块浮层不要跟着上浮,避免遮挡下方内容 */
.kener-public .fixed .rounded-3xl.border:hover {
transform: none;
}
/* ---------- 4. 顶部渐变氛围带 ---------- */
/*
用 body::before 实现,z-index: -1 沉到内容下方。
若 Kener 后续把 .kener-public.status-page-app 也挂到 body 上,
会和第 6 节的顶栏玻璃 ::before 撞车,届时把这节挪到 html::before。
*/
body::before {
content: "";
position: fixed;
inset: 0 0 auto 0;
height: 220px;
pointer-events: none;
z-index: -1;
background: radial-gradient(
60% 100% at 50% 0%,
rgba(79, 70, 229, 0.14) 0%,
rgba(79, 70, 229, 0) 70%
);
}
/* ---------- 5. ButtonGroup 连体分段(核心修复) ---------- */
/*
ButtonGroup 会去掉内侧圆角和起始边框做拼接;
自定义 CSS 不在 @layer 里,优先级更高,必须在这里把
「外端全圆、内端贴平」写死,否则圆角和描边会对不齐。
*/
.rounded-btn-grp {
gap: 0;
isolation: isolate;
}
.rounded-btn-grp > * {
border-radius: 0 !important;
border-width: 1px !important;
border-style: solid !important;
border-color: color-mix(in oklab, var(--border) 70%, var(--foreground) 8%) !important;
position: relative;
margin-inline-start: 0;
}
/* 首段:左侧全圆 */
.rounded-btn-grp > *:first-child {
border-start-start-radius: var(--k-radius-pill) !important;
border-end-start-radius: var(--k-radius-pill) !important;
}
/* 末段:右侧全圆 */
.rounded-btn-grp > *:last-child {
border-start-end-radius: var(--k-radius-pill) !important;
border-end-end-radius: var(--k-radius-pill) !important;
}
/* 中段/邻接:内侧贴平,并压掉重叠边框 */
.rounded-btn-grp > *:not(:first-child) {
border-start-start-radius: 0 !important;
border-end-start-radius: 0 !important;
margin-inline-start: -1px;
}
.rounded-btn-grp > *:not(:last-child) {
border-start-end-radius: 0 !important;
border-end-end-radius: 0 !important;
}
/* 只有两段时:同时具备 first/last 的外端圆角(上面两条已覆盖) */
/* hover 时把当前段抬到上层,避免边框被邻段盖住 */
.rounded-btn-grp > *:hover,
.rounded-btn-grp > *:focus-visible {
z-index: 1;
}
/* 兼容 Select 触发器(语言)不是 button 时 */
.rounded-btn-grp > [data-slot="select-trigger"] {
border-radius: 0 !important;
}
.rounded-btn-grp > [data-slot="select-trigger"]:first-child {
border-start-start-radius: var(--k-radius-pill) !important;
border-end-start-radius: var(--k-radius-pill) !important;
}
.rounded-btn-grp > [data-slot="select-trigger"]:last-child {
border-start-end-radius: var(--k-radius-pill) !important;
border-end-end-radius: var(--k-radius-pill) !important;
}
.rounded-btn-grp > [data-slot="select-trigger"]:not(:first-child) {
border-start-start-radius: 0 !important;
border-end-start-radius: 0 !important;
}
.rounded-btn-grp > [data-slot="select-trigger"]:not(:last-child) {
border-start-end-radius: 0 !important;
border-end-end-radius: 0 !important;
}
/* ---------- 6. 顶栏玻璃层 ---------- */
.kener-public.status-page-app::before {
height: var(--top-glass-h, 7.5rem);
background-color: color-mix(in oklab, var(--background) var(--k-glass-tint), transparent);
-webkit-backdrop-filter: blur(var(--k-glass-blur)) saturate(1.25);
backdrop-filter: blur(var(--k-glass-blur)) saturate(1.25);
}
.kener-public .fixed .rounded-3xl.border {
border-color: color-mix(in oklab, var(--border) 80%, transparent);
box-shadow: 0 1px 0 color-mix(in oklab, var(--foreground) 4%, transparent);
}
/* ---------- 7. 监控数据排版 ---------- */
[data-slot="item-title"] .text-lg,
[data-slot="item-title"] .text-xl,
[data-slot="item-title"] .text-base {
font-weight: 600;
letter-spacing: 0;
font-variant-numeric: tabular-nums;
}
[data-slot="item-description"],
.text-muted-foreground.text-xs {
letter-spacing: 0.01em;
line-height: 1.55;
}
:where(.bg-up, .bg-degraded, .bg-down, .bg-maintenance, .bg-no-data, .bg-no_data) {
transition: background-color 0.2s ease;
}
/* ---------- 8. 链接 / 按钮 / 焦点 ---------- */
a {
text-decoration-thickness: 1px;
text-underline-offset: 0.18em;
text-decoration-color: color-mix(in oklab, currentColor 30%, transparent);
transition: color 0.15s ease, text-decoration-color 0.15s ease, opacity 0.15s ease;
}
a:hover {
text-decoration-color: color-mix(in oklab, currentColor 70%, transparent);
}
:where(button, [role="button"], .rounded-btn) {
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: 2px solid color-mix(in oklab, var(--accent-foreground, #e96e2d) 80%, transparent);
outline-offset: 2px;
}
/* ---------- 9. 选区 / 表格 ---------- */
::selection {
background: color-mix(in oklab, var(--accent-foreground, #e96e2d) 22%, transparent);
color: var(--foreground);
}
[data-slot="table-row"] {
transition: background-color 0.15s ease;
}
[data-slot="table-row"]:hover {
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
}
/* ---------- 10. 页脚 ---------- */
main.kener-public + div,
.kener-public ~ div {
font-size: 0.875rem;
line-height: 1.7;
color: var(--muted-foreground);
padding-block: 1.25rem 2rem;
}
/* ---------- 11. 可访问性 ---------- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

美化前 美化后

5. 和同类方案的取舍(参考官方对比)#

能力KenerUpptimeGatusUptime KumaSaaS(Atlassian等)
技术栈SvelteKit+NodeGitHub ActionsGoNode各家
管理后台✅❌(GitHub UI)❌(YAML)✅✅
监控类型11 种仅 HTTP8+10+多为外购
事件全生命周期✅用 Issues❌❌✅
维护窗口(RRULE)✅基础❌基础✅
订阅者邮件通知✅ 无上限❌❌❌按套餐限量
数据库SQLite/PG/MySQL文件SQLite/PG仅 SQLite托管
REST API✅只读❌✅✅
RBAC✅ 三角色❌❌❌付费档
自定义主题(CSS/字体)✅ 丰富基础❌基础部分付费档
自托管✅仅 GitHub✅✅❌

一句话结论:想要「监控 + 状态页 + 事件管理 + 多库 + 无限量」一体、且默认 UI 就好看、还能深度品牌化的自托管方案,Kener 是目前很均衡的一档;Upptime 适合纯 GitHub 用户,Uptime Kuma 适合监控优先的团队。


6. 小结#

Kener 把「自托管状态页」的门槛压得很低:一个 docker compose up + 一个 Redis 就能跑;把「好看」做成了一等公民:品牌、主题、字体、状态色、公告、i18n 全在后台可视化配置,再不够还能自定义 CSS。对 DevOps 团队、SaaS、以及任何想对外认真交代服务可用性的个人项目,它都是一个「开箱即好看、还能越用越像自己品牌」的轻量选择。如果你只是想要一个自己看的状态页,那么我建议使用 uptime kuma,如果你是想让用户看,哪还是 kener 更好。


7. 参考链接#


第 5 篇,共 5 篇本文属于系列

Kener 自部署与美化
https://blog.goodnightan.com/posts/kener/
作者
晚安
发布于
2026-10-04
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取