Skip to content

Application YAML (v3)

The canonical Application format used by edgible stack deploy. One YAML document describes one application; multiple documents in a single file are separated by ---.

The schema is enforced by the control plane and the agent. Unknown keys are rejected.

apiVersion: v3
kind: Application
metadata: { ... }
spec: { ... }
FieldRequiredTypeNotes
apiVersionyesv3The only supported value today.
kindyesApplicationThe only supported value today.
metadatayesobjectSee Metadata.
specyesobjectSee Spec.
metadata:
name: my-app
organization: <org-id>
labels:
env: production
annotations:
owner: platform-team
dependsOn: [other-app-name]
FieldRequiredTypeNotes
nameyesstringUnique within the organization. Lowercase alphanumeric with hyphens, 1–63 chars.
organizationyesstringYour organization ID. Find with edgible config list.
labelsnomapFree-form key/value strings. Used by placement.deviceSelector.labels.
annotationsnomapFree-form key/value strings. Not interpreted by the platform.
dependsOnnostring[]Names of other applications that must be deployed before this one.
spec:
placement: { ... }
workloads: [ ... ]
storage: [ ... ] # optional
access: [ ... ] # optional but typical
cloud: { ... } # optional, only valid when placement.strategy is cloud or automatic

spec.workloads is the only required field. At least one workload must be declared.

spec.cloud (sleep/wake behavior, minActive, service classes) is accepted by the schema only alongside the cloud placement strategies, which are not available in beta — see placement.

Where the application runs — a specific device you own, chosen by selector.

# Run on a specific device you own
placement:
strategy: serving-device
deviceSelector:
deviceName: my-first
migrationPolicy: manual
FieldRequiredTypeNotes
strategyyesenumserving-device. The schema also accepts cloud and automatic, but those are not available in beta — see the note below.
deviceSelectorrequired for serving-deviceobjectSee below.
gatewaynoobjectPin the application to a specific gateway. See placement.gateway.
regionrequired for cloudstringOnly meaningful with the cloud strategies — not available in beta.
migrationPolicynoenummanual (default), allowed, or automatic. Governs whether the application can be moved between devices. See Migrate between devices.

At least one selector field is required.

FieldTypeNotes
deviceNamestringPin to a device by name.
deviceIdstringPin to a device by ID.
labelsmapMatch devices that carry these labels.
regionstringMatch devices in this region.
capabilitiesstring[]Match devices that report these capabilities (e.g., gpu, kvm).

Optionally pin the application to a specific gateway device in your organization instead of letting the platform assign one from the managed pool. Useful when you run your own gateway.

placement:
strategy: serving-device
deviceSelector: { deviceName: my-first }
gateway: { deviceName: my-gateway }
FieldTypeNotes
deviceIdstringPin to a gateway by device ID.
deviceNamestringPin to a gateway by device name.

At least one of deviceId / deviceName is required when the block is present. When gateway is unset, managed-gateway auto-assignment applies as usual.

A list of one or more workloads. Common fields apply to every type:

workloads:
- name: api
type: compose | docker | managed-process | vm | pre-existing
ports: [ ... ]
storage: [ ... ] # optional
env: { ... } # optional
envFrom: [ ... ] # optional
healthChecks: [ ... ] # optional
resources: { ... } # optional
restartPolicy: always # optional
dependsOn: [ ... ] # optional, names of other workloads
FieldRequiredTypeNotes
nameyesstringUnique within the application.
typeyesenumOne of compose, docker, managed-process, vm, pre-existing.
portsusuallyarrayPorts the workload listens on. Required if access[] references this workload.
storagenoarrayVolumes from spec.storage[] to mount.
envnomapString-to-string environment variables.
envFromnoarrayEnvironment from secret references. See envFrom.
healthChecksnoarraySee Health checks.
resourcesnoobjectCPU/memory requests and limits. See Resources.
restartPolicynoenumalways, on-failure, or never.
dependsOnnostring[]Names of other workloads in this application that must start first.
- name: api
type: compose
composeFile: ./api.compose.yml
projectName: api # optional
FieldRequiredTypeNotes
composeFileone ofstringPath to a Docker Compose file relative to the application YAML, OR base64:<encoded yaml> (the agent decodes it before invoking compose). Exactly one of composeFile / composeArtifact.
composeArtifact.digestone ofstringA sha256:<64 hex> digest naming a compose bundle already delivered to the device’s content store (used by catalog deploys). Exactly one of composeFile / composeArtifact.
projectNamenostringOverride the compose project name. Defaults to the workload name.
build.modenoenumlocal (default) or remote. Where images declared with build: in the compose file are built — see below.
hostAccess.devicesnoenum[]Host device classes the workload may use: video-transcode, gpu. Default: none.
hostAccess.hostNetworknoboolRun the workload on the host network. Default false.

