文档/Luma 部署 YAML

Luma 部署 YAML#

Luma 的部署文件不是 Docker Compose。它是一个更小的 service manifest,用户只描述服务入口、镜像、区域和少量运行参数。luma deploy 会把它提交给控制面,控制面再渲染成 Nomad jobspec、同步 DNS、配置 Traefik 路由,并通过 Nomad HTTP API 部署。

最小公开服务#

国内公开服务通常写成这样:

name: status
image: traefik/whoami:latest
region: cn
exposure: cn-edge
domain: status.example.com
port: 80
replicas: 1

部署:

luma deploy status.yaml

结果:

字段参考#

字段 必填 类型 说明
name string 服务名。Luma 会转成 slug,用作 Nomad job/group/task 名称。
image 是* string 容器镜像,例如 ghcr.io/acme/api:1.0.0latest 或未带 tag 会在部署时解析成 name@sha256:... 再部署。*提供 build 块时可省略,镜像由构建产出。
build map 从源码构建镜像(luma import)。子字段:context(默认 .)、dockerfile(默认 Dockerfile)、platform(默认 linux/amd64)。提供 buildimage 可省略。见下方「从 Git 仓库构建部署」。
region cn / global / home 或已创建的自定义 Region 服务运行区域。自定义 Region 需先 luma region create
engine nomad 该服务的编排后端。当前集群默认是 Nomad,通常不需要填。
node string 指定 Luma 节点名,也就是 luma node join --name 的值。用于把服务钉到某台机器;控制面会渲染成 Nomad 的 ${node.unique.name}(或 meta.luma_node_name)约束,仍会同时加 region 约束。
exposure 见下方 访问方式。新文件必须显式表达公开、隧道或内部访问语义。
domain 公开服务必填 string 用户访问的域名。
port 公开服务必填 integer 容器内部监听端口,不是云服务器安全组端口。
replicas integer 副本数,渲染成 Nomad group count,默认 1,必须大于等于 1
env / environment map 环境变量,会写进 Nomad task 的 env
command string/list 覆盖容器启动命令。
constraints string[] 追加 Nomad placement 约束。Luma 会自动加 region 约束。
labels string[] 追加服务标签。公开 Traefik 路由所需的 service tags 会自动生成。
networks string[] 追加网络声明。公开服务的入口由 Traefik Nomad provider 自动发现。
proxy boolean 服务运行时是否需要走 egress proxy。为 true 时会自动挂上 egress 代理和代理环境变量。调度仍按 region。不是镜像拉取代理。
resources map 渲染到 Nomad task 的 resources 块,CPU 使用 reservations.cpus 弹性共享,不执行 limits.cpus。内存 reservation 映射到 memory、limit 映射到 memory_max;首次部署这类任务时会自动启用 Nomad 内存超卖,确保 limit 真正成为容器硬上限。
healthcheck map 渲染成 Nomad check(脚本/http)。公共 HTTP 服务建议探测本地端口,例如 http://127.0.0.1:<port>/healthz
requirements map 组件依赖契约。支持 capabilitiescloudflare / tailscale / egress / registry)、secretsinitnotesinit 支持 cloudflare-dnscloudflare-tunneltailscale-nodeegressregistrysecrets。部署预览会合并显式声明与运行时推导,并在必需项缺失时阻止提交;正式部署会先执行并记录初始化计划。
publishPort 公开服务可用 integer 显式启用 Nomad bridge 端口映射,把宿主机 publishPort 转到容器 port。Linux 节点可用;Mac/OrbStack 节点不要设置,保持 host mode 并让 route 指向真实 port
relay tailscale-relay 可选 map 覆盖 Tailscale relay 上游。默认跟随实际运行 allocation 所在的 home 节点自动推导。
tcp tcp-relay 可选 map TCP relay 高级上游覆盖。正常情况不需要填写;入口由 publishPort / port 自动派生。
tunnel cloudflare-tunnel 可用 map Cloudflare Tunnel token env 等设置。
dns map 保留给 DNS 相关扩展。
stackPath string 覆盖生成 jobspec 路径。通常不用。
routePath string 覆盖 tailscale route 文件路径。通常不用。

