ROS 2 Docker Guide 2026
Docker is the standard way to ship reproducible ROS 2 environments — isolating dependencies, enabling CI, and simplifying deployment on robots. This guide covers official images, multi-stage builds, GPU passthrough, docker compose, and VS Code devcontainers.
Official ROS 2 Docker Images
| Image tag | Contents | Use case |
|---|---|---|
| ros:jazzy | ros-base (minimal) | Base for custom builds |
| ros:jazzy-ros-base | ros-base + rosdep | Build environment |
| ros:jazzy-ros-core | roscpp rclpy only | Smallest runtime |
| ros:jazzy-desktop | ros-base + rviz2 + rqt | Dev/demo workstation |
| ros:jazzy-desktop-full | desktop + sim packages | Full simulation |
| osrf/ros:jazzy-desktop-nvidia | desktop + CUDA + cuDNN | Isaac ROS / GPU SLAM |
# Quick start — run a ROS 2 shell docker run -it --rm ros:jazzy bash # Source and check source /opt/ros/jazzy/setup.bash ros2 topic list
Multi-Stage Dockerfile Pattern
# syntax=docker/dockerfile:1
ARG ROS_DISTRO=jazzy
# ── Stage 1: dependency cache ──────────────────────────────
FROM ros:{ROS_DISTRO{"}"}-ros-base AS deps
WORKDIR /ros2_ws
# Copy only package manifests to cache rosdep installs
COPY src/my_robot/package.xml src/my_robot/package.xml
COPY src/my_msgs/package.xml src/my_msgs/package.xml
RUN apt-get update && rosdep update && \
rosdep install --from-paths src --ignore-src -y && \
rm -rf /var/lib/apt/lists/*
# ── Stage 2: build ─────────────────────────────────────────
FROM deps AS builder
COPY src/ src/
RUN . /opt/ros/{ROS_DISTRO{"}"}/setup.sh && \
colcon build --symlink-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release
# ── Stage 3: runtime (smallest possible) ──────────────────
FROM ros:{ROS_DISTRO{"}"}-ros-core AS runtime
WORKDIR /ros2_ws
# Copy only install artifacts, not build/
COPY --from=builder /ros2_ws/install ./install
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["ros2", "launch", "my_robot", "bringup.launch.py"]entrypoint.sh
#!/bin/bash set -e source /opt/ros/jazzy/setup.bash source /ros2_ws/install/setup.bash exec "$@"
Build & Run
# Build all stages docker build --target runtime -t my_robot:latest . # Run with host network (ROS 2 discovery uses multicast) docker run -it --rm --network host my_robot:latest # Connect a second ROS 2 terminal to the same network docker run -it --rm --network host ros:jazzy bash # inside: source /opt/ros/jazzy/setup.bash && ros2 node list # Pass environment variables docker run -it --rm \ --network host \ -e ROS_DOMAIN_ID=42 \ my_robot:latest
GPU Passthrough (Isaac ROS / CUDA)
# Prerequisite: nvidia-container-toolkit installed on host # https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html # Run with full GPU access docker run -it --rm \ --gpus all \ --network host \ nvcr.io/nvidia/isaac/ros:jazzy-isaac_ros_base_aarch64 \ bash # Verify CUDA inside container nvidia-smi python3 -c "import torch; print(torch.cuda.is_available())" # X11 display forwarding (for GUI tools on Linux host) docker run -it --rm \ --gpus all \ --network host \ -e DISPLAY=$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ osrf/ros:jazzy-desktop-nvidia \ rviz2
docker compose — Full Simulation Stack
# compose.yaml
services:
gazebo:
image: osrf/ros:jazzy-desktop-full
network_mode: host
environment:
- DISPLAY={DISPLAY{"}"}
- ROS_DOMAIN_ID=0
volumes:
- /tmp/.X11-unix:/tmp/.X11-unix
command: >
bash -c "source /opt/ros/jazzy/setup.bash &&
gz sim -r worlds/empty.sdf"
robot_bringup:
build:
context: .
target: runtime
network_mode: host
environment:
- ROS_DOMAIN_ID=0
depends_on:
- gazebo
command: ros2 launch my_robot sim.launch.py
nav2:
image: ros:jazzy-ros-base
network_mode: host
environment:
- ROS_DOMAIN_ID=0
depends_on:
- robot_bringup
command: >
bash -c "source /opt/ros/jazzy/setup.bash &&
ros2 launch nav2_bringup bringup_launch.py
use_sim_time:=True
params_file:=/config/nav2_params.yaml"
volumes:
- ./config:/config# Start stack docker compose up # Rebuild after Dockerfile change docker compose up --build # Teardown docker compose down
VS Code Dev Container
// .devcontainer/devcontainer.json
{
"name": "ROS 2 Jazzy",
"image": "ros:jazzy-desktop",
"runArgs": ["--network=host", "--privileged"],
"containerEnv": {
"ROS_DOMAIN_ID": "0",
"DISPLAY": "{localEnv:DISPLAY{"}"}",
"RCUTILS_COLORIZED_OUTPUT": "1"
},
"mounts": [
"source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind"
],
"postCreateCommand": "sudo apt-get update && rosdep update && rosdep install --from-paths src --ignore-src -y",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.cpptools",
"ms-python.python",
"smilerobotics.urdf",
"nonanika.ros",
"ms-iot.vscode-ros"
]
}
}
}rosdep in Containers
# Initialize rosdep (first time per container layer) rosdep init || true # silently ignore if already initialized rosdep update # Install deps from workspace cd /ros2_ws rosdep install \ --from-paths src \ --ignore-src \ --rosdistro jazzy \ -y # In CI (no interactive prompts) DEBIAN_FRONTEND=noninteractive rosdep install \ --from-paths src --ignore-src -y
Common Issues
ROS 2 nodes can't discover each other across containers
Use --network host (not bridge). ROS 2 discovery (DDS multicast) requires L2 broadcast — bridge networks block it.
rviz2 / GUI won't open in container
Pass -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix. Run xhost +local:docker on host first.
CUDA not available inside container
nvidia-container-toolkit not installed on host, or --gpus all not passed. Run nvidia-smi on host to verify driver.
rosdep init fails: 'oops in update'
Behind a corporate proxy — set http_proxy/https_proxy env vars, or use ROSDEP_SOURCES_YAML to point to a local mirror.
Layer cache invalidated on every build
COPY src/ too early. Copy package.xml files first (deps stage), run rosdep, THEN COPY src/ in builder stage.
Next Steps
- Add a GitHub Actions CI workflow that builds and runs
colcon testinside your Docker image. - Use launch_testing inside the container for integration tests with a real DDS layer.
- Pin image digests (
ros@sha256:...) in production Dockerfiles to prevent unexpected upstream changes.