Built images ship with the deploy. Compose services that declare build: in the compose file don’t need a registry. With build.mode: local (the default), the images are built on your machine, exported with docker save, and streamed to each serving device over the deploy’s ingest session — the deploy preflight checks they exist in your local Docker daemon first. With build.mode: remote, the build context directory is packed into a content-addressed tar (excluding .git), shipped the same way, and the device runs docker compose build itself. build.contextArtifact is stamped by the deploy tooling — you set only mode. Pullable services (no build: in the compose file) are pulled by the device as usual.

hostAccess grants elevated host access the agent’s host-access gate enforces at deploy time; omit it entirely for the default of no elevated access.

A single image, no compose file.

- name: redis
type: docker
image: redis:7-alpine
command: ["redis-server"]
args: ["--appendonly", "yes"]
FieldRequiredTypeNotes
imageyesstringA pullable image reference, e.g. redis:7-alpine or ghcr.io/org/app:latest. Resolved to a concrete digest at deploy time — see the note below.
commandnostring[]Override the image’s ENTRYPOINT.
argsnostring[]Override the image’s CMD.

Image tags are resolved to digests at deploy time. Whatever a tag (:latest, :7-alpine, …) points to at the moment you deploy is resolved to a concrete sha256:… digest and pinned into the immutable release that the deploy cuts. The device realizes that exact digest, and the running container is tracked by digest — not by tag. Two consequences: (1) rebuilding and re-pushing the same tag with new bytes mints a new release and reaches the device (no silent-stale), and (2) edgible application rollback reproduces the exact bytes a release was cut with, whereas re-deploying old YAML re-resolves its tags fresh. The same resolution applies to every image named in a compose workload’s composeFile.

A long-running native process supervised by the agent.

- name: worker
type: managed-process
command: "./bin/worker --queue jobs"
workingDir: /opt/app
logFile: /var/log/edgible/worker.log
FieldRequiredTypeNotes
commandyesstringCommand line. The agent splits on whitespace; no shell expansion.
workingDirnostringCWD for the process. Defaults to the agent’s working directory.
logFilenostringWhere stdout/stderr are written.

A virtual machine. The disk can be declared inline (legacy) or via a storage[] mount (recommended; required for migration).

# Recommended: storage-backed disk
- name: legacy
type: vm
vmBackend: qemu
memory: 2048
cpus: 2
storage:
- { name: vm-disk, mountPath: /disk }
# Legacy: inline disk image path on the host
- name: legacy
type: vm
vmBackend: qemu
diskImage: /var/lib/edgible/vms/legacy.qcow2
memory: 2048
cpus: 2
FieldRequiredTypeNotes
vmBackendyesenumqemu, wsl, or firecracker.
memoryyesnumberMiB.
cpusyesnumberVirtual CPUs.
diskImageone ofstringHost path to a disk image. Mutually exclusive with a storage[] disk.
storage[]one ofarrayA storage mount referencing spec.storage[]. The first mount is the disk.
vmArchnoenumx86_64 or aarch64.
sshPortnonumberSSH port to expose to the agent.
sshPublicKeynostringPublic key to inject.
vmExtraArgsnostring[]Extra args passed to the backend.

Tells Edgible to forward to a process or service that’s already running on the device — useful for adopting an existing service without changing how it runs.

- name: legacy-svc
type: pre-existing
hostPort: 8080
hostAddress: 127.0.0.1 # optional, defaults to localhost
FieldRequiredTypeNotes
hostPortnonumberA port already listening on the device. Required unless the workload only declares ports[] for access targeting.
hostAddressnostringAddress the existing service listens on. Defaults to 127.0.0.1.
ports:
- { name: http, containerPort: 8080, protocol: tcp }
- { name: dns, containerPort: 53, protocol: udp }
FieldRequiredTypeNotes
nameyesstringUsed by access entries and health checks to reference this port.
containerPortyesnumberThe port number the workload listens on.
protocolnoenumtcp (default) or udp.

Mounts a volume declared in spec.storage[].