更新策略#

Luma 渲染 Nomad update 策略时默认启用 auto_revertmax_parallel = 1 和健康窗口,失败发布会自动回滚到上一版。多副本服务按 Nomad rolling update 逐个替换。

单副本的 cn-edge / external-edge 服务如果使用动态端口(未设置 publishPort),Luma 会渲染 Nomad canary + auto-promote:先启动一个新 allocation,等它健康后再提升为正式版本并停掉旧 allocation。建议公开 HTTP 服务都配置 healthcheck,这样 Nomad 会等 service check 稳定;没有 healthcheck 时只能按 task running 判断健康。

显式 publishPorttailscale-relay 默认 host network、tcp-relay、内部服务和有本地状态的服务不会默认启用 canary,因为新旧 allocation 同时存在可能撞宿主机端口或本地数据。它们仍然使用 auto_revertmax_parallel = 1,但无法保证单副本完全无中断。

Compose 部署也渲染同样的 Nomad update 基线策略。对没有 publishPortcn-edge / external-edge compose 服务,Luma 使用 Nomad dynamic port,并在整组没有固定宿主机端口、tailscale-relay / tcp-relay、持久卷时启用 canary + auto-promote:先起新 compose allocation,健康后再切换。只要 compose 里任一服务需要固定宿主机端口或声明持久卷,就跳过 canary,避免新旧整组同时存在导致端口冲突或状态数据风险。

exposure 选择#

exposure region 是否需要 domain/port 适合场景
cn-edge cn 国内公开 Web/API,走国内 Traefik 和备案域名。
external-edge global 海外公开服务,例如外网 API 网关、低频海外工具。
tailscale-relay home 家里服务通过国内 Traefik + Tailscale 暴露。
tcp-relay 任意 数据库等原生 TCP 服务,公网端口独占,走 Traefik TCP -> task host port。
cloudflare-tunnel 通常 home 家里/私有服务通过 Cloudflare Tunnel 暴露。
none cn / global / home 内部任务、worker、队列消费者,不直接公开。

规则:

常用模板#

国内公开 API#

name: api
image: ghcr.io/acme/api:1.0.0
region: cn
exposure: cn-edge
domain: api.example.com
port: 3000
replicas: 2
env:
  NODE_ENV: production
  DATABASE_URL: ${DATABASE_URL}

环境变量和 Secret#

普通非敏感配置可以直接写在 manifest 里:

env:
  NODE_ENV: production
  LOG_LEVEL: info

敏感值不要写明文。可以手动把 secret 存到控制面:

luma secret set DATABASE_URL --scope api
luma secret set OPENAI_API_KEY --scope api
luma secret list

如果项目已经有 .env 文件,推荐直接在部署时提供它:

luma deploy service.yaml --env .env

Compose 部署同理:

luma compose deploy luma.compose.yml --env .env

--env 会按当前应用名隔离保存 secret。比如 name: api 的服务会把 .env 中实际被 manifest 引用的 DATABASE_URL 保存为 api/DATABASE_URL 这个作用域内的值;另一个 name: worker 的服务即使也有 DATABASE_URL,也不会互相覆盖。.env 里没有被 YAML 引用的变量不会上传。

然后在 YAML 里引用:

env:
  DATABASE_URL: ${DATABASE_URL}
  OPENAI_API_KEY: ${OPENAI_API_KEY}

部署时,客户端只提交 manifest。Luma Control 会从控制面 secret store 读取这些变量,并写入 Nomad task 的 envluma secret list 只显示 key,不显示 value。

如果缺少引用的变量,部署会失败并提示:

missing deployment secrets: DATABASE_URL. Run: luma secret set <NAME>

私有镜像仓库#

镜像拉取凭证不要写进 manifest,也不要作为容器环境变量传给业务服务。先在控制面保存 registry credential:

luma registry login ghcr.io --username <user> --password-stdin
luma registry list

然后 manifest 仍然只写镜像:

image: ghcr.io/acme/private-api:1.0.0

