
我的示例站点
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 三条选型路线
需要对外暴露服务可用性时,市面上有三条路:
| 方案 | 代表 | 适合 |
|---|---|---|
| SaaS | Atlassian 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
mkdir -p /opt/kener && cd /opt/kener.env
# ---------- 必填三项 ----------# 生成:openssl rand -base64 32KENER_SECRET_KEY=换成一段足够长的随机字符串# 公网访问地址:带协议、不带结尾斜杠、不要带子路径ORIGIN=https://status.example.comREDIS_URL=redis://redis:6379
# ---------- 数据库 ----------DATABASE_URL=postgresql://kener:换成强密码@postgres:5432/kener
# ---------- 常用可选项 ----------PORT=3000TZ=UTCBODY_SIZE_LIMIT=10M
# ---------- 邮件(二选一,同时配置时 SMTP 优先) ----------SMTP_HOST=smtp.example.comSMTP_PORT=587SMTP_USER=mailer-userSMTP_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)
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 -ddocker 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接到「失败就重启」的健康检查上(DockerHEALTHCHECK、Railway 之类),重启治不好一个死的数据库,只会把故障期拉成无限重启循环。
3.5 不装 Docker 的裸机跑法(附)
如果就是不想装 Docker,要求 Node ≥ 24.14 + 一个 Redis:
git clone https://github.com/rajnandan1/kener.git && cd kenernpm install# 起一个 Redisdocker run -d --name kener-redis -p 6379:6379 redis:7-alpine # 或裸机装 redisnpm run buildnpm run start.env 至少包含:
KENER_SECRET_KEY=<random>ORIGIN=http://localhost:3000REDIS_URL=redis://localhost:6379PORT=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,就不会出现在公开状态页上。最小可用流程:
最小可用流程
进后台建管理员
访问
/manage,首次进入会引导你创建管理员账号(密码用 bcrypt 存)。站点信息
Manage → Site Configurations,填siteName、siteURL、logo、favicon、导航菜单。建监控
Manage → Monitors → New Monitor,设置 Tag(唯一 id,如homepage)、Name(展示名)、Monitor Type(先用 API/网站监控)。配检查
填目标 URL、检查间隔或 cron 表达式;有条件的把「宽限期/确认阈值」调成连续 N 次失败才判定状态变化,避免瞬断被当成故障。
分配到页面
打开该监控的
Page Visibility,勾到目标页面上。这一步不做,前面全白干。验证
点监控的
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 (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 -ddocker 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 / Logo | siteName、siteURL、logo | 顶部导航品牌区与页面标题 |
| Favicon | favicon | <head> 中的页面图标 |
| 导航菜单 | nav | 顶部导航链接 |
| 监控子菜单 | subMenuOptions | 控制监控详情页是否显示徽章/嵌入的分享入口 |
| 全局页面可见性 | globalPageVisibilitySettings | 页面切换器与页面级导航/事件范围;forceExclusivity 可做页面级隔离(多品牌/多业务线共用一个实例) |
| 社交预览与 SEO | metaSiteTitle、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 | 品牌色 |
调色三条经验:
- 深色主题不要用纯饱和色(
#FF0000这种),在暗底上会「发光刺眼」,加一点亮度、降一点饱和更耐看。 - 浅色主题下色块要保证白底可读,别用低于 3:1 对比度的浅黄/浅绿。
- 状态色只承担「状态」语义,品牌色单独留给链接与按钮,否则页面上会到处是「红色按钮」而失去警示作用。
4.3 字体:从 CDN 到自托管
走 CDN 最省事(Manage → Customizations → Font):
Font CSS URL填样式表地址,例如https://fonts.bunny.net/css?family=lato:400,700&display=swapFont Family Name填字体名,例如Lato- 保存
自托管字体则必须注意 v4 的一个变化:Kener v4 不再从本地 /uploads/ 提供文件,v3 时代把字体丢进 /uploads/ 再引用的做法已经失效。正确做法是把字体文件放到可公网访问的 CDN/静态服务器,然后清空面板里的 Font 配置,改用 Custom CSS 写 @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 配三件事:
- Languages:勾选要启用的语言、设默认语言(只勾中的会出现在访客的语言选择器里);
- Timezone:是否允许访客手动切时区(OFF 时自动按浏览器时区);
- 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(需要构建)
只有在需要注入脚本或做全局替换(例如接入统计脚本、加自定义键盘快捷键)时才走这条路:
- 把文件放进
static/(如static/custom.css、static/custom.js); - 编辑
src/app.html,在<head>加样式表、在</body>前加脚本:
<link rel="stylesheet" href="/custom.css" /><script src="/custom.js"></script>- 子路径部署要带上 base path:
<link rel="stylesheet" href="/status/custom.css" /><script src="/status/custom.js"></script>- 重新构建并部署镜像,然后用 DevTools 确认资源没有 404。
4.10 嵌入到你自己的官网 / 文档站
Kener 提供可嵌入的状态组件(badge、状态条、延迟图表),复制一段脚本粘进你的官网,访客就能直接看到你的服务状态,还能把自定义状态色带进嵌入件里(v4.1.5 修过嵌入件读不到自定义 UP/DOWN 颜色的 bug)。对「状态页只是门面之一,主战场在官网/文档站」的团队很有用。
4.11 我的完整自定义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. 和同类方案的取舍(参考官方对比)
| 能力 | Kener | Upptime | Gatus | Uptime Kuma | SaaS(Atlassian等) |
|---|---|---|---|---|---|
| 技术栈 | SvelteKit+Node | GitHub Actions | Go | Node | 各家 |
| 管理后台 | ✅ | ❌(GitHub UI) | ❌(YAML) | ✅ | ✅ |
| 监控类型 | 11 种 | 仅 HTTP | 8+ | 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. 参考链接
- 项目主页:rajnandan1/kener
- 在线文档(v4):https://kener.ing/docs/v4
- 演示站:https://kener.ing
- 快速开始:Quick Start
- 部署(Docker / Node.js / 预构建镜像 / 健康检查):Deployment
- 环境变量全量参考:Environment Variables
- 数据库与连接池:Database Setup
- Redis:Redis Setup
- 邮件(SMTP / Resend):Email Setup
- 站点配置:Site Configuration
- 视觉自定义(配色/字体/页脚/公告/Custom CSS):Site Customizations
- 自定义字体:How to Add Custom Fonts
- 自定义 JS/CSS:Custom JS and CSS Guide
- 反向代理(Nginx/Apache/Caddy/Traefik):Reverse Proxy Setup
- 子路径部署:Base Path Deployment
- 国际化:Internationalization
- 同类对比:Kener vs Other Status Pages
- 全部文档索引:llms.txt
监控与消息通知系列
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 0 天
部分内容可能已过时