Maker.io main logo

Running ROS on Docker: Complete Guide (Part 3)

158

Programmers

This is the third part of my Docker series. If you have been following along, you already know why Docker exists and how to build a basic container. This part is where it all becomes real for robotics: we apply everything from the previous two parts directly to ROS2, run actual nodes inside a container, and build a setup you can take into your own projects.

Quick Recap: Where We Left Off

Part 1 of this series was about the core problem Docker solves. That classic situation where your code runs perfectly on your machine, you hand it to a teammate, and suddenly nothing works. Different OS, different library versions, different everything. Docker fixes this by wrapping your code and its entire environment together, so the environment travels with the code.

Part 2 got practical. We wrote our first Dockerfile, learned how image layers work, built a container, and went through the best practices that keep images small and maintainable.

If you skipped those two, I would go back and read them first. This part builds directly on them and assumes you have Docker installed and know the basics of how a Dockerfile is structured.

With that out of the way, let's get into ROS.

Why ROS Specifically Needs Docker

ROS2 has a compatibility rule that trips up almost every robotics student at some point. Each distribution of ROS2 is built for one specific version of Ubuntu, and only that version. ROS2 Humble runs on Ubuntu 22.04. ROS2 Jazzy runs on Ubuntu 24.04. There is no official way to run Jazzy on a 22.04 system or Humble on 24.04.

This sounds like a minor inconvenience until you actually run into it. You find an open-source robot driver, a simulation package, or someone else's project that runs on a different ROS version than yours. Suddenly, you have two options: upgrade your entire operating system and risk breaking your existing setup, or find a way to run that environment in parallel.

I went through exactly this while working on a project with the SO-101 ARM, an open-source robotic arm from the Hugging Face LeRobot ecosystem. The arm's official ROS2 bridge was built for ROS2 Jazzy on Ubuntu 24.04. My machine was on Ubuntu 22.04 with ROS2 Humble. I was genuinely about to upgrade my OS and lose every project configuration I had. Docker made that completely unnecessary. I ran Jazzy in a container, connected it to my Humble setup on the host, and the two worked together without either one touching the other.

Image of Running ROS on Docker: Complete Guide (Part 3)

Docker does not just help with version mismatches. It also makes your ROS projects reproducible and shareable. When your container includes everything the project needs, anyone on your team can run it with a single command, on any machine, without a setup guide.

The Official ROS Docker Images

The Open Source Robotics Foundation maintains official Docker images for every supported ROS and ROS2 distribution. These are hosted on Docker Hub at hub.docker.com/r/osrf/ros and are the right starting point for any ROS container.

Each distribution comes in a few sizes. For ROS2 Jazzy:

osrf/ros:jazzy-ros-base is the minimal option. It includes ROS2 core libraries and build tools, but no graphical software. Best for running nodes headlessly or building workspaces.

osrf/ros:jazzy-desktop adds visualization tools like RViz2 and rqt, plus the demo packages we will use in this guide. This is the image we will start with for the hands-on exercises.

The same naming pattern applies to any distribution. Replace jazzy with humble, iron, or rolling to get that version instead.

Exercise 1: Pull the Image and Explore the Container

Let's start by pulling the ROS2 Jazzy image. Open a terminal on your machine and run:

Copy Code
docker pull osrf/ros:jazzy-desktop

Image of Running ROS on Docker: Complete Guide (Part 3)

This downloads the image layers from Docker Hub. The first pull takes a few minutes. You will see progress bars for each layer. When it finishes, verify the image is on your machine:

docker images

Image of Running ROS on Docker: Complete Guide (Part 3)

You should see osrf/ros listed with the tag jazzy-desktop. Now start an interactive container from that image:

Copy Code
docker run -it --rm osrf/ros:jazzy-desktop bash

Your terminal prompt will change. You are now inside a container running Ubuntu 24.04 with ROS2 Jazzy. Confirm it by checking the ROS distro:

Copy Code
echo $ROS_DISTRO

You should see the output: jazzy. Now source the ROS2 environment and list the available topics (there will be none yet, but the command confirms ROS2 is working):

Copy Code
source /opt/ros/jazzy/setup.bash
ros2 topic list

Image of Running ROS on Docker: Complete Guide (Part 3)

Type exit to leave the container. Because we used --rm, Docker removes it automatically.

Exercise 2: Running ROS2 Nodes Inside Docker

Now let's run actual ROS2 nodes. We will use the classic talker and listener demo that comes with the osrf/ros:jazzy-desktop image. This is one of the simplest ways to verify that ROS2 communication is working inside a container.

Open two separate terminal windows on your machine. In the first one, run the talker node:

Copy Code
docker run -it --rm \
  --net=host \
  --ipc=host \
  -e ROS_DOMAIN_ID=0 \
  -e ROS_LOCALHOST_ONLY=0 \
  osrf/ros:jazzy-desktop \
  bash -c "source /opt/ros/jazzy/setup.bash && ros2 run demo_nodes_py talker"

