Documentation/How To Use Luma

How To Use Luma#

This is the operating manual for the first public version of Luma.

Luma keeps five concepts visible:

node / region / exposure / egress / service

Luma Control runs on the manager node and owns login tokens, node registration, DNS sync, jobspec rendering, and Nomad deployment calls. The orchestrator underneath is HashiCorp Nomad. After luma login, luma deploy can be run from a client that does not have Docker, SSH access, Cloudflare credentials, or Nomad credentials. Tailscale is a control-plane network and a relay option for home services. Cloudflare is the DNS provider and optional tunnel provider. Egress Gateway is only for outbound traffic such as pulling images, installing dependencies, or running services that need external network access.

1. Install The CLI#

curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | sh
~/.local/bin/luma preflight

This creates a private venv at ~/.local/share/luma/venv, writes a luma command at ~/.local/bin/luma, and adds ~/.local/bin to your shell profile when needed. Use ~/.local/bin/luma immediately, or open a new shell / run exec $SHELL -l before using the shorter luma command.

Install a specific tag:

curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | LUMA_INSTALL_REF=v0.1.365 sh

For local development from a checkout:

./scripts/install-luma.sh
. .venv/bin/activate

To uninstall the local CLI:

curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh

The default uninstall keeps ~/.luma.config.json and ~/.config/luma. To remove local config and login contexts too:

curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh -s -- --purge

This does not remove Docker, Nomad, Traefik, Luma Control, deployed services, or /opt/luma from a server.

If python3 is missing, the installer prints the package command for macOS or Ubuntu/Debian. Local Docker is optional; it is only used to validate rendered jobspec files before deployment.

A checkout can keep secrets in .env (cp .env.example .env). A curl-installed CLI does not need that file: bootstrap and luma node join prompt for missing values. Luma also loads .env automatically when present. Shell exports win over .env, so CI or one-off commands can override local values.

2. Configure luma.yaml#

Skip this file for the first manager. luma bootstrap manager --domain luma.example.com works from a curl-installed CLI: it prompts for missing secrets, infers the Cloudflare zone from the control domain when needed, and writes /opt/luma/luma.yaml on the manager.

Create a local luma.yaml later when you want to pin node public IPs, extra nodes, or image mirrors. It is the project config file Luma uses for those overrides:

project: example

providers:
  dns:
    type: cloudflare
    zone: example.com
    zoneId: ""
    apiTokenEnv: CLOUDFLARE_API_TOKEN
    edgeTarget: 203.0.113.10
    recordType: A
    ttl: 1
    proxied: false

nodes:
  manager-1:
    host: manager-1
    publicIp: 203.0.113.10
    region: cn
    roles:
      - nomad-server
      - edge
      - egress

defaults:
  exposure: cn-edge
  registry: ghcr.io/liutianjie
  stackRoot: stacks
  routesRoot: routes
  egressNetwork: egress
  entrypoint: websecure
  certResolver: letsencrypt
  engine: nomad
  images:
    egressGateway: docker.1panel.live/metacubex/mihomo:latest

Run the command you actually need:

luma bootstrap manager --domain luma.example.com

If local values are missing, Luma asks for them first, writes ~/.luma.config.json, then continues. On worker nodes, the same happens during luma node join .... .env and exported environment variables still work for local overrides. If CLOUDFLARE_API_TOKEN is configured but providers.dns is missing, bootstrap and luma update manager infer the Cloudflare zone from the control domain and write the provider config before installing /opt/luma/luma.yaml. If no edge DNS target is configured, interactive bootstrap asks for LUMA_DNS_EDGE_TARGET; non-interactive update uses the configured edge node public IP or an existing LUMA_DNS_EDGE_TARGET.

The relevant keys are explained in secrets.md. The common manager values are:

Variable Purpose
CLOUDFLARE_API_TOKEN Cloudflare DNS token used to create/update control and service records.
LUMA_DNS_EDGE_TARGET Public IP or DNS name that Cloudflare records should point to when no edge target is already configured.
TRAEFIK_ACME_EMAIL Let's Encrypt account email used by Traefik for HTTPS certificates.
EGRESS_SUBSCRIPTION_URL Optional proxy subscription URL for image-pull proxying and proxy: true services.
TAILSCALE_AUTHKEY Optional auth key for private worker joins, home nodes, or tailscale-relay services.
LUMA_SUDO_PASSWORD Optional fallback when sudo requires a password.
LUMA_CONTROL_IMAGE Optional development/pinned control API image.

