Skip to content

jfrascon/robotics_dockers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

robotics_dockers

Tools and scripts for setting up Docker on Ubuntu hosts and generating ready-to-use ROS 2 development images.


Table of contents

  1. Installing Docker
  2. Docker and the host filesystem owner matching problem
  3. builder — generating ROS 2 Docker images
  4. Launching graphical user interfaces (GUIs) in Docker containers

Installing Docker

It is recommended to install Docker using the official Docker repository maintained by Docker, Inc., rather than using the default Ubuntu packages or Snap. This ensures you get the latest version of Docker with all features and security updates. If you already have Docker packages installed from the default Ubuntu repositories or via Snap, remove them and next install Docker from the official repository with the provided script install_docker.sh.

# Remove Docker packages installed via apt
dpkg -l | grep docker- | awk '{print $2}' | xargs -I% --no-run-if-empty sudo apt-get purge --auto-remove -y %

# Remove Docker packages installed via snap
snap list | grep docker | awk '{print $1}' | xargs -I% --no-run-if-empty sudo snap remove %

Then use the scripts/install_docker.sh script provided in this repository. It configures your package manager to use the official Docker repository maintained by Docker, Inc., installs the latest Docker tools, and adds your user to the docker group:

bash scripts/install_docker.sh

After the script completes, log out and log back in (or restart) for the group membership to take effect.

Troubleshooting: permission denied on Docker socket

If you see an error like:

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock

your user is not yet in the docker group. Fix it with:

sudo usermod -aG docker $USER

Then log out and back in, or run newgrp docker to apply the change to the current session.


Docker and the host filesystem owner matching problem

What is a UID in Linux?

UID stands for user identifier — a number assigned by the Linux kernel to each user. It is the actual identity used for access control: file ownership, process permissions, and resource access are all based on UIDs, not on usernames. Usernames are just human-readable labels that tools like ls translate from the underlying UID.

id
uid=1000(myuser) gid=1000(myuser) groups=1000(myuser),27(sudo),998(docker)

UIDs 1–999 are typically reserved for system accounts. On Ubuntu, the first interactive user created during installation gets UID 1000.

The problem

Most Docker images only provide the root user (UID 0). Running as root inside a container is a security risk — a mistake as root has no safety net. Beyond security, there is a practical issue with bind mounts.

When you mount a directory from your host into a container (-v /host/path:/container/path), files created inside the container are owned by whatever UID is active in the container. If that UID does not match your UID on the host, you will not be able to edit or delete those files from your host OS without using sudo.

This is a well-known problem:

The solution used here

The images generated by this project do not hardcode a UID. Instead, the container starts as root, reads HOST_UID and HOST_UPGID from the environment, remaps the internal development user to those values at runtime, and then drops to that user. This means files created inside the container will always be owned by your host UID, regardless of what UID value that is.

See Running the container for how to pass HOST_UID and HOST_UPGID.


builder — generating ROS 2 Docker images

The builder/ directory contains the tooling to generate a complete Docker build context for a ROS 2 development image: a Dockerfile, a build.py script, a docker-compose-dev.yaml, and all supporting resources.


Prerequisites

  • Docker Engine
  • Python 3.10+
  • Python packages: jinja2

If you intend to use an NVIDIA GPU:


Quick start

# See help:
python3 builder/create_docker_files.py -h

# Generate the build context:
python3 builder/create_docker_files.py myuser jazzy myorg/ros2-jazzy:latest --output ~/my_docker

# Optionally edit extra packages before building:
echo 'apt-get install -y --no-install-recommends ffmpeg' >> ~/my_docker/.resources/extra.d/apt_packages.sh
echo 'ruff==0.15.14' >> ~/my_docker/.resources/extra.d/requirements.txt
echo 'fd-find' >> ~/my_docker/.resources/extra.d/rust_packages.txt

# Build the image:
cd ~/my_docker
python3 build.py

# Build with ROS package dependencies resolved via rosdep:
python3 build.py --pkgs-dir /path/to/your/workspace/src

# Save the build log:
python3 build.py 2>&1 | tee /tmp/my_build.log

create_docker_files.py reference

usage: create_docker_files.py [-h] [-b BASE_IMG]
                               [--use-host-nvidia-driver] [--output OUTPUT]
                               image_main_user ros_distro img_id
Argument Description
image_main_user Username for the development user inside the container
ros_distro ROS distro: humble, jazzy
img_id Docker image name and tag, e.g. myorg/ros2-jazzy:latest
-b BASE_IMG Base Docker image. Default: ubuntu:X.Y matched to the ROS distro
--use-host-nvidia-driver Enable NVIDIA GPU access via the host driver
--output DIR Directory where the output is written. Default: a temporary directory under /tmp

