WendyOS Docs
Guides & Tutorials

Multi-App Deployments

Run cooperating multi-container stacks on WendyOS from a single wendy.json — with one Dockerfile per service or a companion docker-compose.yml

Run a multi-container stack as one deployment

Some projects are naturally split into cooperating containers. This is common for ROS 2 systems: one service publishes sensor data, another runs planning or inference, and a third exposes a dashboard or bridge.

The recommended way to ship a stack like this is a single wendy.json at the project root. It is your one source of truth for the deployment: which services exist, how they build, their start order, and their device entitlements. wendy run (no extra flags) detects the services map and orchestrates the whole stack.

You can shape that wendy.json two ways:

  • One Dockerfile per service — declare a services map directly in wendy.json, with each service pointing at its own build context. This is the default and gives you full access to Wendy-specific configuration (entitlements, isolation, frameworks).
  • A companion to docker-compose.yml — if you already have, or prefer, a docker-compose.yml, keep it and add a small companion wendy.json that layers Wendy-specific settings (such as GPU or network entitlements) on top of the services Compose already defines.

Both shapes deploy with the same command:

wendy run

Wendy auto-detects the project shape, so you do not need a build-type flag. A wendy.json with a services map takes the multi-service path; a docker-compose.yml is detected as a Compose project (and a companion wendy.json is merged in when present).

Prerequisites

  • Wendy CLI installed on your development machine
  • A WendyOS device reachable over USB, LAN, or Wendy Cloud
  • Docker installed locally so Wendy can build service images
  • A project directory that contains docker-compose.yml, docker-compose.yaml, compose.yml, or compose.yaml

How Wendy runs compose projects

When you run wendy run in a directory with a compose file, Wendy auto-detects the compose project and uses the compose path before the single-Dockerfile path.

For a WendyOS device target, Wendy:

  1. Reads the compose file from the project root.
  2. Builds each service that has a build: directive for the target device platform.
  3. Pushes built images to the device's embedded registry.
  4. Uses declared image: references directly for services without build:.
  5. Generates one device app per service, named <project-folder>-<service>.
  6. Creates service containers in depends_on order.
  7. Starts all services and streams logs with a [service] prefix.

For ROS 2, set network_mode: host on every service that needs DDS discovery, multicast, or direct host-network communication. Wendy converts that field into a host-network entitlement for the generated service app.

Multi-service projects target Linux/WendyOS devices. Headless Mac does not run multi-container stacks — wendy run rejects them before any build when the selected target is a Mac.

Keep one folder per service, each with its own Dockerfile, and describe the stack in wendy.json:

ros2-stack/
  wendy.json
  talker/
    Dockerfile
  listener/
    Dockerfile

Each service can use the same Dockerfile shape. For a ROS 2 demo graph:

