WendyOS Docs
AdvancedApps

Multi-Service Apps with Docker Compose

Wendy supports running multi-service applications defined in a standard docker-compose.yml (or compose.yml) file. This is the recommended approach when your app needs more than one

Multi-Service Apps with Docker Compose

Wendy supports running multi-service applications defined in a standard docker-compose.yml (or compose.yml) file. This is the recommended approach when your app needs more than one container, for example an API service alongside a database, or a perception pipeline with several processing stages.

How it works

When you run wendy run in a directory that contains a compose file, Wendy automatically detects it as a compose project. Each service is built, pushed to the device's embedded container registry, and started in dependency order. No wendy.json is required; each service carries its own configuration, and an optional companion wendy.json in the same directory is merged in when present (see Readiness probes and postStart hooks).

Supported compose file names (checked in order, first found wins):

  • docker-compose.yml
  • docker-compose.yaml
  • compose.yml
  • compose.yaml

A compose file takes precedence over a Dockerfile in the same directory. To force the single-container Docker path instead, pass --build-type docker or --dockerfile <path>.

Quickstart

my-project/
  docker-compose.yml
  api/
    Dockerfile
    main.py
  worker/
    Dockerfile
    worker.py
# docker-compose.yml
services:
  api:
    build: ./api
    network_mode: host

  worker:
    build: ./worker
    network_mode: host
    depends_on:
      - api
wendy run

Wendy selects a device, builds each service image, pushes them to the device, and starts them concurrently with interleaved log output. Each service's lines are prefixed with a color-coded, column-aligned service name. Log lines are never interleaved mid-line; each line is written atomically:

[api]     Listening on :8080
[worker]  Connected to api
[worker]  Processing item 1

Service builds run in parallel, while the device-intensive image preparation phase is limited to two services at a time so a high --max-concurrency value does not overload device storage. A later run with unchanged source and runtime configuration skips image building, transfer, and preparation when the service's container and its content are still present on the device; missing or unverifiable content fails closed and performs a normal preparation. Set WENDY_PUSH_SKIP=0 to disable this optimization while diagnosing builds.

Press Ctrl-C to stop all services. Services are stopped in reverse dependency order; the CLI prints Stopping <name>... for each service and then a final Stopped N service(s). summary.

Detached mode hint

When all services start in detached mode (--detach), the CLI prints a hint showing how to stream logs:

Run 'wendy device logs' to stream logs.

Service fields

Wendy honours the following compose fields. Fields not listed here are ignored.

FieldDescription
buildBuild context: a path string or a { context, dockerfile, args } mapping. dockerfile may name a Dockerfile, Containerfile, a dot/hyphen variant of either, or a Stagefile (prod.stagefile.yaml), and must resolve inside the build context. See Build files for how the default is chosen when dockerfile is omitted. Services without build use a pre-built image via image.
imagePre-built image to pull and run on the device (e.g. redis:7-alpine). Public image names are normalised to their fully-qualified form automatically.
commandOverride the container's default command. Accepts a string (shell-split) or a YAML sequence.
environmentEnvironment variables to inject. Parsed from key-value maps or KEY=VALUE lists. Applied in order: image env → compose env → Wendy system vars → framework vars (e.g., ROS2) → OTEL vars. OCI last-wins semantics apply.
portsRecords host:container mappings on a network entitlement. This does not implement bridge DNAT or publish an isolated container to the host browser.
network_mode: hostAdds a host network entitlement.
volumesNamed volumes are created as persist entitlements. Host bind mounts (paths starting with . or /) are silently skipped.
depends_onDependency order: list or condition-map form. Services are created in dependency order; detached starts follow the same order, but the condition-map's own health-check conditions (e.g. condition: service_healthy) are not evaluated; ordering is purely topological. To gate a service's own postStart hook on its readiness instead, see Readiness probes and postStart hooks.
restartRestart policy: no, on-failure, always, unless-stopped. Overridden by CLI flags if specified.
x-wendyWendy-specific per-service extensions: readiness (a readiness probe) and hooks (postStart hooks). Same camelCase schema as wendy.json's top-level readiness/hooks fields. See Readiness probes and postStart hooks below.