storage:
- { name: pgdata, mountPath: /var/lib/postgresql/data }
- { name: cache, mountPath: /tmp/cache, readOnly: false }
FieldRequiredTypeNotes
nameyesstringMust match a spec.storage[].name.
mountPathyesstringAbsolute path inside the workload.
readOnlynoboolMount read-only.
healthChecks:
- { type: http, port: http, path: /healthz, intervalSeconds: 10 }
- { type: tcp, port: db, intervalSeconds: 30 }
- { type: exec, command: ["pg_isready"], intervalSeconds: 60 }
FieldRequiredTypeNotes
typeyesenumhttp, tcp, or exec.
porthttp/tcpstringPort name from ports[].
pathhttpstringURL path to GET.
commandexecstring[]Command to run inside the workload.
intervalSecondsnonumberHow often to probe.
timeoutSecondsnonumberProbe timeout.
failureThresholdnonumberConsecutive failures before the workload is marked unhealthy.
startupGraceSecondsnonumberGrace window after a (re)deploy during which failing probes don’t degrade the app — covers first-boot installers and model downloads.

When a workload starts but never passes its declared http health check, the deploy fails with HEALTH_CHECK_FAILED — and that failure now carries the probe evidence, not just an opaque status. edgible application get (and the webapp application view) show the last probe under the failing device:

health: GET /healthz:8080 → HTTP 502 after 24 attempts (120s)
body: upstream connect error or disconnect/reset before headers

The health: line is the final attempt — the HTTP status the probe got (or a transport error when nothing answered), the attempt count, and the elapsed time. The body: line is a capped snippet of the response body, captured only from failing responses, so a 500 that returns a stack trace or an error message tells you why it failed. edgible application doctor surfaces the same evidence in its Deployment verdict and points you at edgible application logs --priority debug for the workload’s own output.

resources:
requests:
cpu: "500m"
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
FieldTypeNotes
requests.cpu / limits.cpustringCores or millicores (e.g., 2, 500m).
requests.memory / limits.memorystringSize string (e.g., 512Mi, 2Gi).

Honored by compose, docker, and vm workloads where the backend supports it.

Inject environment variables from a secret stored in the platform.

envFrom:
- name: DATABASE_URL
secretRef: { secret: prod-db, key: url }
- name: API_TOKEN
secretRef: { secret: prod-api } # whole secret as the value
FieldRequiredTypeNotes
nameyesstringEnv var name to set inside the workload.
secretRef.secretyesstringSecret identifier in the platform.
secretRef.keynostringField within the secret.

Application-level storage volumes. Each entry is a logical volume that one or more workloads can mount.

storage:
- name: pgdata
type: persistent
size: 20Gi
mobility: movable
FieldRequiredTypeNotes
nameyesstringReferenced by workloads[].storage[].name.
typeyesenumpersistent or ephemeral.
sizerequired for persistentstringSize like 20Gi, 500Mi.
drivernostringStorage driver hint. Most users leave unset.
mobilitynoenumimmovable (default), movable, replicated, or cloud-only. See below.
backupnoobject{ enabled, scheduleCron, retainCount }. Not yet supported — rejected at apply (see below).
encryptionnoobject{ enabled, keyRef }. Not yet supported — rejected at apply (see below).

Mobility values:

  • immovable — bound to the device it was created on; cannot migrate.
  • movable — the volume can be quiesced, snapshotted, transferred, and restored on another device. Required for Migrate between devices.
  • replicated — the volume is kept consistent on more than one device.
  • cloud-only — only valid with placement.strategy: cloud or automatic, and rejected on serving-device. Accepted by the schema but not available in beta — cloud hosting is out of beta scope (see placement).

Public-facing entry points. Each entry exposes a workload port through Edgible’s edge.