部署时 Luma 会从 image 推断 registry host,使用匹配的凭证,并把 registry auth 注入 Nomad jobspec 的 docker config.auth 块,让被调度的节点可以拉取私有镜像。luma registry list 只显示 registry host 和 username,不显示 password/token。

Compose 组件依赖#

Compose sidecar 的 services.<name> 可以声明与单服务相同的 requirements 契约:

services:
  web:
    exposure: cn-edge
    domain: web.example.com
    port: 3000
    requirements:
      capabilities: [cloudflare]
      init: [cloudflare-dns]

Control 会在 Compose 预览和部署前逐服务检查这些依赖。未知 capability 或初始化动作会在解析阶段直接报错,不会被静默忽略。

常见 GitHub 场景:GitHub Actions 把应用镜像推到私有 GHCR,同一个仓库还可以用 GitHub Pages 发布文档或营销页。Luma 只需要 GHCR 的 registry credential 来拉运行时镜像,不需要把 GitHub token 写进 manifest,也不影响 GitHub Pages 的静态站点发布。

私有 registry 的镜像拉取和服务运行时 proxy: true 是两条路径。proxy: true 只给容器里的出站 HTTP/HTTPS 请求注入代理;镜像拉取走 Docker daemon。Docker Hub 风格镜像会优先使用 manifest 里的原始 image;固定节点部署在 registry 网络失败时会配置目标节点 Docker egress proxy 后重试,仍失败才 fallback 到 defaults.imageMirrors 配置的镜像源。设置 defaults.imageMirrors: [] 可以禁用镜像源 fallback。如果 curl https://<registry>/v2/ 能返回 registry 的 401,但 docker pull 报 EOF/timeout,优先检查 docker info 里的 HTTPProxy/HTTPSProxy/NO_PROXY,并确保私有 registry host 在 Docker daemon 的 NO_PROXY 中。

从 Git provider 构建部署#

默认的 luma deploy 只部署已构建好的镜像。luma import 多走一步:在集群里的构建节点git clone 一个 GitHub/Gitea 仓库、自动发现 .luma.ymlluma.compose.yml、按仓库里的 Dockerfile 或 Compose build: 构建镜像、推送到集群内自托管 registry,再走正常部署链路。适合「源码到上线」的一条龙,不依赖外部 CI。

前提(一次性):

  1. 至少一个节点装好 docker buildx(节点 agent 会自动 advertise docker-build 能力)。
  2. luma registry serve --node <build-node> 起一个集群内 registry(详见运维文档的接入 SOP)。
  3. 私有仓库需先保存 Git provider 凭据;公开仓库可直接用 repo URL。
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

仓库根目录放一个 .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   # 默认值;构建节点是 arm64 而目标节点是 amd64 时尤其重要
env:
  NODE_ENV: production

部署:

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

GitHub 仓库可以短写:

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

短写只表示 GitHub owner/repo,会展开为 https://github.com/acme/myapp.git。Gitea/self-hosted Git 推荐用保存的 provider:

也可以使用保存的 provider 账户:

luma build config --node builder --registry-host 100.66.177.70:5000 --push-host 100.66.177.70:5000
luma import --provider-id gitea:lin --repository acme/myapp --env .env

Compose 仓库同样支持 import:luma import 会发现 luma.compose.yml / .luma.compose.yml / *.luma.compose.yml / *.compose.luma.yml / docker-compose.luma.yml,构建 docker-compose.yml 中带 build: 的服务,推送到 builder registry,并在最终部署 payload 中注入 image:。本地校验 build-only Compose 时用:

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

仓库分别提供预发布和生产 sidecar 时,导入应指定精确的仓库相对 Compose sidecar 路径:

luma import https://github.com/acme/app.git \
  --ref v1.2.3 \
  --compose-sidecar deploy/staging.luma.compose.yml

路径仅在 Builder 克隆仓库后解析。绝对或非规范路径、..、缺失文件、无效 Luma Compose sidecar 和符号链接逃逸都会被拒绝。显式选择失败后不会回退到其他自动发现的清单。

普通 luma compose validate / luma compose deploy 不构建镜像,因此仍要求运行时 Compose 的每个 service 都已经有 image:

CLI 会流式回传 clone → build → push → deploy 每一步。构建节点来自控制面声明的 builder 节点;通常无需传 --build-node,只有临时覆盖时才传,且 Control 会拒绝未声明的构建节点。--env .env 会把运行时环境变量作为 scoped secrets 交给控制面,由最终 .luma.yml / Compose 内容过滤并保存。单服务 import 可用 --region / --exposure / --domain / --port / --platform 覆盖 .luma.yml 里的对应字段;Compose import 只接受 --region 覆盖 sidecar,服务级入口要写在 luma.compose.ymlservices: 里。构建出的镜像 tag 形如 <build-node-tailscale-host>:5000/acme/myapp:<git-sha>,其它区域的节点经 Tailscale 内网拉取(luma registry serve 已为各节点配好 insecure-registries)。构建历史用 luma build list 查看,失败详情用 luma build logs <id>,修好凭据或配置后可 luma build retry <id>

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

build 块字段:

字段 默认 说明
context . Docker 构建上下文(仓库内相对路径)。
dockerfile Dockerfile Dockerfile 路径(仓库内相对路径)。
platform linux/amd64 docker buildx build --platform 的目标平台。

海外 worker#

name: fetch-worker
image: ghcr.io/acme/fetch-worker:1.0.0
region: global
exposure: none
replicas: 1
env:
  QUEUE_URL: redis://redis:6379/0
  OPENAI_BASE_URL: https://api.openai.com/v1

渲染后会自动带上 region 约束:

constraint {
  attribute = "${meta.region}"
  value     = "global"
}

指定部署到某个节点#

如果服务必须固定在某台机器上,例如有本地磁盘状态、只想跑在家里的 Mac mini、或临时调试某个 worker,可以使用 node

name: home-db
image: postgres:16
region: home
node: mac-mini-gaojiu
exposure: none
volumes:
  - home_db_data:/var/lib/postgresql/data

控制面部署时会同时保留 region 约束,并把 Luma 节点名渲染成 Nomad 的节点约束:

constraint {
  attribute = "${meta.region}"
  value     = "home"
}
constraint {
  attribute = "${meta.luma_node_name}"
  value     = "mac-mini-gaojiu"
}

node 使用的是 Luma 节点名,不是 Docker hostname。这个区别对 OrbStack 很重要:多台 Mac 的 Docker hostname 可能都叫 orbstack,但 Luma 用 meta.luma_node_name 指向唯一节点,避免服务跑到错误机器。

Nomad 节点身份是稳定的 UUID。节点离开集群后用同一个 Luma 节点名重新 join,meta.luma_node_name 不变,固定节点服务约束仍然有效;不用手工把 Docker hostname 写进 manifest。

普通服务使用本地卷#

新的持久化原生服务用 named volume 或 bind mount。Control 在首次部署前记录归属节点,后续更新钉在该节点;可写持久化挂载要求 replicas: 1。不要为新应用注册 storage class 或 NFS。

name: home-db
image: postgres:16
region: home
exposure: none
replicas: 1
volumes:
  - home-db-data:/var/lib/postgresql/data

Compose sidecar 把宿主机路径写在 volumes.<name>.local.path,见 compose-storage.md

旧 storageClass(仅迁移)#

已有 NFS 部署仍可用顶层 storage 指向控制面登记的 class。volumes 仍是容器挂载声明;storage 只描述这些 named volume 落到哪个旧存储服务的哪个子目录。新文件不要再写这块。

name: home-db
image: postgres:16
region: home
exposure: none
volumes:
  - pg-data:/var/lib/postgresql/data
storage:
  pg-data:
    storageClass: db-storage
    path: home-db/pg-data
    accessMode: ReadWriteOnce

luma storage list / check / apply / remove 继续管理这些遗留 class。切换后端必须验证 adopted: true 或声明 initialize: empty

需要代理的 worker#

如果服务运行时需要通过 Luma egress proxy 访问外网,声明 proxy: true。不要为了使用默认代理手写 networks: [egress]HTTP_PROXY / HTTPS_PROXY;Luma 会自动渲染这些字段。如果你显式写了同名 env,Luma 会保留你的值。

