Luma CLI#
For the full current command and option inventory, see the generated CLI reference. This guide explains workflows and operational semantics.
Luma is the command line interface for installing nodes, wiring providers, rendering Nomad jobs, and deploying services. The orchestrator underneath is HashiCorp Nomad, so the unit of deployment is a Nomad job.
The default path is control-plane first and Nomad-backed:
luma deploy service.yaml -> Luma Control API -> render jobspec on manager -> sync DNS -> Nomad API (/v1/jobs) -> docker driver
Luma Control is the authentication and orchestration layer. It renders the manifest into a Nomad jobspec and submits it directly to the Nomad HTTP API. Inspect deployments with luma status, the dashboard, or nomad job status on the manager.
Install#
CI runners should install the published package instead of running the shell installer:
python -m pip install "luma-infra==0.1.365"
The package distribution name is luma-infra, but the installed command is still luma.
For interactive machines, use the installer:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | sh
~/.local/bin/luma preflight
The installer uses a GitHub archive, not git clone. It installs into ~/.local/share/luma/venv, writes ~/.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 pinned release:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | LUMA_INSTALL_REF=v0.1.365 sh
Development checkout:
./scripts/install-luma.sh
. .venv/bin/activate
Uninstall the local CLI:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh
By default uninstall keeps ~/.luma.config.json and ~/.config/luma so a reinstall can keep local prompts and login contexts. To remove those local files too:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/uninstall-luma.sh | sh -s -- --purge
This only removes the local CLI install. It does not remove Docker, Nomad, Traefik, Luma Control, deployed services, or server-side /opt/luma state.
CI Usage#
CI can run Luma as a stateless control-plane client. It does not need SSH, Docker, Cloudflare, Nomad, or files under ~/.config/luma.
PR validation:
python -m pip install "luma-infra==0.1.365"
export LUMA_CONTROL_URL="https://luma.example.com"
export LUMA_DEPLOY_TOKEN="$CI_LUMA_MANAGEMENT_TOKEN"
luma validate deploy/app.yaml --format json
luma deploy deploy/app.yaml --dry-run --format json
Main or release deployment:
python -m pip install "luma-infra==0.1.365"
export LUMA_CONTROL_URL="https://luma.example.com"
export LUMA_DEPLOY_TOKEN="$CI_LUMA_MANAGEMENT_TOKEN"
luma status --format json
luma deploy deploy/app.yaml --format ndjson --timeout 3000
The control context priority is CLI flags, then environment variables, then the local login context. CI commonly uses:
LUMA_CONTROL_URLLUMA_DEPLOY_TOKENLUMA_INSECURE=true|falseLUMA_RESOLVE_IPLUMA_CONTROL_CONTEXT(select a saved cluster without changing the current context)
LUMA_RESOLVE_IP keeps the control hostname in the Host header and requires insecure TLS mode.
For commands with shared Control options, --control-context CLUSTER selects a
saved context for that invocation. Explicit flags override environment values,
which override the selected saved context. If an explicit Control URL differs
from the saved endpoint, Luma does not inherit its token, TLS override or IP
override; supply credentials for the target endpoint. This also applies to doctor,
service restart and service remove; they can run in CI without a prior login.
Interactive login can prompt for a hidden token, or read it from standard input:
luma login https://luma.example.com
printf '%s' "$LUMA_DEPLOY_TOKEN" | luma login https://luma.example.com --token-stdin
luma context list --format json
luma doctor --control-context staging --format json
login accepts --token or --token-stdin, then falls back to
LUMA_DEPLOY_TOKEN and finally an interactive hidden prompt. Non-interactive
login without a token fails with a setup hint. Prefer the environment or stdin
to keep tokens out of process arguments.
Commands advertising --format support text, json and ndjson; check their
exit status as well as the response. Doctor returns a healthy boolean and
individual checks; an unhealthy result has a nonzero exit status. Streaming
service logs support text and NDJSON, while JSON is available for snapshots.
Git Provider Credentials#
Repository import can use saved GitHub/Gitea credentials instead of a one-off repo URL token. Tokens are write-only and are injected only into the builder's leased clone task.
printf '%s' "$GITHUB_TOKEN" | luma git-provider set github personal --username octo --token-stdin
printf '%s' "$GITEA_TOKEN" | luma git-provider set gitea lin \
--base-url https://gcode.example.com \
--username lin \
--token-stdin
List accounts and discover repositories:
luma git-provider list
luma git-provider repos gitea:lin
luma git-provider refs gitea:lin acme/app
Use the selected provider account with import:
luma import --provider-id gitea:lin --repository acme/app --build-node builder --env .env
For public GitHub repositories, the positional source can also be owner/repo; Luma expands it to https://github.com/owner/repo.git. Use a full URL or --provider-id ... --repository ... for Gitea/self-hosted Git.
Configuration#
luma.yaml is the single project config source:
project: example
providers:
dns:
type: cloudflare
zone: example.com
zoneId: ""
apiTokenEnv: CLOUDFLARE_API_TOKEN
edgeTarget: 203.0.113.10
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
Secrets stay outside Git. For normal use, run the command you actually need:
luma bootstrap manager --domain luma.example.com
If required local values are missing, Luma prompts for them before continuing and saves them to ~/.luma.config.json with mode 0600. On worker servers, the same happens during:
luma node join https://luma.example.com --token <node-join-token> --region global --name global-sg-1
luma configure --role manager|worker remains available if you want to edit local secrets ahead of time, and luma configure --show lists configured keys without printing values. Luma loads .env and ~/.luma.config.json automatically. Use --env-file <path> to load another project-local env file or --no-env to disable local secret loading. Values already exported in your shell take priority. On the manager node, bootstrap and update save the required Cloudflare values in Manager Control state (control.sqlite3 after legacy import) so client machines do not need those secrets. 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.
Commands#
Initialize a config:
luma init
Check local requirements and .env:
luma preflight
Show control-plane and cluster state from any logged-in client:
luma status
luma status prints DNS readiness, the orchestrator (Nomad) with its server leader, and registered Luma nodes from the control state with role=client.
Store private registry credentials for image pulls:
printf '%s' "$GHCR_TOKEN" | luma registry login ghcr.io --username <user> --password-stdin
luma registry list
luma registry remove ghcr.io
Deploy an in-cluster registry for source-to-image builds:
luma registry serve --node build-1
luma registry serve deploys a registry:2 service on a docker-build-capable node and wires insecure-registries into every non-manager ready Linux node so they can pull built images over the Tailscale network. BuildKit and target nodes both use the reachable Builder Tailscale endpoint <build-node-tailscale-host>:5000; do not configure the removed localhost:5000 push endpoint. Optional flags: --port (default 5000), --storage-class (optional legacy class; omit to use a node-local Docker volume), --image (default registry:2), --name (default luma-registry), --timeout (default 1800). See the repository-import walkthrough in how-to-use-luma.md for the full source-to-image flow.
The same control plane also serves a read-only Web status panel:
https://<control-domain>/dashboard/
Paste the management token to view readiness, nodes, services, and inferred traffic paths from a trusted browser.
Luma has two user-facing tokens:
- Management token: for trusted CLI clients and the dashboard. Use it with
luma login, dashboard login, deployments, storage, secrets, registries, and node operations. - Node join token: for servers that are joining the cluster or refreshing their local node agent. Use it with
luma node joinand, for older nodes without saved agent metadata,luma update --control-url ... --token ....
The per-node agent credential is internal and is installed automatically on the node. Users should check agent status with luma node status, not copy or manage agent credentials.
List nodes from local luma.yaml only:
luma node list
Bootstrap the manager by running this directly on the manager server:
luma bootstrap manager --domain luma.example.com
For single-node, it installs Docker, connects Tailscale when configured, installs and starts the Nomad server, applies node meta, deploys Traefik and Luma Control as Nomad jobs, configures firewall rules, and sets up egress. Set EGRESS_SUBSCRIPTION_URL first 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.
It streams progress:
[start] Install Nomad server
[ok] Nomad server ready
[start] Deploy Luma Control
[fail] Deploy Luma Control
Fix: Re-run luma bootstrap manager after fixing the error
Skip egress only 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
Login from any client machine:
luma login https://luma.example.com --token <management-token>
luma context list
luma context use <cluster-id>
Join additional servers by running this on each server:
luma region create batch-a --egress proxy
luma region list
luma node join https://luma.example.com --token <node-join-token> --region cn --name cn-worker-1
luma node join https://luma.example.com --token <node-join-token> --region global --name global-sg-1
luma node join https://luma.example.com --token <node-join-token> --region home --name home-mac-mini
luma node join https://luma.example.com --token <node-join-token> --region batch-a --name batch-a-01
--region is the scheduling pool. Built-in values are cn, global, and home. Create more with luma region create <name> --egress proxy|direct (also available on the Nodes page), then join machines into that name. Custom regions default to internal workloads (exposure: none). --egress proxy uses the manager gateway for join/image pulls, same as cn/home.
--name is the Luma node name used by luma status and by service manifests. Luma writes it to the Nomad client meta.luma_node_name and uses it for pinned scheduling; the Nomad node identity is a stable UUID, so a rejoin under the same name keeps pinned services valid. Add --engine nomad to force the Nomad client agent path explicitly; it is the default.
Refresh a joined node agent after upgrading an older node:
luma update --control-url https://luma.example.com --token <node-join-token>
Update every registered node that has a ready node agent:
luma update fleet
luma update fleet --install-ref v0.1.365 --timeout 900
luma update fleet --include-manager
--install-ref accepts a release tag, branch, or full 40-character Git commit.
Use a full commit for a coordinated candidate rollout so manager and node agents
cannot resolve different revisions while a branch moves.
Fleet update runs through the node agents. It updates the CLI on each ready non-manager node and then refreshes the local node-agent service and Tailscale watchdog. The Nomad server (manager) node is skipped by default; update the manager separately with luma update manager from the manager host. --include-manager is available for explicit repair workflows, but normal fleet updates should leave the active control plane alone. Nodes whose agent is too old to advertise luma-update are reported as skipped; run luma update once on those nodes, then they can participate in later fleet updates.
Manager control state under /opt/luma/control is root-only. On a manager
where the operator account does not have passwordless sudo, run the break-glass
CLI update with elevation (for example sudo ~/.local/bin/luma update
manager) or configure LUMA_SUDO_PASSWORD through luma configure --role
manager. Without that access, luma update must not fall back to the user's
client login context. Older releases can take that wrong path and return a 401
if the client token is stale; restore access to manager state before changing
credentials.
The Dashboard Update center is the preferred manager path. It first mirrors
the selected Control image into the configured internal Registry, then starts
the persisted manager operation. A direct CLI manager update pulls the image
itself; on a network where GHCR is unavailable, pass a pullable internal image
with LUMA_CONTROL_IMAGE or use the Dashboard flow.
Recover a manager whose public IPv4 address changed. Run the preview on the
manager host first, then repeat without --dry-run:
luma manager ip-change \
--old 8.147.65.253 \
--new 8.145.62.128 \
--domain luma.itool.tech \
--dry-run
luma manager ip-change \
--old 8.147.65.253 \
--new 8.145.62.128 \
--domain luma.itool.tech
The command validates the new address through HTTPS while preserving the
control hostname for SNI and certificate checks. It then changes only the
manager node publicIp, providers.dns.edgeTarget, and Cloudflare A records
whose content exactly equals the old address. It backs up luma.yaml, reuses
the image from the running luma-control Nomad job, and reconciles the manager
control plane without reinstalling the CLI or redeploying user applications.
It never performs a global replacement across Control state; historical incident
messages remain untouched. The operation is idempotent, so rerunning it safely
finishes any records left behind by a partial Cloudflare failure.
Drain the local Nomad client and optionally unregister the node from the control plane:
luma node exit --endpoint https://luma.example.com --token <management-or-node-join-token> --name home-mac-mini
Remove a node from any logged-in client:
luma node remove home-mac-mini
The control plane removes the Luma registration record and then drains the matching Nomad client on the manager. Matching uses the saved Nomad node ID, meta.luma_node_name, or the node name. Luma refuses to remove a Nomad server (manager) node through this command.
Connect Cloudflare and write providers.dns.zoneId:
luma cloudflare connect --zone example.com
Repair or refresh the outbound gateway:
luma egress setup
luma egress refresh
Install/login Tailscale:
luma tailscale connect
Managers and joined nodes install a lightweight Tailscale watchdog during bootstrap/update. The manager watchdog verifies Tailscale peers plus Nomad gossip/RPC TCP reachability; node watchdogs verify manager Tailscale plus the Nomad server ports. Consecutive failures restart local Tailscale. This is intended to recover tailnet TCP stalls without restarting Docker, Traefik, the Nomad agent, or application jobs.
Generate a service manifest interactively:
luma service new
Validate and render:
luma validate examples/public-cn-service.yaml
luma render examples/public-cn-service.yaml
luma render examples/public-cn-service.yaml --engine nomad
luma render renders locally. --engine nomad forces the Nomad jobspec renderer; this is also the default on current clusters.
Deploy through the control plane:
luma deploy examples/public-cn-service.yaml
Roll back or inspect version history of a deployed service:
luma history public-cn-service
luma rollback public-cn-service
luma rollback public-cn-service --to-version 3
luma history lists prior versions of the Nomad job (GET /v1/job/<id>/versions). luma rollback reverts to the previous version, or to the version given by --to-version N (POST /v1/job/<id>/revert). The web dashboard exposes the same operation from Applications -> Versions. Jobspecs also render update { auto_revert = true }, so a new version that fails its health checks rolls back automatically.
Rollback changes the running Nomad job only. It does not rewrite Git, update the stored manifest in Luma Control, roll back databases, or restore volumes. For predictable production rollback, deploy immutable image tags or digests rather than latest.
Search retained Control build and deployment attempts:
luma service history public-cn-service --kind deployment --status failed --limit 50 --format json
luma service history --source dashboard --since 2026-09-01T00:00:00+08:00 --format json
luma service history --id RECORD_ID --kind deployment --limit 50 --format json
luma build list --app public-cn-service --status failed --limit 50 --format json
luma build logs BUILD_ID --limit 50 --format json
Once a build ID is accepted, transient connection failures resume waiting for that same task until the original --timeout deadline; stopping the wait does not cancel the server build. Queries and read-only workflow checks make at most three attempts; authentication, certificate and response-format errors fail immediately. Build and deployment submissions are not automatically replayed. When Control provides event positions, the wait fetches only new events; older Control versions retain full pagination.
service history [NAME] searches Control records across builds and deployment
attempts. Filters include kind (build or deployment), source (build, cli
or dashboard), status, application, --since and --until. Time filters accept
Unix seconds or RFC3339 timestamps with a timezone. The default page size is
50, with a maximum of 100; pass the returned nextCursor as --cursor while
keeping the other filters unchanged. JSON responses include page metadata
(limit, nextCursor, hasMore); text mode prints continuation information to
stderr. Pagination reads more retained records rather than increasing retention.
service history --id ID --kind KIND reads one record's paged steps and cannot
be combined with list filters. build logs ID also pages steps oldest first.
These are build/deployment execution events; application stdout/stderr belongs
to service logs. Existing luma history NAME remains the Nomad job-version
list used with rollback, and is separate from Control attempt history. Importing
legacy state cannot recover records that were already trimmed before migration.
See Control storage for the retention and backup boundary.
Inspect a service before taking action:
luma service list --region cn --format json
luma service inspect public-cn-service --format json
luma service events public-cn-service --format json
luma service logs public-cn-service --tail 100
luma service logs public-cn-service --allocation ALLOCATION_ID --previous
luma service logs public-cn-service --follow --format ndjson
list --stack NAME narrows the inventory to one stack. Logs are scoped to the
selected deployment; use --allocation when comparing individual replicas.
--tail accepts a 1–500 line budget shared across selected sources. Text output
labels each entry with [allocation/task/stream]; [partial] marks an unfinished
fragment and [continued] marks its continuation, including when other sources
are interleaved. This is a diagnostic view, not a byte-exact log export. Use
JSON/NDJSON to retain the source, cursor and fragment metadata. --previous reads a previous instance where the
runtime has retained it. Follow mode resumes from the latest line or heartbeat byte cursor after a
clean EOF, transport failure or temporary HTTP failure. Reconnects use
exponential backoff from 0.5 to 15 seconds, with at most eight consecutive
reconnects without progress. Press Ctrl-C to stop. Authentication, invalid cursor
and protocol errors fail immediately. An older Control that provides no resume
cursor fails rather than replaying log snapshots. There is no --since flag: application log
timestamps are not guaranteed to be parseable, so this CLI does not claim a
server-side time filter.
Restart a running service without redeploying:
luma service restart public-cn-service
luma service restart my-stack --service web --mode task
--mode recreate reschedules the allocation; --mode task restarts the task in place. Omitting --mode uses recreate for a whole stack and task when --service targets one task. After the runtime action, Control reconciles the saved deployment's routes and DNS and probes its public HTTP services; Compose reconciles every exposed service. Restart refuses the system stacks traefik, egress, and luma-control. See operations.md for details.
Remove a deployed service:
luma service remove public-cn-service
luma service remove public-cn-service --dry-run
Check authentication, remote Control status, DNS configuration readiness, node agents and Nomad health (this does not audit the local Docker/OS environment):
luma doctor
luma doctor --deep
--deep also evaluates the remote node diagnostics reported in Control status (Docker mirrors/proxy and Nomad configuration). DNS readiness means configuration is present; it does not prove public DNS resolution or application availability.
Service Manifest#
name: app
image: ghcr.io/me/app:latest
region: cn
exposure: cn-edge
domain: app.example.com
port: 3000
replicas: 2
Required fields:
nameimageregion:cn,global,home, or a custom region created withluma region createexposure:cn-edge,tailscale-relay,tcp-relay,cloudflare-tunnel,external-edge, ornone
Public services also require:
domainport
Optional fields:
engine:nomad. Selects the orchestration backend for this service. Omit it to inherit the cluster default.node: Luma node name fromluma node join --namefor pinning the service to one node. The control plane renders it as a Nomad constraint on${node.unique.name}(ormeta.luma_node_name) and still adds theregionconstraint.env/environmentcommandconstraintslabelsnetworksproxy: whentrue, runtime traffic uses the egress proxy; Luma attaches the egress proxy and default proxy env. Scheduling still followsregion.resources: rendered into the Nomad task'sresourcesblock; CPU usesreservations.cpus(default 100 MHz) for elastic scheduling shares; legacylimits.cpusis ignored with a warning, not enforced as a hard ceiling. Memory reservations map tomemoryand limits tomemory_max; limit-only memory reserves min(256 MiB, limit). Before registering such a job, Control automatically enables Nomad memory oversubscription so the declared limit is the live container hard limit.stackPathroutePathdns.targetdns.typedns.proxiedpublishPortrelay.host: optional tailscale-relay upstream override; usually omit it.relay.url: optional full tailscale-relay upstream URL override; usually omit it.tcp-relayusespublishPortorportto derive the Traefik TCP entrypoint automatically.tunnel.tokenEnv
Example worker that needs the Luma egress proxy:
name: ai-worker
image: ghcr.io/acme/ai-worker:1.0.0
region: cn
exposure: none
proxy: true
Example bounded service for a small manager:
name: api
image: ghcr.io/acme/api:1.0.0
region: cn
exposure: none
resources:
limits:
memory: 512M
reservations:
cpus: "0.10"
memory: 128M
Deploy Order#
For luma deploy service.yaml, Luma does:
- parse and validate the service manifest;
- if
--env <file>is provided, parse that.envlocally and keep only variables referenced as${NAME}by the manifest; - read the current login context from
~/.config/luma; - submit the manifest and filtered scoped env secrets to the manager's Luma Control API;
- store incoming env secrets under the service
namescope and resolve${NAME}before rendering; - render
stacks/<region>/<service>/<service>.nomad.json(the jobspec) on the manager; - render
routes/<service>.ymlon the manager fortailscale-relayortcp-relay; - upsert Cloudflare DNS unless skipped;
- submit the job to Nomad through
PUT /v1/jobs(create or update); - probe the public route for
cn-edgeandexternal-edgeservices.
The client prints local progress before submitting the request, while waiting for the control plane, and for 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 is reachable but the application 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. Single-service and Compose jobs allow 30 minutes for cold image acquisition and 40 minutes for rollout progress; the default deploy response timeout is 3000 seconds so Control remains attached beyond that bounded Nomad window. Use --timeout <seconds> to override it.
Deploy is an upsert. Re-running luma deploy service.yaml with the same service name updates the existing Nomad job (the job id is the service slug) instead of creating a duplicate. The update uses the current rendered jobspec as the source of truth, and Nomad keeps the previous version so luma rollback or the dashboard's Applications -> Versions action can return to it.
Use luma deploy service.yaml --env .env when the project already has a deployment env file. Scoped env secrets are isolated by service name, so api/DATABASE_URL and worker/DATABASE_URL are distinct values. Legacy global luma secret set NAME values are still used only for applications that have no scoped secrets.
For source-to-image deploys, luma import uses the same scoped secret model:
luma import https://github.com/acme/app --build-node builder --env .env
luma import acme/app --build-node builder --env .env # GitHub owner/repo shortcut
When the shared Builder is slow, build the current checkout on your own computer and keep the artifact in the same Luma project:
cd app
luma build local . --env .env
luma build local infers the project identity from the checkout's origin
(use --repo-url when there is no origin), reserves that project in Control,
builds with local Docker Buildx, pushes to the configured Luma registry under
the same owner/repository namespace used by luma import, and then runs the
normal deploy path. The local computer must be able to reach the configured
build.registryHost; for an authenticated registry, log in with Docker first.
Single-service manifests and Compose sidecars are supported, including
--compose-sidecar, --platform, --context, and --dockerfile overrides.
Use --builder <name> to reuse an existing local Buildx builder when it has
the required target platform or local registry/mirror configuration. Use
--proxy <url> when local base-image and Dockerfile network access should use
an HTTP proxy; the internal registry is kept in NO_PROXY.
Both this command and Builder import derive the container architecture from the
deployment node or currently ready nodes in the target region. A Darwin/ARM
node therefore builds linux/arm64; a region with both amd64 and arm64 targets
builds a multi-platform image. An explicit --platform is accepted only when
it covers every resolved target architecture.
With a Control advertising build-queue-v1, the CLI automatically submits
Repository Import and build retry to a persistent per-project FIFO. Local
builds may build and upload concurrently using unique Control-assigned tags;
after upload, their deployment joins the same FIFO. Queue order is submission
order (upload completion for local builds), not local build start order. Only
one queued operation per repository project runs at a time; unrelated projects
can use other worker slots, subject to existing Builder capacity and runtime
deployment locking. A failed or canceled attempt does not discard later work.
The CLI displays the build ID, queue position and blocking task while waiting.
--timeout bounds the client wait; closing the CLI after acceptance does not
cancel the server task. Inspect it with luma build logs <id> or cancel a waiting
task with luma build cancel <id>. Queued requests survive Control restart.
Interrupted active tasks are explicitly failed rather than automatically
replaying deployment side effects; inspect runtime before retrying. Local
builds that have not finished uploading still require the caller's machine.
Per-attempt environment values are stored only in private Control state while
queued/executing, isolated from other attempts and public build history.
Older Control versions retain the previous fail-fast active-build restriction;
upgrade both Control and CLI to use the queue. Prebuilt-image deploy and
compose deploy keep their existing synchronous runtime lock. Local uploads
are still rejected if images escape the reserved project repository/tag.
or, with a saved Git provider account:
luma import --provider-id gitea:lin --repository acme/app --ref main --build-node builder --env .env
Import auto-discovers single-service Luma manifests (.luma.yml, luma.yml, nested *.luma.yml) and Compose sidecars (luma.compose.yml, .luma.compose.yml, *.luma.compose.yml, *.compose.luma.yml, docker-compose.luma.yml). Compose sidecar filenames are excluded from single-service manifest matching so docker-compose.luma.yml is treated as Compose, not as a service manifest.
If the repository does not contain a deployment file yet, provide the deployment file from the CLI:
luma import --provider-id github:personal --repository acme/app \
--build-node builder \
--manifest deploy/app.luma.yml \
--env .env
Unlike plain luma deploy, import may discover the final deployment manifest on the builder after cloning the repository. Therefore the CLI sends the .env values to Luma Control, and the control plane keeps only values referenced by the resolved manifest or Compose content under the final service/stack scope.
For Compose repositories, luma import builds services that still have build: and injects the resulting image: before deployment. Plain luma compose validate and luma compose deploy do not build, so use import-mode validation when checking that path locally:
luma compose validate --import-mode luma.compose.yml
When a repository contains more than one deployment sidecar, select the exact Compose sidecar inside the cloned repository instead of relying on discovery:
luma import https://github.com/acme/platform.git \
--ref v1.4.0 \
--build-node builder \
--compose-sidecar deploy/staging.luma.compose.yml \
--env .env
--compose-sidecar accepts only a canonical POSIX repository-relative path and
cannot be combined with --manifest. The CLI requires a Control capability
before starting the build; Control requires the Builder to echo the same path;
the Builder rejects absolute paths, .., missing/invalid YAML, and symlink
escapes. An explicit selection never falls back to an auto-discovered sidecar.
Update the manager and Builder agent first when either side lacks this
capability.
--dry-run renders locally and does not submit a deployment. When local rendering cannot read optional cluster context such as node or storage metadata, JSON output includes validationMode: "degraded" plus warnings; text output prints [warn] lines. --skip-dns and --skip-orchestrator are sent to the control API. --commit and --push are deprecated in control-plane deploy mode.
Luma records deployment state before running external operations. A successful deploy is marked active; if DNS, the Nomad submission, route rendering, or probing fails after earlier steps have changed the manager, the recorded deployment is kept with status: failed_partial so the dashboard and luma service remove <name> can still find the partially applied job.
For luma service remove <name>, Luma looks up the manifest recorded by the control plane during the last successful deploy and removes the matching single-service or Compose deployment slug. This recorded manifest is the source of truth, so remove and storage cleanup also work for deployments created from the web UI when the client running the command has no YAML file. By default Luma deletes Luma-managed Cloudflare DNS, deregisters and purges the Nomad job, and deletes generated jobspec files such as stacks/<region>/<service>/<service>.nomad.json or stacks/compose/<name>/<name>.nomad.json. tailscale-relay and tcp-relay route files are removed too. Use --dry-run to preview, --skip-dns to keep the DNS record, and --skip-orchestrator only when you intentionally want to remove generated Luma files without stopping the Nomad job. Storage data is preserved by default; add --delete-storage to delete removable storage declared by the recorded deployment. For single-service deployments this removes managed storage paths referenced by storage.<volume>.path and removes named Docker volume objects such as data:/data; bind mounts are skipped. For Compose deployments this removes managed storage paths referenced by the sidecar. --delete-storage cannot be combined with --skip-orchestrator. cloudflare-tunnel public hostnames are still managed in Cloudflare Zero Trust, so Luma reports that cleanup as skipped.
Automatic build/deploy workflow checks#
Luma records each application's successful CLI build/deploy workflow on Control. The record is shared across machines and agents, independently of the checkout and of the bounded build/deployment history. No backfill of existing applications is required.
Before luma import, luma build local, luma build retry, luma deploy, or luma compose deploy
starts a build, reserves a local upload, or submits a deployment:
- No record: continue normally, then create the record after success.
- Matching record: continue normally and refresh the success record.
- Different workflow: show the differences and require confirmation before doing build/deploy work. This includes switching remote/local/image/Compose methods, explicit Git repository/ref, builder, platform, context/Dockerfile, sidecar, region, exposure/domain/port, explicit deployment env-file path, and network options. Output format, timeout, credentials and explanatory notes do not trigger a change.
An interactive text terminal asks Confirm this workflow change and deploy? [y/N].
JSON/NDJSON, quiet and non-interactive callers stop with a nonzero exit status.
An agent must show the differences to the user and obtain approval before retrying
with --accept-workflow-change; it must not add that flag on its own to fix an
error. CI can use the flag only where a workflow change has been approved.
# Normal deployment: automatic check and recording; no extra flag needed.
luma import --provider-id github:me --repository acme/app --ref main --build-node builder
# Read the shared record, including the command and last recorded success.
luma workflow show app
luma workflow list --format json
# Add context to the record on the next successful deployment.
luma import acme/app --ref main --workflow-note 'Build on Builder; this project requires remote network access'
# Only after the user has approved the displayed differences:
luma build local . --platform linux/amd64 --accept-workflow-change
# Explicitly record/edit a workflow without executing it.
luma workflow record app --note 'Release from the main branch on Builder' -- \
import acme/app --ref main --build-node builder
# Execute the recorded command from a checkout, using current login credentials.
luma workflow run app --path /path/to/app
The record retains the argument array, method, optional note and last successful
recipe/evidence. Manual edits are labeled manual; an earlier success is kept
with its original recipe and is not proof that the edited command has run.
Failures, interrupted streams, dry runs and --skip-orchestrator do not overwrite
success records. If deployment succeeds but saving the workflow fails, the CLI
reports deployment success with workflow.saved: false and a warning; do not
blindly redeploy to repair a recording error. Concurrent edits are preserved.
Application manifests identify service/Compose targets. Imports discover the
record from the repository, or the selected Compose sidecar in a monorepo. When
several applications match, select one with --workflow-app APP. A different
application name has its own record. Repository-relative file paths can move with
a checkout; external absolute config/env paths must exist on the next machine.
Env-file contents, management tokens and URL credentials are never stored in the
recipe. Free-text notes are user-authored: do not put secrets in them.
A retry records the original build parameters and build retry ID; replaying that
command requires the original build record to remain available. The guard compares
CLI workflow parameters, not source-code changes, image tags or manifest contents.
CLI and Control must both support deployment-workflow-v1. Upgrade Control before
using the new CLI for deployments. An unreachable or older Control is an
unavailable check, not proof that no record exists, so deployment stops with an
explanation. The workflow is a CLI coordination mechanism; older clients and direct
API/dashboard deployments do not participate in these CLI checks.