The following fields are recognized but intentionally ignored, each with a warning at deploy time: devices, privileged, cap_add, security_opt, ipc, pid, shm_size, healthcheck, profiles, secrets, and extra_hosts. Hardware access goes through Wendy entitlements in a companion wendy.json instead. Anything else (such as networks, deploy, entrypoint, env_file, or long-form volume syntax) is ignored silently.

Build files

A service's dockerfile may name a Stagefile as well as a Dockerfile. Stagefiles are compiled to a Dockerfile before the image build, so nothing else about the service changes:

services:
  api:
    build:
      context: ./api
      dockerfile: prod.stagefile.yaml

When dockerfile is omitted, Wendy resolves the default from the build context, in order:

  1. build.stagefile.yaml — the canonical Stagefile.
  2. Otherwise the first <name>.stagefile.yaml in the context, alphabetically. A context that holds only variants has no conventional default, and falling through to Dockerfile would silently ignore the Stagefiles plainly sitting there. Name one explicitly with dockerfile: to pick a different variant.
  3. Otherwise Dockerfile.

Two services may share one build context and point at different build files. Each Stagefile compiles to its own artifact (Dockerfile.generated for the canonical one, Dockerfile.generated.<variant> otherwise), so concurrent service builds in a shared context never overwrite each other's compiled Dockerfile. See Stagefile naming for the filename rules.

docker compose build itself knows nothing about Stagefiles, so wendy build compiles them first and passes a generated override file pointing the affected services at their compiled Dockerfile. Running plain docker compose build on the same project — without Wendy — will fail on a dockerfile: that names a Stagefile.

Networking

Set network_mode: host for services that must share host ports, use multicast, pass CLI readiness probes, or be reached from the developer's browser.

Omitting network_mode while declaring ports currently creates a legacy mode-less network entitlement, which maps to host networking with a deprecation warning; the mappings do not create an isolated bridge or DNAT rules. To request isolated outbound networking, declare { "type": "network", "mode": "bridge" } in a companion wendy.json, without relying on Compose ports: for inbound access. Bridge mode supplies a private namespace, DNS, and outbound NAT only. network.ports mappings on mesh entitlements are for mesh-peer traffic, not host/LAN browser ingress.

Volumes

Named volumes declared in volumes: become persistent storage on the device and survive container restarts and re-deployments. Two services sharing a volume name share the same storage.

services:
  producer:
    build: ./producer
    volumes:
      - shared-data:/data/out

  consumer:
    build: ./consumer
    volumes:
      - shared-data:/data/in

# Declare named volumes at the top level so the file stays valid for
# plain `docker compose` too. Wendy itself only reads the `services:`
# block; the service-level references above are what create the storage.
volumes:
  shared-data:

Host bind mounts (e.g. ./local-path:/container/path) are not supported on device; they are skipped.

Readiness probes and postStart hooks

A compose service can declare its own readiness probe and postStart hooks under x-wendy, using the same camelCase schema as wendy.json's readiness/hooks fields:

services:
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    x-wendy:
      readiness:
        tcpSocket:
          port: 3000
        timeoutSeconds: 30
      hooks:
        postStart:
          openURL: "http://${WENDY_HOSTNAME}:3000"

readiness here gates only frontend's own postStart hook; it never delays any other service's startup (depends_on ordering is unaffected). hooks.postStart.agent runs directly on the device host and is triggered only by the declaring service's own container start.

A companion wendy.json in the same directory can also declare services.<name>.readiness / services.<name>.hooks for a service. When both are present, the companion wins wholesale per field: its readiness and hooks each replace the x-wendy value entirely rather than merging with it.