You should see the talker publishing messages, printing something like:

[INFO] [talker]: Publishing: "Hello World: 1"

[INFO] [talker]: Publishing: "Hello World: 2"

[INFO] [talker]: Publishing: "Hello World: 3"

Image of Running ROS on Docker: Complete Guide (Part 3)

In the second terminal window, run the listener node:

Copy Code
docker run -it --rm \
  --net=host \
  --ipc=host \
  -e ROS_DOMAIN_ID=0 \
  -e ROS_LOCALHOST_ONLY=0 \
  osrf/ros:jazzy-desktop \
  bash -c "source /opt/ros/jazzy/setup.bash && ros2 run demo_nodes_py listener"

The listener should immediately start receiving the talker's messages:

[INFO] [listener]: I heard: "Hello World: 4"

[INFO] [listener]: I heard: "Hello World: 5"

[INFO] [listener]: I heard: "Hello World: 6"

Image of Running ROS on Docker: Complete Guide (Part 3)

Notice the --net=host flag in both commands. This is what makes the two containers able to see each other. Without it, each container lives in its own isolated network and cannot discover the other's ROS nodes.

What is --net=host and Why Does It Matter for ROS2?

ROS2 uses a communication layer called DDS (Data Distribution Service) to allow nodes to automatically discover each other and exchange messages. DDS relies on the network to detect available nodes and communicate through UDP-based discovery mechanisms.

By default, Docker isolates each container inside its own private network. This isolation prevents DDS discovery traffic from reaching other containers or the host machine, which means ROS2 nodes running in separate containers may not be able to find each other.

The --net=host flag removes this network isolation. The container directly shares the host machine's network interface, allowing DDS discovery and communication to work as if ROS2 nodes were running natively on the machine.

For ROS2 development on Linux, using --net=host is the simplest and most reliable way to enable communication between containers.

In addition, we used --ipc=host to share the host's IPC namespace, improving compatibility with some ROS2 communication mechanisms. ROS_DOMAIN_ID=0 ensures all ROS2 nodes run in the same DDS domain and can discover each other. ROS_LOCALHOST_ONLY=0 allows communication beyond the local container, enabling discovery across the network.

Exercise 3: Verifying Cross-Container Topic Visibility

Let's take it one step further. With the talker still running in the first terminal, open a third terminal and check what topics are available:

Copy Code
docker run -it --rm \
  --net=host \
  --ipc=host \
  -e ROS_DOMAIN_ID=0 \
  osrf/ros:jazzy-desktop \
  bash -c "source /opt/ros/jazzy/setup.bash && ros2 topic list"

You should see the talker's topic listed:

/chatter

/rosout

/parameter_events

Image of Running ROS on Docker: Complete Guide (Part 3)

Now echo the messages on that topic to confirm the data is flowing:

Copy Code
docker run -it --rm \
  --net=host \
  --ipc=host \
  -e ROS_DOMAIN_ID=0 \
  osrf/ros:jazzy-desktop \
  bash -c "source /opt/ros/jazzy/setup.bash && ros2 topic echo /chatter"

You should see the messages appearing in real time, pulled from the talker running in the other container. This confirms that multiple containers can communicate seamlessly when using host networking.

Image of Running ROS on Docker: Complete Guide (Part 3)

Writing Your Own Dockerfile for ROS2

Running containers directly with docker run works for quick tests. For a real project, you want a Dockerfile so your environment is reproducible and you can add your own code and dependencies on top of the base image.

Create a new folder for this exercise, and inside it create a file named Dockerfile (no extension) with the following content:

Image of Running ROS on Docker: Complete Guide (Part 3)

Build the image from inside that folder:

Copy Code
docker build -t my_ros2_project

Image of Running ROS on Docker: Complete Guide (Part 3)

You will see Docker executing each step: pulling the base image, running apt-get, updating rosdep, creating the workspace. When it finishes, run a container from your new image:

Copy Code
docker run -it --rm --net=host my_ros2_project bash

Image of Running ROS on Docker: Complete Guide (Part 3)

Inside the container, ROS2 is already sourced because we added it to .bashrc. Confirm it:

echo $ROS_DISTRO

Persisting Your Work with Volumes

One thing that trips people up early on: anything you create inside a container disappears when the container stops. If you write code inside the container without saving it to a mounted folder, it is gone when you exit.

The solution is to mount a folder from your host machine into the container using the -v flag. Files in that folder exist on your host and are shared with the container simultaneously. Edits on either side are immediately visible on the other.

Create a folder on your host for the ROS workspace:

mkdir -p ~/my_ros2_ws/src

Then run your container with that folder mounted:

