Luma
Your servers. One deployment workflow.
A self-hosted control plane for deploying containers across cloud and home infrastructure.
Website · Getting started · Docs · Console guide · Releases · 简体中文
Luma turns a handful of servers into a deployment platform you can operate from your laptop, CI, or browser. Describe a service in YAML, choose where it runs and how it is reached, then deploy through one authenticated API.
Underneath, Nomad schedules containers, Traefik routes traffic, and Cloudflare manages DNS. Luma connects those pieces with application history, secrets, builds, node management, and a web console. Deployment clients carry a management token; infrastructure credentials stay with the control plane.
# status.yaml
name: status
image: traefik/whoami:v1.10.3
region: cn
exposure: cn-edge
domain: status.example.com
port: 80
luma validate status.yaml
luma deploy status.yaml --dry-run
luma deploy status.yaml
The example assumes an initialized manager and a domain in your Cloudflare zone. Set up your first manager below.
Why Luma
- One workflow across your machines. Place services in
cn,global,home, or custom regions. Pin a workload to a named node when placement matters. - Separate placement from networking. Run a service at home and expose it through a public relay, or run a cloud worker with no public ingress.
- Start from an image or a repository. Deploy existing images, build on your machine, or import a GitHub/Gitea repository through a configured Builder.
- Operate from the console. Inspect applications, logs, routes, builds, registry images, and node health. Manage upgrades with visible progress and route checks.
- Keep delivery reproducible. Validate manifests, preview a deployment, inspect job history, and roll back a Nomad job version. CI uses the same API as the CLI.
- Keep configuration scoped. Application secrets and private registry credentials are managed centrally; manifests reference values without embedding them.
Luma fits personal infrastructure and small teams running web apps, APIs, and workers on a few machines. The control plane currently uses one Manager with local SQLite storage. Multi-active Manager high availability and Kubernetes-style tenant isolation are outside that model. See storage and recovery before relying on it for production.
Quick start
1. Install the CLI
On the manager and any machine you want to deploy from:
curl -fsSL https://raw.githubusercontent.com/LiuTianjie/luma/main/scripts/install-luma.sh | sh
~/.local/bin/luma doctor --local
The installer creates an isolated Python environment and places luma in ~/.local/bin. Open a new shell if that directory is not yet on your PATH. Installing the CLI does not bootstrap a server.
Install a pinned version with pip
Python 3.9+ is required. Use a virtual environment on systems with externally managed Python:
python -m venv .venv
. .venv/bin/activate
python -m pip install "luma-infra==0.2.1"
See installation lifecycle for runtime diagnostics and uninstall behavior.
2. Bootstrap one manager
Have these ready:
| Requirement | Purpose |
|---|---|
| A Linux server; Ubuntu 22.04+ is the documented starting point | Runs Luma Control, Nomad, and Traefik; 2 CPU cores / 2 GB RAM is an evaluation starting point |
| A domain managed in Cloudflare | Control API and application domains |
| A Cloudflare API token with Zone Read and DNS Edit | DNS record management |
| Public ports 80/443 and an ACME email address | HTTP ingress and HTTPS certificates |
| Access to the configured container registry | Pulls the Control image and application images |
Run on the manager:
luma bootstrap --domain luma.example.com
The CLI prompts for missing values, provisions the runtime, initializes SQLite, and prints the dashboard URL, management token, and node join token. Keep both tokens private.
If the manager needs a proxy to pull the default GHCR image, configure EGRESS_SUBSCRIPTION_URL before bootstrap. This is especially relevant on mainland China hosts. Tailscale and a Builder Registry are not prerequisites for the first single-manager workload. See getting started for network and host setup.
3. Run the first workload
Open https://luma.example.com/dashboard/, sign in with the management token, and choose Applications → Create application → hello-world first install.
This deploys the repository's hello-world template with exposure: none. It verifies scheduling without adding an application domain. For a public service, use status.yaml above with a domain you own.
From a laptop or CI machine, authenticate and deploy through Control:
luma login https://luma.example.com --token-stdin < token.txt
luma deploy status.yaml
luma status
luma app versions status
luma init scaffolds a manifest like status.yaml, including a memory limit and health check. Image deployment clients need the CLI and access to Control. Local source builds additionally need Docker/Buildx.
How it works
flowchart LR
Clients[CLI / CI / Dashboard] -->|Authenticated API| Control[Luma Control]
Control -->|Job specifications| Nomad[Nomad]
Nomad --> Cloud[Cloud nodes]
Nomad --> Home[Home nodes]
Control -->|DNS records| DNS[Cloudflare]
Visitors[Application traffic] --> Edge[Traefik ingress]
Edge --> Cloud
Edge -->|Configured relay| Home
The deployment path and the application traffic path are separate. Control submits jobs; application requests go through the configured ingress. Each workload runs on a Nomad client using the Docker driver.
Placement and exposure
| Field | Answers | Examples |
|---|---|---|
region |
Where may this service run? | cn, global, home, or a custom region |
node |
Must it run on a particular machine? | The name registered by luma node join --name |
exposure |
How do clients reach it? | cn-edge, external-edge, tailscale-relay, tcp-relay, cloudflare-tunnel, none |
proxy |
Does the container need runtime outbound proxying? | true attaches configured egress |
For example, region: home with exposure: tailscale-relay places the workload on a home node and sends public traffic through an edge relay over Tailscale. region: cn with exposure: none runs an internal workload without public ingress.
A node pin still respects the region constraint. Runtime proxy: true and image-pull networking are separate concerns. The concepts and exposure model explain these boundaries.
Deployment workflows
| Starting point | Entry point | What happens |
|---|---|---|
| Published container image | luma deploy app.yaml |
Deploys the image declared in the manifest |
| Local source checkout | luma build local . --platform linux/amd64 |
Builds with local Docker/Buildx, uploads to the configured Builder Registry, then deploys |
| GitHub or Gitea repository | luma import <repository-url> |
Builds on a configured Builder and deploys the result |
| Multi-service Compose app | luma compose deploy luma.compose.yml |
Deploys an existing Compose file with Luma placement and exposure configuration |
luma deploy does not build your source. Choose the build/import workflow when you need a new image. Builder workflows require Builder and Registry setup; plain image deployment does not.
For public services, add a meaningful application health check and explicit memory limits. Rolling behavior depends on ports, volumes, and placement; a successful local validation alone does not prove runtime readiness. Field definitions and examples live in the manifest reference.
Secrets and CI
Keep sensitive values in a local environment file or the secret store:
env:
DATABASE_URL: ${DATABASE_URL}
luma deploy app.yaml --env .env
# Or store a value interactively under the manifest's application name:
luma secret set DATABASE_URL --scope app
Only referenced variables are imported, under the application's scope. Registry credentials use luma registry login, separately from application environment variables.
CI can use environment variables without creating a persistent login context:
export LUMA_CONTROL_URL="https://luma.example.com"
export LUMA_DEPLOY_TOKEN="$CI_LUMA_MANAGEMENT_TOKEN"
luma validate app.yaml --format json
luma deploy app.yaml --dry-run --format json
luma deploy app.yaml --format ndjson --timeout 3000
LUMA_DEPLOY_TOKEN is the compatibility name for the management token. Treat it as an administrative credential.
Add a node
Run this on the new node, using the node join token printed during bootstrap:
luma node join https://luma.example.com \
--token '<node-join-token>' \
--region global \
--name global-worker-1
Home/private nodes require Tailscale. macOS home nodes also need a running Docker environment such as Docker Desktop or OrbStack. Per-node agent credentials are installed and managed automatically. See getting started and concepts.
Operations at a glance
| Task | Entry point |
|---|---|
| Check cluster health | luma status and luma doctor |
| Inspect deployment versions | luma app versions <app> |
| Roll back a Nomad job version | luma app rollback <app> --to-version <N> |
| Upgrade Control and nodes | Dashboard → Nodes → Update center |
| Inspect registry storage | Dashboard → Registry |
| Configure application telemetry and alerts | Observability and the independent Observe stack |
| Back up or recover Manager state | Control storage and recovery |
Rollback restores a Nomad job version; it does not restore application data. Use immutable images and keep volume/database backups separately. Registry garbage collection is irreversible, and Dashboard Delete and reclaim has no recovery window; the CLI's queued deletion workflow offers a cancellable window before GC. Read the console guide before reclaiming storage.
Documentation
New to Luma? Read Getting started, then Concepts. The documentation index lists every guide.
Development and contributions
git clone https://github.com/LiuTianjie/luma.git
cd luma
python3 -m venv .venv
.venv/bin/python -m pip install -e '.[test]'
npm ci
PATH="$PWD/.venv/bin:$PATH" bash scripts/check-luma.sh
Tests need Python 3.10+ (the runtime still supports 3.9). The gate checks version references, generated CLI docs, ruff, manifest templates, dashboard types/builds, Python and dashboard tests, and whitespace. Tests never read your real ~/.luma.config.json or ./.env. Dashboard source lives in dashboard-src/; its build is packaged into luma/assets/dashboard/.
Focused fixes, reproducible bug reports, and documentation improvements are welcome. For infrastructure bugs, include the Luma version, node role, manifest with secrets removed, expected behavior, and relevant diagnostics. Never post tokens or proxy subscription URLs. See release process and website maintenance for maintainer workflows.
Security and license
Management tokens grant broad access to your cluster. Use the dashboard only on trusted devices; it stores the token in browser local storage. Keep Cloudflare credentials, registry credentials, join tokens, and agent credentials out of repositories and issue reports.
Luma is released under the MIT License. Bundled and external dependencies retain their own licenses.
Metadata
Release files for luma-infra 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| luma_infra-0.2.1.tar.gz | 1.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| luma_infra-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.9 MB
Release files / luma_infra-0.2.1.tar.gz
| Download URL | luma_infra-0.2.1.tar.gz |
|---|---|
| Size | 1.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e7eda0706922f235c281f5fdb612096506e8bbb4ae67ddbce3d4591ece11673
|
|
BLAKE2b-256 checksum How to use checksums |
2ac6cf1d528afbf1647d791e33bb5089fe6c5587a7e97b199c3b166992931167
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / luma_infra-0.2.1-py3-none-any.whl
| Download URL | luma_infra-0.2.1-py3-none-any.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ca2836e902b43a4223cd54073fbeba1e29798845066d3af5c6f5261aecb6a549
|
|
BLAKE2b-256 checksum How to use checksums |
bc7a35dca68c5205c9d960f4e7537d75fd9d0cb4b6c6aa84a9d68e56c6d7cc00
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency log