levelrail-cli apply, and the control plane converges to it. The same files can be exported from a live control plane, reviewed in a pull request, and applied from CI.
Everything goes through the ordinary REST API with your own token. If a policy denies you an app, that one item is reported as denied and nothing else is touched by it. There is no separate write path.
Quick start
Document format
A file holds one or more YAML documents separated by---. Every document has the same envelope:
GET /api/v1/apply/schema. Point your editor at it for completion. Errors are reported with file and line: infra/web.yaml:14: additional properties 'prot' not allowed (spec.service.prot).
An App’s spec.service is the app.yaml service definition, validated by the same schema. The document schema embeds the app.yaml definitions unchanged, with one addition: an env value may be { secretRef: NAME }.
Project
Environment
Environments belong to a project. Identity is project plus name.Tag
Database
App
build.type: image). Git builds are configured with a git source, not here. Not supported yet, and rejected with a clear error: volumes, egress policies, GPU requests, registry credentials, from and vault env references. A live app that uses them keeps them when you apply; the plan warns.
Domain
Adds a hostname to an app. It is additive with the App’s owndomains, and a hostname can belong to one app only.
LoadBalancer
metadata.name is the app. The spec is the app.yaml loadbalancer block.
Pipeline
pipelines validate, and issues point at the line inside your file. A pipeline synced from the repository cannot be changed here.
AlertRule
Secrets
Documents never contain secret values.- Reference a secret by name:
API_KEY: { secretRef: API_KEY }orAPI_KEY: ${{ secrets.API_KEY }}. Secrets are stored per app under the env var name, so the reference must match it. - A literal value on a key that looks like a secret (
PASSWORD,TOKEN,API_KEY, and similar) or a value shaped like a known token is refused with an error. The error names the key and line and never echoes the value. - To create or update the value at apply time, pass it explicitly from the environment or a file:
NAME to apply to every app referencing that name, or app/NAME for one app. The plan only ever shows that a secret is set, never the value. A secret that is referenced but has no value yet is reported as a warning.
${{ env.NAME }}placeholders in plain values are filled at apply time from the sources described in Providing values safely. Export uses them when you pass--include-env-values=false.
secretRef, and a plain variable whose name or value looks like a secret is written as a ${{ env.NAME }} placeholder with a warning that also says how to supply it.
Providing values safely
A resource file can be written by someone else: a teammate, a template, a pull request. So the CLI never fills a${{ env.NAME }} placeholder from your shell unless you said it may. A placeholder is resolved from, in this order:
--var NAME=VALUE(repeatable).--var-file PATH(repeatable):KEY=VALUElines in dotenv format, with#comments, quotes and an optionalexportprefix. A line that is not a validNAME=VALUEis an error naming the line. Keep the file readable only by you (chmod 600), and out of the repository.--allow-env NAME[,NAME...](repeatable): reads exactly those names from your environment. A trailing*allows a prefix, for example--allow-env 'APP_*'.
# comments are ignored.
Names that look like credentials are never covered by a wildcard: cloud and CI prefixes (AWS_*, AZURE_*, GOOGLE_*, GCP_*, S3_*, SSH_*, ACTIONS_* and similar), GITHUB_TOKEN, GH_TOKEN, NPM_TOKEN, and any name containing TOKEN, SECRET, PASSW, CREDENTIAL, PRIVATE or _KEY. If a file really needs one, name it exactly: --allow-env AWS_REGION. A shared file that quietly asks for ${{ env.AWS_SECRET_ACCESS_KEY }} therefore fails instead of sending your key.
The control plane never reads its own environment for placeholders. The dashboard and the MCP tools cannot pass values at all, so files applied there must not use ${{ env.NAME }}.
Export
Apply
-f may be repeated and accepts a file, a directory (searched recursively for .yaml and .yml) or - for stdin.
How it runs
1
Validate everything first
Every document is checked against the schema and the semantic rules. If any document has an issue, nothing is planned and nothing is applied.
2
Plan
Live state is read through the API with your permissions, and a plan is computed: create, update (with a field level diff), delete (only with
--prune) or no change. Env values are never shown, only (hidden). Items are ordered by dependency: project, environment, tag, database, app, domain, load balancer, pipeline, alert rule.3
Apply
Each item is executed through the existing endpoint. Apply is safe to repeat and to resume: a re-run recomputes the plan and only does what is still missing.
- Validation and planning are all or nothing. Execution is per item: by default the first failure stops the run and later items are reported as skipped. With
--continue-on-errorthe rest proceed. - A plan that contains an error item (a missing project, a read you are not permitted to make) applies nothing unless
--continue-on-erroris set. - Per resource permissions apply to each item. A denied item is reported as
deniedwith its key. - The plan carries a hash. The CLI, dashboard and MCP pass it back so an apply only proceeds if live state still matches what was reviewed.
- Lists and maps on an App (
domains,tags,env,labels, secret names) are additive. Live entries the files do not mention are kept and shown askeepin the plan. Everything else on a declared resource is authoritative.
Deploys
Apply saves desired state and the reconciler acts on it. A changed image is a deploy, by the same path as any other image change. When an env var or secret changes on a running app, apply restarts it so the change takes effect.--no-deploy skips that restart; the app keeps running with the old values until you restart it. It does not hold back an image change, which the control plane starts on save. An app is only touched when its plan is not empty.
Prune
--prune deletes things that are in the control plane but not in your files, and it is deliberately narrow:
- It requires
--source NAME. Apps created or updated by an apply with a source carry amanaged-by:NAMEtag. - Only apps carrying that source’s tag are ever deleted. Anything created by hand, or by another source, is never touched.
- On a managed app it also removes load balancers and extra domains, tags, env vars and labels that are absent from the files.
- Projects, environments, databases, pipelines and alert rules are never pruned.
--pruneasks for confirmation unless--yesis set.
Exit codes
Drift
levelrail-cli diff -f infra/ shows how live state differs from the files and exits 2 when it does. Run it on a schedule to catch hand edits.
Dashboard and MCP
Settings, Infrastructure as code lets you paste or upload YAML, see the plan with diffs, and apply with a confirmation. It also has an Export button per project and per app. MCP has two tools:plan_apply (read only, a dry run) and apply_resources (changes state, subject to the usual approval and IAM rules).
Git workflow
There is no sync engine to run. Keep the files in a repository and use the CLI from CI. A pull request shows the plan, and merging applies it.--var or name them with --allow-env; the runner’s own credentials (GITHUB_TOKEN, ACTIONS_*, cloud keys) are never read unless you name them exactly. The plan job needs the same --var or --allow-env flags as the apply job.
Planning needs only a read token. Use a separate token with write ability for the apply job. The install step is yours to supply: download the levelrail-cli release binary for the runner, the way the bundled deploy action does. See GitHub Actions for token setup and that action.
Examples
A single app
A multi environment project with a database and domains
Adopting an existing project
--source infra-repo so the apps are tagged as managed by it.
Not in scope
Git sync is a recipe, not an engine. Backup schedules, volumes, egress policies, node placement, registry credentials and vault or database env references are not expressible yet and are left as they are.Next steps
app.yaml reference
The service fields an App document embeds.
Pipelines
The pipeline definition format.
Identity and access
Tokens and per resource policies.
Migrating from other platforms
Import from Coolify, Dokploy or CapRover.