文档/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.0。latest 或未带 tag 会在部署时解析成 name@sha256:... 再部署。*提供 build 块时可省略,镜像由构建产出。
build 否 map 从源码构建镜像(luma import)。子字段:context(默认 .)、dockerfile(默认 Dockerfile)、platform(默认 linux/amd64)。提供 build 时 image 可省略。见下方「从 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 单服务 cn-edge / external-edge 渲染成 Nomad HTTP 或 TCP 检查;可提取的 HTTP URL 提供路径,否则回退到 TCP,不会在容器中执行任意 shell 命令。其他暴露模式应检查实际生成的 Checks。
requirements 否 map 组件依赖契约。支持 capabilities(cloudflare / tailscale / egress / registry)、secrets、init 和 notes。init 支持 cloudflare-dns、cloudflare-tunnel、tailscale-node、egress、registry、secrets。部署预览会合并显式声明与运行时推导,并在必需项缺失时阻止提交;正式部署会先执行并记录初始化计划。
publishPort 公开服务可用 integer 显式启用 Nomad bridge 端口映射,把宿主机 publishPort 转到容器 port。原生 Mac/OrbStack 单服务仍需省略此项;Mac Compose 支持 Docker 管理的端口映射,见自定义 Region 公网入口。
relay tailscale-relay 可选 map 覆盖 Tailscale relay 上游。默认由 node 指定的节点及其 Tailscale 地址推导;Compose 可继承同组服务或本地卷的节点约束。
tcp tcp-relay 可选 map TCP relay 高级上游覆盖。正常情况不需要填写;入口由 publishPort / port 自动派生。
tunnel cloudflare-tunnel 可用 map Cloudflare Tunnel token env 等设置。
dns.servers 否 IP 或 IP 数组 容器使用的 DNS 解析服务器;省略时继承原有默认设置。
stackPath 否 string 覆盖生成 jobspec 路径。通常不用。
routePath 否 string 覆盖 tailscale route 文件路径。通常不用。

容器 DNS#

单服务 manifest 使用 dns.servers:

dns:
  servers: [100.100.2.136, 100.100.2.138]

Compose 使用标准 docker-compose.yml 中的服务字段,不能放在 Luma sidecar:

services:
  worker:
    image: example/worker:1
    dns: [100.100.2.136, 100.100.2.138]

支持 IPv4、IPv6 和单个 IP 字符串。仅设置容器的运行时解析器,不修改宿主机、Control、镜像拉取或出口代理的 DNS。阿里云 SLS 私网端点需使用云上内网 DNS;其他云或本地节点应配置其实际可达的解析服务器。

独立 worker 和 host 网络下的服务使用 Docker dns_servers;Linux CNI bridge 使用 Nomad 网络层的 DNS.Servers;Mac Compose 将配置传给共享网络的容器。共享网络内所有服务必须显式声明相同的 dns,否则拒绝部署,避免一个服务的设置改变其他服务的解析行为。

CLI 和 Control 都需升级到支持 container-dns-v1 的版本。修改 Git 中的配置后必须先部署一次,升级 Control 或直接重启旧任务不会读取新的 Git 配置。部署成功后,后续容器重建沿用已提交的 DNS 设置。

更新策略#

Luma 渲染 Nomad update 策略时默认启用 auto_revert、max_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 判断健康。

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

Compose 部署也渲染同样的 Nomad update 基线策略。多副本单服务使用逐个滚动更新,不额外启动 canary;副本配置与限制见 Compose 副本数。单副本时,对没有 publishPort 的 cn-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 或已开启此能力的自定义 Region 是 通过现有 Traefik + Tailscale 暴露私网节点服务。
tcp-relay 已允许此入口的 Region 是 数据库等原生 TCP 服务,公网端口独占,走 Traefik TCP -> task host port。
cloudflare-tunnel 通常 home 是 家里/私有服务通过 Cloudflare Tunnel 暴露。
none 任意已注册 Region 否 内部任务、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 的 env。luma 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

已配置 Builder Registry 时,Luma 将匹配外部 image 的源仓库凭据租给 Builder,由 Builder 复制镜像并返回已验证的内部镜像 digest。最终 Nomad job 使用内部镜像引用;只有内部仓库需要认证时,才把对应凭据写入 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 发布文档或营销页。在上述 Builder Registry 链路中,GHCR 凭据供 Builder 复制源镜像,运行节点拉取内部镜像,不需要把 GitHub token 写进 manifest,也不影响 GitHub Pages 的静态站点发布。

镜像获取和服务运行时 proxy: true 是两条路径。proxy: true 只给容器里的出站 HTTP/HTTPS 请求注入代理。配置 Builder Registry 后,外部源镜像访问、进程级代理及 Docker Hub 风格镜像的 defaults.imageMirrors 回退由 Builder 负责;不会因为源仓库拉取失败就改运行节点的 Docker egress proxy。设置 defaults.imageMirrors: [] 可以禁用镜像源回退。

最终镜像使用托管 HTTP Registry 时,正式部署会在提交 Nomad job 前检查全部符合落点约束的 Docker 目标节点,并在修改任何配置前要求所有 agent 声明 managed-registry-access-v2。新 agent 保留 v1/v2 并共用同一修正实现,兼容仍要求 v1 的旧 Control。Linux 使用 Docker 实际代理字段;OrbStack 使用其原生代理设置及 auto 模式下的系统有效代理,不能仅凭内置 broker 地址判断外部代理。Manager 只做只读验证;配置修改要求空闲引擎。具体 HTTP 地址范围、历史兼容例外和维护边界见运维说明。

从 Git provider 构建部署#

默认的 luma deploy 只部署已构建好的镜像。luma import 多走一步:在集群里的构建节点上 git clone 一个 GitHub/Gitea 仓库、自动发现 .luma.yml 或 luma.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.64.0.70:5000 --push-host 100.64.0.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.yml 的 services: 里。构建出的镜像 tag 形如 <build-node-tailscale-host>:5000/acme/myapp:<git-sha>,其它区域的节点经 Tailscale 内网拉取;HTTP 访问由目标节点在 join 或最终部署预检时按需准备,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-home
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-home"
}

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 在首次部署前记录归属节点,后续更新钉在该节点;显式改成其他 node 时部署会继续,但会给出 Storage placement 警告,旧节点上的数据不会被复制;可写持久化挂载要求 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(仅迁移)#

旧部署可能保留顶层 storage 元数据,但原生 Nomad 服务的实际挂载来自 volumes:命名源渲染为 Docker volume,路径源渲染为 bind mount。storageClass 元数据不会自动把它们改成 NFS 挂载;迁移前应检查运行中的 Nomad mount、Docker volume driver options 和宿主机挂载。新文件不要再写这块。

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。切换后端会给出 Storage placement 警告但不再阻止部署;数据不会自动迁移,验证后可设置 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: ledger-db
image: mysql:8.4.9
region: home
node: lab
exposure: tcp-relay
domain: ledger-db.example.net
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 并使用真实监听端口;Mac Compose 则支持独立的 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

单服务 luma validate 在本地校验并渲染,默认文本输出为摘要;--format json 包含渲染产物。luma deploy --dry-run 打印本地生成的 Nomad job 和适用的路由,不向 Control 提交部署,也不读取实时集群放置、存储或镜像状态。正式部署时 Control 仍会解析实际节点、存储归属、密钥和镜像,因此本地预览不保证与最终提交的 job 完全一致,也不证明运行时就绪。