Do not commit secrets.

3. Bootstrap The First Node#

For a single public server that runs the Nomad server, Traefik, and egress:

luma bootstrap manager --domain luma.example.com

This does:

Set EGRESS_SUBSCRIPTION_URL before running an egress profile when the manager needs a proxy to pull the configured control image. Mainland managers using the default GHCR control image should not use --skip-egress. Bootstrap prints live step logs with [start], [ok], and [fail] markers. If one step fails, either re-run bootstrap after fixing the issue or run the focused repair command for that layer.

If Tailscale login was skipped during bootstrap:

luma tailscale connect

If egress was skipped or needs repair:

luma egress setup

To intentionally skip egress during first bootstrap, only do this when the control image registry is directly reachable, or when LUMA_CONTROL_IMAGE / defaults.images.lumaControl points at a registry the manager can pull:

luma bootstrap manager --domain luma.example.com --skip-egress

The bootstrap output includes a management token, a node join token, and the dashboard URL. Open https://luma.example.com/dashboard/, paste the management token, then deploy hello-world first install. That smoke service does not need extra DNS, Tailscale, registry, or LAE.

Use the management token on client machines:

luma login https://luma.example.com --token <management-token>
luma context list
luma deploy templates/hello-world.yml

Use the node join token on additional servers:

luma node join https://luma.example.com --token <node-join-token> --region global --name global-sg-1

The manager records the node's region and Luma node meta automatically after the node joins. --name is the Luma node name used in service manifests. Luma writes it to the Nomad client meta.luma_node_name and uses it for pinned scheduling, so generic Docker hostnames such as OrbStack's orbstack do not collide. The Nomad node identity is a stable UUID, so a rejoin under the same name keeps pinned services valid.

For --region home, the node must be connected to Tailscale before it can reach a manager address on the tailnet. If the node is not connected yet, luma node join treats TAILSCALE_AUTHKEY as required and asks for it before registering the node. You can also run luma tailscale connect first to fill the key and connect Tailscale without attempting a Nomad enroll.

4. Connect Cloudflare#

luma cloudflare connect --zone example.com

The command verifies the token, finds the zone, and writes providers.dns.zoneId back to luma.yaml. Run this before luma bootstrap manager when possible. If you connect Cloudflare afterward, rerun manager bootstrap so /opt/luma/luma.yaml and /opt/luma/control/control.json are refreshed.

For cn-edge services, DNS defaults to the public IP of the configured edge node. A service can override this with:

dns:
  target: 203.0.113.10

5. Repair Or Refresh Egress#

luma egress setup

Bootstrap already runs this for single-node unless --skip-egress is used. Run it directly when egress was skipped, failed, or the subscription needs repair. It downloads the subscription, strips it into a minimal Mihomo config, writes it to /opt/luma/egress-gateway/config.yaml, deploys egress_mihomo, and configures Docker daemon proxy:

HTTP_PROXY=http://127.0.0.1:7890
HTTPS_PROXY=http://127.0.0.1:7890

Service runtime proxy is opt-in per service. Declare proxy: true in the service manifest; do not hand-write the egress network or default proxy env unless you need to override them:

name: ai-worker
image: ghcr.io/acme/ai-worker:1.0.0
region: cn
exposure: none
proxy: true
env:
  OPENAI_BASE_URL: https://api.openai.com/v1

Luma attaches the egress proxy to the service and injects HTTP_PROXY=http://egress_mihomo:7890 plus HTTPS_PROXY=http://egress_mihomo:7890 when those env vars are not already set. Scheduling still follows the service region.

Refresh subscription output later:

luma egress refresh

6. Create A Service#

Interactive mode:

luma service new

Manual manifest:

name: app
image: ghcr.io/me/app:latest
region: cn
exposure: cn-edge
domain: app.example.com
port: 3000
replicas: 2

To pin a service to one machine, add node with the Luma node name passed to luma node join --name:

region: home
node: home-mac-mini

7. Deploy#

Default deploy path:

luma deploy app.yaml

