Best practices for Docker-based ROS2 development including multi-stage Dockerfiles, docker-compose for multi-container robotic systems, DDS discovery across containers, GPU passthrough for perception, and dev-vs-deploy container patterns. Use this skill when containerizing ROS2 workspaces, setting up docker-compose for robot software stacks, debugging DDS communication between containers, configuring NVIDIA Container Toolkit for GPU workloads, forwarding X11/Wayland for rviz2 and GUI tools, or managing USB device passthrough for cameras and serial devices. Trigger whenever the user mentions Docker with ROS2, docker-compose for robots, Dockerfile for colcon workspaces, container networking for DDS, GPU containers for perception, devcontainer for ROS2, multi-stage builds for ROS2, or deploying ROS2 in containers. Also trigger for CI/CD with Docker-based ROS2 builds, CycloneDDS or FastDDS configuration in containers, shared memory in Docker, or X11 forwarding for rviz2. Covers Humble, Iron, Jazzy, and Rolling di
Best practices for Docker-based ROS2 development including multi-stage Dockerfiles, docker-compose for multi-container robotic systems, DDS discovery across containers, GPU passthrough for perception, and dev-vs-deploy container patterns. Use this skill when containerizing ROS2 workspaces, setting up docker-compose for robot software stacks, debugging DDS communication between containers, configuring NVIDIA Container Toolkit for GPU workloads, forwarding X11/Wayland for rviz2 and GUI tools, or managing USB device passthrough for cameras and serial devices. Trigger whenever the user mentions Docker with ROS2, docker-compose for robots, Dockerfile for colcon workspaces, container networking for DDS, GPU containers for perception, devcontainer for ROS2, multi-stage builds for ROS2, or deploying ROS2 in containers. Also trigger for CI/CD with Docker-based ROS2 builds, CycloneDDS or FastDDS configuration in containers, shared memory in Docker, or X11 forwarding for rviz2. Covers Humble, Iron, Jazzy, and Rolling distributions across Ubuntu 22.04 and 24.04 base images.
Docker-Based ROS2 Development Skill
When to Use This Skill
Writing Dockerfiles for ROS2 workspaces with colcon builds
Setting up docker-compose for multi-container robotic systems
Debugging DDS discovery failures between containers (CycloneDDS, FastDDS)
Configuring GPU passthrough with NVIDIA Container Toolkit for perception nodes
Forwarding X11 or Wayland displays for rviz2 and rqt tools
Managing USB device passthrough for cameras, LiDARs, and serial devices
Building CI/CD pipelines with Docker-based ROS2 builds and test runners
Creating devcontainer configurations for VS Code with ROS2 extensions
Optimizing Docker layer caching for colcon workspace builds
Designing dev-vs-deploy container strategies with multi-stage builds
ROS2 Docker Image Hierarchy
Official OSRF images follow a layered hierarchy. Always choose the smallest base that satisfies dependencies.
Each ROS2 subsystem runs in its own container with process isolation, independent scaling, and per-service resource limits.
# docker-compose.ymlversion:"3.8"x-ros-common:&ros-commonenvironment:-ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-0}-RMW_IMPLEMENTATION=rmw_cyclonedds_cpp-CYCLONEDDS_URI=file:///cyclonedds.xmlvolumes:-./config/cyclonedds.xml:/cyclonedds.xml:ro-/dev/shm:/dev/shmnetwork_mode:hostrestart:unless-stoppedservices:rosbridge:<<:*ros-commonimage:my_robot:latestcommand:ros2launchrosbridge_serverrosbridge_websocket_launch.xmlport:=9090perception:<<:*ros-commonimage:my_robot_perception:latestcommand:ros2launchmy_robot_perceptionperception.launch.pydeploy:resources:reservations:devices:-driver:nvidiacount:1capabilities: [gpu]
devices:-/dev/video0:/dev/video0# USB camera passthroughnavigation:<<:*ros-commonimage:my_robot_navigation:latestcommand:>
ros2 launch my_robot_navigation navigation.launch.py
use_sim_time:=false map:=/maps/warehouse.yaml
volumes:-./maps:/maps:rodriver:<<:*ros-commonimage:my_robot_driver:latestcommand:ros2launchmy_robot_driverdriver.launch.pydevices:-/dev/ttyUSB0:/dev/ttyUSB0# Serial motor controller-/dev/ttyACM0:/dev/ttyACM0# IMU over USB-serialgroup_add:-dialout
Service Dependencies with Health Checks
services:driver:<<:*ros-commonimage:my_robot_driver:latesthealthcheck:test: ["CMD", "bash", "-c",
"source /opt/ros/humble/setup.bash && ros2 topic list | grep -q /joint_states"]
interval:5stimeout:10sretries:5start_period:15snavigation:<<:*ros-commonimage:my_robot_navigation:latestdepends_on:driver:condition:service_healthy# Wait for driver topicsperception:<<:*ros-commonimage:my_robot_perception:latestdepends_on:driver:condition:service_healthy# Camera driver must be ready
docker compose --profile dev up # Dev tools (rviz, rosbag)
docker compose --profile deploy up -d # Production (watchdog, no GUI)
DDS Discovery Across Containers
CycloneDDS XML Config for Unicast Across Containers
When containers use bridge networking (no multicast), configure explicit unicast peer lists.
<!-- cyclonedds.xml --><?xml version="1.0" encoding="UTF-8"?><CycloneDDSxmlns="https://cdds.io/config"><Domain><General><Interfaces><NetworkInterfaceautodetermine="true"priority="default"/></Interfaces><AllowMulticast>false</AllowMulticast></General><Discovery><!-- Peer list uses docker-compose service names as hostnames --><Peers><Peeraddress="perception"/><Peeraddress="navigation"/><Peeraddress="driver"/><Peeraddress="rosbridge"/></Peers><ParticipantIndex>auto</ParticipantIndex><MaxAutoParticipantIndex>120</MaxAutoParticipantIndex></Discovery><Internal><SocketReceiveBufferSizemin="10MB"/></Internal></Domain></CycloneDDS>
DDS shared memory (zero-copy) requires /dev/shm sharing between containers. This provides highest throughput for large messages (images, point clouds).
services:perception:shm_size:"512m"# Default 64 MB is too small for image topicsvolumes:-/dev/shm:/dev/shm# Share host shm for inter-container zero-copy
<!-- Enable shared memory in CycloneDDS --><CycloneDDSxmlns="https://cdds.io/config"><Domain><SharedMemory><Enable>true</Enable></SharedMemory></Domain></CycloneDDS>
Constraints: all communicating containers must share /dev/shm or use ipc: host. Use --ipc=shareable on one container and --ipc=container:<name> on others for scoped sharing.
Networking Modes and ROS2 Implications
Host Networking
services:my_node:network_mode:host# Shares host network namespace; DDS multicast works natively
networks:ros_macvlan:driver:macvlandriver_opts:parent:eth0ipam:config:-subnet:192.168.1.0/24gateway:192.168.1.1services:my_node:networks:ros_macvlan:ipv4_address:192.168.1.50# Real LAN IP; DDS multicast works natively
services:perception:image:my_robot_perception:latestdeploy:resources:reservations:devices:-driver:nvidiacount:1# Number of GPUs (or "all")capabilities: [gpu]
environment:-NVIDIA_VISIBLE_DEVICES=all-NVIDIA_DRIVER_CAPABILITIES=compute,utility,videoshm_size:"1g"# Large shm for GPU<->CPU transfers
For Dockerfiles that need CUDA, start from NVIDIA base and install ROS2 on top:
For CI/CD or remote machines without a physical display:
# Run rviz2 headless with Xvfb for screenshot capture or testing
docker run --rm my_robot:dev bash -c '
apt-get update && apt-get install -y xvfb mesa-utils &&
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
source /opt/ros/humble/setup.bash
ros2 run rviz2 rviz2 -d /config/test.rviz --screenshot /output/frame.png
'
Volume Mounts and Workspace Overlays
Source Mounts for Dev
Mount only src/ during development. Let colcon write build/, install/, and log/ inside named volumes to avoid bind mount performance issues.
# BAD: mounting entire workspace — build artifacts on bind mount are slow# volumes:# - ./my_ros2_ws:/ros2_ws# GOOD: mount only source, use named volumes for build artifactsservices:dev:image:my_robot:devvolumes:-./src:/ros2_ws/src:rw# Source code (bind mount)-build_vol:/ros2_ws/build# Build artifacts (named volume)-install_vol:/ros2_ws/install# Install space (named volume)-log_vol:/ros2_ws/log# Log output (named volume)working_dir:/ros2_wsvolumes:build_vol:install_vol:log_vol:
ccache Caching
Persist ccache across container rebuilds for faster C++ compilation:
Keep upstream packages cached and only rebuild custom packages:
# Stage 1: upstream dependencies (rarely changes)
FROM ros:humble-ros-base AS upstream
RUN apt-get update && apt-get install -y --no-install-recommends \
ros-humble-nav2-bringup ros-humble-slam-toolbox \
ros-humble-robot-localization \
&& rm -rf /var/lib/apt/lists/*
# Stage 2: custom packages overlay on top
FROM upstream AS workspace
WORKDIR /ros2_ws
COPY src/ src/
RUN . /opt/ros/humble/setup.sh && colcon build --symlink-install
# install/setup.bash automatically sources /opt/ros/humble as underlay
USB Device Passthrough
Cameras and Serial Devices
services:camera_driver:image:my_robot_driver:latestdevices:-/dev/video0:/dev/video0# USB camera (V4L2)-/dev/video1:/dev/video1group_add:-video# Access /dev/videoN without rootmotor_driver:image:my_robot_driver:latestdevices:-/dev/ttyUSB0:/dev/ttyUSB0# USB-serial motor controller-/dev/ttyACM0:/dev/ttyACM0# Arduino/Teensygroup_add:-dialout# Access serial ports without root
Udev Rules Inside Containers
Create stable device symlinks on the host so container paths remain consistent regardless of USB enumeration order.
Order Dockerfile instructions from least-frequently-changed to most-frequently-changed:
1. Base image (ros:humble-ros-base) — changes on distro upgrade
2. System apt packages — changes on new dependency
3. rosdep install (from package.xml) — changes on new ROS dep
4. COPY src/ src/ — changes on every code edit
5. colcon build — rebuilds on source change
Problem: Putting perception, navigation, planning, and drivers in a single container defeats the purpose of containerization. A crash in one subsystem takes down everything.
Fix: Split into one service per subsystem. Use docker-compose to orchestrate.
# BAD: monolithic containerservices:robot:image:my_robot:latestcommand:ros2launchmy_roboteverything.launch.py# GOOD: one container per subsystemservices:perception:image:my_robot_perception:latestcommand:ros2launchmy_robot_perceptionperception.launch.pynavigation:image:my_robot_navigation:latestcommand:ros2launchmy_robot_navigationnavigation.launch.pydriver:image:my_robot_driver:latestcommand:ros2launchmy_robot_driverdriver.launch.py
2. Using Bridge Networking Without DDS Config
Problem: DDS uses multicast for discovery by default. Docker bridge networks do not forward multicast. Nodes in different containers will not discover each other.
Fix: Use network_mode: host or configure DDS unicast peers explicitly.
# BAD: bridge network with no DDS configservices:node_a:networks: [ros_net]
node_b:networks: [ros_net]
# GOOD: host networking (simplest)services:node_a:network_mode:hostnode_b:network_mode:host# GOOD: bridge with CycloneDDS unicast peersservices:node_a:networks: [ros_net]
environment:-RMW_IMPLEMENTATION=rmw_cyclonedds_cpp-CYCLONEDDS_URI=file:///cyclonedds.xmlvolumes:-./cyclonedds.xml:/cyclonedds.xml:ro
3. Building Packages in the Runtime Image
Problem: Installing compilers and build tools in the runtime image bloats it by 1-2 GB and increases attack surface.
Fix: Use multi-stage builds. Compile in a build stage, copy only the install space to runtime.
# BAD: build tools in runtime image (2.5 GB)
FROM ros:humble-ros-base
RUN apt-get update && apt-get install -y build-essential python3-colcon-common-extensions
COPY src/ /ros2_ws/src/
RUN cd /ros2_ws && colcon build
CMD ["ros2", "launch", "my_pkg", "bringup.launch.py"]
# GOOD: multi-stage build (800 MB)
FROM ros:humble-ros-base AS build
RUN apt-get update && apt-get install -y python3-colcon-common-extensions
COPY src/ /ros2_ws/src/
RUN cd /ros2_ws && . /opt/ros/humble/setup.sh && colcon build
FROM ros:humble-ros-core AS runtime
COPY --from=build /ros2_ws/install /ros2_ws/install
CMD ["ros2", "launch", "my_pkg", "bringup.launch.py"]
4. Mounting the Entire Workspace as a Volume
Problem: Mounting the full workspace means colcon writes build/, install/, and log/ to a bind mount. On macOS/Windows Docker Desktop, bind mount I/O is 10-50x slower. Builds that take 2 minutes take 20+ minutes.
Fix: Mount only src/ as a bind mount. Use named volumes for build artifacts.
Problem: Placing COPY src/ . before rosdep install means every source change invalidates the dependency cache. All apt packages are re-downloaded on every build.
Fix: Copy only package.xml files first, install dependencies, then copy source.
# BAD: source copy before rosdep
COPY src/ /ros2_ws/src/
RUN rosdep install --from-paths src --ignore-src -r -y
RUN colcon build
# GOOD: package.xml first, then rosdep, then source
COPY src/my_pkg/package.xml /ros2_ws/src/my_pkg/package.xml
RUN . /opt/ros/humble/setup.sh && rosdep install --from-paths src --ignore-src -r -y
COPY src/ /ros2_ws/src/
RUN . /opt/ros/humble/setup.sh && colcon build
7. Hardcoding ROS_DOMAIN_ID
Problem: Hardcoding ROS_DOMAIN_ID=42 causes conflicts when multiple robots or developers share a network. Two robots with the same domain ID will cross-talk.
Fix: Use environment variables with defaults. Set domain ID at deploy time.