Docker for ROS 2 Guide 2026Dockerfile · DevContainer · GPU · Compose
Complete 2026 guide to containerizing ROS 2 robots with Docker. Covers multi-stage Dockerfile, VS Code DevContainer with one-click environment, NVIDIA GPU passthrough for SLAM and inference, docker-compose for full Nav2 + SLAM stacks, X11 forwarding for RViz2, and production best practices.
Install Docker & NVIDIA Runtime
Install Docker Engine on Ubuntu 24.04
Install Docker Engine (not Desktop) for headless robot development.
# Remove old versions
sudo apt remove -y docker docker-engine docker.io containerd runc
# Add Docker repo
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Add user to docker group (no sudo needed after re-login)
sudo usermod -aG docker $USER
newgrp docker
# Verify
docker run --rm hello-world⚡ After usermod, log out and back in for group membership to take effect.
Enable GPU Access (NVIDIA Container Toolkit)
Required for GPU-accelerated ROS 2 nodes — SLAM, neural networks, sensor simulation.
# Install NVIDIA Container Toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt update
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Test GPU access in container
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smiMulti-Stage Dockerfile
Build stage compiles the workspace; runtime stage copies only the install/ directory into a lean image.
Dockerfile — ROS 2 Jazzy Multi-Stage
Two-stage build: compile workspace with build tools in stage 1, copy only install/ into lean runtime image in stage 2.
# syntax=docker/dockerfile:1
ARG ROS_DISTRO=jazzy
# ===== Stage 1: Build =====
FROM ros:${ROS_DISTRO} AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-colcon-common-extensions \
python3-rosdep \
python3-vcstool \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /ros2_ws
COPY src/ src/
RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \
rosdep update && \
rosdep install --from-paths src --ignore-src -y
RUN . /opt/ros/${ROS_DISTRO}/setup.sh && \
colcon build \
--symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release \
--event-handlers console_direct+
# ===== Stage 2: Runtime =====
FROM ros:${ROS_DISTRO} AS runtime
COPY --from=builder /ros2_ws/install /ros2_ws/install
RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc && \
echo "source /ros2_ws/install/setup.bash" >> /root/.bashrc
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["bash"]💡 Multi-stage build shrinks the final image by 60-80% — build tools don't ship to the robot.
entrypoint.sh
Sources ROS 2 and workspace setup files before any command runs.
#!/bin/bash
set -e
source /opt/ros/jazzy/setup.bash
if [ -f /ros2_ws/install/setup.bash ]; then
source /ros2_ws/install/setup.bash
fi
exec "$@"VS Code DevContainer
DevContainers give every team member an identical ROS 2 environment — open the repo in VS Code, click "Reopen in Container", and get a fully configured dev shell in 60 seconds.
.devcontainer/devcontainer.json
VS Code DevContainer for ROS 2 — one click to get a full dev environment with extensions pre-installed.
{
"name": "ROS 2 Jazzy",
"image": "osrf/ros:jazzy-desktop",
"runArgs": [
"--network=host",
"--ipc=host",
"--pid=host",
"--privileged",
"--gpus=all"
],
"containerEnv": {
"DISPLAY": "${localEnv:DISPLAY}",
"ROS_DOMAIN_ID": "42",
"RCUTILS_COLORIZED_OUTPUT": "1"
},
"mounts": [
"source=${localWorkspaceFolder},target=/ros2_ws/src/${localWorkspaceFolderBasename},type=bind",
"source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind,readonly"
],
"postCreateCommand": "sudo apt update && rosdep update && rosdep install --from-paths /ros2_ws/src --ignore-src -y",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-iot.vscode-ros",
"twxs.cmake",
"ms-vscode.cpptools-extension-pack"
],
"settings": {
"python.defaultInterpreterPath": "/usr/bin/python3",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}✅ --network=host lets ROS 2 DDS discovery work without manual domain configuration.
docker-compose — Full Robot Stack
docker-compose.yml — Full Robot Stack
Compose file launching robot_state_publisher, Nav2, and SLAM as separate containers sharing a single DDS domain.
version: "3.9"
x-ros-common: &ros-common
image: my_robot:latest
network_mode: host
ipc: host
environment:
- ROS_DOMAIN_ID=42
- RCUTILS_COLORIZED_OUTPUT=1
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix:ro
restart: unless-stopped
services:
robot_state_publisher:
<<: *ros-common
command: >
bash -c "source /ros2_ws/install/setup.bash &&
ros2 run robot_state_publisher robot_state_publisher
--ros-args -p robot_description:='$(xacro /robot.urdf.xacro)'"
volumes:
- ./urdf:/urdf:ro
slam_toolbox:
<<: *ros-common
command: >
bash -c "source /ros2_ws/install/setup.bash &&
ros2 launch slam_toolbox online_async_launch.py
use_sim_time:=false"
depends_on:
- robot_state_publisher
nav2:
<<: *ros-common
command: >
bash -c "source /ros2_ws/install/setup.bash &&
ros2 launch nav2_bringup navigation_launch.py
use_sim_time:=false
params_file:=/config/nav2_params.yaml"
volumes:
- ./config:/config:ro
depends_on:
- slam_toolbox
foxglove_bridge:
image: ghcr.io/foxglove/ros-foxglove-bridge:latest
network_mode: host
environment:
- ROS_DOMAIN_ID=42
command: ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765💡 Use `docker compose up -d` to start the full stack. `docker compose logs -f nav2` to follow logs.
Production Best Practices
Cache apt layers
Place RUN apt-get install after COPY package.xml to maximize cache hits. Source code changes won't invalidate the apt cache.
Use .dockerignore
Add build/ log/ .git/ .vscode/ to .dockerignore to prevent bloating the build context.
X11 display forwarding
Run `xhost +local:docker` on host before starting container to allow GUI apps (RViz2, Gazebo) to open.
ROS_DOMAIN_ID isolation
Set different ROS_DOMAIN_ID on each robot/sim to avoid DDS crosstalk between parallel containers.
Volume vs COPY for workspace
Use volume mounts for active development (edits reflected immediately). Use COPY + colcon build for production/CI images.
Multi-arch builds
Build for Raspberry Pi / Jetson ARM: `docker buildx build --platform linux/arm64 -t my_robot:arm64 .`