Tutorial · Beginner · 1 hour

Install ROS 2 on a Raspberry Pi and Build a Workspace

Get ROS 2 running on a Raspberry Pi: choose the right OS, install from apt, build a colcon workspace, and make a laptop and robot find each other over Wi-Fi.

Published

Introduction

Nothing in ROS 2 is difficult until you try to install it on a robot, at which point three unrelated things go wrong at once: the operating system is the wrong one, the workspace will not build in the Pi’s memory, and the laptop cannot see the robot’s nodes. None of these are ROS problems exactly, which is why the official tutorials do not warn you about them.

This gets you from a blank SD card to ros2 topic list working from your laptop against nodes running on the robot.

Pick the operating system, not just the board

ROS 2 binaries are built for Ubuntu, not Raspberry Pi OS. Raspberry Pi OS is Debian-based, so the apt packages simply are not there, and every workaround — building from source, Docker, conda — costs you hours before you have written any robot code.

So install Ubuntu Server 24.04 LTS (64-bit) and match it to the ROS 2 release built against it. At the time of writing that pairing is Jazzy Jalisco, supported to 2029; check the installation page for whatever is current when you read this and substitute the name throughout.

Three things to get right at flash time:

  • 64-bit. ROS 2 arm64 packages exist; armhf ones do not.
  • Server, not Desktop. A robot has no monitor, and the desktop environment costs RAM you want for vision.
  • Pi 4 with 4 GB, or a Pi 5. A 2 GB board runs ROS 2 fine but cannot comfortably compile it.

Install ROS 2 from apt

Add the ROS 2 apt source, then install. The repository setup is the part that changes between releases, so this is the shape rather than a snippet to memorise:

sudo apt update && sudo apt install -y software-properties-common curl
sudo add-apt-repository universe

