What this covers
An app is a container (or a set of them) that Levelrail keeps running for you. You describe it once, from the dashboard, the CLI, or anapp.yaml in your repo. Levelrail builds it if needed, starts it, waits until it is healthy, switches traffic to it, and keeps every previous image so you can go back. This page follows an app from creation through deploys, rollbacks, configuration, promotion, and day to day operation.

Common tasks
Domains and TLS, managed databases, and git provider setup have their own pages: Domains and ingress, Managing databases, and Git integrations.
The model in one minute
- An app is desired state. It is a record: an image, a port, and the settings around them. Saving it changes the record. A level-triggered reconciler then converges the running containers to match. Nothing is edge-triggered, so a missed event never leaves an app out of sync.
- One mutation changes the image.
POST /api/v1/apps/{name}/deployspoints the app at a tag and returns immediately. A rollback is the same call with an older tag.apps rollback, the dashboard rollback button, and automatic rollback on a crashloop all use it, so there is no second code path that can drift. - Traffic only moves once the new container is ready. The readiness probe has to pass before the old container is drained. If it never passes, the old container keeps serving and the deploy fails with a reason.
For contributors: where this lives in the source
For contributors: where this lives in the source
- Backend:
internal/api/apps.go,apps_multi.go,apps_compose.go,deploys.go,promote.go,exec.go,resources_live_apply.go,scheduled_tasks.go - CLI:
cmd/levelrail-cli/apps*.go - Dashboard:
web/src/routes/apps/$name/*.tsx - An app is one or more
store.DesiredServicerows under onestore.App. Redeploy goes throughsetDesiredImage, restart throughRestartService, stop and start throughUpdateServiceSuspended.
Create an app
There are four ways to create one. All four accept inline secret values (--secret KEY=VALUE on the CLI) that are encrypted as part of the same call, so a { secret: true } variable declared in the spec needs no follow-up call.
The dashboard’s New wizard offers an existing image, a git repository, and Docker Compose as step-one cards, plus templates. The multi-service app.yaml route starts from an existing app’s Services tab.
From an existing Docker image
Skips the build step. Sendimage and port to POST /api/v1/apps, or:
levelrail-cli apps scale NAME --replicas N. In an app.yaml, a prebuilt-image service can be written as image: traefik/whoami:v1.10 next to replicas and strategy.
In the dashboard, the Docker image card has an image picker that browses the built-in registry, a connected registry credential, or public Docker Hub (GET /api/v1/dockerhub/search and GET /api/v1/dockerhub/repositories/{namespace}/{repo}/tags). The plain text field always works too.
From a git repository

POST /api/v1/apps with a :pending placeholder image, then POST /api/v1/apps/{name}/builds, which replaces the placeholder with the real tag when the build succeeds. levelrail-cli apps builds trigger does the second step.
One build per app at a time. A second
POST .../builds while one runs returns 409 Conflict (“a deploy for this app is already running”). If the control plane restarts mid-build, the orphaned attempt is marked failed on the next startup, so it never blocks new builds.Disk preflight. Before BuildKit starts, the control plane checks free space on the build context directory (and the local cache directory when one is configured). Below the floor, the build fails fast with “N bytes free, need at least M bytes”. The floor is 1 GiB by default, set in MiB with
APP_MIN_BUILD_DISK_MB. A path that cannot be read counts as unknown, not as a failure.Railpack and framework detection
Whenbuild.type is railpack (no Dockerfile), Railpack detects the framework from the repository. Levelrail builds only providers it has verified end to end:
Anything else Railpack can detect (Ruby, PHP, Rust, Deno, .NET, and so on) is rejected with a clear error rather than attempted. A Django app needs nothing beyond what
django-admin startproject generates plus a requirements.txt that lists Django and a production server such as gunicorn. Railpack runs python manage.py migrate on start and serves with gunicorn bound to $PORT, never the Django dev server.
How framework detection works without building
How framework detection works without building
The Deploy from git wizard checks what would be detected before you pick a build type, without building anything:The control plane makes a shallow single-branch clone (depth 1) into a temporary directory, with a 20 second timeout and a checkout size cap. It runs Railpack’s provider detection, which only reads files such as
package.json, go.mod, or pom.xml, then discards the checkout. No repository code is executed. A repository that matches none of the providers above, or cannot be cloned, answers {"detected": false} rather than an error, and the wizard falls back to its manual build-type tabs. levelrail-cli build detect --repo-url URL runs the same check.The detected name is stored on the deploy attempt (detected_framework). It appears on the deploy detail page, in a FRAMEWORK column of levelrail-cli apps deploys list, and in --output json. apps create on the git path and apps builds trigger run the same detection first.From a Docker Compose file
Each Compose service becomes its ownDesiredService under one app, in one synchronous call. Each service gets its own deploy attempt.
depends_on, on the Services panel.
How Compose is translated, and what fails validation
How Compose is translated, and what fails validation
- Magic variables.
SERVICE_<KIND>_<KEY>variables that need generated secrets are resolved and stored automatically. A compose file that bind-mounts a host directory needs therootability on top ofdeploy. - Ports and volumes accept the short form (
"8080:80",web-data:/data) and the long mapping form, so an upstreamdocker-compose.ymlusually pastes in unchanged. Port ranges, UDP, andtmpfsornpipemounts have no equivalent and are rejected. depends_on(list or map form) enforces start order. A dependent container is not created until every service it names has a running container. This matches Compose’s own default (service_started): it waits for the container to exist and run, not for its health check. A dependency must name a real sibling, and a cycle fails validation rather than deadlocking the reconciler.pull_policy: alwaysforces a fresh pull on every deploy, which suits mutable tags like:latest. The default is pull if absent.restart:andnetworks:parse but have no effect, because the reconciler alone decides whether a container runs and every service in an app shares one network. They come back as non-blockingnotices, as does anything else that cannot be translated, such as a health check with no readiness-probe equivalent.
deploy:subkeys other thanreplicasandresources.reservations.devices(GPU reservations). The rest of the Swarm-specificdeploy:surface (mode,placement,restart_policy,update_config, and so on) has no meaning outside a Swarm cluster.- Top-level
secrets:andconfigs:, and a service’s ownsecrets:orconfigs:references. Move the value into the app’s env vars or Levelrail secrets.
From a multi-service app.yaml
POST /api/v1/apps/{name}/deploy-spec builds and deploys each service in services: in key order, under one app. It is synchronous per service, and one service’s build failure does not stop the others. The response is 207 Multi-Status when at least one service failed. Unlike Compose, it records no per-service deploy attempt yet, so the response is the only record of what happened. The dashboard form is on an existing app’s Services tab.
Every field of the spec is in the app.yaml reference.
Deploy and watch a rollout
- CLI
- Dashboard
- API
apps wait exits 5 when the deploy failed and 6 when it timed out, which makes it a CI gate. See the CLI reference for every exit code.
What a rollout does
An app’s Deploy settings tab sets the strategy (rolling, recreate, or blue-green, with blue-green the default) and the replica count:

OOMKilledDuringReadiness, ExitedDuringReadiness, or ReadinessFailed. The first two are reported the moment the container dies, instead of after the full readiness budget has been spent probing a dead address. The budget is 60 seconds by default and health.readyTimeout in app.yaml overrides it per service. The reason shows in the deploy history and in levelrail-cli apps deploys list. Guarantees around stale deploys, freeze windows, and a short hold of the previous release are in Deploy safety.
Live deploy view
Once a build starts, the deploy detail page (/apps/{name}/deploys/{deployId}/logs) shows a checklist of named steps above the raw build log:
GET /api/v1/apps/{name}/deploys/{deployId}/steps. Each event is { "step": string, "status": "running" | "done" | "failed", "timestamp": string }. The log underneath still shows every build line. Reconnects are handled by EventSource, and receiving a step event twice is harmless. The stream only reports on the build flow. It never feeds back into the reconciler.
When a deploy fails
A failed attempt opens with a What went wrong card: the failing stage, the recorded error, a likely cause with a suggested fix, and three actions: View full logs, Retry deploy, and Roll back to last good (shown only when an earlier attempt succeeded). The cause is a heuristic match against a fixed rule table, not a diagnosis. It never changes the attempt’s real status. The rules cover a missing environment variable, a port mismatch, a container killed for memory (OOMKilled or exit 137), registry auth, an image or tag that cannot be found, a health check timeout, a wrong Dockerfile path, a failed dependency install, and a few older npm, pip, heap, disk, and permission patterns. For the structured failure object that the API, CLI, and MCP tools return, see Deploy failures.levelrail-cli apps diagnose NAME explains a failed deploy or crashloop and can apply a fix.
Deploys across all apps

Filters, derived fields, summary and stream
Filters, derived fields, summary and stream
GET /api/v1/deployments lists deploy attempts across every app the caller can read, newest first. It is cursor paginated (limit defaults to 50, maximum 200, pass next_cursor back as cursor). An IAM Deny on app:web hides web’s deployments from the list, the summary, and the stream. The dashboard page is described in Deployments page.- Filters:
status(queued, held, awaiting_approval, building, ready, failed, canceled, rolled_back, superseded; comma separated or repeated),app,branch,trigger(git push, manual, rollback, api, preview, schedule, pipeline),environment(name or ID),sinceanduntil(RFC3339 or a duration such as24h,7d),q(commit message, SHA prefix, app name),live=true(only the release currently serving each app), andpr(previews of one pull request). - How fields are derived:
rolled_backis a succeeded deploy that a later rollback replaced (rolled_back_by). A rollback carriesrollback_of, the attempt whose image it re-deployed.heldis a deploy parked by a freeze window.canceledis a failed attempt whose error is a canceled context.image_refpins the tag to a digest only when the digest is registry verified, anddigest_reasonsays why when it is not. Commit message, author, and branch are recorded for git push deploys only.stepsand the failing step are known only while an attempt runs, because step history is not persisted. - Summary:
GET /api/v1/deployments/summary?window=24hreturns counts by status (window up to 30 days),in_progress,needs_attention(held plus digest mismatch),failure_rate_24h, median and p95duration, andper_dayfor 14 days. - Stream:
GET /api/v1/deployments/streamis server-sent events of{type: created|step|finished, step?, deployment}. Only deploys that run through the build recorder emit events. A plain image redeploy appears in the list but not on the stream.
list_deployments and deployments_summary are read-only.
Roll back
- Dashboard
- CLI
- API
Open the deploy history and press Rollback on the row you want, one click per row.
- Auto-rollback on crashloop is opt-in per app and computes the last known-good tag itself:
levelrail-cli apps auto-rollback enable NAME. An SLO burn variant isapps auto-rollback-slo-burn set. Both are covered in Observability. - Deploy freeze windows hold automatic deploys on a cron schedule:
levelrail-cli apps freeze set|show|clear NAME.
Change configuration
Save semantics
A save keeps the stored settings a body cannot express: volumes, bind mounts, entrypoint, database env, registry credential, pull policy, and the pinned image ID.secret_env and vault_env change only when the body carries them. Omit them to keep the stored set, send [] to clear. Secret values are never accepted here (a body with secrets is rejected). Use PUT /api/v1/apps/{name}/secrets/{key}.
Fields with their own endpoints cannot be moved by an ordinary edit: node_id, project_id, environment_id, storage_target_id, suspended, database_attachment, and log_drain. An edit can never silently move an app between nodes or projects.
Pending changes
Changing env vars or secrets does not touch the running container until it restarts.
GET /api/v1/apps/{name}/pending-changes returns { pending, changes: [{ kind: env | secret | config, keys, since }], apply_action }, with key names only and never values. Rotating a secret counts as a pending secret change. Resources and health checks apply live and are not listed. POST /api/v1/apps/{name}/apply-pending restarts the app and returns 202. For a container created before this tracking existed, only the env_dirty flag is known.
From the CLI, apps status NAME prints a “pending changes” line, and apps env import and apps secrets set print “N changes pending”. Apply them with levelrail-cli apps apply NAME, or pass --apply to the command that made the change. The MCP tool is get_app_pending_changes.
App timeline
GET /api/v1/apps/{name}/timeline?limit=50&before=<cursor> merges recorded events (restart, env_change, secret_change, config_change, scale, suspend, resume, freeze_override) with deploy attempts (deploy, rollback, and in_progress for a running one), newest first. Each item has id, at, kind, status, actor (a user, token:<name>, or system), title, an optional detail, and for deploys ref: { type: "deploy_attempt", id }. Events carry env and secret key names and non-secret scalar from and to values only. Pass the previous page’s next_cursor as before. CLI: levelrail-cli apps timeline NAME. MCP: get_app_timeline.
Environment variables and secrets

.env file and exported again:
- The parser handles comments, an
exportprefix, single and double quotes, multiline quoted values, inline# commentson unquoted values,=inside values, empty values, and Windows line endings. When a key appears twice, the last one wins. - Import prints each key as new (
+), changed (~), unchanged (=), or skipped (!). Keys already set as secrets are skipped. Pass--keep-existingto leave keys that already have a different value alone. Changes apply on the next restart, or now with--apply. - Export writes secret keys empty with a comment. Secret values are never exported.
- Setting declares the key as secret-backed on the app (the list
secret_envshows). Deleting undeclares it. The value reaches the container on its next creation, so runapps apply NAMEor pass--apply. The API isPUTandDELETE /api/v1/apps/{name}/secrets/{key}, with?force=truefor a locked key. There is no batch endpoint, so a bulk import makes one call per key. - Locks guard against accidental overwrites: a locked secret cannot be changed from the dashboard or CLI until unlocked (
POST /api/v1/apps/{name}/secrets/{key}/lockwith{ "locked": true }). A lock does not change how the value is stored. - Age. Every secret records when it was last set, and is flagged stale at 90 days by default.
APP_SECRET_ROTATION_WARN_DAYSchanges the threshold cluster-wide. The dashboard shows “Set N days ago” and a Needs rotation badge,apps secrets listhasAGEandSTALEcolumns, andlevelrail-cli shared-env list --scope project|organization|environment --id IDshows the same for shared variables.GET /api/v1/system/doctorincludes astale_secretscheck across all apps and scopes.
External secrets from HashiCorp Vault
A variable can resolve live from Vault instead of being stored in Levelrail:path is the secret’s path in Vault’s KV v2 engine and key is the field inside it. vault is mutually exclusive with from and secret on the same variable. The field table is in the app.yaml reference.
Configure the connection once for the instance, under Settings, Vault, with levelrail-cli vault set, or with PUT /api/v1/settings/vault. You need the Vault address, an auth method (token or approle), and a token or a role ID plus secret ID. The credential is envelope encrypted, write-only over the API, and never logged.
The reconciler reads the value from Vault immediately before it creates the container, with the same resolved-at-create, never-persisted trust model as { secret: true }. If Vault is unreachable, not configured, or disabled, or the secret or field does not exist, the deploy fails with a clear error. The container never starts with the variable empty or missing.
An app created directly can add or remove a Vault variable at any time, from the Environment tab, with levelrail-cli apps vault-env set NAME KEY --path PATH --key FIELD (or clear), or with PUT and DELETE /api/v1/apps/{name}/vault-env/{key}. This touches no other field.
Health checks
An app has up to two probes, readiness and liveness. Readiness gates the cutover of a deploy. Liveness runs on every reconcile pass against a running container and restarts it after consecutive failures. A probe is an HTTP(S) request (path) or a command run in the container (exec). The full field table and the Compose healthcheck: translation are in the app.yaml reference.
health: in app.yaml. The dedicated endpoints are GET, PUT, and DELETE /api/v1/apps/{name}/health. On the wire, intervals and timeouts are time.Duration JSON (nanoseconds), and the dashboard converts to whole seconds. A probe is either absent or fully specified with a path or command, and the dashboard fills sensible defaults for blank fields.
Resource limits
Four optional dimensions, each independent. A dimension you leave off runs unbounded:
Set them on the Resources tab, in
app.yaml, or with PUT /api/v1/apps/{name}. New limits are applied live: right after the save, they are pushed to every running replica through the Docker Engine API’s ContainerUpdate, with no recreate. The response’s resources_applied_live says whether the push reached at least one replica. With no container running, the values are saved and take effect on the next create, and the dashboard shows a “restart required” toast.

GET /api/v1/apps/{name}/resource-recommendation is a read-only, deterministic suggestion (internal/rightsizing, no LLM, never applied automatically). It looks at memory and CPU samples over a lookback window (7 days by default), computes p95 and p99 against the current limit, and checks recent logs for OOM-kill signatures. The result has a sample count, a data_sufficient and confidence pair, and a suggested limit with a plain-English reason. See it in the Optimise panel on the Resources tab, or run levelrail-cli apps resource-recommendation NAME. With the resource advisor beta on, the same suggestion arrives as an advisor finding you can apply after a preview.
Outbound network (egress) allowlist
By default an app has unrestricted outbound access. Opting a service into an allowlist restricts its container to the declaredhost:port pairs. A reconciled sidecar enforces it and re-resolves each host on an interval instead of pinning an IP at deploy time.
levelrail-cli apps egress set NAME --allow api.example.com:443, then apps egress get NAME, and apps egress clear NAME to go back to unrestricted. The dashboard has an Outbound network card on the app’s Network tab. DNS lookups and loopback traffic always stay open. After a deploy or restart there is a brief window before the sidecar finishes attaching where outbound traffic is unrestricted.
Promote between environments
An environment is a label inside a project. Promote moves one app’s current image onto a sibling app in another environment of the same project:POST /api/v1/apps/{name}/promote looks for an app tagged with the destination environment in the source app’s project. There is no declared one-to-one link between apps in different environments, since apps are independently named. If exactly one candidate exists it is used. With more than one, pass --target. GET .../promote/preview?to=ENV_ID[&target=NAME] shows the diff first.
By default only the image changes. Env vars, ports, domains, volumes, node placement, and resource limits stay the target’s own. --include-env also applies added and removed plain env keys (values of keys both apps have are never overwritten). --force promotes even when the source is unhealthy or its last deploy failed. --override-freeze with --override-reason promotes through an active freeze window.
Protected environments
Deploying to or promoting into a protected environment needs a second person to approve it.1
Request
The requester sends the deploy or promote with
confirm: true. Without it the request fails with a 409. The CLI asks for an interactive “yes” when --confirm is missing. With it, nothing is applied yet: the call creates a pending approval and returns 202 Accepted with pending_approval set instead of app.2
Approve
A different user or token holding the
deploy ability approves it with POST /api/v1/deploy-approvals/{id}/approve. The requester cannot approve or reject its own request, and the server answers 403, comparing both the principal type (user or token) and its ID. A CI token’s request must be approved by another token or a human.3
Run
Approving runs the same path an unprotected deploy uses: desired state changes, an attempt is recorded, and the reconciler picks it up.
- Rejecting (
POST .../reject, optional{"reason": "..."}) leaves desired state untouched. - A request nobody decides expires after 24 hours (
APP_DEPLOY_APPROVAL_TTL, a Go duration) and can no longer be decided. Expiry is applied lazily on read, and a background sweep marks expired rows for the UI.
deploy, directly or through the operator and admin roles, is enough.
/approvals in the sidebar, badged with the pending count. The list endpoint defaults to status=pending, and status=all includes decided and expired requests. Approvals can also be decided from chat, see Chat deploy approvals.
Clone an environment
Cloning copies a whole environment, with all its apps and their configuration, into a new environment in the same project. Use it for staging, per-pull-request copies, or onboarding a tenant. It copies configuration, not data. Preview first, then clone:
After the clone, the apps start deploying through the normal reconcile path. Check them with
levelrail-cli apps status NAME, add domains with apps domains add NAME DOMAIN, set secrets with apps secrets set NAME KEY VALUE, and attach databases with apps database set NAME --database-name DB.
The API is GET /api/v1/environments/{id}/clone/preview?new_environment_name=NAME and POST /api/v1/environments/{id}/clone:
apps array is optional. Omit it for auto-suggested names and no domains, or list only the apps you want to customize.
Troubleshooting a clone
Some apps were created and others were not
Some apps were created and others were not
The environment exists. Check
levelrail-cli apps list, finish the rest by hand, or delete the partial environment with levelrail-cli apps environments delete ENV_ID and retry.A domain assignment fails with "domain already taken"
A domain assignment fails with "domain already taken"
Another app owns it. Pick a different domain or remove it from the other app first.
Secrets have no value even with --copy-secret-values
Secrets have no value even with --copy-secret-values
Only secrets that had a value in the source are copied. Set the rest by hand on the clone.
A cloned app stays pending
A cloned app stays pending
Read
levelrail-cli apps deploys list CLONE for the error. Usual causes are limits that are too tight, a failed image pull, or a readiness probe that times out.levelrail-cli apps clone NAME NEW_NAME.
Operate a running app
Lifecycle actions
Restart gets a fresh
restart_nonce folded into the container name hash, so “same image, new nonce” looks like a change to the cutover logic and the usual readiness-gated rollout applies.
In the dashboard, each row of the Apps list has an actions menu with Restart, Stop or Start, Redeploy, View logs, View deploys, and Open domain (only when the app has a domain). Stop asks for confirmation. Redeploy re-triggers the current image tag, and lands in the approvals queue when the environment requires approval. The app header has the same Restart, Stop or Start, and Redeploy buttons, plus Promote, Clone, and Delete. The empty Apps list and the welcome screen offer Create app, Browse templates, and Connect Git.
One-off commands and the terminal
POST /api/v1/apps/{name}/exec runs one command and returns its result.
- Input:
commandis required.argsis optional and never shell interpreted (raw argv).timeout_secondsmay shorten but never extend the 30 second server ceiling. - Output:
stdout,stderr, andexit_code, capped at 1 MiB (truncated: truepast that). A nonzero exit code still returns HTTP 200, because the exec worked and the command failed. - Permission: the
rootability, notdeploy. Exec can be switched off per app (it is on by default) withlevelrail-cli apps exec-access disable NAME, and a disabled app answers 403.
GET /api/v1/apps/{name}/terminal over WebSocket: a full PTY with resize, a shell kept alive across requests, arrow keys and Ctrl-C working, behind the same root gate. In the dashboard, the Exec tab shows the terminal above the one-shot runner. The one-shot endpoint is deliberately not a shell. A remote command stuck on a container-side read is not guaranteed to stop the instant the timeout fires, only the HTTP handler is guaranteed to return on time.
Scheduled tasks
A scheduled task runs an argv command, with no shell, inside an app’s currently running container on a standard 5-field cron expression. It is cron inside a container.update is a full replace: resupply command, schedule, and concurrency_policy, not only the field you are changing.
Each task tracks last_run_at, last_run_status, last_run_output, and a consecutive_failures counter (the input for scheduled_task_failure alert rules). That is the whole run history: there is no log of every past run.
POST .../scheduled-tasks/{id}/run (CLI run) uses the exact code path of a real cron tick. It dispatches from a background goroutine and returns 202 Accepted right away, not when the command finishes.
concurrency_policy decides what happens when a run is due while the previous one is still going:
Delete and clean up
Deleting something never relies on a single best-effort call. Every path that removes containers (delete an app, move it to another node, prune a stale compose service, tear down a preview, drain a node) first records a tombstone and clears it only when the containers are confirmed gone, so a failed stop retries on every reconcile pass and shows up asteardown_pending. A tombstone for a node that was since removed from the fleet is dropped instead of retrying forever.
Projects, environments and organizations
DELETE /api/v1/{projects|environments|organizations}/{id}?cascade=true. Apps go first, then databases, then the container itself is removed last. If anything cannot be removed (a database still used by an app outside the scope, a database error) the response is 207 with the per-resource result, the container is kept, and repeating the same request resumes where it stopped. Database data volumes and app volumes are kept, they show up under leftovers below.
Leftovers and the orphan reaper
A reaper compares what exists on every node with desired state and removes what nothing accounts for, so a crash between “delete desired state” and “stop the container” cannot leave something running forever. It looks at three kinds of leftovers:
A resource is removed only after it has stayed orphaned for the grace period, counted from the first pass that saw it. Each removal writes an audit log entry (
REAP, actor orphan-reaper). A pass removes at most APP_ORPHAN_MAX_REMOVALS resources, and removes nothing at all while no app or database exists, so an empty database read cannot wipe a node.
APP_ORPHAN_INTERVAL). Remote nodes are scanned through their agent for containers and networks; volumes are only scanned on the control plane’s own node. An unreachable node is reported and skipped, never treated as empty.
Changing or removing an app’s domain drops its route on the next ingress pass, and its certificate is removed by the reaper after the certificate grace period.
Removing a node
DELETE /api/v1/nodes/{id} is refused (409) while apps or databases are placed on the node. Drain it first (POST /api/v1/nodes/{id}/drain), which moves every placement. Draining an unreachable node still works: the placements move, and the leftover containers on the dead node are dropped from the retry list when the node is deleted. If the node later re-enrols, the reaper removes any copy of an app that now lives elsewhere. Volumes are not moved by a drain: an app with data on a dead node needs apps set-node NAME NODE --with-volumes while the node is still reachable, or a restore from backup.
API and CLI quick reference
The CLI groups below cover an app’s own lifecycle. Run
levelrail-cli apps SUBCOMMAND -h for any command’s flags, and see the CLI reference for the rest.
Known limits
apps rollbackneeds a tag. Useapps deploys rollback-towith a deploy ID to roll back without knowing the tag.- No per-service history for
deploy-spec. It writes no deploy attempt per service, only the synchronous response. A per-service log is a known, deferred schema change. GET /apps/{name}/deploysis current reconcile status, not a log. It keeps only the latest condition per controller and type.GET .../deploy-attemptsis the append-only history.- Compose and
deploy-specare synchronous. Neither streams build progress the way a single-service build does, so a large multi-service spec is a slow HTTP request. - Scheduled task history is last run only, and
replacecancellation is best effort.
See also
Rollback
One-click and automatic rollback in depth.
Deploy safety
Freeze windows, stale deploys, and what protects a rollout.
Deploy failures
How failures are structured and explained.
app.yaml reference
Every field of the deployment spec.
Git integrations
Trigger deploys from git push and pull requests.
Domains and ingress
Custom domains and HTTPS certificates.
Managing databases
Attach Postgres, MySQL, Redis, and others.
CLI reference
Every command, flag, and exit code.