Running Multiple Reachy2 Simulations in Parallel Docker Containers with Isolated gRPC Ports
Running Three Reachy2 Simulations Simultaneously in Docker
The good news: you can absolutely run three Reachy2 simulation containers at once, each reachable on its own gRPC port, without any changes to the image itself. The work is almost entirely in how you hand Docker its port-mapping arguments — and in avoiding one networking flag that quietly breaks everything.
What Each Reachy2 Container Exposes
Every Reachy2 container listens on three internal ports:
- 50051 — the gRPC SDK server, the one your Unity clients talk to
- 6080 — VNC display for the Gazebo or MuJoCo viewport
- 8888 — a Jupyter Notebook interface
These are container-side ports. Docker’s -p HOST_PORT:CONTAINER_PORT flag is what exposes them on your actual machine. The inside stays 50051 regardless; only the host-side number changes per container.
The –network host Problem
Some documentation — notably the LeRobot integration guide — uses --network host in its docker run example. That’s fine for a single container, but it’s incompatible with running multiple instances. Host networking bypasses Docker’s port-translation layer entirely: the container shares the host’s network namespace, so -p flags do nothing and every container tries to own port 50051 on the host directly. The second container simply fails to start.
Drop --network host. Use bridge networking — Docker’s default — and let explicit -p mappings do the work.
Approach 1: Three Separate docker run Commands
The most direct path. Each command gets a unique container name, unique host-side gRPC port, and unique host-side ports for VNC and Jupyter so you can view all three displays independently.
# Robot 1 — host gRPC port 50051
docker run --rm -it \
--name reachy2_sim1 \
--platform linux/amd64 \
-p 50051:50051 \
-p 6080:6080 \
-p 8888:8888 \
-e ROS_DOMAIN_ID=1 \
pollenrobotics/reachy2 \
start_sdk_server:=true fake:=true mujoco:=true
# Robot 2 — host gRPC port 50052
docker run --rm -it \
--name reachy2_sim2 \
--platform linux/amd64 \
-p 50052:50051 \
-p 6081:6080 \
-p 8889:8888 \
-e ROS_DOMAIN_ID=2 \
pollenrobotics/reachy2 \
start_sdk_server:=true fake:=true mujoco:=true
# Robot 3 — host gRPC port 50053
docker run --rm -it \
--name reachy2_sim3 \
--platform linux/amd64 \
-p 50053:50051 \
-p 6082:6080 \
-p 8890:8888 \
-e ROS_DOMAIN_ID=3 \
pollenrobotics/reachy2 \
start_sdk_server:=true fake:=true mujoco:=true
Each -p 5005X:50051 line is the key: it tells Docker to forward host port 5005X to the container’s internal 50051. Three different host numbers, three isolated gRPC endpoints.
Approach 2: Docker Compose (Cleaner for Ongoing Use)
Three separate terminal windows gets old fast. A docker-compose.yml defines all three containers in one file, and a single command starts them all.
version: '3.8'
services:
reachy_sim1:
image: pollenrobotics/reachy2
container_name: reachy2_sim1
platform: linux/amd64
ports:
- "50051:50051"
- "6080:6080"
- "8888:8888"
environment:
- ROS_DOMAIN_ID=1
command: "start_sdk_server:=true fake:=true mujoco:=true"
reachy_sim2:
image: pollenrobotics/reachy2
container_name: reachy2_sim2
platform: linux/amd64
ports:
- "50052:50051"
- "6081:6080"
- "8889:8888"
environment:
- ROS_DOMAIN_ID=2
command: "start_sdk_server:=true fake:=true mujoco:=true"
reachy_sim3:
image: pollenrobotics/reachy2
container_name: reachy2_sim3
platform: linux/amd64
ports:
- "50053:50051"
- "6082:6080"
- "8890:8888"
environment:
- ROS_DOMAIN_ID=3
command: "start_sdk_server:=true fake:=true mujoco:=true"
Start everything with docker compose up, or add -d to detach. Logs for all three containers stream in one view; docker compose down stops them all cleanly.
Why Set Different ROS_DOMAIN_IDs?
Reachy2 runs on ROS 2, which uses DDS for internal pub/sub. All ROS 2 nodes on the same host default to domain ID 0 and can hear each other’s topics. With bridge networking, the containers are already isolated at the IP level, so this matters less — but if you ever attach to a container for debugging, or if you experiment with host networking, identical domain IDs mean all three robots’ joint state topics are visible to each other. Giving each container a unique ROS_DOMAIN_ID (1, 2, 3) costs nothing and prevents that crosstalk.
Connecting Your Unity Clients
Each gRPC connection in your Unity project just points at a different host port. The Reachy2 Unity Digital Twin package lets you configure the server address on the data client prefab — drag three prefab instances into your scene and set them to localhost:50051, localhost:50052, and localhost:50053 respectively.
If Unity is running on a different machine than the Docker host, replace localhost with the host machine’s IP address. The port numbers stay the same.
A Note on Resources
Three MuJoCo physics simulations running in parallel is real work. CPU usage can spike, especially inside Docker on non-Linux platforms (Rosetta emulation on Apple Silicon is particularly taxing). If simulation frequency degrades, the LeRobot docs describe a workaround using LD_LIBRARY_PATH to point MuJoCo at the host’s rendering libraries — worth knowing about before you hit it. Gazebo trades CPU for GPU overhead and three simultaneous VNC-rendered viewports will push your graphics stack. Monitor with docker stats while the containers are running to see which resource is actually the bottleneck.