Copy Code
docker run -it --rm \
  --net=host \
  -v ~/my_ros2_ws:/ros2_ws \
  my_ros2_project bash

Now anything you put in /ros2_ws inside the container shows up in ~/my_ros2_ws on your host, and vice versa. Your source code is safe on the host even after the container closes.

Image of Running ROS on Docker: Complete Guide (Part 3)

Building a Real ROS2 Package Inside the Container

With the volume mounted, let's clone and build a real ROS2 package so you can see the full workflow in action. We will use the official ROS2 examples repository, which contains minimal publisher and subscriber nodes that work out of the box.

Inside the container, navigate to the src folder and clone the examples:

Copy Code
cd /ros2_ws/src
git clone https://github.com/ros2/examples.git -b jazzy --depth 1

Image of Running ROS on Docker: Complete Guide (Part 3)

Now go back to the workspace root, install dependencies with rosdep, and build the minimal publisher package:

Copy Code
cd /ros2_ws
rosdep install --from-paths src/examples/rclpy --ignore-src -r -y
source /opt/ros/jazzy/setup.bash
colcon build --packages-select examples_rclpy_minimal_publisher examples_rclpy_minimal_subscriber

Image of Running ROS on Docker: Complete Guide (Part 3)

You will see colcon output as the package compiles. When it finishes, source the workspace and run the publisher:

Copy Code
source install/setup.bash
ros2 run examples_rclpy_minimal_publisher publisher_member_function

You should see it publishing messages every half second.

Image of Running ROS on Docker: Complete Guide (Part 3)

Exercise 4: Managing Everything with Docker Compose

Once your project has more than one container, typing long docker run commands gets old fast. Docker Compose lets you describe your entire setup in a single YAML file and bring it all up with one command.

In your project folder, create a file named docker-compose.yaml:

Copy Code
version: "3.9"

services:
  talker:
    image: osrf/ros:jazzy-desktop
    network_mode: host
    ipc: host
    environment:
      - ROS_DOMAIN_ID=0
    command: bash -c "source /opt/ros/jazzy/setup.bash && ros2 run demo_nodes_py talker"

  listener:
    image: osrf/ros:jazzy-desktop
    network_mode: host
    ipc: host
    environment:
      - ROS_DOMAIN_ID=0
    command: bash -c "source /opt/ros/jazzy/setup.bash && ros2 run demo_nodes_py listener"

To start both services at once:

Copy Code
docker compose up

Image of Running ROS on Docker: Complete Guide (Part 3)

Docker Compose will pull the image if needed, start both containers, and stream their output together in the terminal. You will see the talker and listener messages interleaved in a single view.

To stop everything at once, press Ctrl+C. To start in the background (detached mode):

Copy Code
docker compose up -d

And to stop and remove the containers:

Copy Code
docker compose down

The power of Compose is that your entire multi-container setup lives in one file that is version-controlled alongside your code. A new team member can clone the repo and be up and running with a single command.

Common Issues and How to Fix Them

The container starts but ros2 commands are not found.

This means the ROS2 environment has not been sourced. Either add source /opt/ros/jazzy/setup.bash to your container's .bashrc (as we did in the Dockerfile), or source it manually each time you open a shell in the container.

Nodes cannot see each other between containers or the host

Almost always means --net=host is missing. Check that all containers involved are using host networking. Also verify that the ROS_DOMAIN_ID environment variable is the same on all sides.

rosdep install fails saying it cannot find a package

Run rosdep update first to refresh the package index, then try again. If a specific package is still not found, it may not be in the rosdep database. Search for it manually with apt-cache search ros-jazzy- followed by part of the package name.

colcon build fails with a missing library error

The error message will name the missing library. Search for which apt package provides it using apt-cache search, install it with apt-get install, and add that install line to your Dockerfile so it is included in the image from now on.

Files built inside the container are owned by root on the host

When you mount a folder and build inside the container, the generated files are owned by root because Docker runs as root by default. On the host, you cannot edit or delete them without sudo. A quick fix is to run sudo chown -R $USER ~/my_ros2_ws on the host to take back ownership.

Wrapping Up

Docker and ROS2 solve a real problem together. The ROS version-to-Ubuntu pairing is not going away, and as the ecosystem evolves and new distributions come out, the need to run multiple ROS environments on the same machine is only going to grow.

What we covered in this part goes from the basics of pulling an official ROS image all the way to building a real package inside a container with a persistent workspace. The exercises are designed to work exactly as written, so take the screenshots as you go and verify each step before moving on.

From here, the next logical step is to take one of your own ROS2 packages, write a Dockerfile for it, build the image, and run it in a container. Apply the volume mount so you can edit code on the host while building inside the container. Once that workflow is comfortable, Docker stops feeling like an extra layer of complexity and starts feeling like the way things should have always worked.

Hector Eduardo Tovar Mendoza, RoBorregos, Tecnologico de Monterrey

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.