Blog

Docker on Robots: When Containers Help and When They Quietly Hurt Performance

Tom Cosgrove,

Reading Room: The Fictionlab Blog

TL;DR: Docker on a mobile robot gives you reproducibility, clean multi-version ROS 2 setups, and reliable CI, but it can complicate DDS discovery, add GPU friction, and slow boot on ARM SBCs. Use containers for the perception and application stack, keep drivers and real-time control on the host where practical, and configure ROS 2 networking deliberately, often with --network host and a matching ROS_DOMAIN_ID. On a Leo Rover with Raspberry Pi 4, container cold start can add several seconds, while DDS latency can remain close to native performance when host networking is used. This article shows how to measure overhead, a working Dockerfile for a mobile stack, and the cases where containers quietly hurt docker ros 2 robot performance.

Why docker ros 2 robot performance matters on real hardware

Containers are a common packaging format for ROS 2 because they solve a real problem: ROS distributions change, Ubuntu base images drift, and researchers need to reproduce a colleague’s stack from six months ago. On a workstation, the runtime cost of Docker is usually negligible. On a mobile robot with a Raspberry Pi 4, a Jetson Orin Nano, or a Rockchip SBC, the picture can change. CPU is limited, memory is shared with the kernel and middleware, and communication and startup delays can matter when a robot is moving.

The question is not whether to use Docker. The question is which parts of your stack belong inside a container and which parts belong on the host. Fictionlab’s team runs both configurations on the Leo Rover platform in research deployments, and the answer depends on what you optimize for: reproducibility, latency, or hardware access.

When containers help your ROS 2 stack

Docker earns its place when the pain of environment drift outweighs the runtime overhead. In practice, this happens in several concrete situations.

  • Reproducibility across machines. A Dockerfile based on ros:humble-ros-base-jammy, preferably pinned to an immutable image digest, with explicit package versions gives you a more consistent environment on a developer laptop, a CI runner, and the robot running the same CPU architecture.
  • Multiple ROS 2 distributions on one robot. If you need Humble for a stable navigation stack and Jazzy for a newer perception node, containers let you install and run both without dependency conflicts on the host. Communication between them still requires compatible message definitions and DDS configuration.
  • CI and simulation. GitHub Actions or GitLab CI can pull the same image used on the robot and run Gazebo or unit tests against it. This is where containers pay off with the least friction.
  • Field updates. Pulling a new image tag over LTE can be safer than running apt upgrade on a robot in a forest. If the new image fails, you can roll back to the previous image tag.
  • Team isolation. Two researchers can share the same Leo Rover, each running their own container with their own dependencies, without changing the host installation.

These benefits are real and measurable in developer hours saved. They are why Docker is commonly used in modern ROS 2 development and deployment workflows.

When containers quietly hurt performance

The failure modes of Docker on robots are rarely dramatic. They show up as intermittent DDS discovery failures, missed camera frames, or a robot that takes 40 seconds instead of 8 to reach the ready state. Watch for these patterns.

DDS discovery and network latency

ROS 2 uses DDS, which relies on multicast for peer discovery by default. Docker’s default bridge network can complicate multicast discovery between nodes inside a container and nodes on the host or another network, depending on the DDS implementation, interfaces, firewall rules, and Docker configuration. The simplest fix is often --network host, which puts the container on the host network stack and usually makes multicast discovery behave like a native process. Alternatives include configured unicast peers, a Fast DDS Discovery Server, or an appropriate network design such as macvlan. These alternatives require deliberate configuration; a discovery server also becomes infrastructure that must be monitored.

On a Leo Rover with Raspberry Pi 4 and Cyclone DDS, compare native and containerized nodes using the same message type, middleware configuration, and network. With host networking, steady-state latency can be close to native performance. With the default bridge network, discovery and communication behavior should be tested under load because results vary with the network and DDS configuration.

GPU and device access

If your robot uses a Jetson for perception, passing the GPU into a container requires the NVIDIA Container Toolkit and a Jetson-compatible container image, such as an appropriate nvcr.io/nvidia/l4t-base or l4t-jetpack image. The container’s CUDA, TensorRT, and L4T/JetPack userspace must be compatible with the host JetPack release; they do not simply need to match the host driver version exactly. Getting this wrong can cause initialization failures or force an inference framework to use a CPU execution provider, increasing inference latency substantially.

The same applies to USB devices, CAN interfaces, and GPIO. You need explicit --device flags or, less securely, --privileged, and udev rules on the host still apply. This is manageable, but it is friction that a host install does not have.

Startup overhead on ARM SBCs

Cold-starting a container with a full ROS 2 stack on a Raspberry Pi 4 can take longer than starting the same nodes natively. The difference depends on image size, whether the image is already present locally, filesystem performance, launch dependencies, and the number of processes started. This matters if your robot autostarts on power-up and users expect it ready quickly.

Real-time control loops

If you run a motor control loop at 1 kHz with PREEMPT_RT, keep it on the host or on a dedicated microcontroller unless you have validated the container configuration under worst-case load. Containers share the host kernel and can run real-time workloads, but cgroup limits, CPU affinity, capabilities, and scheduling configuration make the timing model more complex. Fictionlab’s approach on custom platforms built through custom robotics projects is to keep firmware and low-level control on the host or on a dedicated microcontroller, and containerize only the higher-level stack.

How to measure Docker overhead on your robot

