Fix ROS 2 Discovery Issues in Docker in 15 Minutes

Solve DDS discovery failures between ROS 2 nodes in Docker containers with proper network configuration and FastDDS settings.

Problem: ROS 2 Nodes Can't Find Each Other in Docker

You launched ROS 2 nodes in separate Docker containers and ros2 topic list shows nothing. Nodes can't discover each other even though they're on the same host.

You'll learn:

  • Why DDS discovery fails in Docker by default
  • How to configure Docker networking for ROS 2
  • When to use host mode vs bridge with multicast

Time: 15 min | Level: Intermediate


Why This Happens

ROS 2 uses DDS (Data Distribution Service) for discovery via multicast UDP packets on 239.255.0.1:7400. Docker's default bridge network doesn't forward multicast traffic between containers, so nodes never see each other's announcements.

Common symptoms:

  • ros2 topic list is empty or only shows local topics
  • ros2 node list doesn't show nodes from other containers
  • Works fine when running natively, breaks in Docker
  • No errors, just silent failure

Solution

Step 1: Verify the Issue

# Terminal 1: Start first container
docker run -it --rm --name ros2_pub \
  ros:humble-ros-base \
  ros2 topic pub /test std_msgs/String "data: hello"

# Terminal 2: Try to see it from another container
docker run -it --rm --name ros2_sub \
  ros:humble-ros-base \
  ros2 topic list

Expected: You'll only see /rosout and /parameter_events, not /test


Step 2: Choose Your Network Strategy

Option A: Host Network (Simplest)

Use when all containers run on the same physical machine:

# Terminal 1
docker run -it --rm --network host --name ros2_pub \
  ros:humble-ros-base \
  ros2 topic pub /test std_msgs/String "data: hello"

# Terminal 2
docker run -it --rm --network host --name ros2_sub \
  ros:humble-ros-base \
  ros2 topic echo /test

Why this works: Host mode bypasses Docker networking entirely. Containers share the host's network stack, so multicast works like native ROS 2.

Trade-off: No network isolation. Port conflicts possible.


Option B: Bridge with Multicast (Production)

Use when you need network isolation or multi-host setup:

# Create a custom network with multicast support
docker network create \
  --driver bridge \
  --subnet 172.18.0.0/16 \
  --gateway 172.18.0.1 \
  ros2_network

# Run containers with specific IPs
docker run -it --rm \
  --network ros2_network \
  --ip 172.18.0.10 \
  -e ROS_DOMAIN_ID=42 \
  -e FASTRTPS_DEFAULT_PROFILES_FILE=/tmp/fastdds.xml \
  --name ros2_pub \
  ros:humble-ros-base bash

Inside the container, create FastDDS config:

<!-- /tmp/fastdds.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<dds>
  <profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">
    <transport_descriptors>
      <transport_descriptor>
        <transport_id>udp_transport</transport_id>
        <type>UDPv4</type>
        <sendBufferSize>1048576</sendBufferSize>
        <receiveBufferSize>1048576</receiveBufferSize>
      </transport_descriptor>
    </transport_descriptors>

    <participant profile_name="default_participant" is_default_profile="true">
      <rtps>
        <userTransports>
          <transport_id>udp_transport</transport_id>
        </userTransports>
        
        <useBuiltinTransports>false</useBuiltinTransports>
        
        <!-- Force discovery through specific interfaces -->
        <builtin>
          <metatrafficUnicastLocatorList>
            <locator>
              <udpv4>
                <address>172.18.0.10</address>
              </udpv4>
            </locator>
          </metatrafficUnicastLocatorList>
          
          <initialPeersList>
            <locator>
              <udpv4>
                <address>172.18.0.11</address>
              </udpv4>
            </locator>
          </initialPeersList>
        </builtin>
      </rtps>
    </participant>
  </profiles>
</dds>

Why this works: Unicast discovery with explicit peer lists bypasses multicast issues. Each node knows where to look for peers.


Option C: Docker Compose (Recommended for Dev)

Best for local development with multiple services:

# docker-compose.yml
version: '3.8'

services:
  talker:
    image: ros:humble-ros-base
    command: ros2 run demo_nodes_cpp talker
    network_mode: host
    environment:
      - ROS_DOMAIN_ID=0
    ipc: host

  listener:
    image: ros:humble-ros-base
    command: ros2 run demo_nodes_cpp listener
    network_mode: host
    environment:
      - ROS_DOMAIN_ID=0
    ipc: host

Why ipc: host: Enables shared memory transport for faster communication when containers are on same host.


Step 3: Use Domain IDs to Isolate

# Prevent interference from other ROS 2 systems
export ROS_DOMAIN_ID=42  # 0-101 for ROS 2, 215-232 for custom

# In Docker run command
docker run -e ROS_DOMAIN_ID=42 ...

Best practice: Use unique domain IDs per project to avoid cross-talk.


Step 4: Verify Discovery

# Check if nodes see each other
ros2 node list

# Verify topic connections
ros2 node info /talker

# Monitor discovery traffic (debug)
ros2 run demo_nodes_cpp talker --ros-args --log-level debug

You should see:

  • All nodes from both containers in ros2 node list
  • Publisher/subscriber counts match expectations
  • Messages flowing with ros2 topic echo

If it fails:

  • "No nodes found": Check ROS_DOMAIN_ID matches in all containers
  • Firewall blocking: Run sudo ufw allow 7400:7500/udp on host
  • Still broken: Use tcpdump to verify multicast packets: tcpdump -i any -n udp port 7400

Verification

Run the full test:

# Start compose stack
docker-compose up

# In another terminal
docker exec -it <container_id> ros2 topic list

You should see: Topics from both talker and listener, messages flowing without errors.


What You Learned

  • Docker bridge networks drop multicast by default
  • Host mode works for single-machine setups (simplest)
  • FastDDS unicast discovery solves multi-host scenarios
  • Always set ROS_DOMAIN_ID to avoid interference

Limitations:

  • Host mode sacrifices network isolation
  • Unicast discovery requires knowing peer IPs upfront
  • Multi-host needs proper network planning

When NOT to use this:

  • High-security environments (host mode exposes everything)
  • Kubernetes (use ROS 2 DDS config for K8s networking instead)

Bonus: Production Dockerfile

FROM ros:humble-ros-base

# Install your packages
RUN apt-get update && apt-get install -y \
    ros-humble-demo-nodes-cpp \
    && rm -rf /var/lib/apt/lists/*

# Copy FastDDS config
COPY fastdds.xml /etc/fastdds.xml
ENV FASTRTPS_DEFAULT_PROFILES_FILE=/etc/fastdds.xml

# Set domain ID at runtime
ENV ROS_DOMAIN_ID=42

# Use cyclonedds for simpler multicast (alternative to FastDDS)
RUN apt-get update && apt-get install -y ros-humble-rmw-cyclonedds-cpp
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

ENTRYPOINT ["/ros_entrypoint.sh"]
CMD ["bash"]

CycloneDDS tip: Often handles Docker networking better than FastDDS out of the box. Try switching RMW implementations if discovery issues persist.


Tested on ROS 2 Humble, Docker 25.x, Ubuntu 22.04 & macOS with Docker Desktop