FROM ros:humble
RUN apt-get update && apt-get install -y --no-install-recommends \
      ros-humble-demo-nodes-cpp ros-humble-rmw-cyclonedds-cpp \
 && rm -rf /var/lib/apt/lists/*
CMD ["bash","-lc","source /opt/ros/humble/setup.bash && exec ros2 run demo_nodes_cpp talker"]

The wendy.json declares both services, the start order, and — for ROS 2 — the framework and isolation settings the stack needs:

{
  "appId": "sh.wendy.examples.ros2",
  "platform": "linux",
  "version": "1.0.0",
  "isolation": "shared-ipc",
  "frameworks": {
    "ros2": { "domainId": 42, "rmw": "rmw_cyclonedds_cpp", "distro": "humble" }
  },
  "services": {
    "talker": { "context": "./talker" },
    "listener": {
      "context": "./listener",
      "dependsOn": ["talker"]
    }
  }
}

What this gives you:

  • frameworks.ros2 auto-injects ROS_DOMAIN_ID and RMW_IMPLEMENTATION into every service container, so you do not hand-set ROS environment variables per service.
  • isolation: "shared-ipc" shares the network and IPC namespaces plus /dev/shm across all services, so CycloneDDS can use zero-copy intra-host transport — something host networking alone cannot provide.
  • dependsOn starts services in dependency order.

Run the stack from the project root:

cd ros2-stack
wendy run
The Wendy CLI building and deploying a multi-service stack to a WendyOS device

Attached mode streams logs from all services, each line prefixed with the service name:

[talker]    Publishing: 'Hello World: 1'
[listener]  I heard: [Hello World: 1]

Press Ctrl-C to stop the stack. Wendy stops services in reverse dependency order.

Build and run only one service and its dependencies with wendy run --service listener. Wendy resolves the named service through its dependsOn graph and skips everything else.

For the full services schema — including per-service entitlements, frameworks, and validation rules — see Multi-Service Apps with wendy.json.

Alternative: a companion wendy.json over docker-compose.yml

If you already maintain a docker-compose.yml, keep it as the build plan and add a small wendy.json next to it. Compose defines the services, builds, ports, and dependency order; the companion wendy.json adds the Wendy-specific configuration that Compose does not express — such as GPU access or explicit network entitlements.

my-project/
  docker-compose.yml
  wendy.json
  api/
    Dockerfile
  client/
    Dockerfile
# docker-compose.yml
services:
  api:
    build: ./api
    ports:
      - "8080:8080"

  client:
    build: ./client
    depends_on:
      - api

The companion wendy.json references the same service names but omits build context (that comes from Compose). It only layers on Wendy settings — here, a GPU entitlement for api:

{
  "appId": "sh.wendy.examples.hellocompose",
  "platform": "linux",
  "services": {
    "api": {
      "entitlements": [
        { "type": "gpu" }
      ]
    },
    "client": {}
  }
}

Deploy it the same way — Wendy detects the Compose project and merges the companion:

wendy run

Wendy maps a focused subset of Compose into device runtime configuration:

Compose fieldWendy behavior
buildBuilds a service image. Supports build: ./path and { context, dockerfile, args }.
imageUses a prebuilt image. Short public image names are normalized before the device pulls them.
commandOverrides the container command. Use YAML sequence form for shell scripts or ROS launch commands.
environmentInjected into the container. Applied with OCI last-wins semantics.
portsAdds a network entitlement with explicit port mappings.
network_mode: hostAdds a host-network entitlement. Useful for ROS 2 service discovery.
volumesConverts named volumes into persistent storage entitlements. Host bind mounts are skipped on device.
depends_onCreates dependencies before dependents. Detached starts follow the same order, but Wendy does not wait for health checks or readiness conditions. Both list and map forms are accepted.
restartApplies no, on-failure, always, or unless-stopped unless a CLI restart flag overrides it.

Host networking helps ROS 2 discovery, but it does not join IPC namespaces or share /dev/shm between containers. If your stack depends on ROS 2 shared-memory transport or zero-copy camera frames, prefer the one-wendy.json shape above with "isolation": "shared-ipc", which shares the network and IPC namespaces plus /dev/shm across all services.

For the full Compose field reference, see Multi-Service Apps with Docker Compose.

Add persistent data

Use named volumes for data that should survive redeployments — ROS bags, calibration files, generated maps. In a Compose-based project, declare them in docker-compose.yml:

services:
  recorder:
    build: ./recorder
    network_mode: host
    volumes:
      - ros-bags:/bags
    command:
      - bash
      - -lc
      - |
        source /opt/ros/humble/setup.bash
        ros2 bag record -o /bags/session /camera/image_raw

volumes:
  ros-bags:

In a services-map wendy.json, attach a persist entitlement to the service instead:

{
  "services": {
    "recorder": {
      "context": "./recorder",
      "entitlements": [
        { "type": "persist", "name": "ros-bags", "path": "/bags" }
      ]
    }
  }
}

Avoid host bind mounts such as ./bags:/bags for device deployments. Wendy skips bind mounts because local development-machine paths do not exist on the device.

Run in the background

For long-running robot stacks, start the services without attaching logs:

wendy run --detach

Generated app names use the project folder and service name. For the ROS 2 example above, the apps are ros2-stack-talker and ros2-stack-listener.

Useful follow-up commands:

wendy device apps list
wendy device logs --app ros2-stack-talker
wendy device apps stop ros2-stack-listener
wendy device apps stop ros2-stack-talker
The Wendy CLI listing the deployed stack's apps with their version, state, and failure count The Wendy CLI streaming logs for a single service of the deployed stack

Choosing between the two shapes

Both shapes start from a single wendy.json. Pick by what your services need:

  • One Dockerfile per service (services map). The default. Use it when services need Wendy-specific configuration that Compose does not express — direct camera, GPU, audio, Bluetooth, USB, I2C, GPIO, SPI, shared IPC, runtime presets, framework injection, or service-specific files. Required for ROS 2 shared-memory transport via "isolation": "shared-ipc".
  • Companion to docker-compose.yml. Use it when you already have a Compose file or want to keep using Compose tooling, and only need to layer a few Wendy entitlements (GPU, network) on top.

Service-specific readiness probes and postStart hooks are not available for multi-service stacks yet. Today, hooks.postStart applies to single-container wendy.json apps, so a stack cannot trigger a browser opener only after one frontend service becomes ready.

On this page