Multi-Service Apps with wendy.json
When your project needs more than one container managed through a wendy.json file (rather than a docker-compose.yml), declare a services map in wendy.json. wendy run detects the ma
Multi-Service Apps with wendy.json
When your project needs more than one container managed through a wendy.json file (rather than a docker-compose.yml), declare a services map in wendy.json. wendy run detects the map and automatically orchestrates a parallel multi-service build and deployment.
wendy.json structure
{
"appId": "com.example.myapp",
"platform": "linux",
"services": {
"db": {
"context": "db"
},
"api": {
"context": "api",
"dependsOn": ["db"],
"entitlements": [
{ "type": "network", "mode": "host" }
]
},
"frontend": {
"context": "frontend",
"dependsOn": ["api"]
}
}
}services map
Each key is a service name. Each value is a ServiceConfig object:
| Field | Type | Required | Description |
|---|---|---|---|
context | string | yes | Build context directory, relative to wendy.json. Must be a relative path and must not contain .. components. |
entitlements | array | no | Entitlements to apply to this service's container. Same schema as the top-level entitlements field. |
dependsOn | array of strings | no | Names of other services in this services map that must be created before this one. All referenced names must exist in the same map. |
Validation rules
wendy.json validation rejects the following:
- A service with an empty or missing
context. - A
contextthat is an absolute path. - A
contextthat contains..path components. - A
dependsOnentry that references a service name not present in theservicesmap. - A service
entitlementsentry with an unknown or missing type. - A service
persistentitlement missingnameorpath, or with a non-absolute or..-containingpath. - A service
networkentitlement with amodeother than"host"or"none". - A service
i2centitlement with a device not ini2c-Nformat. - A service
serialentitlement with a device not matching the USB-onlyttyACM0/ttyUSB0(tty*N) pattern. - A service
mcpentitlement with a port outside the range 1–65535. - More than one
mcpentitlement within a single service'sentitlementsarray.
ValidateJSON additionally warns on deprecated entitlement types and unknown entitlement keys within service-level entitlements arrays, using the same rules applied to the top-level entitlements field.
Readiness and lifecycle hooks
Any service in the services map may declare its own readiness probe and hooks.postStart, using the same schema as the top-level readiness/hooks fields:
{
"appId": "com.example.stack",
"services": {
"db": { "context": "db" },
"cache": { "context": "cache" },
"api": { "context": "api", "dependsOn": ["db", "cache"] },
"frontend": {
"context": "frontend",
"dependsOn": ["api"],
"readiness": {
"tcpSocket": { "port": 3000 },
"timeoutSeconds": 30
},
"hooks": {
"postStart": {
"openURL": "http://${WENDY_HOSTNAME}:3000"
}
}
}
}
}Only a service that declares readiness or hooks runs the readiness→postStart sequence; db, cache, and api above are unaffected.
Scoping
readinessgates only the declaring service's ownpostStarthook — it never delays other services' startup order (dependsOnordering is a separate mechanism).hooks.postStart.agent(a command run on the device) is delivered only to the declaring service's own container start call; it never runs in any other service's container.- Hook commands may reference
${WENDY_HOSTNAME}(the device host),${WENDY_APP_ID}, and${WENDY_SERVICE_NAME}— the declaring service's name, empty for single-container apps and for the app-level fallback below. Windows-style%VAR%forms are accepted too.
App-level fallback
A top-level readiness/hooks in wendy.json acts as an app-level fallback: it fires once after every service has started, rather than gating any single service. Both the fallback and a service's own readiness/hooks fire if both are declared. Two exceptions:
- A top-level
hooks.postStart.agentis ignored for multi-service apps — there is no app-level container to run it in.wendy runwarns about this when it loadswendy.json; declare it underservices.<name>.hooksinstead. - The fallback is skipped when
wendy run --serviceselects a subset of services, since "every service has started" can't be guaranteed on a partial run.
Attached vs. detached
In attached mode, each service's readiness→postStart sequence fires asynchronously right after that service's start is acknowledged, so a slow or failing probe never delays starting the next service. Ctrl-C cancels any in-flight readiness wait and kills cli hook child processes. If the run ends on its own — every service's log stream closes — while a hook (per-service or the app-level fallback) is still waiting on readiness, that hook is suppressed rather than fired, so wendy run never opens a browser onto a stack that has already exited. In detached mode, readiness is waited sequentially in dependency order after every service has started; hooks outlive the CLI once it exits, and a readiness failure only prints a warning — it never fails the command.
How wendy run handles multi-service projects
When appCfg.Services is non-empty, wendy run routes to the multi-service pipeline:
- Parallel build — all service images are built and pushed concurrently. By default, up to 4 simultaneous builds run; for large groups (8+ services), builds throttle to 2 concurrent to protect the device registry tunnel. Override with
--max-concurrency. In interactive terminals a per-service spinner displays each service's status (waiting→building…→built (Xs)/failed). In non-interactive terminals plain log lines are printed instead. - Ordered container creation — containers are created one at a time in topological dependency order. A service listed in another service's
dependsOnis created first. - Start and stream — all containers are started and their combined stdout/stderr is multiplexed to the terminal. Each line is prefixed with
[serviceName].
Press Ctrl-C to stop all services. The CLI cancels all streams, issues a StopContainer for each service concurrently, and waits up to 30 seconds before exiting.
Container naming
Each service container ID follows the {appId}_{serviceName} convention (_
is the separator because / is not permitted in containerd container IDs). For
example, with appId: "com.example.myapp" and service "api", the containerd
container ID is com.example.myapp_api. The corresponding snapshot key uses
@ as the separator (wendy-com.example.myapp@api) to remain unambiguous when
either component contains a hyphen. The cgroup path component uses @ as the
separator: system.slice:edge-agent:com.example.myapp@api (the systemd service segment
reflects the WENDY_SYSTEMD_SERVICE_NAME env var, which defaults to edge-agent;
@ is used because it cannot appear in either a valid appId or serviceName,
eliminating any collision risk from the hyphen separator).
Note: Single-container apps (no
serviceNamein the top-levelwendy.json) are unaffected — their container ID remains the bareappId.
Filtering with --service
To build and run only a specific service (and its transitive dependencies):
wendy run --service apiThis resolves api and all services reachable through its dependsOn graph. Services outside this subset are not built or started. Passing an unknown service name returns an error immediately.
Flags
All standard wendy run flags apply. The following are particularly relevant for multi-service projects:
| Flag | Description |
|---|---|
--service <name> | Build and run only the named service and its transitive dependsOn dependencies. |
--deploy | Build and create all containers but do not start them. |
--detach | Start all containers but do not stream logs. |
--keep-going | Deploy services that build successfully instead of aborting the whole group on the first build/push failure. |
--max-concurrency <n> | Max service images to build+push at once. 0 = auto-throttle large groups (default). |
Example layout
my-project/
wendy.json
db/
Dockerfile
api/
Dockerfile
frontend/
Dockerfilewendy run # builds and starts all three services
wendy run --service api # builds db and api only (frontend excluded)Crash-looping services
When a service within a group crashes and the agent's restart policy is
automatically restarting it, that service's individual entry in
wendy device apps list shows a red ↻ crash-looping state (nested under
the group header). The top-level app entry stays Running as long as at least
one service is up; it flips to Crash-looping only when every service is down
and the restart policy is still restarting at least one of them.
wendy device logs --app <appId> surfaces crash output from all service
members of the group, so a crash-looping service's logs are reachable without
naming the individual service.
Limitations
- Log output is multiplexed with a
[serviceName]prefix on each line. Per-service log stream routing is not yet available. - Containers are created via individual
CreateContainercalls in dependency order. A groupedCreateAppGroupRPC for atomic creation is planned as a follow-up. - Headless Mac is not supported.
wendy runrejects multi-servicewendy.jsonprojects when the selected target is Headless Mac, before any build or registry operation. Target a Linux/WendyOS device for multi-service workloads.
template.schema.json
template.schema.json is an optional file inside a Wendy project template that defines multi-phase, conditional configuration questions. Answers are collected interactively during w
wendy.json
wendy.json is the configuration file for a Wendy app. It lives at the root of your project and describes the app's identity, target platform, required capabilities, and runtime beh