If the application already has a project .env, pass it as scoped deployment secrets:

luma deploy app.yaml --env .env

Luma imports only variables referenced by the manifest, stores them under the service name, and then resolves ${NAME} placeholders during manager-side render. This lets different applications reuse common names such as DATABASE_URL without colliding.

This submits the manifest to the logged-in Luma Control endpoint. The manager renders generated files under /opt/luma, syncs DNS, creates or updates the Nomad job through the Nomad HTTP API, and probes the public route for cn-edge and external-edge services.

luma deploy prints client-side progress and each control-plane step. Luma validates generated Traefik file-provider routes, stages them outside the watched routes directory, then atomically publishes the final route file. A public route probe reports the HTTP status from /; an application-level 404 means the route reached the application but the app may not serve a root page, while Traefik's default 404 page not found is treated as a missing router and a failed public route. When the probe reports the route unhealthy (Traefik router not found, or a transient 502/503/504), Control recreates the service's allocation once and re-probes before failing the deploy. Luma gives single-service and Compose allocations 30 minutes for cold image acquisition and 40 minutes for rollout progress. The client waits up to 3000 seconds by default so it does not disconnect before that bounded rollout finishes. Override it when needed:

luma deploy app.yaml --timeout 3600

Repeated deploys are updates. The same service name maps to the same Nomad job (the job id is the service slug); running deploy again rewrites the generated jobspec and updates that job. Nomad keeps the previous version, so luma rollback app or the dashboard's Applications -> Versions action can return to it. This is a running job rollback, not a Git/manifest rewrite; use pinned image tags or digests for production. Changing name creates a different job.

Preview without side effects:

luma deploy app.yaml --dry-run

Submit to the control plane, render/write files on the manager, but skip DNS sync and the Nomad deploy step:

luma deploy app.yaml --skip-dns --skip-orchestrator

Remove a service or Compose application by its deployed name:

luma service remove app

Luma Control uses the manifest recorded during the last successful deploy. This deletes the Cloudflare DNS record for public services, deregisters and purges the Nomad job, and deletes generated manager files such as /opt/luma/stacks/<region>/<service>/<service>.nomad.json, /opt/luma/stacks/compose/<name>/<name>.nomad.json, and routes/<service>.yml for tailscale-relay. The same command removes single-service and Compose deployments. Preview first or keep DNS when needed:

luma service remove app --dry-run
luma service remove app --skip-dns

8. Exposure Modes#

cn-edge:

user -> Cloudflare DNS -> CN Traefik -> cn service

Use this for domestic public services.

tailscale-relay:

user -> Cloudflare DNS -> CN Traefik -> Tailscale -> home service

Use this for low-frequency home services that should still share the same public domain experience.

cloudflare-tunnel:

user -> Cloudflare -> cloudflared -> service

Use this for home services that should not depend on the CN edge.

external-edge:

user -> Cloudflare DNS -> global edge -> global service

Use this for overseas services that need external network access and a public endpoint.

none:

No public entrypoint. Use it for workers and internal services.

9. Diagnose#

luma doctor

Each failed check includes a concrete fix command or environment variable.

10. First Real Smoke Test#

Use the reference node first:

luma doctor
luma bootstrap manager --domain luma.example.com
luma egress setup
luma deploy examples/public-cn-service.yaml

Then check:

nomad job status
curl -I https://whoami.example.com

Rotate any token or subscription URL that has been pasted into chat or logs before open-sourcing the repository.

11. Build And Deploy From Repository (可插拔)#

默认的 luma deploy 只部署已经构建好的镜像。如果想直接从 GitHub/Gitea 仓库的源码构建并上线,用 luma import:它在集群里的构建节点上 clone 仓库、自动发现 .luma.ymlluma.compose.yml 这类部署文件、按 Dockerfile/Compose build: 构建镜像、推送到集群内自托管 registry,再走正常部署链路。这一整套是可插拔的——不用它,集群和现有部署不受任何影响;用它,只需下面这套一次性接入。

接入 SOP(已部署好 Luma 的前提下)#

假设你已经走完上面 1–10 节,集群里 manager 正常、至少有一个 worker 节点、luma login 能用。接入「从 Git 仓库构建部署」分四步:

