Creating a ROS or ROS2 workspace in Docker (Part 1)
At AICA, we focus on simplifying the programming of industrial robotic arms. For some years now, the Robot Operating System (ROS) has been widely use by academics and industrial alike to create their robotic tasks and standardize their developed algorithms. With ROS2 LTS versions being released, you might want to include this framework into your arsenal.
If you are like us, and have tried to setup ROS on a computer, you have probably been puzzled by how not straightforward this is. Now, imagine having to deal with algorithms that run on a specific version of ROS or a robot that have been hooked on a computer that have not been updated for years by fear of it not working anymore. Well, we've got you covered. Thanks to containerized solution like Docker you can create and deploy ROS workspaces in minutes. Without further ado let's dive into it.
Disclaimer: This tutorial is not an introduction to ROS or Docker. For this we refer to the plethora of articles on both topics. Please not that we are using Docker here as our container solution thanks to the availability of official ROS images that drastically simplify the process. The proposed solution works on a Linux based host machine. It will require modifications to work on Windows or Mac systems.
ROS images on DockerHub
Assuming that you have already Docker installed on your machine (if not you can follow the guide here), the first step is to find an image that will serve as a base. Luckily, official ROS images are released and regularly updated as soon as a new version comes in. However, they are dispersed in multiple repository on DockerHub and it is sometime confusing to understand which image you need.
The official ROS repository on DockerHub
ros on DockerHub contains all the images you need to create your application. If you are familiar with ROS nomenclature, you will find again the bare-bones or core version of each release. Be aware that they do not include all the tools like rviz or gazebo you might need (those tools actually need some tricks to work in Docker, we will cover this in Part 2 of this tutorial series). If you are unsure, we recommend you to simply use the full version. It is easy to recognize them, their tags are simply the name of the ROS version you are looking for. For example, if you need noetic, the latest version of ROS simply do:
docker pull ros:noetic
Here you are, you have downloaded a full working version of ROS noetic. Using it in practice it not that easy, we will come back to this. ROS2 images are also available and are just as easy to get.
Nightly images
If you are a developer and want to use the latest version before it is even officially released you might be interested in the nightly versions. Those are located in different repositories. For ROS1 you will need to go to osrf/ros and for ROS2 to osrf/ros2. The rest is similar and the images are used in the same way.
All good. Now let's see how we can actually use them and create our ROS workspace.
Setting up a ROS workspace in a container
If you are here, it is to actually do something with those images. When you install ROS on your computer, you usually create a ROS workspace. So, we will do exactly that, but inside a Docker container.
Let us start by creating a Dockerfile that will contain all the installation steps. The first thing is to decide which base image to use:
FROM ros:kinetic
Now if you want to be a bit more generic you can use Docker ARG command:
ARG BASE_IMAGE=ros
ARG BASE_TAG=noeticFROM ${BASE_IMAGE}:${BASE_TAG}
The first two lines are to set a default version, but we will see later how to pass the arguments when building the image. Then, we want to specify that the shell is not interactive. We had some issues in the past when building our images and this helped a lot. It prevents bash to ask for user input, that would break the building process otherwise.
ENV DEBIAN_FRONTEND=noninteractive
By default Docker creates the container as root. We do not recommend that for using ROS and it is much better to create a user in the container that share the same rights as the host user. This is especially better if you use this image for development. So we start by installing sudo and creating a new user:
RUN apt-get update && apt-get install -y \
wget \
git \
bash-completion \
build-essential \
sudo \
&& rm -rf /var/lib/apt/lists/*# Now create the same user as the host itself
ARG UID=1000
ARG GID=1000
RUN addgroup --gid ${GID} ros
RUN adduser --gecos "ROS User" --disabled-password --uid ${UID} --gid ${GID} ros
RUN usermod -a -G dialout rosADD config/99_aptget /etc/sudoers.d/99_aptget
RUN chmod 0440 /etc/sudoers.d/99_aptget && chown root:root /etc/sudoers.d/99_aptget
As you have noticed we also install some packages like git or wget. Everytime we install a package we clear the apt cache with
rm -rf /var/lib/apt/lists/*
It reduces the image size and is good practice to do, especially when combined with grouped installation of all the packages at the beginning of the Dockerfile. The second part takes care creating a user with the same UID and GID as the host user. Those will be specified at build time. As you can see, we also use a set of config files to setup the user. You can create a config folder and copy the following:
mkdir config
echo "ros ALL=(ALL) NOPASSWD: ALL" > config/99_aptget
This will allow the creation of the user without a password. Now let's create the workspace:
# Choose to run as user
ENV USER ros
USER ros # Change HOME environment variable
ENV HOME /home/${USER} # workspace setup
RUN mkdir -p ${HOME}/ros_ws/src
The next lines will initialize the workspace and they will be different whether you are using ROS1 or ROS2. Let us start with ROS1:
WORKDIR ${HOME}/ros_ws/src
RUN /bin/bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash; catkin_init_workspace"
WORKDIR ${HOME}/ros_ws
/bin/bash -c "source source /opt/ros/${ROS_DISTRO}/setup.bash; catkin_make"
${ROS_DISTRO} is an environment variable already defined by ROS so you don't need to modify it. Voila, your workspace is now initialized and compiled. If you are using ROS2 images you would do:
WORKDIR ${HOME}/ros_ws
/bin/bash -c "source source /opt/ros/${ROS_DISTRO}/setup.bash; colcon build --symlink-install"
Finally, we need to setup the environment variables and modifying the bashrc file as we would do in a normal installation.
# set up environment
COPY config/update_bashrc /sbin/update_bashrc
RUN sudo chmod +x /sbin/update_bashrc ; sudo chown ros /sbin/update_bashrc ; sync ; /bin/bash -c /sbin/update_bashrc ; sudo rm /sbin/update_bashrc# Change entrypoint to source ~/.bashrc and start in ~
COPY config/entrypoint.sh /ros_entrypoint.sh
RUN sudo chmod +x /ros_entrypoint.sh ; sudo chown ros /ros_entrypoint.sh ;
We use two scripts here that you can copy paste and put in the config folder. First, we start with entrypoint.sh:
#!/bin/bash
set -e # setup environment
source $HOME/.bashrc # start in home directory
cd
exec bash -i -c $@
And last but not least a script to modify the bashrc:
# Adding all the necessary ros sourcing
echo "" >> ~/.bashrc
echo "## ROS" >> ~/.bashrc
echo "source /opt/ros/$ROS_DISTRO/setup.bash" >> ~/.bashrc
echo "source ~/ros_ws/devel/setup.bash" >> ~/.bashrc
Note that for ROS2, the last line should be changed to:
echo "source /home/ros/ros_ws/install/setup.bash" >> ~/.bashrc
We finish by a bit of cleaning and by calling the entrypoint:
# Clean image
RUN sudo apt-get clean && sudo rm -rf /var/lib/apt/lists/* ENTRYPOINT ["/ros_entrypoint.sh"]
CMD ["bash"]
Now, to compile the image you will need to invoke the docker build command. As it will include some arguments we recommend to create a script. We usually put a script build.sh alongside our Dockerfile containing:
REBUILD=0
while getopts 'r' opt; do
case $opt in
r) REBUILD=1 ;;
*) echo 'Error in command line parsing' >&2
exit 1
esac
done
shift "$(( OPTIND - 1 ))" if [ $# -eq 0 ] ; then
echo 'Specifiy the ros distrib to use: e.g. melodic, noetic...
fi BASE_IMAGE=ros
BASE_TAG=$1docker pull ${BASE_IMAGE}:${BASE_TAG}NAME=ros_wsUID="$(id -u $USER)"
GID="$(id -g $USER)" if [ "$REBUILD" -eq 1 ]; then
docker build \
--no-cache \
--build-arg BASE_IMAGE=${BASE_IMAGE} \
--build-arg BASE_TAG=${BASE_TAG} \
--build-arg UID=${UID} \
--build-arg GID=${GID} \
-t ${NAME}:${BASE_TAG} .
else
docker build \
--build-arg BASE_IMAGE=${BASE_IMAGE} \
--build-arg BASE_TAG=${BASE_TAG} \
--build-arg UID=${UID} \
--build-arg GID=${GID} \
-t ${NAME}:${BASE_TAG} .
fi
The first block handles the possibility to rebuild the image from scratch (along with the no-cache option in docker build). This can be useful, especially when you clone repositories in the container and you know that a new version of them is available. Without this, Docker will simply use the cache and you will not pull the latest version of your repositories. Then, the script also let you choose which ROS version to use as base image. Finally, the docker pull command is not necessarily needed as Docker will automatically download the latest version if not present on your computer. However, it will not update it if you already have it, preventing you to get the latest updates. Using this docker pull ensures that you always have the latest version of the image.
And here you are, a fully compiled workspace for both ROS and ROS2. Now, to be useful we need to be able to add packages in there. So let's do that.
Mounting volumes and run script
For a workspace to be functional you might want to create a folder, shared between the host and the docker container. We usually create a source folder that will be mounted at runtime. In order to simplify the runtime command you might want to create a run script:
#!/bin/bash
NAME=ros_ws # replace by the name of your image
TAG=noetic # the tag of your built imagemkdir -p source# create a shared volume to store the ros_ws
docker volume create --driver local \
--opt type="none" \
--opt device="${PWD}/source/" \
--opt o="bind" \
"${NAME}_src_vol"
xhost +
docker run \
--net=host \
-it \
--rm \
--volume="${NAME}_src_vol:/home/ros/ros_ws/src/:rw" \
"${NAME}:${TAG}"
The workspace will be directly mounted and matched to ros_ws/src folder in the container. In the run command we also use the net=host argument to share the same network as the host machine. The -it argument will start an interactive shell in the container when running the script. You could also directly call a command like roslaunch or roscore. In that case, you simply need to put it after the “${NAME}:${TAG}”.
Go ahead and try to put a package in the source folder. You will see it appearing on both the host and the container and you will be able to invoke catkin (or colcon) in the container to build your workspace. Now, if you want to have it directly built when building the image you need to add the following lines to your Dockerfile just before building the workspace:
WORKDIR ${HOME}/ros_ws/
COPY --chown=${USER} ./source/ ./src/
Note that every time you modify or add something in the source folder, Docker will notice the changes and will reinvoke the COPY command at build time, creating a new version of the image. This can quickly take some disk usage. Remember to monitor this and to clean the old version of your images:
Docker system prune
With all this you have a fully functional ROS workspace, with the ROS version of your choice, that you can deploy in minutes.
What’s next? Well, if you try to start rviz in the container, it will simply fail. For applications that needs to have access to the X server you need some extra commands. We will cover this in Part 2 of this tutorial.
We are looking forward to your comments and have fun deploying your workspaces.