# Fetch and install the package that registers the ROS 2 apt repository and key.
export ROS_APT_SOURCE_VERSION=$(curl -s \
  https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest \
  | grep -F "tag_name" | awk -F\" '{print $4}')
curl -L -o /tmp/ros2-apt-source.deb \
  "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo $VERSION_CODENAME)_all.deb"
sudo apt install -y /tmp/ros2-apt-source.deb

sudo apt update
sudo apt install -y ros-jazzy-ros-base ros-dev-tools

Note ros-base, not desktop. The desktop metapackage pulls in RViz and the demo GUIs, which a headless robot will never run and which take a long time to install on an SD card. Visualisation belongs on your laptop, where you install the full ros-jazzy-desktop.

ros-dev-tools is what gives you colcon and rosdep. Initialise rosdep once:

sudo rosdep init      # skip if it says the file already exists
rosdep update

Source the underlay, then your overlay

ROS 2 does nothing until its environment is sourced, and this is where most early confusion lives.

source /opt/ros/jazzy/setup.bash

That is the underlay — the installed distribution. Your own workspace, once built, is an overlay on top of it. Sourcing an overlay implicitly brings in the underlay it was built against.

echo "source /opt/ros/jazzy/setup.bash" >> ~/.bashrc

Putting the underlay in .bashrc is standard and safe. Putting your overlay there is a trap worth avoiding early: the day you have two workspaces, or you rebuild against a different distribution, every new shell silently starts with a stale environment and you debug a phantom for an afternoon. Source the overlay explicitly, per shell, in the terminal that needs it.

Create a workspace and a package

A colcon workspace is a directory with a src/ in it. That is the whole specification.

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws
ros2 pkg create --build-type ament_python --license Apache-2.0 my_robot

Then build and overlay it:

cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -y
colcon build --symlink-install
source install/setup.bash

Use --symlink-install. It links your Python files into the install space instead of copying them, so editing a node takes effect the next time you run it rather than after a rebuild. On a Pi, where a rebuild is slow, that is the difference between iterating and waiting.

After the build, ~/ros2_ws holds build/, install/ and log/ alongside src/. Only src/ belongs in version control.

Build without running the Pi out of memory

colcon build compiles packages in parallel, one process per core, and each C++ compile can take most of a gigabyte. On a four-core Pi that is four heavy compilers at once, and the build dies with a message about a killed process — or the whole board locks up.

MAKEFLAGS="-j1" colcon build --parallel-workers 1 --symlink-install

Slower, and it finishes. Pure-Python packages do not have this problem; it bites the moment you build anything with C++ in it. Adding a couple of gigabytes of swap works too, though on an SD card it is punishingly slow.

Talk to the robot from your laptop

ROS 2 has no master. Nodes find each other by DDS discovery over the network, which means a laptop and a Pi on the same LAN can form one graph with no configuration at all — when it works.

Set the same domain on both machines:

export ROS_DOMAIN_ID=42        # any value 0–101; the same on every machine

The domain partitions the network. Two robots in the same room on the default domain will hear each other’s topics and fight over them, which is a memorable way to learn what this setting does.

Diagram of a two-machine ROS 2 setup: a Raspberry Pi on the robot running camera, detector and drive nodes, and a laptop running RViz and command-line tools, joined by a Wi-Fi cloud labelled DDS discovery over multicast. Both machines are annotated with the same ROS_DOMAIN_ID, and a note marks the access point as the component that blocks multicast.
One ROS 2 graph across two machines: nodes on the robot, visualisation on the laptop, and a discovery step in between that depends on the access point forwarding multicast. Download SVG

Verify from the laptop, with a talker running on the Pi:

ros2 topic list
ros2 node list
ros2 topic echo /chatter

Make discovery survive Wi-Fi

ros2 topic list showing nothing while the Pi is plainly publishing is the single most common ROS 2 networking failure, and it is almost never ROS’s fault.

Discovery uses multicast, and many access points do not forward it. Consumer Wi-Fi routers frequently rate-limit or drop multicast, and “client isolation” or guest-network modes block client-to-client traffic outright. The nodes are running; the packets that would introduce them never arrive.

Work through it in this order:

  1. Same subnet? A laptop on 5 GHz and a Pi on a guest SSID are not on the same network, however similar the addresses look.
  2. Same ROS_DOMAIN_ID, in the shell that is actually running the node? An export in one terminal does not reach another, and it does not reach a systemd service.
  3. Try wired. If Ethernet works and Wi-Fi does not, you have proved it is multicast, not ROS.
  4. Check the firewall. sudo ufw allow in proto udp from <subnet> on both ends, or disable it while testing.
  5. Fall back to a discovery server. Fast DDS can use a known endpoint instead of multicast — set ROS_DISCOVERY_SERVER to the robot’s address on both machines. This is the reliable answer on a network you do not control.

One more, and it is a quiet one: keep the clocks in sync. TF2 compares timestamps across machines, and a Pi with no RTC boots to a wrong time. Install chrony on both ends, or every transform lookup fails with an extrapolation error that looks like a ROS bug.

Start the robot’s nodes on boot

A robot you have to SSH into to start is a robot that is switched off. A systemd unit fixes it:

# /etc/systemd/system/my-robot.service
[Unit]
Description=Robot bringup
After=network-online.target

[Service]
User=ubuntu
Environment=ROS_DOMAIN_ID=42
ExecStart=/bin/bash -lc 'source /opt/ros/jazzy/setup.bash && \
  source /home/ubuntu/ros2_ws/install/setup.bash && \
  ros2 launch my_robot bringup.launch.py'
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now my-robot
journalctl -u my-robot -f

The Environment= line matters more than it looks. systemd does not read your .bashrc, so a service inherits none of the ROS variables your interactive shell has — which is exactly why a robot that works over SSH goes silent when it starts itself.

When it goes wrong

Symptom Usually
ros2: command not found The underlay was never sourced in this shell
ros2 topic list empty from the laptop Multicast blocked, or mismatched ROS_DOMAIN_ID
Build killed part-way with no error Out of memory — limit parallel workers
Your code change had no effect Built without --symlink-install, or overlay not re-sourced
Package not found after building it Overlay not sourced, or sourced before the build finished
TF errors about extrapolation into the future Clocks not synchronised between machines
Works over SSH, dead on boot systemd service missing the ROS environment

With the environment behaving, the next question is what to put in it — which is a design problem rather than an install one. Nodes, topics, services, and actions covers choosing between the communication patterns, and TF2 coordinate frames covers the part that catches people after that.

Explore the graph

Part of these builds

Projects and learning paths that include this tutorial.

Further reading

References