Claude Cursor Skill

integration

Wire robotics modules with ROS 2 interfaces, Docker Compose, or non-ROS transports.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download robium-ai-robium-skills_integration-498ea4e.zip · 13 KB
Part of robium-ai/robium — 44 skills

Install

skills CLI npx skills add https://github.com/robium-ai/robium/tree/main/skills/integration
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install robium-ai-robium@llmmart
Git git clone https://github.com/robium-ai/robium.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole robium-ai/robium collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Integration

Every module boundary needs an explicit contract: why the boundary exists, what crosses it, how peers discover each other, and how failure is observed.

Draw boundaries for a reason

  • Split components with different rates when sharing an executor or process can starve the faster loop.
  • Split failure domains when one component must restart, scale, or exhaust memory without taking down another.
  • Keep tightly coupled code together when neither rate nor failure isolation justifies serialization and deployment overhead.
  • Make each container a supervisable unit. Multiple processes may belong together, but their shared lifecycle should be intentional.

Choose the boundary contract

  • Inside one ROS 2 system, default to ROS topics, services, and actions.
  • Crossing hosts or containers does not by itself require a new application protocol; configure ROS discovery explicitly first.
  • Use gRPC or REST at a genuine non-ROS system or organizational boundary.
  • Use shared memory or zero-copy paths only after measurement shows copying is the bottleneck.
  • Treat names, schemas, QoS, time, backpressure, health, restart behavior, and ownership as part of the interface.

Read communication selection when choosing between ROS 2, DDS discovery options, Zenoh, gRPC, REST, or shared memory.

Package the running system

  • Use separate build and runtime stages for shipped modules; leave compilers and build-only dependencies behind.
  • Give each service a health signal that proves readiness, not merely a live process.
  • Make network mode, ROS_DOMAIN_ID, discovery, volumes, devices, and shutdown behavior visible in the composition.
  • A successful docker compose up proves process creation, not communication. Verify real cross-boundary messages.

Read the Dockerfile guide for build/runtime structure and signal handling. Read compose patterns for DDS discovery, health, domains, and networking. Use the examples only as starting shapes and verify them in the target environment.

Use environments for one module's reproducibility and GPU/runtime contract. Use ros2 when the failed interface is within a healthy ROS graph. Use foxglove only when the boundary is specifically remote visualization.

Done

  • Each split has a rate, failure, deployment, or ownership reason.
  • Every boundary has an explicit protocol and interface contract.
  • Peers discover each other in the real host/container topology.
  • Health and shutdown behavior are observable and scoped to the failed unit.
  • A real message or request proves each changed boundary end to end.
Files (robium)
  • examples
    • docker-compose.ros2-app.yml 3 KB
      # status: unverified
      # source: https://docs.docker.com/compose/how-tos/networking/ (compose networking),
      #         https://fast-dds.docs.eprosima.com/en/latest/fastdds/ros2/discovery_server/ros2_discovery_server.html
      #         (alternative to host networking, for cross-host setups; see
      #         references/compose-patterns.md), https://hub.docker.com/_/ros (image tags).
      #
      # Minimal two-service ROS 2 app: `sim` stands in for a simulator publishing
      # data, `app` stands in for the module that consumes it, the same shape as
      # a real sim+controller or sim+perception pair. Both run on the *same*
      # ROS 2 graph, so per the boundary guidance in SKILL.md this uses native
      # ROS 2 topics, not a non-ROS transport.
      #
      # DDS discovery is configured EXPLICITLY, not assumed:
      #   - `network_mode: host` puts both containers on the host's real network
      #     namespace, so default multicast-based DDS discovery works unmodified
      #     (see references/compose-patterns.md, option 1). Linux-only; see
      #     references/compose-patterns.md for the macOS/Windows Docker Desktop caveat;
      #     for cross-host or non-Linux-host setups use option 2 or 3 in that
      #     reference instead (Fast DDS Discovery Server / static CycloneDDS
      #     peers) rather than switching to host networking blindly.
      #   - `ROS_DOMAIN_ID` is set explicitly and identically on both services so
      #     they're isolated from any other ROS 2 system on the same host network
      #     and from each other's default (0) collisions.
      #
      # Re-verify the `ros:jazzy-ros-base-noble` tag against hub.docker.com/_/ros
      # and swap `jazzy` for the current default distro (see architect's Platform
      # gotchas) before using this in a real project. `demo_nodes_cpp` is used as
      # a stand-in publisher/subscriber pair; it is not included in the `-base`
      # image variant by default, hence the apt-get install shown; a real module
      # should bake its dependencies into the image at build time instead (see
      # references/dockerfile-guide.md, examples/Dockerfile.multistage-ros2);
      # installing at container start, as done here for a self-contained minimal
      # example, is not the production pattern.
      
      services:
        sim:
          image: ros:jazzy-ros-base-noble
          network_mode: host
          environment:
            - ROS_DOMAIN_ID=42
          command: >
            bash -c "apt-get update &&
                     apt-get install -y --no-install-recommends ros-jazzy-demo-nodes-cpp &&
                     source /opt/ros/jazzy/setup.bash &&
                     ros2 run demo_nodes_cpp talker"
          healthcheck:
            test: ["CMD", "bash", "-c", "source /opt/ros/jazzy/setup.bash && ros2 topic list | grep -q /chatter"]
            interval: 5s
            timeout: 3s
            retries: 20
      
        app:
          image: ros:jazzy-ros-base-noble
          network_mode: host
          environment:
            - ROS_DOMAIN_ID=42
          depends_on:
            sim:
              condition: service_healthy
          command: >
            bash -c "apt-get update &&
                     apt-get install -y --no-install-recommends ros-jazzy-demo-nodes-cpp &&
                     source /opt/ros/jazzy/setup.bash &&
                     ros2 run demo_nodes_cpp listener"
      
    • Dockerfile.multistage-ros2 3.2 KB · in bundle
  • references
    • comms-selection.md 6 KB
      # Comms selection
      
      The detailed decision behind the boundary choices introduced in the
      [entrypoint](../SKILL.md):
      what each transport is, when it's the right default, and what was verified
      about current status (vs. carried from memory) as of this writing (2026-07).
      
      ## ROS 2 native: topics, services, actions
      
      Inside one ROS 2 system, these three are the default; don't reach for
      anything else without a reason.
      
      - **Topics**: one-to-many, continuous streams, no response expected
        (sensor data, robot state, odometry). Publisher/subscriber, decoupled in
        time and space.
      - **Services**: one request, one response, synchronous-feeling, no
        progress feedback. Right for short, bounded operations ("get current
        pose", "trigger a reset").
      - **Actions**: request with progress feedback, a result, and cancellation,
        for anything that takes a meaningful amount of time (navigate to a pose,
        run a manipulation trajectory). Built on topics + services under the
        hood; use it instead of a service when the caller needs to track progress
        or cancel.
      
      Source: [ROS 2 Topics vs Services vs
      Actions](https://docs.ros.org/en/rolling/How-To-Guides/Topics-Services-Actions.html);
      a direct fetch of this docs.ros.org page was blocked by an anti-bot
      challenge, so this rests on WebSearch snippet synthesis (consistent across
      Rolling/Jazzy/Humble in those snippets) rather than a full-text read;
      re-verify directly before relying on distro-specific details.
      
      All three ride on DDS by default (the RMW layer is swappable; see below).
      Inside one host or one un-firewalled network, default DDS discovery (Simple
      Discovery Protocol, multicast-based) works with zero config. Crossing a
      container or host boundary is where discovery needs explicit setup; that's
      a docker-compose concern, covered in `compose-patterns.md`, not a reason to
      switch transports.
      
      ## rmw_zenoh: an alternative RMW, opt-in, not the default
      
      **Status checked 2026-07.** The [ros2/rmw_zenoh
      README](https://github.com/ros2/rmw_zenoh) was fetched and verified
      directly. The [docs.ros.org Zenoh
      page](https://docs.ros.org/en/rolling/Installation/RMW-Implementations/Non-DDS-Implementations/Working-with-Zenoh.html)
      could not be fetched directly (blocked by an anti-bot challenge) and is
      corroborated only via WebSearch snippet synthesis; re-verify it directly
      before relying on distro-support specifics:
      
      - rmw_zenoh has been available since **ROS 2 Jazzy Jalisco** (it does not
        support Humble or earlier). It is **opt-in, not the default RMW**: you
        must explicitly set `RMW_IMPLEMENTATION=rmw_zenoh_cpp` (default RMWs
        remain the DDS-based implementations, e.g. Fast DDS / CycloneDDS,
        distro-dependent).
      - Install: `sudo apt install ros-<DISTRO>-rmw-zenoh-cpp`.
      - It requires a running **Zenoh router**
        (`ros2 run rmw_zenoh_cpp rmw_zenohd`) reachable by all participants;
        discovery is gossip-based through that router rather than multicast. This
        is *why* it's attractive for containerized/cross-host setups (no
        multicast dependency), but it is an extra process to run and wire in,
        not a drop-in replacement with no setup cost.
      - It claims shared-memory (SHM) optimization for messages passing through
        it, transparently interoperating with non-SHM/remote nodes.
      - Do not claim rmw_zenoh is "the new default" in a real project without
        re-checking the current docs; this status has moved before (zenoh
        support itself only landed in early 2025) and may move again.
      
      A related, distinct tool: **zenoh-plugin-ros2dds**
      ([eclipse-zenoh/zenoh-plugin-ros2dds](https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds))
      bridges a *standard DDS-based* ROS 2 system to Zenoh at the edge, rather
      than replacing the RMW. Reach for this when the ROS 2 side should stay on
      its normal DDS RMW and only the boundary to a non-ROS/remote Zenoh peer
      needs bridging; it fits the "cross-boundary to a non-ROS peer" row of the
      comms table better than swapping the whole system's RMW to rmw_zenoh.
      
      ## gRPC and REST: for non-ROS boundaries only
      
      Use these when the peer on the other side doesn't speak ROS 2/DDS at all:
      another team's microservice, a cloud API, a mobile app, a system that will
      never run ROS 2:
      
      - **gRPC**: typed contracts (protobuf), efficient binary encoding,
        supports streaming. Prefer it when both sides can adopt a shared `.proto`
        schema and the extra tooling is worth it (higher throughput, lower
        latency than REST, native streaming for continuous data crossing the
        boundary).
      - **REST**: simplest, most universally interoperable (any HTTP client),
        best when the peer is unknown/heterogeneous, low-frequency, or a human/UI
        is in the loop.
      
      Do not run either of these *inside* one ROS 2 system in place of native
      topics/services/actions; that's the anti-pattern the "prefer ROS 2 native
      comms" key directive exists to block. The boundary case is the exception,
      not the template for internal wiring.
      
      ## Shared memory
      
      Two forms come up in robium builds:
      
      - **ROS 2 intra-process comms / loaned messages**: when publisher and
        subscriber are in the same process (composed nodes in one executor),
        rclcpp/rclpy can avoid a serialize/deserialize round-trip entirely. This
        only applies within one process; it is not a cross-process IPC
        mechanism.
      - **rmw_zenoh's SHM path**: if already on rmw_zenoh, same-host
        cross-process messages can use Zenoh's shared-memory transport
        transparently, without changing application code.
      
      Adopt either only when profiling shows serialization/copy cost is the
      actual bottleneck (large images, point clouds, high-rate large messages).
      Don't reach for shared memory as a first move; the comms-choice table's
      default for same-host ROS 2 nodes is still plain topics/services/actions.
      
      ## Discovery across containers/hosts (comms-selection summary)
      
      This reference covers *which transport*; the *how do containers on that
      transport find each other* mechanics (Discovery Server, static peers, host
      networking, the zenoh router as a compose service) live in
      `compose-patterns.md`; the two are deliberately split so this file stays
      about picking a transport and that one stays about wiring it into compose.
      
    • compose-patterns.md 7.3 KB
      # Compose patterns
      
      How to wire multiple robotics-module containers together with
      docker-compose, and (the part that actually breaks in practice) how to
      make DDS discovery work across that boundary. Checked 2026-07 against
      [Docker Compose networking docs](https://docs.docker.com/compose/how-tos/networking/)
      and a worked cross-host example at [Husarnet's ROS 2 + Docker
      writeup](https://husarnet.com/blog/ros2-docker); the Husarnet writeup was
      fetched directly. The [Fast DDS Discovery Server
      docs](https://fast-dds.docs.eprosima.com/en/latest/fastdds/ros2/discovery_server/ros2_discovery_server.html)
      page loaded but didn't render past its table of contents, so the
      `ROS_DISCOVERY_SERVER` details in option 2 below rest on WebSearch
      corroboration rather than a full direct read; re-verify before relying on
      them.
      
      ## First prove discovery in the target topology
      
      By default, `docker compose` puts services on a bridge network. Most DDS
      implementations' default discovery (Simple Discovery Protocol) uses
      multicast UDP to find peers. Whether that succeeds depends on the RMW,
      selected interfaces, Docker and host platform, and cross-host topology. Start
      two minimal participants in the actual deployment network and prove discovery
      and data flow; do not infer either success or failure from `docker compose up`.
      
      If the probe fails, choose and document one topology deliberately:
      
      ### 1. Host networking (same host, Linux, simplest)
      
      ```yaml
      services:
        sim:
          network_mode: host
          environment:
            - ROS_DOMAIN_ID=42
        app:
          network_mode: host
          environment:
            - ROS_DOMAIN_ID=42
      ```
      
      On Linux, containers share the host's network namespace, which often makes a
      same-host DDS topology behave like host processes without extra discovery
      configuration. Verify it with the selected RMW. Host-network behavior on
      Docker Desktop has changed across releases and is not equivalent to Linux;
      check current Docker documentation and probe it. This gives up port-mapping
      isolation. The compose example (`examples/docker-compose.ros2-app.yml`) uses
      this shape for a same-host Linux target, not as a universal fix.
      
      ### 2. Fast DDS Discovery Server (cross-host, or containers without host networking)
      
      Runs a small discovery-server process; participants register as clients
      instead of relying on multicast:
      
      ```yaml
      environment:
        - ROS_DISCOVERY_SERVER=discovery-server-host:11811
      ```
      
      set on every participant, plus a `discovery-server` service (or an
      external process) actually running the server. Reduces discovery traffic
      too, which matters at fleet scale, not just for the multicast problem. See
      the Fast DDS docs linked above for the full super-client/server role
      config; this is Fast-DDS-specific (the RMW must be Fast DDS, the distro
      default in several ROS 2 releases; confirm before relying on it if a
      project has pinned a different RMW).
      
      ### 3. Static peers with multicast disabled (CycloneDDS, cross-host)
      
      Mount a `cyclonedds.xml` pointing at explicit peer addresses and turn
      multicast off:
      
      ```xml
      <CycloneDDS>
        <Domain>
          <General><AllowMulticast>false</AllowMulticast></General>
          <Discovery><Peers><Peer address="sim-host"/><Peer address="app-host"/></Peers></Discovery>
        </Domain>
      </CycloneDDS>
      ```
      
      ```yaml
      environment:
        - CYCLONEDDS_URI=file:///config/cyclonedds.xml
      ```
      
      This is the pattern in the Husarnet writeup above (there applied across a
      VPN, but the peers/no-multicast shape is the same for any cross-host
      compose deployment where host networking isn't an option).
      
      ### (Alternative to all three) rmw_zenoh
      
      Switching the whole system's RMW to `rmw_zenoh_cpp` sidesteps multicast
      entirely via its router-based gossip discovery, but it's a bigger decision
      than a compose-networking tweak (it changes the RMW for the whole system;
      see `references/comms-selection.md`). If chosen, the router is its own
      compose service:
      
      ```yaml
      services:
        zenoh-router:
          image: eclipse/zenoh:latest
          command: ["-l", "tcp/[::]:7447"]
        app:
          environment:
            - RMW_IMPLEMENTATION=rmw_zenoh_cpp
            - ZENOH_ROUTER_CHECK_ATTEMPTS=5
          depends_on:
            - zenoh-router
      ```
      
      Don't assume a node will start the router itself; it's a shared
      dependency, model it as one.
      
      ## `ROS_DOMAIN_ID`
      
      Set it explicitly, per project, on every service that should discover each
      other, and treat it like a port number: unique enough that this project's
      containers don't cross-talk with another ROS 2 system running on the same
      host-network segment (default `0` is the collision risk).
      
      The constant in this file (`42`) is right for the shape compose models: one
      copy of the stack per host. It is **wrong for concurrent copies**: if
      something spawns N containers from this image at once, they all land on
      domain 42, their graphs merge, and you get two `/clock` publishers and a
      `Moved backwards in time, re-publishing joint transforms!` flood rather
      than an honest error. Concurrent-instance spawners assign a per-instance
      domain ID at start time and keep it outside the IDs already in use. The
      1–200 allocation range was the convention used by Robium's 2026-07-13
      nav-trial orchestrator, not a universal ROS rule.
      
      ## `depends_on` and healthchecks
      
      `depends_on` alone only waits for the dependency's container to *start*,
      not for its ROS 2 graph to be ready (a sim that takes 10s to load a world
      before publishing `/clock` is a common case). Pair it with a healthcheck
      that reflects actual readiness, not just process liveness:
      
      ```yaml
      services:
        sim:
          healthcheck:
            test: ["CMD", "ros2", "topic", "list"]
            interval: 5s
            timeout: 3s
            retries: 10
        app:
          depends_on:
            sim:
              condition: service_healthy
      ```
      
      A bare `ros2 topic list` is a weak healthcheck (it only proves the ROS 2
      CLI can reach the graph, not that the sim published anything useful);
      tighten it to check for a specific expected topic/service in a real
      project rather than copying this as-is.
      
      ## Compose sharp edges (all three hit in one real build: nav-trial, 2026-07-11)
      
      - **Profile-gated `build` is a silent no-op.** With every service behind a
        `profiles:` key (the one-image/one-service-per-profile shape), bare
        `docker compose build` exits 0 with `No services to build` and builds
        NOTHING; a Makefile/CI `build` target wrapping it "succeeds" without
        producing an image. Name a service explicitly (`docker compose build
        <svc>` auto-activates its profile) or pass `--profile "*"`.
      - **YAML merge keys are shallow.** Declaring `environment:` on a service
        that merges an `x-` anchor (`<<: *app`) REPLACES the anchor's whole
        environment map (silently dropping every var the anchor set) rather
        than merging into it. Redeclare the anchor's vars alongside the new one,
        and leave a comment on the anchor warning the next editor.
      - **`docker compose exec` bypasses the image ENTRYPOINT** (unlike
        `compose run`), so an entrypoint that sources the ROS env doesn't run and
        in-container commands fail with unsourced-env symptoms
        (`ModuleNotFoundError: rclpy`, `command not found: ros2`). Prefix
        manually: `docker compose exec <svc> /entrypoint.sh <cmd>`.
      
      ## Volumes
      
      Mount shared config (a `cyclonedds.xml`, a shared parameters file) and any
      persistent data (bags, logs) as named volumes or explicit host binds,
      scoped to what actually needs to be shared; don't bind-mount an entire
      workspace into a container that only needs one config file, and don't rely
      on a shared writable volume as an IPC mechanism (that's what topics/
      services are for; see `references/comms-selection.md`).
      
    • dockerfile-guide.md 4.7 KB
      # Dockerfile guide (multi-module build quality)
      
      How to write a Dockerfile for **one module** in a multi-container robotics
      system. This is a different concern from the `environments` skill's
      docker-patterns.md reference: that reference is about a *single
      environment's* reproducibility (uv inside a ROS 2 image, GPU base tags,
      local/remote parity). This reference is about *build quality* for a module
      that's going to run as one service among several in compose: image size,
      build-cache efficiency, and container-runtime behavior (signals, one
      process). Read both; they compose, they don't duplicate.
      
      ## One process per container
      
      Restated from the key directive because it's a Dockerfile-shape decision,
      not just a compose one: a Dockerfile that `CMD`s a single `ros2 launch` (or
      a single node executable) is the default shape. If a Dockerfile's `CMD`
      starts multiple unrelated long-running processes (a supervisor script
      backgrounding several nodes), that's the signal to split into multiple
      Dockerfiles/services instead; supervisord-in-a-container patterns exist,
      but they trade away independent restart/scaling and should be a stated
      exception, not a default reach.
      
      ## Multi-stage: builder vs runtime
      
      A ROS 2 module's build toolchain (colcon, compilers, `-dev` apt packages,
      rosdep-resolved build dependencies) is large and mostly irrelevant at
      runtime. Split it:
      
      ```dockerfile
      # Stage 1: build; full toolchain, discarded after this stage.
      # (re-verify `jazzy-ros-base-noble` against hub.docker.com/_/ros and the
      # current default distro before using this tag in a real project)
      FROM ros:jazzy-ros-base-noble AS builder
      RUN apt-get update && apt-get install -y --no-install-recommends \
            python3-colcon-common-extensions build-essential \
          && rm -rf /var/lib/apt/lists/*
      WORKDIR /workspace
      COPY ./src ./src
      RUN . /opt/ros/jazzy/setup.sh && \
          colcon build --merge-install --install-base /opt/module_install
      
      # Stage 2: runtime; only the built install/ tree and runtime deps.
      FROM ros:jazzy-ros-base-noble
      COPY --from=builder /opt/module_install /opt/module_install
      # runtime-only apt deps (no compilers, no -dev packages) go here if needed
      ENTRYPOINT ["/bin/bash", "-c", \
        "source /opt/ros/jazzy/setup.bash && source /opt/module_install/setup.bash && exec \"$@\"", "--"]
      CMD ["ros2", "launch", "my_module", "my_module.launch.py"]
      ```
      
      The runtime stage never sees `build-essential`, colcon, or the raw `src`
      tree: smaller image, smaller attack surface, and a build-cache boundary
      that keeps "recompile" and "ship" concerns separate. See
      `examples/Dockerfile.multistage-ros2` for the complete, runnable-shape
      version this pattern is drawn from, and cross-reference `environments`'
      Dockerfile.gpu-ml example for the same technique applied to a heavy-Python
      (uv/torch) build instead of colcon.
      
      ## Layer-cache ordering
      
      Order instructions from least-to-most frequently changing, same as any
      Docker build: apt packages and other system-level deps first, then
      dependency manifests (`package.xml`, `pyproject.toml`/`uv.lock` for any
      Python glue), then source code last. Changing one line of application code
      should not force a full apt-get/colcon-dependency-resolution re-run.
      
      ## Signal handling for `ros2 launch`
      
      `ros2 launch` and the nodes it starts need to receive `SIGINT`/`SIGTERM`
      cleanly for graceful shutdown (lifecycle transitions, clean DDS
      participant teardown). Two things commonly break this in containers:
      
      - **Use exec form, not shell form, for the final `CMD`/`ENTRYPOINT` step** so
        an untracked setup shell does not swallow the signal. Then account for PID 1
        semantics: `ros2 launch` may still fail to follow Docker's default SIGTERM
        path cleanly. Prefer a small init such as `tini`, or configure and test the
        stop signal the launch process handles. The `ENTRYPOINT` above solves shell
        indirection only; it is not proof of graceful shutdown.
      - **`docker compose stop`'s default timeout (10s)** may not be enough for a
        ROS 2 graph with several lifecycle nodes to shut down cleanly; raise
        `stop_grace_period` in compose for modules with real shutdown work to do,
        rather than accepting SIGKILL as the normal path.
      
      ## Non-root runtime user
      
      Same guidance as `environments`' `docker-patterns.md`: run the runtime
      stage as a non-root user for a security default and permission parity with
      mounted volumes:
      
      ```dockerfile
      RUN useradd --create-home --uid 1000 robium && \
          chown -R robium:robium /opt/module_install
      USER robium
      ```
      
      ## `.dockerignore`
      
      Exclude colcon build artifacts (`build/`, `install/`, `log/`) and any local
      venv from the build context; same reasoning as the single-environment case
      in `environments`, and it matters more here because a multi-module repo's
      build context is larger and slower to send to the daemon if left unfiltered.
      
  • SKILL.md 2.8 KB
    ---
    name: integration
    description: Wire robotics modules with ROS 2 interfaces, Docker Compose, or non-ROS transports.
    ---
    
    # Integration
    
    Every module boundary needs an explicit contract: why the boundary exists, what
    crosses it, how peers discover each other, and how failure is observed.
    
    ## Draw boundaries for a reason
    
    - Split components with different rates when sharing an executor or process can
      starve the faster loop.
    - Split failure domains when one component must restart, scale, or exhaust
      memory without taking down another.
    - Keep tightly coupled code together when neither rate nor failure isolation
      justifies serialization and deployment overhead.
    - Make each container a supervisable unit. Multiple processes may belong
      together, but their shared lifecycle should be intentional.
    
    ## Choose the boundary contract
    
    - Inside one ROS 2 system, default to ROS topics, services, and actions.
    - Crossing hosts or containers does not by itself require a new application
      protocol; configure ROS discovery explicitly first.
    - Use gRPC or REST at a genuine non-ROS system or organizational boundary.
    - Use shared memory or zero-copy paths only after measurement shows copying is
      the bottleneck.
    - Treat names, schemas, QoS, time, backpressure, health, restart behavior, and
      ownership as part of the interface.
    
    Read [communication selection](references/comms-selection.md) when choosing
    between ROS 2, DDS discovery options, Zenoh, gRPC, REST, or shared memory.
    
    ## Package the running system
    
    - Use separate build and runtime stages for shipped modules; leave compilers
      and build-only dependencies behind.
    - Give each service a health signal that proves readiness, not merely a live
      process.
    - Make network mode, `ROS_DOMAIN_ID`, discovery, volumes, devices, and
      shutdown behavior visible in the composition.
    - A successful `docker compose up` proves process creation, not communication.
      Verify real cross-boundary messages.
    
    Read [the Dockerfile guide](references/dockerfile-guide.md) for build/runtime
    structure and signal handling. Read [compose patterns](references/compose-patterns.md)
    for DDS discovery, health, domains, and networking. Use the examples only as
    starting shapes and verify them in the target environment.
    
    Use `environments` for one module's reproducibility and GPU/runtime contract.
    Use `ros2` when the failed interface is within a healthy ROS graph. Use
    `foxglove` only when the boundary is specifically remote visualization.
    
    ## Done
    
    - Each split has a rate, failure, deployment, or ownership reason.
    - Every boundary has an explicit protocol and interface contract.
    - Peers discover each other in the real host/container topology.
    - Health and shutdown behavior are observable and scoped to the failed unit.
    - A real message or request proves each changed boundary end to end.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related