proxy: true 只管容器自己的出站 HTTP/HTTPS 请求,和服务如何被访问是两件事。比如 region: home + exposure: tailscale-relay + proxy: true 是有效组合:用户入站流量走公网 Traefik -> Tailscale -> home task,容器访问外网时走 egress_mihomo

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

渲染后会自动带上:

env {
  HTTP_PROXY  = "http://egress_mihomo:7890"
  HTTPS_PROXY = "http://egress_mihomo:7890"
}
constraint {
  attribute = "${meta.region}"
  value     = "cn"
}

小机器资源限制#

如果 manager 只有 2c2g,并且业务服务也部署在 manager 上,建议给每个非核心服务显式设置资源边界。CPU 只通过 reservations.cpus 声明调度份额,运行时可使用空闲 CPU,不设独立硬上限。limits.memory 是内存硬上限,reservations.memory 是调度预留量。未填 CPU 预留时使用 100 MHz;只填内存上限时预留 min(256 MiB, 上限)。旧 limits.cpus 会明确告警并忽略,不再预占调度额度;它不是可执行的 CPU 硬上限:

name: api
image: ghcr.io/acme/api:1.0.0
region: cn
exposure: cn-edge
domain: api.example.com
port: 3000
resources:
  limits:
    memory: 512M
  reservations:
    cpus: "0.10"
    memory: 128M

这些规则在普通服务和 Compose 的 deploy.resources 中一致。内存预留不能超过上限;不填内存上限时,保留 Nomad 以内存预留量为容器上限的行为。内存超额分配不等于增加物理内存,多个服务同时冲高仍可能 OOM。

控制面升级只改变后续生成的 Job;已有服务需要重新部署才生效,不需要批量升级 worker。

家里内部服务#

name: backup-job
image: ghcr.io/acme/backup-job:1.0.0
region: home
exposure: none
replicas: 1

家里服务通过 Tailscale Relay 暴露#

name: home-panel
image: ghcr.io/acme/home-panel:1.0.0
region: home
exposure: tailscale-relay
domain: panel.example.com
port: 8080
publishPort: 8080
replicas: 1

默认情况下,Luma Control 会在服务部署后查看 Nomad allocation 实际运行在哪些 home 节点,并把 route 上游指向这些节点的 host port。若服务必须固定到某台机器,再显式指定 node

node: home-mac-mini

也可以手动覆盖完整上游 URL:

relay:
  url: http://home-1.your-tailnet.ts.net:8080

公开 TCP 服务#

服务 manifest:

name: granary-db
image: mysql:8.4.9
region: home
node: lab
exposure: tcp-relay
domain: granary-db.itool.tech
port: 3306
publishPort: 3306
replicas: 1

Luma 会把 DNS 指到公网 edge,自动确保 Traefik 监听 tcp-3306 entrypoint,并写入 Traefik TCP route。domain 用于 DNS;普通 MySQL 连接无法提供 HTTP Host 或可靠起始 SNI,所以同一个发布端口一次只应给一个 TCP 服务使用。 publishPort 是目标 task 节点上的宿主机端口,并会在 Nomad bridge 模式下映射到容器 port。如果同一台机器已有本机容器或非 Luma 服务占用 3306,请选择其它端口并同步调整客户端连接端口或入口配置。Mac/OrbStack 节点不支持这种映射,需省略 publishPort 并使用容器真实监听端口。

Cloudflare Tunnel 服务#

name: home-tool
image: ghcr.io/acme/home-tool:1.0.0
region: home
exposure: cloudflare-tunnel
domain: tool.example.com
port: 8080
replicas: 1
tunnel:
  tokenEnv: CLOUDFLARE_TUNNEL_TOKEN

生成前检查清单#

验证命令#

luma validate service.yaml
luma deploy service.yaml --dry-run

validate 会校验 manifest 并输出渲染后的 Nomad jobspec。deploy --dry-run 不会提交控制面,只展示会生成什么。若本地校验无法读取控制面的节点或 storageClass 信息,JSON 输出会带 validationMode: "degraded"warnings,文本输出会打印 [warn],表示这次校验没有覆盖真实集群放置/存储可达性。