Step 1 — 选一个构建节点并装好 buildx。 构建在某个 Luma 节点上跑,需要 docker buildx(Linux 节点通常随 Docker 一起就有;跨架构构建还需要 qemu/binfmt)。节点 agent 会自动 advertise docker-build 能力,可以这样确认:

luma node list                 # 找到要用作构建节点的节点名,例如 build-1

如果该节点没有 buildx,先在节点上安装;装好后 agent 会自动带上 docker-build 能力。

Step 2 — 起一个集群内 registry。 构建出的镜像要有地方存,并让其它区域的节点能拉。一条命令搞定(部署 registry 服务 + 给非 manager 的就绪 Linux 节点配 insecure-registries):

luma registry serve --node build-1

它会把 registry:2 部署到 build-1(默认 5000 端口、节点本地卷、仅 Tailscale 内网可达),并遍历非 manager 的就绪 Linux 节点配置 insecure-registries,让它们能经 Tailscale 内网从这个 registry 拉镜像。BuildKit 推送和目标节点拉取都走 <build-1-tailscale-host>:5000;不要再用 localhost:5000 作为 pushHost(在 BuildKit 容器里那是容器自己)。随后用 luma build configregistryHostpushHost 都设成这个 Tailscale 端点。

0.1.162 起,CLI 会先完成 Docker daemon 配置、再创建 registry allocation;重复写入相同的 insecure-registries 也不会重启 Docker。这个顺序避免首次启用 registry 时由 Docker 重启打断刚创建的 Nomad CNI 网络。

可选 flag:

manager 节点会被跳过:Control 跑在 manager 的容器里,重启它的 docker 会杀掉 Control 自己。如果 manager 也要跑从该 registry 拉取的服务,手动在 manager 的 /etc/docker/daemon.jsoninsecure-registries 并重启 docker。

Step 3 —(私有仓库才需要)保存 Git provider token。 公开仓库跳过这步。私有 GitHub/Gitea 仓库:

printf '%s' "$GITEA_TOKEN" | luma git-provider set gitea lin \
  --base-url https://gcode.example.com \
  --username lin \
  --token-stdin

luma git-provider repos gitea:lin

同一个 provider 可以保存多个账户,例如 github:personalgithub:workgitea:lin。Token 只写不回显,构建任务被 builder node-agent lease 时才注入 git clone,不写入部署文件或 agent task state。

Step 4 — 在仓库里放 Luma 部署文件。 单服务用普通 .luma.yml / luma.yml service manifest,并用 build 块代替 image

name: myapp
region: cn
exposure: cn-edge
domain: myapp.example.com
port: 8080
build:
  context: .
  dockerfile: Dockerfile
  platform: linux/amd64

Compose 仓库用 luma.compose.yml 指向标准 docker-compose.yml。Repository Import 会构建 Compose 里带 build: 的服务,推送到 builder registry,然后把这些服务改写成 image: 再部署:

# luma.compose.yml
name: my-stack
compose: docker-compose.yml
region: cn
services:
  web:
    exposure: cn-edge
    domain: myapp.example.com
    port: 8080

支持的 Compose sidecar 文件名包括 luma.compose.yml.luma.compose.yml*.luma.compose.yml*.compose.luma.ymldocker-compose.luma.yml。如果本地 Compose 还只有 build:、没有最终 image:,用 import 模式校验:

luma compose validate --import-mode luma.compose.yml

导入并部署#

luma import https://github.com/acme/myapp --build-node build-1

GitHub 仓库也可以用短写:

luma import acme/myapp --build-node build-1

短写会展开成 https://github.com/acme/myapp.git。Gitea/self-hosted Git 用保存的 provider 账户或完整 clone URL。

使用保存的 Git provider 账户:

luma import --provider-id gitea:lin --repository acme/myapp --build-node build-1 --env .env

CLI 流式回传 clone → build → push → deploy 每一步。命令行可覆盖单服务 .luma.yml

luma import https://github.com/acme/myapp \
  --ref release \
  --region cn --exposure cn-edge --domain myapp.example.com --port 8080

如果 Builder 排队或构建较慢,也可以直接用当前电脑的 Docker Buildx 构建:

cd myapp
luma build local . --env .env