access:
# HTTPS with a generated hostname — the primary (bare <app>.<org>.edgible.com)
- name: public
type: https
primary: true # optional; defaults to the first https entry
target: { workload: web, port: http }
hostname: { generated: true }
tls: { managedBy: edgible }
publish: true
policies:
auth: { modes: [none] }
# HTTPS with a custom domain — additive, rides alongside the generated name
- name: api
type: https
target: { workload: api, port: http }
hostname: { custom: api.example.com } # single FQDN; generated name stays live
tls: { managedBy: edgible }
policies:
auth: { modes: [api-key] }
ipRules:
allow: ["10.0.0.0/8", "192.168.0.0/16"]
rateLimit:
requestsPerMinute: 600
burst: 100
# HTTPS with path routing — different paths to different workloads
- name: site
type: https
hostname: { generated: true }
tls: { managedBy: edgible }
routes:
- path: /api/*
target: { workload: api, port: http }
stripPrefix: false # default: workload receives /api/health unchanged
policies: { auth: { modes: [api-key] } }
- path: /*
target: { workload: web, port: http }
policies: { auth: { modes: [none] } }
# Raw TCP — public port must be in the allowed L4 range 20000-29999
- name: postgres
type: tcp
target: { workload: db, port: pg }
listenPort: 25432
# Raw UDP — same L4 range applies
- name: dns
type: udp
target: { workload: dns, port: dns }
listenPort: 20053
FieldRequiredTypeNotes
nameyesstringUnique within the application. Becomes the hostname prefix for non-primary https accesses.
typeyesenumhttps, tcp, or udp.
primarynoboolhttps only. Marks the access whose generated hostname is the bare <app>.<org>.edgible.com. At most one per app (MULTIPLE_PRIMARY_ACCESS otherwise). Defaults to the first https entry.
target.workloadhttps: one ofstringA name from spec.workloads[]. Shorthand for a single /* route. Give either target or routes, not both (ACCESS_TARGET_OR_ROUTES).
target.portwith targetstringA port name from that workload.
routeshttps: one ofarrayPer-path routing table. See routes.
hostnamehttps onlyobject{ generated: true } and/or { custom: <fqdn> }. The generated hostname always exists; custom is a single FQDN, additive (never replaces the generated name). Two accesses with the same customDUPLICATE_CUSTOM_HOSTNAME.
tlshttps onlyobject{ managedBy: edgible } (default, platform-issued cert), { managedBy: device, certPath, keyPath } (the agent serves your own cert from those on-device paths), or { managedBy: passthrough } (the workload terminates TLS).
listenPorttcp/udp onlynumberPublic port the platform listens on. Must be in 2000029999 — the range the shared gateway opens. Ports below 1024 (including 80/443) and the WireGuard range 5182052819 are rejected; each tcp/udp access needs a distinct listenPort.
publishnoboolDefaults to true. If false, the route is configured but not exposed publicly.
policiesnoobjectAccess-level policies. See Policies. With routes, auth lives per-route instead.

An https access may replace its single target with a routes[] table that sends different paths to different workloads. Exactly one of target/routes per access.

routes:
- path: /api/*
target: { workload: api, port: http }
stripPrefix: false
policies: { auth: { modes: [api-key] } }
- path: /*
target: { workload: web, port: http }
policies: { auth: { modes: [none] } }
FieldRequiredTypeNotes
pathyesstring/x (exact) or /x/* (prefix). Matching is longest-prefix, exact beats wildcard, order-independent. Duplicate paths → DUPLICATE_ROUTE_PATH.
target.workloadyesstringWorkload this route proxies to.
target.portyesstringPort name on that workload.
stripPrefixnoboolDefault false — the path passes through untouched (/api/health → workload sees /api/health). true strips the matched prefix (/api/health/health).
policiesnoobjectPer-route auth (and other policies).

A missing /* route is legal (API-only apps); edgible stack validate warns but does not fail. Requests matching no route return the platform no-route page — HTTP 404 with header X-Edgible-Route: none — distinguishing a platform 404 from a workload 404.

The legacy policies.auth.pathPolicies (path-scoped auth only) is deprecated in favour of routes[] but still accepted; declaring both on one access is rejected with ROUTES_PATHPOLICIES_CONFLICT.

Changing hostnames. An apply that removes a live hostname (renaming/deleting an access, reassigning primary, or removing a custom domain) is refused with 409 HOSTNAME_REMOVAL_BLOCKED. Re-run edgible stack deploy … --allow-hostname-removal to confirm. Additions are never guarded.

Available on every access type, with tcp/udp supporting only ipRules and rateLimit.

FieldTypeNotes
auth.modesstring[]One or more of none, edgible-login, api-key, short-code. OR-combined — a request satisfying any listed mode is served (e.g. [edgible-login, api-key] lets browsers log in while API keys bypass the redirect). none is exclusive and cannot be combined. A fifth mode, app-native (pass-through; the app’s own login is the control), is exclusive and valid only per-route, never at the access root. See Authentication modes.
auth.allowedOrganizationsstring[]When modes includes edgible-login, restrict to specific orgs.
ipRules.allowstring[]Source IP allow-list (IPv4/IPv6 addresses or CIDR blocks). Enforced at the gateway. When set, only listed sources reach the app; every other source is rejected (default-deny).
ipRules.denystring[]Source IP deny-list (IPv4/IPv6 addresses or CIDR blocks). Enforced at the gateway. Listed sources are dropped. Deny wins: a source that matches both lists is denied.
rateLimit.requestsPerMinutenumberPer-source request budget.
rateLimit.burstnumberAllowed burst above the steady rate.
waf.enabledboolEnable web-application firewall.
waf.modeenumenforce or log-only.

You can put several Applications in one file, separated by ---:

apiVersion: v3
kind: Application
metadata:
name: db
organization: <org-id>
spec: { ... }
---
apiVersion: v3
kind: Application
metadata:
name: api
organization: <org-id>
dependsOn: [db]
spec: { ... }

edgible stack deploy -f all.yml deploys both, in dependsOn order. See Stack with dependencies for the full pattern.