How to Use Docker for ROS 2 Development: A Practical Setup for Robotics Research Teams
Adrian Krzemiński,

TL;DR: Using ROS 2 Docker containers lets you isolate dependencies, reproduce builds across x86 and ARM64 (Jetson) hardware, and ship the same image tag from a developer laptop to a field robot. This guide shows a working Dockerfile based on ros:humble, how to run RViz and Gazebo with GPU acceleration, how to build multi-arch images with buildx for NVIDIA Jetson, and how to structure a workspace your team can rebuild in minutes.
Why ROS 2 Docker Containers Solve Real Problems for Robotics Teams
If you have ever spent an afternoon debugging a colleague’s “but it works on my machine” ROS 2 build, you already understand the problem. ROS 2 distributions are tied to specific Ubuntu versions (Humble on 22.04, Jazzy on 24.04), and Python, CMake, and system library versions drift quickly. A ros 2 docker workflow pins most of that into a single image you can rebuild predictably, especially when base image digests and package versions are pinned.
For research teams working on mobile robots like the Leo Rover, containers also solve a second problem: the development machine (often x86_64 with an NVIDIA GPU) does not match the robot’s onboard computer (often an ARM64 Raspberry Pi or NVIDIA Jetson). Docker, combined with buildx and QEMU or native ARM64 builders, lets you produce one multi-arch image tag that runs on both.
The practical wins for a typical research group include:
- Reproducibility: a pinned image tag or digest is a snapshot of the entire userland, not just your
src/folder. - Onboarding: new team members run one
docker compose upcommand instead of following a 40-step setup wiki. - Field deployment: the image you tested in simulation is the same one that runs on the rover in the field.
- Parallel ROS distros: Humble for legacy stacks and Jazzy for new work, side by side on the same host.
What a Minimal ROS 2 Docker Setup Looks Like
The official osrf/ros and ros images on Docker Hub cover the standard ROS 2 distributions. For most robotics work you want the -ros-base or -desktop tag, depending on whether you need RViz and demo packages inside the container.
Here is a working Dockerfile for a Humble-based development image. It adds colcon, common build tools, and a non-root user so files created in mounted volumes do not end up owned by root on the host.
# Dockerfile
FROM ros:humble-ros-base
ARG USERNAME=dev
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update && apt-get install -y \
python3-colcon-common-extensions \
python3-rosdep \
python3-vcstool \
ros-humble-rviz2 \
ros-humble-rmw-cyclonedds-cpp \
git nano sudo \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
&& echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \
&& chmod 0440 /etc/sudoers.d/$USERNAME
USER $USERNAME
WORKDIR /home/$USERNAME/ws
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
SHELL ["/bin/bash", "-c"]
CMD ["bash"]
Build it with docker build -t myteam/ros2-dev:humble . and run it interactively:
docker run -it --rm \
-v $(pwd)/ws:/home/dev/ws \
--network host \
myteam/ros2-dev:humble
The --network host flag is important. ROS 2 default DDS discovery uses UDP multicast, and bridged Docker networks often do not forward it to the host or LAN correctly by default. Host networking is the simplest fix for development. For production, configure a CycloneDDS or Fast DDS XML profile that uses unicast peers explicitly.
How to Run RViz and Gazebo Inside a Container with GPU
GUI tools inside containers need access to the host’s X server (or Wayland) and, for anything 3D, the host’s GPU. On Linux with an NVIDIA GPU, install the nvidia-container-toolkit package, then extend the run command:
xhost +local:docker
docker run -it --rm \
--gpus all \
--network host \
--env DISPLAY=$DISPLAY \
--env QT_X11_NO_MITSHM=1 \
--env NVIDIA_DRIVER_CAPABILITIES=all \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v $(pwd)/ws:/home/dev/ws \
myteam/ros2-dev:humble \
ros2 run rviz2 rviz2
A few notes from practice:
- RViz2 works reliably with OpenGL forwarding once
NVIDIA_DRIVER_CAPABILITIESincludesgraphicsandutility. - Gazebo Fortress and Harmonic need the same flags plus
--device /dev/drion systems with Intel/AMD integrated GPUs. - Wayland users can mount
$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY, but XWayland with the X11 method above is usually less trouble. - macOS and Windows hosts work through XQuartz or WSLg, with a noticeable rendering cost. For heavy 3D work, run RViz on the host and only the ROS nodes in the container.
How to Build Multi-Arch Images for NVIDIA Jetson and Raspberry Pi
If your fleet mixes architectures (a common case: x86_64 workstations, a Jetson Orin Nano on one robot, a Raspberry Pi 4 on another), build once and push a multi-arch manifest. Docker’s buildx handles this with QEMU emulation when no native builder is available.
First, register QEMU and create a builder:
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
Then build and push for both architectures in one command:
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.com/myteam/ros2-dev:humble \
--push .
For Jetson with CUDA, the base image matters. The ros:humble-ros-base image runs on arm64 but does not include CUDA, cuDNN, or TensorRT. If you need GPU-accelerated perception on Jetson, start from nvcr.io/nvidia/l4t-jetpack or the dustynv/ros community images, which package ROS 2 on top of the L4T base that matches your JetPack version. Mixing incompatible JetPack and L4T versions across the host and container is the single most common cause of “CUDA initialization failed” errors, so pin both explicitly.
QEMU emulation is slow. A full colcon build of a Nav2-sized workspace under emulation can take 10x longer than native. For serious arm64 builds, use a native arm64 runner (a Jetson, a Raspberry Pi 5, or a cloud arm64 instance) and let buildx assemble the manifest from native builders.
How to Structure a Team Workspace with Docker Compose
Once more than one person is involved, hand-typed docker run commands become a liability. A docker-compose.yml file captures the run configuration in version control:
services:
ros2-dev:
build: .
image: myteam/ros2-dev:humble
network_mode: host
ipc: host
gpus: all
environment:
- DISPLAY=${DISPLAY}
- QT_X11_NO_MITSHM=1
- NVIDIA_DRIVER_CAPABILITIES=all
- ROS_DOMAIN_ID=42
- RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
volumes:
- ./ws:/home/dev/ws
- /tmp/.X11-unix:/tmp/.X11-unix
- /dev:/dev
privileged: true
stdin_open: true
tty: true
A few recommendations for the workspace layout itself:
- Keep
src/outside the image, mounted as a volume. Rebuilding the image to change code is a sign the workflow is wrong. - Use a
.reposfile withvcstoolto declare which repositories make up the workspace. New team members runvcs import src < my_project.reposafter cloning. - Run
sudo rosdep install --from-paths src --ignore-src -yinside the container during development, or as a dedicated image rebuild step for deployment, so dependencies stay in sync with the currentsrc/contents. - Tag images with both the ROS distro and a date or git SHA:
myteam/ros2-dev:humble-2024-11-14. This lets you roll back when a base image update breaks something.
Teams running this setup on Leo Rover for research applications typically keep two compose files: one for the developer laptop (with GUI, GPU, simulation packages) and a slimmer one for the rover’s onboard computer (headless, with hardware device passthrough for sensors and motor controllers). The shared base image guarantees the ROS 2 nodes behave identically in both environments.
When Docker Is the Wrong Tool
Containers are not free. There are situations where the overhead is not worth it:
- Hard real-time control loops running on a PREEMPT_RT kernel. Container runtime jitter is usually negligible, but if you are chasing sub-millisecond determinism, measure first.
- Tight kernel module dependencies, for example custom CAN drivers or RT-patched DDS implementations. These often need to match the host kernel exactly.
- Single-developer prototypes that will never leave one machine. A plain
apt install ros-humble-desktopis faster.
For everything else, especially multi-person research projects and any deployment that crosses architectures, a ros 2 docker workflow pays for itself within the first week.
FAQ
Does ROS 2 DDS discovery work across Docker containers?
Yes, but only with the right network configuration. The simplest option is --network host, which gives the container direct access to the host’s network interfaces and multicast. For containers on different hosts, configure your DDS implementation (CycloneDDS or Fast DDS) with an explicit peers list via an XML profile and set ROS_DOMAIN_ID consistently.
Can you run ROS 2 in Docker on a Raspberry Pi inside Leo Rover?
Yes. A Raspberry Pi 4-based Leo Rover setup can run arm64 Linux, and the official ros:humble-ros-base image has an arm64 variant. Pass through the serial and GPIO devices the rover’s firmware needs with --device or by mounting /dev.
How do you handle ROS 2 parameters and launch files across environments?
Keep launch files and YAML parameter files in the mounted workspace, not baked into the image. Use launch arguments and environment variables to switch between simulation and real hardware. A common pattern is a use_sim_time argument and a robot_namespace argument set per container.
What is the performance cost of running ROS 2 nodes in Docker?
On Linux with host networking and host IPC, the runtime overhead for CPU-bound ROS 2 nodes is typically within a few percent of bare metal. GPU workloads through nvidia-container-toolkit show similar overhead. Disk I/O on overlay filesystems is slower than native, so keep large datasets on bind-mounted volumes.
How do you debug ROS 2 nodes running inside a container?
Attach with docker exec -it <container> bash and use the standard tools: ros2 topic, ros2 node, ros2 bag, and rqt. For GDB, run the container with --cap-add=SYS_PTRACE and --security-opt seccomp=unconfined. VS Code’s Dev Containers extension attaches a full IDE to a running container, which is the most productive setup for most teams.
Should you use the same image for development and deployment?
Use the same base image but build a slimmer deployment variant. The development image includes compilers, debuggers, and editors; the deployment image contains only the built workspace install space and runtime dependencies. A multi-stage Dockerfile handles this cleanly and typically cuts the deployed image size by 60 to 80 percent.
How do you keep image sizes manageable?
Use multi-stage builds, clean apt caches in the same RUN layer that installs packages, and avoid ros-humble-desktop-full on robots that do not need simulation. Tools like dive show which layers are bloating the image.
Build Your Robotics Platform on a Reproducible Stack
A clean ROS 2 Docker workflow is the foundation, but it only pays off when paired with hardware that supports the same software stack from prototype to field deployment. If you are scoping a research platform or need a custom rover configuration matched to your sensor and compute requirements, explore Fictionlab’s custom robotics services to discuss how the Leo Rover and Raph Rover platforms can be adapted to your project.