这个命令从本地 Git origin 识别项目(没有 origin 时传 --repo-url https://github.com/acme/myapp.git),先向 Control 申请项目构建租约, 再把本地构建结果推到 Luma 内部 registry,最后沿用正常部署链路。镜像仍写入 acme/myapp 这一项目命名空间,不会因为改走本地构建而产生另一套项目。 本机需要能访问 build.registryHost;registry 开启鉴权时,先执行对应的 docker login。单服务与 Compose 都支持,也可以传 --compose-sidecar--platform--context--dockerfile。 如果本机已有带镜像加速或多架构能力的 Buildx builder,可以传 --builder <名称> 直接复用;本地拉取镜像和 Dockerfile 网络访问需要代理时, 可以传 --proxy <地址>,内部 registry 会自动保留在 NO_PROXY

本地构建和 Builder 构建都会以实际部署目标为准选择容器架构:固定到 Mac/ARM 节点时构建 linux/arm64,目标区域同时存在 amd64 和 arm64 节点时 构建多架构镜像。显式 --platform 只能用于覆盖全部目标架构,不能把镜像 强制构建成与部署节点不兼容的架构。

Control 支持 build-queue-v1 时,新 CLI 自动使用同项目 FIFO 队列:远程 import / build retry 提交后排队;本地构建可以并行构建和上传,上传完成后 才将部署加入同一队列。顺序按服务端接受排队请求的先后计算,不按本地构建 开始时间计算。同一项目一次执行一个队列任务;不同项目可使用其它执行槽位, 但仍受已有 Builder 容量和运行时部署锁约束。前一个成功、失败或取消后, 后一个继续,不自动覆盖或取消旧任务。

CLI 显示任务 ID、队列位置和等待的任务。--timeout 到期或客户端退出只停止 等待,已接受的任务仍由服务端执行;用 luma build logs <id> 查状态, luma build cancel <id> 取消尚未开始的任务。排队请求持久化,Control 重启后 保留;正在执行的任务明确标记中断,不自动重放可能已经生效的部署,重试前 先检查运行时。尚未上传完成的本地构建仍依赖调用者电脑。

环境变量按任务独立保存在私有 Control 状态中,不进入公开构建历史,任务结束 后清理队列载荷。旧 Control 仍按原来的活动构建互斥规则处理,需要同时升级 CLI 和 Control 才能使用队列。直接部署预构建镜像的 deploy / compose deploy 继续使用已有同步锁。Control 仍为本地上传分配唯一 tag,并校验镜像属于项目 预留的 registry 路径。

构建节点来自控制面声明的 builder 节点;通常不用传 --build-node,只有需要临时覆盖到另一个已声明 builder 时才传。单服务 import 还可用 --context(build 上下文目录,默认 .)、--dockerfile(默认 Dockerfile)、--registry-host(其它节点拉取用的 registry 主机,默认 <build-node>:5000)覆盖仓库里的 build: 字段。对 Compose import,--region 会覆盖 sidecar 的 region;--exposure--domain--port 是单服务覆盖项,会被忽略并打印 warning。Compose 的服务级路由请写在 luma.compose.ymlservices: 里。luma import 默认等待 3600 秒的 build+deploy 响应,用 --timeout <seconds> 覆盖。

预声明 builder 节点和内部 registry 默认值,之后 import/build 就能省掉 --build-node

luma build config --node build-1 --default-node build-1 \
  --registry-host <build-1-tailscale-host>:5000 \
  --push-host <build-1-tailscale-host>:5000

--node 可重复声明多个 builder;--default-nodeluma import 缺省用的 builder;--registry-host 是 target node 拉镜像的地址,--push-host 是 BuildKit 推镜像的地址。两者都必须是 BuildKit 容器和所有目标节点可达的 Builder Tailscale endpoint;不要使用已移除且在 BuildKit namespace 内含义错误的 localhost:5000。不带参数运行 luma build config 只打印当前配置和各 builder 的就绪/能力表。

构建历史和失败日志用 CLI 查看:luma build list(打印 ID/状态/节点/provider/仓库/ref)、luma build logs <id>(某次构建的分步日志)。修好凭据或配置后 luma build retry <id> 重跑整条 build+deploy;retry 也接受 --env .env 重新提供 scoped secrets、--timeout(默认 3600)。

dashboard 的「创建应用」页顶部也有「仓库导入」入口:选择 Git provider、账户、仓库和 ref;或手填 URL。进度实时显示。

CN 节点的 git clone 会自动走 manager 的 egress 网关(http://<manager-host>:7890),和镜像拉取、节点 join 用的是同一个出口;global 节点直连不走代理。代理出口由控制面的 egress 配置决定,无需在 luma import 上单独指定。

升级已部署的应用#

升级 = 改完代码、推到 GitHub,再跑一次同样的 luma import

luma import https://github.com/acme/myapp --build-node build-1

原理:每次构建按 git commit 打 tag(<registry>:5000/acme/myapp:<git-sha>),注入 manifest 的是这个不可变的 sha 标签;而 Nomad job id 来自 .luma.ymlname。所以同名 + 新 SHA = 对同一个 Nomad job 做滚动更新,旧版本自动保留。这和普通 luma deploy 的「同名即更新」是同一条链路。

升级特定分支/Tag:

luma import https://github.com/acme/myapp --build-node build-1 --ref v2.1.0

要点:

回滚#

因为每个历史版本的 jobspec 钉的是各自的 :<git-sha>,registry 默认不回收旧镜像,所以回滚拉得回原镜像字节:

luma history myapp                 # 看版本列表
luma rollback myapp                # 回到上一个版本
luma rollback myapp --to-version <N>

dashboard 的 Applications → Versions 也能做同样的回滚。注意这是 Nomad job 版本回退(拉回那个版本钉的镜像),不会改 Git、不会回滚数据库迁移或卷数据。

跨架构注意#

构建节点是 arm64(比如 Mac mini)而目标运行节点是 amd64 时,必须让构建产出 linux/amd64.luma.ymlbuild.platform 默认就是 linux/amd64,但构建节点要装好 qemu/binfmt 才能跨架构构建。

取消接入#

不想再用时,删掉 registry 服务即可(luma service remove luma-registry),已经在跑的服务不受影响。insecure-registries 配置留在各节点的 docker daemon 里是无害的。

12. 通过 Dashboard 升级 Luma#

日常升级不需要 SSH manager,也不需要在每台节点运行命令。使用管理 Token 登录 https://<control-domain>/dashboard/fleet,在「升级中心」完成整条流程:

  1. 填写不可变 release tag(例如 v0.1.175)。标准 tag 会自动建议同 tag 的 Control 镜像;自定义分支或 commit 需要明确填写已发布镜像。
  2. 点击「检查全部公网路由」保存升级前基线。HTTP 成功、重定向和鉴权拒绝都能证明路由已发布;404、网关错误和连接失败会明确标红,但不会悄悄阻断管理员操作。
  3. 第一次点击「升级 Control」会展示影响和基线结果,第二次确认后,Builder 先通过受管出网代理把外部 Control 镜像缓存到内部 registry 并校验 digest。该任务可恢复、有明确进度,避免 manager 的 Docker daemon 在替换窗口直接依赖 GHCR。
  4. 内网镜像就绪后才启动 Control 更新。更新运行在独立 systemd 单元中,不会因 node agent 自身重启而被中断。
  5. Control 替换期间页面自动重连。完成后自动重新检查全部公网路由,并展示有界日志;无需手动重启应用。
  6. 点击「更新未对齐节点」。操作会只选择版本落后的非 manager 节点,并持久化逐节点状态。安装完成后,节点必须以目标版本重新心跳,才会显示成功。
  7. 失败或中断的镜像准备、Control 或节点任务都会保留原因,可在同一页面重试。关闭页面、刷新浏览器或 Control 重启都不会丢失操作结果。

首次接入一个早于托管更新能力的历史 agent 时,Dashboard 会明确显示缺失的 capability;只需对该历史节点做一次 CLI 更新。之后的发布均可在升级中心完成。

13. 应用级可观测(luma-observe)#

Dashboard 里的 CPU/内存是节点样本,不是应用是否可用。HTTP 5xx、延迟、Nomad 失败/重启和节点水位走独立的 observe/ 栈,和 Traefik/Control 同机、只绑 loopback。部署该栈后 Control 会把它钉到 manager、把 Grafana 挂到控制面域名的 /grafana,告警规则里会出现 p95 / 5xx / 失败 alloc 预设。详见 Observability