Available ROS distros:

  • humble - ROS 2, Ubuntu 22.04
  • jazzy - ROS 2, Ubuntu 24.04

Custom base image:

You can pass any Docker image as the base, for example a CUDA image:

python3 builder/create_docker_files.py myuser jazzy myorg/ros2-jazzy:latest \
    -b nvidia/cuda:12.5.0-devel-ubuntu24.04 \
    --use-host-nvidia-driver \
    --output ~/my_docker

build.py reference

usage: build.py [-h] [-c] [-p] [--pkgs-dir DIR] ...
Argument Description
-c, --cache Reuse cached Docker layers
-p, --pull Pull the latest base image before building
--pkgs-dir DIR Path to the ROS packages directory on the host (e.g. ~/workspace/src). If provided, rosdep installs the dependencies of those packages into the image

build.py writes directly to your terminal. Pipe through tee to keep a log file:

python3 build.py 2>&1 | tee /tmp/my_build.log

Customizing the output

After running create_docker_files.py, the output directory contains a .resources/extra.d/ folder with three files you can edit before building:

extra.d/apt_packages.sh

Shell script executed as root after ROS is installed. Add apt packages, third-party repositories or any other system-level setup here.

#!/usr/bin/env bash
apt-get update
apt-get install -y --no-install-recommends libopencv-dev ros-jazzy-moveit

# Adding a third-party repository:
curl -sSL https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add -
add-apt-repository "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main"
apt-get update && apt-get install -y clang-18

If you generated without --use-host-nvidia-driver, this file already contains the default Mesa packages script. Edit it as needed.

extra.d/requirements.txt

Standard pip requirements file. Installed as --user for the container user.

numpy
torch==2.3.0
--index-url https://download.pytorch.org/whl/cu121
torchvision
git+https://github.com/user/repo.git@main

extra.d/rust_packages.txt

One Rust crate per line. Two modes:

# Binary mode (fast): downloads a pre-built binary
ripgrep
fd-find

# Source mode (slow): compiles from source, use when features are needed
source: broot --features clipboard

If the file contains only comments or blank lines, the Rust toolchain is not installed.


Startup scripts (entrypoint.d)

When the container starts, the custom entrypoint runs all scripts found in ~/.entrypoint.d/ in alphabetical order. Scripts with a .sh extension are sourced (they run in the same process and can set environment variables used by later scripts). Scripts with a .txt extension are printed to stdout.

Every file must follow the naming convention NN-name.sh or NN-name.txt, where NN is exactly two digits (e.g. 01, 50, 99). Files that do not match this pattern cause the container to abort at startup.

Two scripts are always included and are mandatory:

Script Purpose
00-checks.sh Validates the naming convention of all files in entrypoint.d/ and checks that 99-uid-gid-adapt.sh is present. Runs first.
99-uid-gid-adapt.sh Remaps the internal user UID/GID to match HOST_UID/HOST_UPGID and performs the final exec that starts the user session. Runs last.

When --use-host-nvidia-driver is passed, an additional script is included:

Script Purpose
98-gpu-driver-check.sh Warns at startup if the NVIDIA driver is not visible in the container (e.g. --gpus all was omitted or the NVIDIA Container Toolkit is not installed). Based on the upstream NVIDIA script.

Adding your own startup scripts

You can add custom scripts to .resources/entrypoint.d/ before running build.py. They will be copied into the image and executed at every container startup.

# Example: print a banner at startup
cat > .resources/entrypoint.d/10-banner.txt <<'EOF'
Welcome to my ROS 2 development container!
EOF

# Example: set custom environment variables at startup
cat > .resources/entrypoint.d/20-env.sh <<'EOF'
export MY_VAR=hello
EOF

Rules to follow:

  • Filename must be NN-name.sh or NN-name.txt with exactly two digits.
  • Do not use 00 (reserved for checks) or 99 (reserved for UID/GID adaptation).
  • If --use-host-nvidia-driver was used, do not use 98 either.
  • .sh scripts are sourced — they run in the entrypoint process. Keep them fast and side-effect-free (no exit, no long-running commands).

Using a base image that has its own entrypoint

This project always sets its own entrypoint (/usr/local/bin/entrypoint.sh), which overrides any entrypoint defined by the base image. If the base image you chose performs initialization logic that you want to preserve (common with NVIDIA images, for example), do not rely on the base entrypoint being called automatically.

