The setup wraps a simple Python app in a ROS2 node and then builds it into a Docker image. Each image can then be run inside their own Docker containers which can then "talk" to each other as long as they are on the same network.
The sample ros-base config creates 2 containers, myapp and myapp-tester, each of which is a ROS2 node. The myapp-tester sends one-way commands and calls services to the myapp node, which can also reply with messages of its own. The APIs of the myapp node is exposed to the myapp-tester using a myapp_apis package. The sample node+app can be configured to fit different use-cases and applications, and can scale up to communicate with many more nodes.
The ROS aspect provides a standard messaging format and interface for different apps. It isolates the app development with the communication and coordination between other apps and resources. The app developer does not need to care about how their app will talk with other apps because that would be the responsibility of the node developer.
The Docker aspect facilitates deployment as the host machine ideally only needs to install Docker and docker-compose
to run the nodes. All the node + app setup steps and dependencies are contained in the building of the image and in the running of the containers. The containers can also be run on a local server, on a remote machine (for CI/CD pipelines), or hosted on cloud platforms.
- Host Machine
- Docker 19.x
- docker-compose 1.24.x
- ROS Docker Image
- osrf/ros2:nightly (sha256: 6776bc1367b7248a709cf3d13559cc33ee2b693415382dee1f89429e141a2aa7)
- ROS 2 Eloquent
- Python 3.7.3
- CMake 3.10.x
$ docker build \
--file config/ros-base.dockerfile \
--tag <registry>/ros-base:latest \
.
<registry>
is the URL of a remote/local registry, or blank
Sample Build Output
...
Step 14/25 : RUN tree -L 3 ${WS}
---> Running in 2428ebe13532
/ws/ros-base
├── myapp
│ ├── myapp
│ │ ├── __init__.py
│ │ ├── app.py
│ │ └── app_node.py
│ ├── package.xml
│ └── setup.py
├── myapp_apis
│ ├── CMakeLists.txt
│ ├── msg
│ │ ├── AppCommand.msg
│ │ └── AppData.msg
│ ├── package.xml
│ └── srv
│ └── GetAppData.srv
├── myapp_tester
│ ├── myapp_tester
│ │ ├── __init__.py
│ │ └── test_node.py
│ ├── package.xml
│ └── setup.py
├── requirements.txt
└── run-node.sh
...
Step 21/25 : RUN /bin/bash -c "source /opt/ros/eloquent/setup.bash; colcon build;"
---> Running in 704465103ad2
Starting >>> myapp
Starting >>> myapp_apis
Starting >>> myapp_tester
Finished <<< myapp [1.71s]
Finished <<< myapp_tester [1.95s]
Finished <<< myapp_apis [8.60s]
Summary: 3 packages finished [9.14s]
...
- Create the node+app under the src directory
- Create the node+app .dockerfile under the config directory
- Build the Docker image
$ docker build \ --file config/<target>.dockerfile \ --tag <registry>/ros-base:<version> \ .
<registry>
is the URL of a remote/local registry, or blank<version>
is the semantic versioning of the app
$ docker-compose --file test/ros-base.yml up
Sample Run Output:
Creating myapp_tester ... done
Creating myapp ... done
Attaching to myapp_tester, myapp
myapp | APP is UP.
myapp_tester | APP_TESTER is UP.
myapp_tester | APP_TESTER will send 5 msgs
myapp_tester | APP_TESTER sent: x=12.0,y=23.0,z=39.0
myapp_tester | APP_TESTER sent: action=hello!
myapp_tester | APP_TESTER requested for user_id=5
myapp | APP received: x=12.0,y=23.0,z=39.0,action=hello!
myapp | APP applied command with params x=12.0,y=23.0,z=39.0,action=hello!
myapp | APP is getting data for user=5
myapp_tester | APP_TESTER sent: x=16.0,y=22.0,z=38.0
myapp_tester | APP_TESTER sent: action=hello!
myapp_tester | APP_TESTER requested for user_id=4
myapp_tester | APP TESTER received: x=3.0, y=7.0, z=12.0
myapp | APP received: x=16.0,y=22.0,z=38.0,action=hello!
myapp | APP applied command with params x=16.0,y=22.0,z=38.0,action=hello!
myapp | APP is getting data for user=4
- Create the node+app docker-compose .yml file under the test directory
- Run
$ docker-compose --file test/<target>.yml up
- When finished, make sure to
down
the containers.$ docker-compose --file test/<target>.yml down
In the current structure, the nodes and apps are COPY
-ed over to the Docker container, and compiled/built also inside a container. To modify the codes for the nodes and the apps, one option is to run an interactive Docker container, modify the codes directly, then commit
a new image with a new version.
# --- from the host machine ---
$ docker run \
-it \
--name myapp \
192.168.1.65:5006/ros-base:0.0.1 \
/bin/bash
# --- inside the myapp container ---
$ vim myapp/myapp/app.py
$ colcon build
$ myapp_node &
$ myapp_tester &
# ..verify that the app is working..
$ exit
# --- back in the host machine ---
$ docker commit \
-m "Fixed request/response" \
myapp \
192.168.1.65:5006/ros-base:0.0.2
This approach is problematic if you also need to commit the modified codes to a git repository. In this case, you'll have to copy the modified codes from the container to your host machine holding the git repo. A possible workaround to that is to git clone
the codes inside the container. That could work, but then you'll have to setup git
inside the container, then copy and store your private SSH keys to access the repo from inside the container (so it would be shareable as an image).
A better option is to create a container from the Docker image with a mounted directory from the host machine. This would put a copy of the version-controlled codes inside the container (replacing the original from the image), and then allow you to commit the changes afterwards.
# --- from the host machine ---
$ docker run \
-it \
--name myapp \
-v /host/path/to/repo/src/ros-base:/ws/ros-base \
192.168.1.65:5006/ros-base:0.0.2
# --- inside the myapp container ---
$ vim myapp/myapp/app.py
$ colcon build
$ myapp_node &
$ myapp_tester &
# ..verify that the app is working..
$ exit
# --- back in the host machine ---
$ docker build config/myapp.dockerfile ... # rebuild a new version
$ git commit ... # commit the modified codes
If you are using Visual Studio Code, check the Using Visual Studio Code section on how to edit the codes inside the container directly from your editor.
Note that we reuse the .dockerfile to rebuild the image. It is possible just use docker commit
on the container, but then the new image would now expect you to always mount
the codes every time you run it in a container. Also, by reusing the .dockerfile, we ensure that we can check that we rebuild the modified image from scratch every time.
-
When mounting the codes from the host machine into the Docker container, you can use VS Code's Docker and Remote Development extensions to connect to the Docker container and edit the codes directly from the IDE.
-
Linting for Python files
- You'll need to point the Python intellisense and linter engines to the built site-packages:
- Update settings.json inside .vscode:
"python.autoComplete.extraPaths": [ "/opt/ros/eloquent/lib/python3.6/site-packages/", "/path/to/host/src/ros-base/install/myapp_apis/lib/python3.6/site-packages" ]
- Create a .env at the root of the project (or wherever the setting
python.envFile
points to):PYTHONPATH=$PYTHONPATH:/opt/ros/eloquent/lib/python3.6/site-packages/:/path/to/host/src/ros-base/install/myapp_apis/lib/python3.6/site-packages
- ROS
- Configure ROS to use Python3.7
- Move services and topic names to myapp_apis
- Tools
- Add VS Code workspace settings
- Docs
- ROS topics and services
- API (auto-generated, ex. Sphinx for Python)
- What is ROS?
- ROS 2 Overview
- Samples/Tutorials
- Libraries