Writing Your Own Nodes

How to add your own ROS 2 package to the container. Covers only the container-specific parts — for ROS 2 concepts (nodes, topics, pub/sub) see the official tutorials.


How the workspace works

The ./workspace directory on your host is mounted into the container at /workspace:

host: ./workspace/        ←→   container: /workspace/
         src/                          src/         ← your packages
         install/          ←  colcon build writes here
         build/
         log/

You edit code on the host with your normal tools; the container builds and runs it. install/setup.zsh is sourced automatically when you open a new shell (see container_zshrc), so freshly built packages are immediately available.


1. Create a package

Open a shell in the container:

docker compose exec ros-gazebo zsh

Use the ROS 2 scaffolder — it generates a correct package.xml, setup.py, and resource markers in one step:

cd /workspace/src
ros2 pkg create --build-type ament_python my_package --node-name my_node

For C++ packages use --build-type ament_cmake; everything else below is identical.

The generated my_node.py is a stub main() that prints “Hi from my_package.” — enough to prove the build/run pipeline works.


2. Build

From /workspace in the container:

cd /workspace
colcon build --symlink-install

--symlink-install symlinks Python sources into install/ so edits on your host take effect without rebuilding. You still need to rebuild if you add new entry points or change setup.py.


3. Run

ros2 run my_package my_node

Expected output: Hi from my_package.

If you get Package 'my_package' not found, the overlay isn’t sourced. Open a new shell (auto-sources) or run source /workspace/install/setup.zsh manually.


4. Edit and iterate

Edit ./workspace/src/my_package/my_package/my_node.py on your host. For a minimal publisher:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class MyNode(Node):
    def __init__(self):
        super().__init__('my_node')
        self.pub = self.create_publisher(String, '/my_topic', 10)
        self.create_timer(1.0, self.tick)

    def tick(self):
        msg = String()
        msg.data = 'hello'
        self.pub.publish(msg)


def main():
    rclpy.init()
    rclpy.spin(MyNode())
    rclpy.shutdown()

Because of --symlink-install you don’t need to rebuild. Just re-run:

ros2 run my_package my_node

Verify from a second container shell:

docker compose exec ros-gazebo zsh
ros2 topic echo /my_topic

Package structure reference

workspace/src/my_package/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│   └── my_package            # empty marker — ros2 run finds packages via this
└── my_package/
    ├── __init__.py
    └── my_node.py

You rarely need to touch any of these by hand — ros2 pkg create and colcon build manage them.


This site uses Just the Docs, a documentation theme for Jekyll.