Instead:

  1. Find the relevant script(s) in the base image entrypoint.
  2. Copy or adapt that logic into a new .sh file in .resources/entrypoint.d/ using an appropriate numeric prefix (e.g. 10-nvidia-env.sh).
  3. Run build.py as usual — the script will be picked up automatically.

To inspect what entrypoint a base image defines:

# Show the entrypoint declared in the image metadata:
docker inspect <base_img> --format '{{.Config.Entrypoint}}'

# Read the contents of that script:
docker run --rm --entrypoint cat <base_img> /path/to/entrypoint.sh

NVIDIA GPU support

Pass --use-host-nvidia-driver when generating. This configures the docker-compose file to use deploy.resources with the NVIDIA driver.

You also need to provide the GID of the render device so all processes (including those started by VS Code) can access /dev/dri/renderD*:

# Add the render device GID to a .env file next to docker-compose-dev.yaml:
echo "RENDER_GID=$(stat -c %g /dev/dri/renderD128)" >> .env

Running the container

The output directory contains a docker-compose-dev.yaml. Copy it next to your workspace and create a .env file with the required variables:

# .env
HOST_UID=1000          # your UID: id -u
HOST_UPGID=1000        # your primary GID: id -g
WORKSPACE=/home/myuser/my_workspace   # path to your workspace on the host
RENDER_GID=992         # required if --use-host-nvidia-driver was used: stat -c %g /dev/dri/renderD128

Then:

docker compose -f docker-compose-dev.yaml up

The container starts as root, remaps the internal user to your HOST_UID/HOST_UPGID, and then drops to the development user. Files created inside the container will be owned by you on the host.


Examples

The builder/examples/ directory contains reference scripts for installing specific Mesa driver variants (default, Kisak PPA, Oibaf PPA, locked versions). These are not used automatically, copy the relevant parts into your extra.d/apt_packages.sh.


Launching graphical user interfaces (GUIs) in Docker containers

Running GUI applications inside a Docker container requires giving the container access to the host's display server.

IMPORTANT: This graphical setup has been tested on X11 and is not currently validated on Wayland hosts. On a Wayland host you may need XWayland — the compatibility layer that allows X11 applications to run inside a Wayland session. On Ubuntu it is usually provided by the xwayland package and started automatically by most desktop sessions when needed.

The generated docker-compose-dev.yaml already mounts /tmp/.X11-unix and forwards DISPLAY, which covers the X11 transport layer. What remains is telling the X server on the host to allow connections from the container process.

Option 1: xhost (quick, per-session)

Run this command on the host before starting the container:

xhost +SI:localuser:$(id -un)

This grants the current user's X11 access token to local connections without opening the display to everyone. When you are done with the container, revoke it:

xhost -SI:localuser:$(id -un)

This approach works immediately with no extra configuration to docker-compose-dev.yaml. It is the simplest way to test GUI applications. The downside is that you need to run it every time you open a new session or restart the machine.

Option 2: XAuth cookie service (persistent, recommended)

A more robust approach uses XAuth cookies: a file containing authentication tokens that the X server accepts. The scripts/install_docker_gui_support.sh script sets this up once on the host:

bash scripts/install_docker_gui_support.sh

The script:

  1. Installs /usr/local/bin/set_xauth_cookies.sh, which generates ${XDG_RUNTIME_DIR}/cookies.xauth — a file containing the X11 authentication tokens for the current session.
  2. Installs and enables a systemd user service (set-xauth-cookies.service) that runs the above script automatically every time a graphical session starts, via graphical-session.target. This works across all major desktop environments (GNOME, KDE, XFCE, etc.).
  3. Runs the script immediately so GUI support is available without requiring a re-login.

After installation, the service can be managed with:

systemctl --user restart set-xauth-cookies   # force cookie regeneration
systemctl --user status  set-xauth-cookies   # check current status
journalctl --user -u     set-xauth-cookies   # view logs

To use this approach with your container, add the following volume to your docker-compose-dev.yaml:

volumes:
  - ${XDG_RUNTIME_DIR}/cookies.xauth:/tmp/.cookies.xauth:rw

And add these environment variables:

environment:
  - XAUTHORITY=/tmp/.cookies.xauth
  - QT_X11_NO_MITSHM=1

The generated docker-compose-dev.yaml does not include this volume by default because the choice between xhost and the cookie service is left to the user. Add it manually once you decide to use Option 2.

Note: ${XDG_RUNTIME_DIR} is a per-user private directory managed by systemd-logind (typically /run/user/<UID>). Each user on the host has their own cookie file, so multiple users can run containers simultaneously without conflicts.

About

Generate ROS2 Docker dev images in minutes: configurable users, automatic UID/GID host matching, NVIDIA GPU and GUI support out of the box.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors