Running Three Reachy2 Simulations Simultaneously in Isolated Docker Containers

Running Three Reachy2 Simulations Simultaneously in Isolated Docker Containers

Three Reachy2 simulations, each in its own container, each independently reachable on a separate port — this works out of the box with the official pollenrobotics/reachy2 Docker image. The core idea is straightforward: remap every exposed port, not just the gRPC one, to a unique host port for each container you launch.

What the Reachy2 Docker image exposes

The image binds three ports inside each container by default:

  • 50051 — the gRPC SDK server (the port your Python client connects to)
  • 6080 — VNC display for viewing the Gazebo or MuJoCo simulation in a browser
  • 8888 — Jupyter Notebook

Run a single container with -p 50051:50051 and everything works. Try a second container with the same mapping and Docker refuses — that host port is already bound. Every container needs its own unique host-side ports for all three services. Miss one and the second container silently fails to start, or starts but collides on the display port, which is the one people overlook most.

Launching three containers with docker run

A clean, consistent numbering scheme makes this easy to reason about later. Here is one that works:

# Container 1
docker run --rm --platform linux/amd64 \
  --name reachy_sim_1 \
  -p 50051:50051 \
  -p 6080:6080 \
  -p 8888:8888 \
  docker.io/pollenrobotics/reachy2 \
  fake:=true start_sdk_server:=true gazebo:=true

# Container 2
docker run --rm --platform linux/amd64 \
  --name reachy_sim_2 \
  -p 50052:50051 \
  -p 6081:6080 \
  -p 8889:8888 \
  docker.io/pollenrobotics/reachy2 \
  fake:=true start_sdk_server:=true gazebo:=true

# Container 3
docker run --rm --platform linux/amd64 \
  --name reachy_sim_3 \
  -p 50053:50051 \
  -p 6082:6080 \
  -p 8890:8888 \
  docker.io/pollenrobotics/reachy2 \
  fake:=true start_sdk_server:=true gazebo:=true

The internal port stays 50051 on every container. Only the left-hand (host) side changes. Container 2 maps host port 50052 to container port 50051; container 3 maps 50053 to 50051. The VNC and Jupyter ports follow the same increment pattern.

If you do not need the browser visualizer or Jupyter, drop those -p flags. Three Gazebo instances is a real GPU and memory load — only expose what you actually use.

A repeatable setup with Docker Compose

For anything beyond a quick one-off test, a docker-compose.yml is easier to manage and safe to commit to version control:

version: '3.8'

services:
  reachy_sim_1:
    image: docker.io/pollenrobotics/reachy2
    platform: linux/amd64
    container_name: reachy_sim_1
    ports:
      - 50051:50051
      - 6080:6080
      - 8888:8888
    command: fake:=true start_sdk_server:=true gazebo:=true

  reachy_sim_2:
    image: docker.io/pollenrobotics/reachy2
    platform: linux/amd64
    container_name: reachy_sim_2
    ports:
      - 50052:50051
      - 6081:6080
      - 8889:8888
    command: fake:=true start_sdk_server:=true gazebo:=true

  reachy_sim_3:
    image: docker.io/pollenrobotics/reachy2
    platform: linux/amd64
    container_name: reachy_sim_3
    ports:
      - 50053:50051
      - 6082:6080
      - 8890:8888
    command: fake:=true start_sdk_server:=true gazebo:=true

Bring all three up with docker compose up. Start them individually with docker compose up reachy_sim_2. Each container is independently controllable and each gRPC endpoint is addressable on its own host port.

Connecting from Python

The Reachy2 SDK communicates over gRPC. The basic connection takes a host address:

from reachy2_sdk import ReachySDK

reachy = ReachySDK(host='localhost')

To address three separate containers, check whether your installed SDK version accepts a port keyword argument:

robot_a = ReachySDK(host='localhost', port=50051)
robot_b = ReachySDK(host='localhost', port=50052)
robot_c = ReachySDK(host='localhost', port=50053)

If port is not a supported parameter in your version, try passing it as part of the host string in standard gRPC target format — host='localhost:50052'. Verify the exact constructor signature against the reachy2-sdk repository or the installed package source. Once connected, each ReachySDK instance is completely independent and you can command all three from the same Python script without any cross-talk.

If the containers are on a remote machine rather than localhost, substitute that machine’s IP address for 'localhost'. The port logic stays the same.

Keeping simulations isolated at the ROS layer

Docker port remapping handles SDK-level network isolation. But ROS 2 nodes communicate over DDS, which is multicast-based and not port-scoped the same way. If you run ROS 2 nodes on the host or attach external nodes to the containers, topics and services can bleed across simulations. Setting a distinct ROS_DOMAIN_ID per container walls them off cleanly:

docker run ... -e ROS_DOMAIN_ID=1 ... reachy_sim_1
docker run ... -e ROS_DOMAIN_ID=2 ... reachy_sim_2
docker run ... -e ROS_DOMAIN_ID=3 ... reachy_sim_3

Or in docker-compose, add an environment: block under each service with ROS_DOMAIN_ID: 1 etc. If you are only using the Python SDK and not touching ROS directly, this is optional — but it is a cheap safeguard that prevents confusing cross-simulation message bleed if the setup grows later.

Platform note

The --platform linux/amd64 flag is required on Apple Silicon. On native x86_64 Linux you can drop it. The image itself does not require a login — it is publicly available on Docker Hub.

Sources

Similar Posts