Skip to main content
🐳 Docker GuideUpdated June 2026

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.

multi-stage buildDevContainerGPU passthroughdocker-composeX11

Install Docker & NVIDIA Runtime

Install Docker Engine on Ubuntu 24.04

Install Docker Engine (not Desktop) for headless robot development.

bash
# 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.

bash
# 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-smi

Multi-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.

dockerfile
# 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.

bash
#!/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.

json
{
  "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.

yaml
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 .`

Related Guides