[Robosynapse] Control Server: Running ROS2 on Docker
The first thing a robot manipulation system needs is a control server: the machine that sends commands to the arm and reads its state back. This post walks through how I set one up for two different arms, a UR7e and an AgileX Piper, as part of my ongoing robosynapse project. It covers running ROS2 in Docker, wiring up the network to the robot, and launching the drivers and MoveIt, in that order.
The overall shape is simple. The control server runs the robot driver and MoveIt inside a ROS2 Humble container, and passes commands down to the robot from the inference server above it, where the VLA model runs. The two servers may share one machine or sit on separate ones.
ur_robot_driver acts as the bridge that converts it into
ROS2 topics.
The reason to split the two servers is that their requirements pull in opposite directions. The control server has to sit physically next to the robot: everything that connects by cable hangs off it, from the ethernet link into the control box to the USB-CAN adapter. The inference server, on the other hand, only needs a GPU big enough for the VLA model and has no reason to be in the same room. It can just as well be a shared lab GPU box or a cloud instance.
0. Why ROS2 on Docker
Each ROS2 distribution (Humble, Iron, and so on) is tightly bound to a
particular Ubuntu release, and installing it straight onto the host tends to
pollute the machine. Using the osrf/ros:humble-desktop-full image
reproduces the same environment on any host, and it also makes it easy to give
each robot its own container isolated by ROS_DOMAIN_ID. That advantage grows
once you drive several robots from one server.
1. UR7e control server
1-1. Starting the ROS2 Humble container
Share the X11 socket so GUI tools like RViz work inside the container, and
start it with --net=host for DDS communication.
ROS2 Docker Entryxhost +local:root
docker run -it \
--name ros2_humble \
--privileged \
--net=host \
--ipc=host \
--gpus all \
-e DISPLAY=$DISPLAY \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v ~/ros2_ws:/root/ros2_ws \
--device /dev/video0:/dev/video0 \
--device /dev/input:/dev/input \
osrf/ros:humble-desktop-full
# reusing it later
docker start ros2_humble
docker exec -it ros2_humble /ros_entrypoint.sh bash
1-2. Installing the UR driver
The UR series is released into the official ROS distribution (rosdistro), so a single apt install is all it takes.
ROS2 UR Package Installsudo apt update
sudo apt install -y ros-humble-ur
sudo apt install -y ros-humble-ros2controlcli # for force control
One caveat: these packages land in the container's own filesystem under
/opt/ros/humble/..., so they disappear the moment you
rm the container. If you rebuild often, bake them into a
Dockerfile or commit the image.
1-3. Setting up the robot network
The control server and the robot have to share a switch, and the server's ethernet interface needs a static IP in the same range as the robot control box.
Robot networksudo ip addr add 172.16.0.10/24 dev enp1s0
ip -br addr # check
That assignment is lost on reboot, so pin it down with netplan.
/etc/netplan/01-robot.yamlnetwork:
version: 2
ethernets:
enp1s0:
addresses: [172.16.0.10/24]
sudo netplan apply
1-4. Launching the UR driver and MoveIt
UR Driver Init & MoveItsource /robo-synapse/config/setup_env.sh
ros2 launch ur_robot_driver ur_control.launch.py \
ur_type:=ur7e robot_ip:=172.16.0.3 launch_rviz:=false
ros2 launch ur_moveit_config ur_moveit.launch.py \
ur_type:=ur7e
Once both launches are up, you still have to press the play button on the
teach pendant before External Control actually starts. Forget
it and you end up with a driver that is running while the robot refuses to
move. In robosynapse I wrap all of this in scripts
(run_ur_driver.sh, run_ur_moveit.sh) that detect
whether the control and inference servers are split across machines.
2. Piper control server
The AgileX Piper works differently from the UR in two important ways.
- The package is not released into rosdistro. There is no
apt binary like
ros-humble-piper; you clone AgileX'spiper_rosfrom GitHub and build it with colcon yourself. - It talks over CAN rather than ethernet, which adds a step for bringing up the CAN interface through a USB-CAN adapter.
2-1. Starting the container
Start from the same ros:humble image as the UR, but mount
/dev and udev wholesale so the CAN device is reachable.
Piper Docker Entrydocker run -it \
--name ros2_humble \
--privileged \
--net=host \
--ipc=host \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v /dev:/dev \
-v /run/udev:/run/udev:ro \
-v $HOME/workspace:/root/workspace \
osrf/ros:humble-desktop-full
# in robosynapse this is a single script:
bin/control_server/create_docker.sh piper # container ros2_humble_piper, domain=42
2-2. Building piper_ros
ROS2 Piper Package Install# 1. clone the Piper ROS package
sudo chown -R $USER:$USER ~/workspace
git clone -b humble https://github.com/agilexrobotics/piper_ros.git ~/workspace/piper_ros
ls ~/workspace/piper_ros # OK if src/ can_activate.sh find_all_can_port.sh show up
# 2. install dependencies
apt update && apt install -y python3-pip can-utils ethtool \
ros-humble-ros2-control ros-humble-ros2-controllers ros-humble-controller-manager
pip3 install python-can scipy piper_sdk
# 3. colcon build
cd /root/workspace/piper_ros
source /opt/ros/humble/setup.bash
colcon build
source install/setup.bash
ros2 pkg list | grep piper # check
2-3. Bringing up CAN
Piper ships scripts that find the CAN port and bring it up at 1 Mbps. I use
can_piper0 as the default interface name.
CAN communicationcd /root/workspace/piper_ros
apt install -y iproute2
bash find_all_can_port.sh
bash can_activate.sh can_piper0 1000000 # interface name + bitrate
ip -br link show can_piper0 # confirm it is UP
candump can_piper0 # confirm the arm is sending frames (Ctrl+C)
If something looks wrong inside the container, you can verify from the host as well:
lsusb | grep -i can
ip -d link show can_piper0 | grep bitrate # bitrate 1000000
candump can_piper0
2-4. Launching the Piper driver and MoveIt
Piper Driver Init & MoveIt# 1. piper driver
cd /root/workspace/piper_ros
source install/setup.bash
source /root/workspace/robo-synapse/config/setup_env_container.sh
ros2 launch piper start_single_piper.launch.py \
can_port:=can_piper0 auto_enable:=true
# check the state
ros2 topic list
ros2 topic echo /joint_states_feedback
# 2. moveit
apt update && apt install -y ros-humble-moveit
ros2 launch piper_with_gripper_moveit demo.launch.py
auto_enable:=truematters. Leave it false and the driver comes up, but the motors are never enabled, so execute does nothing.
3. Bimanual Piper: separating the arms by container
The bimanual setup that runs two Pipers as a pair is still in progress. I will fill this section in once it settles.
Takeaways
- Don't install ROS2 on the host; use the
osrf/ros:humble-desktop-fullcontainer. Because of DDS,--net=hostis effectively mandatory. - Robots released into rosdistro, like the UR, are one apt line. Unreleased packages like Piper mean clone plus colcon build.
- For ethernet robots (UR), pin the static IP with netplan; for CAN robots
(Piper), verify the link first with
can_activateandcandump.
The next post will cover the inference server (VLA) setup that sits on top of this control server.
← Back to Blog