A companion wendy.json's top-level readiness/hooks or http entitlement act instead as an app-level fallback: they fire once after every service in the project has started, rather than gating any single service. Inherited top-level HTTP remains in every service's container create configuration, but it is not executed once per service. A companion services.<name>.entitlements HTTP declaration is service-scoped; if both scopes declare HTTP, both actions run at their respective scopes. Examples/WendyMC uses a top-level { "type": "http", "port": 8080 } entitlement with readiness.timeoutSeconds: 180, so the CLI preserves that timeout and opens the UI once after both services start. A top-level hooks.postStart.agent is the one exception: a compose app has no single app-level container start to trigger it, so it is ignored (with a warning); declare an agent-side hook under a service's x-wendy.hooks or the companion's services.<name>.hooks instead.

In attached mode, each service's readiness→postStart sequence runs 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. In detached mode none of this runs: no readiness wait, no App reachable line, and no host-side postStart action — only the agent-side postStart.agent hooks run, on the device. With wendy run --watch, each container's openURL and cli actions run once per session after its first successful readiness check; later saves do not repeat them, while a failed or canceled attempt may retry after a later deploy. --watch --detach skips these actions. A non-cancellation timeout in attached mode warns without failing the command: explicitly configured multi-service hooks still run, while App reachable and any synthesized HTTP browser open are suppressed. Cancellation suppresses all three. An explicit readiness TCP port remains the probe target; presentation prefers a hostname-templated openURL, then the HTTP entitlement port, then the readiness port. See Watch mode for details.

Hook commands may reference ${WENDY_HOSTNAME} (the device host), ${WENDY_APP_ID}, and ${WENDY_SERVICE_NAME} (the declaring service's name; empty for the app-level fallback). Windows-style %VAR% forms are accepted too.

For a compose file with more than one service, WENDY_APP_ID is the project directory name and WENDY_SERVICE_NAME is the service name. A single-service compose project without a companion wendy.json uses <project>-<service> as WENDY_APP_ID (for example, myproj-web) and leaves WENDY_SERVICE_NAME empty. When a companion file is present, its appId is used and grouped naming applies even when there is only one service.

Readiness probes and browser URLs dial the device host, not the container directly, so browser-reachable HTTP currently requires network_mode: host. Bridge mode provides outbound NAT only, and mesh port mappings serve mesh peers rather than the developer's browser.

Flags

All wendy run flags work with compose projects:

FlagDescription
--deployBuild and create all containers but do not start them.
--detachStart all containers but do not stream logs. See Readiness probes and postStart hooks.
--restart-unless-stoppedSet restart policy to unless-stopped for all services (overrides per-service setting).
--restart-on-failureSet restart policy to on-failure for all services.
--no-restartDisable restart for all services.
--debugEnable debug logging during build and run.
--yes / -yAccept all device-selection prompts automatically.
--watchRedeploy on changes and stream app logs between deploys. Requires a Wendy agent target when attached; use --watch --detach for provider targets.

Limitations

  • Headless Mac is not supported. wendy run rejects compose projects before any registry or Docker setup when the selected target is Headless Mac. Target a Linux/WendyOS device to use compose.
  • Wendy-specific hardware access entitlements such as gpu, display, camera, audio, bluetooth, usb, i2c, gpio, spi, input, and serial are not inferred from compose fields.
  • Host networking does not imply shared IPC or shared /dev/shm; ROS 2 shared-memory transport requires an app shape that can explicitly share namespaces.
  • Linux containers on macOS require a target WendyOS device; local Docker Desktop compose is used as a fallback when no device is targeted.
  • Compose extends, profiles, and secrets are not supported.
  • wendy fleet run does not support compose projects yet; deploy to one device at a time.
  • wendy run --service applies only to wendy.json services maps; the compose path always deploys every service.
  • wendy build for a compose project shells out to docker compose build. The Apple Container builder is not supported for compose; use --builder docker.

On this page