Before you decide, measure. The following procedure works on any Debian-based robot with ROS 2 Humble or Jazzy installed both natively and in a container.

  1. Install Docker and pull ros:humble-ros-base on the robot.
  2. Run a native talker: ros2 run demo_nodes_cpp talker.
  3. In a second terminal, run a containerized listener with host networking: docker run --rm --network host ros:humble-ros-base ros2 run demo_nodes_cpp listener.
  4. Verify messages arrive. If they do not, check ROS_DOMAIN_ID, DDS middleware settings, network interfaces, and firewall rules.
  5. Measure latency with ros2 topic delay /chatter, where available, or with a custom timestamped publisher and subscriber.
  6. Repeat with the default bridge network and compare.
  7. Measure cold start: time docker run --rm --network host your-image ros2 launch your_pkg bringup.launch.py versus time ros2 launch your_pkg bringup.launch.py on the host.

Log the numbers. If the container adds less than 10% to your critical path and gives you reproducibility, use it. If it adds 30% and you already have a stable environment, the tradeoff is different.

A working Dockerfile for a mobile robot stack

The following Dockerfile is a starting point for a ROS 2 Humble mobile robot stack. It installs Nav2 and a camera driver, and sets up a non-root user for safer runtime. Your workspace packages must have all of their dependencies declared and available before colcon build can succeed.

Save this as Dockerfile:

FROM ros:humble-ros-base-jammy
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
    ros-humble-navigation2 \
    ros-humble-nav2-bringup \
    ros-humble-rmw-cyclonedds-cpp \
    ros-humble-v4l2-camera \
    ros-humble-image-transport-plugins \
    python3-colcon-common-extensions \
    && rm -rf /var/lib/apt/lists/*
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
ENV ROS_DOMAIN_ID=0
RUN useradd -m -s /bin/bash robot && \
    echo "source /opt/ros/humble/setup.bash" >> /home/robot/.bashrc
WORKDIR /home/robot/ws
COPY --chown=robot:robot ./src ./src
USER robot
RUN . /opt/ros/humble/setup.sh && colcon build --symlink-install
CMD ["bash", "-lc", "source install/setup.bash && ros2 launch your_pkg bringup.launch.py"]

Build and run:

docker build -t my-robot-stack:humble .
docker run --rm -it \
  --network host \
  --device /dev/video0 \
  --device /dev/ttyUSB0 \
  -v /dev/shm:/dev/shm \
  my-robot-stack:humble

The -v /dev/shm:/dev/shm mount can be useful when processes in different containers or on the host must share a shared-memory transport, but it is not universally required. Containers already have their own /dev/shm, and shared-memory support and configuration differ between DDS implementations. For example, Fast DDS supports shared-memory transport, while Cyclone DDS shared-memory transport depends on the installed version and configuration. Test the transport you actually use, and consider the security implications of sharing the host shared-memory namespace.

How to split your stack between host and container

A practical split that Fictionlab’s team uses on research deployments looks like this.

  • Host: firmware bridge (micro-ROS agent or serial driver), motor controller, IMU driver, network configuration, systemd services for autostart, watchdog.
  • Container: perception, SLAM or localization, Nav2, application logic, teleop bridge, and camera drivers where device passthrough has been validated.
  • Optional second container: data recording (rosbag2), which can be started and stopped independently without touching the main stack.

This split keeps the parts that need direct hardware access simple and stable, and lets you iterate quickly on the parts that change often. It is the same pattern used in many field robotics research applications where reproducibility of the perception stack matters more than shaving milliseconds off the control loop.

FAQ

Does Docker add latency to ROS 2 topics?

With --network host and a correctly configured DDS transport, added steady-state latency is often small for typical topic rates. Measure it on your hardware and workload. Without host networking, DDS discovery and communication may require additional configuration and can be less reliable if multicast or interfaces are misconfigured.

Can I run Nav2 in a container on a Raspberry Pi 4?

Yes. Nav2 can run in a container on Raspberry Pi 4 with acceptable performance for indoor and slow outdoor navigation, provided that the map size, sensor rates, costmaps, and planner configuration fit the available CPU and memory. Startup and runtime CPU use should be measured on the target robot.

How do I access a GPU from a ROS 2 container on Jetson?

Install the NVIDIA Container Toolkit, use an l4t-jetpack or other Jetson-compatible base image matching your JetPack/L4T release, and use the runtime option supported by your installed NVIDIA Container Toolkit, such as --runtime nvidia or --gpus all. Verify GPU access by checking that CUDA or TensorRT initializes successfully and that your inference framework selects a GPU execution provider. nvidia-smi is generally not available as a GPU verification tool on Jetson devices.

Should I use Docker Compose or plain Docker for robots?

Docker Compose is useful when you have multiple containers (perception, navigation, recording) that need to start together. For a single-container stack, plain docker run managed by a systemd unit is often simpler. Startup time depends mainly on the image, application initialization, and orchestration configuration rather than Compose alone.

What about Podman or rootless Docker?

Both can run ROS 2 workloads, but rootless setups can have important limitations for host networking, multicast, device access, and privileged operations. USB, CAN, and GPIO access require careful user-namespace, udev, and group-permission configuration. For robots that require broad hardware access, a rootful container runtime is often operationally simpler, though it should be secured appropriately.

Does containerization help with over-the-air updates?

Yes. Pulling a tagged image and restarting a systemd service can be more atomic than apt upgrade. Keep the previous image tag on disk so you can roll back if the new one fails to start.

Is there a case where containers are the wrong choice entirely?

For hard real-time control loops with PREEMPT_RT kernels, or for very small SBCs with under 1 GB of RAM, the overhead and complexity may not be worth it unless the configuration has been thoroughly validated. In those cases, use apt pinning and Ansible for reproducibility instead.

Where to go next

If you want to try this on real hardware, the Leo Rover has a documented ROS 2 stack with native and container-oriented deployment materials. Setup guides, Dockerfiles, and integration examples are available at docs.fictionlab.pl.


Read more