Docker and containers
Docker packages software with everything it needs (libraries, ROS 2, Python packages) into an image that runs the same way on a laptop, a CI runner and the robot. Strido uses one image, strido-ros:jazzy, for both the Gazebo simulation and the real robot.
In Strido
docker/ros2/Dockerfilebuilds the ROS 2 Jazzy + Gazebo Harmonic workspace image for amd64 and arm64 (Raspberry Pi 5, Apple silicon).docker/compose.yamldefines three services:ros(builds the image),sim-smoke(headless Gazebo walk test) androbot(runs on the Pi with access to/dev).docker/ros2/entrypoint.shsources ROS 2 and the workspace;docker/ros2/sim_smoke.shis the automated simulation test behindmake sim-gazebo.ROS_DOMAIN_IDdefaults to42(configuration).
Containers versus virtual machines
A virtual machine emulates a whole computer: its own kernel, virtual disk and virtual hardware. A container is just a group of normal processes on the host kernel, isolated with Linux namespaces (own file system view, process list, network) and limited with cgroups.
| Virtual machine | Container | |
|---|---|---|
| Kernel | Own guest kernel | Shares the host kernel |
| Start-up | Tens of seconds | About a second |
| Overhead | Reserved RAM and CPU | Almost none |
| Hardware access | Through emulated or passed-through devices | Host device files can be mapped in directly |
For a robot this matters: the Pi 5 has 4 GB of RAM in this build, and the mcu_bridge polls a USB serial port every 2 ms. A container gives near-native performance and direct access to /dev/strido_mcu; a VM would cost memory and add latency.
On macOS, Docker Desktop runs a small Linux VM and the containers live inside it. That is why the Gazebo test works on a Mac but USB devices are hard to reach there: you test simulation on the laptop and hardware on the Pi.
Images and layers
An image is a stack of read-only layers, one per file-changing instruction in the Dockerfile. Docker caches layers: if an instruction and everything before it is unchanged, the cached layer is reused. The Strido Dockerfile is ordered so that slow, rarely changing steps come first:
Editing a ROS node only invalidates the last few layers, so a rebuild after a code change takes seconds to minutes instead of re-downloading Gazebo.
Details worth noticing:
- Pinned dependencies. The Slamtec LiDAR driver is not packaged for Jazzy, so it is cloned at a fixed commit (
ARG SLLIDAR_COMMIT=...). Pinning means a rebuild next year produces the same driver. --no-depsinstall ofstrido-core. The core package (gait, kinematics, protocol, numpy policy runtime) has only NumPy as a runtime need, which apt already installed. No PyTorch, CadQuery or MuJoCo enters the robot image (ADR-003 in decisions).rm -rf /var/lib/apt/lists/*in the sameRUNasapt-get installkeeps the package index out of the layer. Deleting it in a later layer would not shrink the image, because earlier layers are immutable.- Multi-architecture.
ros:jazzy-ros-baseis published for amd64 and arm64, so the same Dockerfile builds natively on the Pi and on an Apple silicon Mac.
Containers at run time
A container is a running instance of an image plus a thin writable layer. Anything written inside disappears when the container is removed (--rm). That suits the robot: state that must persist (servo calibration) lives in the ESP32's flash, not in the container.
The entrypoint is three lines:
source /opt/ros/jazzy/setup.bash
source /ws/install/setup.bash
exec "$@"It makes every command you pass (for example ros2 launch ...) see both ROS 2 and the Strido packages, and exec replaces the shell so signals such as Ctrl-C reach ROS directly.
Compose: the repo's docker/compose.yaml
Docker Compose describes several containers in one YAML file so you do not have to remember long docker run command lines.
| Service | What it does | Key settings |
|---|---|---|
ros | Builds strido-ros:jazzy from docker/ros2/Dockerfile (context = repo root) | network_mode: host, ipc: host, ROS_DOMAIN_ID |
sim-smoke | Runs /sim_smoke.sh: Gazebo headless, stand up, walk 8 s at 0.15 m/s, check ≥ 0.4 m progress and sensor topics | exit code is the test result |
robot | Runs ros2 launch strido_bringup robot.launch.py on the Pi | privileged: true, /dev:/dev, host network |
Commands you will use:
docker compose -f docker/compose.yaml build ros # build or update the image
docker compose -f docker/compose.yaml run --rm sim-smoke # headless Gazebo test (what make sim-gazebo runs)
docker compose -f docker/compose.yaml run --rm robot # on the Pi: bring up the robot
docker compose -f docker/compose.yaml run --rm ros bash # interactive shell with ROS 2 sourced
docker compose -f docker/compose.yaml run --rm ros \
ros2 run teleop_twist_keyboard teleop_twist_keyboard # drive from a second terminalmake sim-gazebo POLICY=<policy.npz> mounts a learned policy read-only into the container (-v ...:/policy.npz:ro) and sets POLICY=/policy.npz, which makes sim_smoke.sh launch policy_node instead of gait_node.
Why host networking and host IPC
ROS 2 discovers nodes with DDS multicast and, between processes on the same machine, can use shared memory. Docker's default bridge network hides multicast, and a private IPC namespace hides shared memory. network_mode: host and ipc: host put the container on the host's network and IPC so that a teleop container, the robot container and a laptop on the same Wi-Fi (same ROS_DOMAIN_ID) see each other's topics without extra configuration.
Use a different ROS_DOMAIN_ID per robot when several share a network:
ROS_DOMAIN_ID=43 docker compose -f docker/compose.yaml run --rm robotDevice access
A container cannot see host hardware unless you grant it. The robot service does it bluntly:
privileged: true
volumes:
- /dev:/devBind-mounting the whole /dev matters because the udev symlinks (/dev/strido_mcu, /dev/rplidar) are created by the host's udev, and a USB device that is unplugged and replugged gets a new device node. A fixed devices: list would only map nodes that exist when the container starts. privileged: true lifts the device cgroup restrictions so the new nodes are usable.
WARNING
A privileged container has nearly full control of the host. That is acceptable on a dedicated robot computer; do not use the robot service on your everyday laptop. For the simulation services, which need no hardware, no privileges are set.
The udev rules must be installed on the host, not in the image (Linux on the Pi).
What is not in the image
- Camera node. The Pi 5 camera stack needs the Raspberry Pi libcamera fork; the image has no camera node, and
robot.launch.pydefaults tocamera:=false. Buildcamera_roson the host or userpicam-vidwith a GStreamer bridge (software). - Firmware tools. PlatformIO is not installed; flash the ESP32-S3 from a development machine or install PlatformIO on the Pi (firmware).
- GUI. Gazebo with a window (
gazebo.launch.pywithoutheadless:=true) needs X11/Wayland forwarding into the container (simulation).
Check yourself
- Why does a container start faster and use less memory than a VM?
- Which Dockerfile change triggers the shortest rebuild, and why?
- What breaks if you remove
network_mode: hostfrom therobotservice? - Why does the
robotservice mount all of/devinstead of listing two devices? - Where must
99-strido.rulesbe installed for the container to see/dev/strido_mcu?
Answers
- It shares the host kernel: no guest OS to boot and no reserved RAM; it is just isolated processes.
- Changing files in
ros2/only invalidates theCOPY ros2/...layers and thecolcon buildafter them; the apt and Git layers come from the cache. - DDS discovery across containers and machines; teleop or RViz in another container or on a laptop would no longer see the robot's topics.
- udev on the host creates the symlinks and replugged devices get new nodes; a static device list would miss them.
- On the host, in
/etc/udev/rules.d/; the container sees the result through the/devbind mount.