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.
Top level
Section titled “Top level”apiVersion: v3kind: Applicationmetadata: { ... }spec: { ... }| Field | Required | Type | Notes |
|---|---|---|---|
apiVersion | yes | v3 | The only supported value today. |
kind | yes | Application | The only supported value today. |
metadata | yes | object | See Metadata. |
spec | yes | object | See Spec. |
Metadata
Section titled “Metadata”metadata: name: my-app organization: <org-id> labels: env: production annotations: owner: platform-team dependsOn: [other-app-name]| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Unique within the organization. Lowercase alphanumeric with hyphens, 1–63 chars. |
organization | yes | string | Your organization ID. Find with edgible config list. |
labels | no | map | Free-form key/value strings. Used by placement.deviceSelector.labels. |
annotations | no | map | Free-form key/value strings. Not interpreted by the platform. |
dependsOn | no | string[] | 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 automaticspec.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.
spec.placement
Section titled “spec.placement”Where the application runs — a specific device you own, chosen by selector.
# Run on a specific device you ownplacement: strategy: serving-device deviceSelector: deviceName: my-first migrationPolicy: manual| Field | Required | Type | Notes |
|---|---|---|---|
strategy | yes | enum | serving-device. The schema also accepts cloud and automatic, but those are not available in beta — see the note below. |
deviceSelector | required for serving-device | object | See below. |
gateway | no | object | Pin the application to a specific gateway. See placement.gateway. |
region | required for cloud | string | Only meaningful with the cloud strategies — not available in beta. |
migrationPolicy | no | enum | manual (default), allowed, or automatic. Governs whether the application can be moved between devices. See Migrate between devices. |
placement.deviceSelector
Section titled “placement.deviceSelector”At least one selector field is required.
| Field | Type | Notes |
|---|---|---|
deviceName | string | Pin to a device by name. |
deviceId | string | Pin to a device by ID. |
labels | map | Match devices that carry these labels. |
region | string | Match devices in this region. |
capabilities | string[] | Match devices that report these capabilities (e.g., gpu, kvm). |
placement.gateway
Section titled “placement.gateway”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 }| Field | Type | Notes |
|---|---|---|
deviceId | string | Pin to a gateway by device ID. |
deviceName | string | Pin 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.
spec.workloads[]
Section titled “spec.workloads[]”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| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Unique within the application. |
type | yes | enum | One of compose, docker, managed-process, vm, pre-existing. |
ports | usually | array | Ports the workload listens on. Required if access[] references this workload. |
storage | no | array | Volumes from spec.storage[] to mount. |
env | no | map | String-to-string environment variables. |
envFrom | no | array | Environment from secret references. See envFrom. |
healthChecks | no | array | See Health checks. |
resources | no | object | CPU/memory requests and limits. See Resources. |
restartPolicy | no | enum | always, on-failure, or never. |
dependsOn | no | string[] | Names of other workloads in this application that must start first. |
type: compose
Section titled “type: compose”- name: api type: compose composeFile: ./api.compose.yml projectName: api # optional| Field | Required | Type | Notes |
|---|---|---|---|
composeFile | one of | string | Path 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.digest | one of | string | A 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. |
projectName | no | string | Override the compose project name. Defaults to the workload name. |
build.mode | no | enum | local (default) or remote. Where images declared with build: in the compose file are built — see below. |
hostAccess.devices | no | enum[] | Host device classes the workload may use: video-transcode, gpu. Default: none. |
hostAccess.hostNetwork | no | bool | Run 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.
type: docker
Section titled “type: docker”A single image, no compose file.
- name: redis type: docker image: redis:7-alpine command: ["redis-server"] args: ["--appendonly", "yes"]| Field | Required | Type | Notes |
|---|---|---|---|
image | yes | string | A 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. |
command | no | string[] | Override the image’s ENTRYPOINT. |
args | no | string[] | 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 concretesha256:…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 rollbackreproduces 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 acomposeworkload’scomposeFile.
type: managed-process
Section titled “type: managed-process”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| Field | Required | Type | Notes |
|---|---|---|---|
command | yes | string | Command line. The agent splits on whitespace; no shell expansion. |
workingDir | no | string | CWD for the process. Defaults to the agent’s working directory. |
logFile | no | string | Where stdout/stderr are written. |
type: vm
Section titled “type: vm”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| Field | Required | Type | Notes |
|---|---|---|---|
vmBackend | yes | enum | qemu, wsl, or firecracker. |
memory | yes | number | MiB. |
cpus | yes | number | Virtual CPUs. |
diskImage | one of | string | Host path to a disk image. Mutually exclusive with a storage[] disk. |
storage[] | one of | array | A storage mount referencing spec.storage[]. The first mount is the disk. |
vmArch | no | enum | x86_64 or aarch64. |
sshPort | no | number | SSH port to expose to the agent. |
sshPublicKey | no | string | Public key to inject. |
vmExtraArgs | no | string[] | Extra args passed to the backend. |
type: pre-existing
Section titled “type: pre-existing”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| Field | Required | Type | Notes |
|---|---|---|---|
hostPort | no | number | A port already listening on the device. Required unless the workload only declares ports[] for access targeting. |
hostAddress | no | string | Address the existing service listens on. Defaults to 127.0.0.1. |
spec.workloads[].ports[]
Section titled “spec.workloads[].ports[]”ports: - { name: http, containerPort: 8080, protocol: tcp } - { name: dns, containerPort: 53, protocol: udp }| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Used by access entries and health checks to reference this port. |
containerPort | yes | number | The port number the workload listens on. |
protocol | no | enum | tcp (default) or udp. |
spec.workloads[].storage[]
Section titled “spec.workloads[].storage[]”Mounts a volume declared in spec.storage[].
storage: - { name: pgdata, mountPath: /var/lib/postgresql/data } - { name: cache, mountPath: /tmp/cache, readOnly: false }| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Must match a spec.storage[].name. |
mountPath | yes | string | Absolute path inside the workload. |
readOnly | no | bool | Mount read-only. |
Health checks
Section titled “Health checks”healthChecks: - { type: http, port: http, path: /healthz, intervalSeconds: 10 } - { type: tcp, port: db, intervalSeconds: 30 } - { type: exec, command: ["pg_isready"], intervalSeconds: 60 }| Field | Required | Type | Notes |
|---|---|---|---|
type | yes | enum | http, tcp, or exec. |
port | http/tcp | string | Port name from ports[]. |
path | http | string | URL path to GET. |
command | exec | string[] | Command to run inside the workload. |
intervalSeconds | no | number | How often to probe. |
timeoutSeconds | no | number | Probe timeout. |
failureThreshold | no | number | Consecutive failures before the workload is marked unhealthy. |
startupGraceSeconds | no | number | Grace window after a (re)deploy during which failing probes don’t degrade the app — covers first-boot installers and model downloads. |
When a health check fails a deploy
Section titled “When a health check fails a deploy”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 headersThe 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
Section titled “Resources”resources: requests: cpu: "500m" memory: 512Mi limits: cpu: "2" memory: 2Gi| Field | Type | Notes |
|---|---|---|
requests.cpu / limits.cpu | string | Cores or millicores (e.g., 2, 500m). |
requests.memory / limits.memory | string | Size string (e.g., 512Mi, 2Gi). |
Honored by compose, docker, and vm workloads where the backend supports it.
envFrom
Section titled “envFrom”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| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Env var name to set inside the workload. |
secretRef.secret | yes | string | Secret identifier in the platform. |
secretRef.key | no | string | Field within the secret. |
spec.storage[]
Section titled “spec.storage[]”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| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Referenced by workloads[].storage[].name. |
type | yes | enum | persistent or ephemeral. |
size | required for persistent | string | Size like 20Gi, 500Mi. |
driver | no | string | Storage driver hint. Most users leave unset. |
mobility | no | enum | immovable (default), movable, replicated, or cloud-only. See below. |
backup | no | object | { enabled, scheduleCron, retainCount }. Not yet supported — rejected at apply (see below). |
encryption | no | object | { 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 withplacement.strategy: cloudorautomatic, and rejected onserving-device. Accepted by the schema but not available in beta — cloud hosting is out of beta scope (see placement).
spec.access[]
Section titled “spec.access[]”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| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Unique within the application. Becomes the hostname prefix for non-primary https accesses. |
type | yes | enum | https, tcp, or udp. |
primary | no | bool | https 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.workload | https: one of | string | A name from spec.workloads[]. Shorthand for a single /* route. Give either target or routes, not both (ACCESS_TARGET_OR_ROUTES). |
target.port | with target | string | A port name from that workload. |
routes | https: one of | array | Per-path routing table. See routes. |
hostname | https only | object | { 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 custom → DUPLICATE_CUSTOM_HOSTNAME. |
tls | https only | object | { 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). |
listenPort | tcp/udp only | number | Public port the platform listens on. Must be in 20000–29999 — the range the shared gateway opens. Ports below 1024 (including 80/443) and the WireGuard range 51820–52819 are rejected; each tcp/udp access needs a distinct listenPort. |
publish | no | bool | Defaults to true. If false, the route is configured but not exposed publicly. |
policies | no | object | Access-level policies. See Policies. With routes, auth lives per-route instead. |
routes
Section titled “routes”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] } }| Field | Required | Type | Notes |
|---|---|---|---|
path | yes | string | /x (exact) or /x/* (prefix). Matching is longest-prefix, exact beats wildcard, order-independent. Duplicate paths → DUPLICATE_ROUTE_PATH. |
target.workload | yes | string | Workload this route proxies to. |
target.port | yes | string | Port name on that workload. |
stripPrefix | no | bool | Default false — the path passes through untouched (/api/health → workload sees /api/health). true strips the matched prefix (/api/health → /health). |
policies | no | object | Per-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.
Policies
Section titled “Policies”Available on every access type, with tcp/udp supporting only ipRules and rateLimit.
| Field | Type | Notes |
|---|---|---|
auth.modes | string[] | 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.allowedOrganizations | string[] | When modes includes edgible-login, restrict to specific orgs. |
ipRules.allow | string[] | 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.deny | string[] | 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.requestsPerMinute | number | Per-source request budget. |
rateLimit.burst | number | Allowed burst above the steady rate. |
waf.enabled | bool | Enable web-application firewall. |
waf.mode | enum | enforce or log-only. |
Multi-document files
Section titled “Multi-document files”You can put several Applications in one file, separated by ---:
apiVersion: v3kind: Applicationmetadata: name: db organization: <org-id>spec: { ... }---apiVersion: v3kind: Applicationmetadata: 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.