Part I: The ROS 2 stack · §5 of 43
Current container (openarm, image openarm-jazzy:rt2)
The container was created like this on 2026-10-10 (docker run is only run once per container; see below for how it was made):
docker run -it --name openarm --network host --ipc host --gpus all \
-e NVIDIA_DRIVER_CAPABILITIES=all -e __NV_PRIME_RENDER_OFFLOAD=1 -e __GLX_VENDOR_LIBRARY_NAME=nvidia \
-e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \
--cap-add SYS_NICE --ulimit rtprio=99 --ulimit memlock=-1 \
-v "$HOME/Documents/vault/Prandium/robot_scripts:/root/scripts" \
openarm-jazzy:rt2(Your user is in the docker group, so sudo isn’t needed.)
Containers and images on zeus:
| Name | What it is | Use? |
|---|---|---|
openarm (image openarm-jazzy:rt2) | current: real-time permissions + shared scripts folder | yes, the only one |
openarm-noshare (image openarm-jazzy:rt) | backup from 2026-10-10, real-time, no shared folder | no |
openarm-deprecated (image openarm-jazzy:built) | the original container, no real-time | no |
image openarm-jazzy:latest | probably the base image before the workspace was built (not verified) | — |
Never run two of these containers at once: they all use host networking and would fight over ROS and the CAN buses.
Daily use:
xhost +SI:localuser:root # on the HOST, once per login; lets the container open windows
docker start openarm # if it stopped (it stops when its main shell exits, e.g. at logout)
docker exec -it openarm bash # as many shells as you like; they start in /wsImage and container: which command moves you between the states.
stateDiagram-v2 direction TB image: image openarm-jazzy#colon;rt2 running: container openarm · running stopped: container openarm · stopped [*] --> image image --> running: docker run · once, flags fixed here running --> running: docker exec -it openarm bash · per terminal running --> stopped: main shell exits (e.g. logout) stopped --> running: docker start openarm stopped --> [*]: docker rm · ⚠ deletes every change
What each Docker command does (an image like openarm-jazzy:rt2 is a frozen template; a container like openarm is one instance of it, with its own writable filesystem):
| Command | Acts on | Creates a container? | What it does |
|---|---|---|---|
docker run | image | Yes | Creates a new container from the image and starts its main process (here, a bash shell). All flags (--network, --gpus, -e, -v, …) are fixed at this moment and cannot be changed later. Run it only once: a second run with the same --name fails, and with a different name it would create a separate container without any of your changes. |
docker start | stopped container | No | Restarts an existing container that has stopped. Everything done inside it (installed packages, build output, .bashrc edits) is preserved, along with the original flags. A container stops when its main process exits, i.e. when the shell opened by docker run -it is closed. |
docker exec | running container | No | Starts an additional process (e.g. bash) inside a running container. Use it once per terminal you need. Exiting an exec shell does not stop the container. |
docker ps -a --format '{{.Names}} {{.Status}}' shows every container’s state: Up … means running, Exited … stopped. docker rm <name> deletes a container and every change made inside it.
/root/.bashrc in the container already sources /opt/ros/jazzy/setup.bash and /root/ros2_ws/install/setup.bash, so every docker exec shell is ready to use. After rebuilding the workspace, run source /root/ros2_ws/install/setup.bash again in shells that were already open.
Why each flag matters:
| Flag | Why |
|---|---|
--network host | DDS discovery works across host and container. Also required for real hardware: SocketCAN interfaces (can0, can1) live in the host’s network namespace and are only visible in the container with host networking. |
--ipc host | Fast DDS shared-memory transport between processes. |
--gpus all + __NV_PRIME_RENDER_OFFLOAD=1 + __GLX_VENDOR_LIBRARY_NAME=nvidia | Hybrid-graphics laptop: forces RViz onto the NVIDIA GPU instead of failing on Mesa/iris. |
-e DISPLAY + X11 socket | GUI. Needs xhost +SI:localuser:root on the host. |
--cap-add SYS_NICE --ulimit rtprio=99 --ulimit memlock=-1 | Lets ros2_control_node run its loop with real-time priority (see below). The launch log then says Successful set up FIFO RT scheduling policy with priority 50. |
-v …/robot_scripts:/root/scripts | The shared scripts folder (see below). |
--cap-add NET_ADMIN (not used) | Only needed to configure can0/can1 from inside the container. On zeus they’re configured on the host. |
Why real-time scheduling, and why Docker doesn’t allow it by default
The controller manager runs one loop 750 times a second: read the motors over CAN → run the controllers → send the next command to every motor, i.e. 1.33 ms per cycle. A normal Linux process is scheduled “fairly”: when RViz, the browser or Docker want the CPU, the loop may have to wait, and a cycle that misses its slot is logged as Overrun detected! … missed cycles: 2. On real motors a late cycle means the next target arrives late and then jumps further, felt as small jerks. Real-time scheduling (SCHED_FIFO) means the loop always runs before every normal process; ros2_control asks for priority 50 automatically and only needs permission. It also locks its memory so it never waits for memory to be paged in.
Docker gives containers as few permissions as possible: CAP_SYS_NICE (the right to raise priority and use real-time scheduling) isn’t in the default set, the default rtprio limit is 0 (no real-time priority at all), and memlock allows only a few MB. That’s a deliberate safe default: a stuck real-time thread is never interrupted by normal programs and can freeze a CPU core. (The kernel keeps a safety net: real-time tasks may use at most 95 % of each second, /proc/sys/kernel/sched_rt_runtime_us.) The three flags grant exactly those permissions.
Shared scripts folder
A container’s files are isolated from the laptop by default; the only ways across are docker cp, working inside with docker exec, or a shared folder (bind mount, -v):
| On the laptop | In the container |
|---|---|
~/Documents/vault/Prandium/robot_scripts/ | /root/scripts/, also reachable as /ws/scripts (a link) |
Same folder: a file saved on one side is there on the other immediately. Write and edit scripts on the laptop, run them in the container. Files created inside the container belong to root on the laptop. Shells start in /ws, so bash scripts/openarm_launch.sh real works from there. The /ws/scripts link (ln -s /root/scripts /ws/scripts) lives in the container’s own filesystem. The scripts are described in §8.7 and in robot_scripts/README.md.
To copy other files out (e.g. a launch log): docker cp openarm:/root/<file> ~/Documents/…. Don’t browse Docker’s storage under /var/lib/docker directly.
Changing container flags later
Flags and shared folders can only be set by docker run, so changing them means a new container. Without losing anything done inside the current one:
# all ROS stopped (openarm_stop.sh), motor power off
docker stop openarm
docker commit openarm openarm-jazzy:<new-tag> # snapshot of everything inside
docker rename openarm openarm-<backup-name> # keep the old one as a backup
docker run -it --name openarm … <all the flags, old and new> … openarm-jazzy:<new-tag>Then check: ulimit -r; ulimit -l (99, unlimited), ls /root/scripts, git diff --stat in /root/ros2_ws/src/openarm_ros2 (the 3 local changes), nvidia-smi -L. Recreate the /ws/scripts link if the snapshot predates it.
Rebuilding the workspace
cd /root/ros2_ws
colcon build --symlink-install
source install/setup.bashDeprecation warnings from openarm_hardware (old on_init(HardwareInfo) signature, written for Humble) are expected and harmless on Jazzy. The zeus patches live in the source tree, so rebuilding keeps them.