ROBOTICS

Devcontainers for ROS2 + Nav + Simulation + CI

July 7, 2026

by

Martin Llofriu Alonso

Devcontainers for ROS2 + Nav + Simulation + CI

In my previous article, I argued in favor of using dev containers for robotics development. I demonstrated how it can enhance both the developer experience and that of the AI agents. I also argued that one can reuse the same dev container to create an initial version of a CI. If you haven't read that one yet, I'd recommend you start by doing that.

Now, in this article, I provide details on how this can be done, accompanied by an implementation that has been released as open source by my team Focus.

Our dev container

You can find the dev container we use for ROS2 development available here.

It provides:

  • a fully equipped ROS2 Jazzy distribution,
  • rviz2,
  • Gazebo Harmonic,
  • Nav2, and
  • a few tools we have found useful to develop

As stated in the readme file, you'll need Docker installed. If you are using Windows, you will also need WSL and Docker Desktop running. You can find more details here.

After installing those, you can:

  • Add it as a submodule to your repo by doing git submodule add https://<youruser>@bitbucket.org/focusuy/ros-devcontainer.git .devcontainer
  • Just copy all files to a .devcontainer folder.

Then, open it in VSCode/Cursor. Accept when prompted to reopen using a devcontainer.

This step can take up to 10 minutes on a modern computer. But only the first time, when the Docker image is first built.

The dev container is also packed with TurtleBot packages, so you can easily smoke-test the whole setup.

If you execute the following, you can test the setup:

export TURTLEBOT3_MODEL=waffle
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

Refer to the readme file if you are experiencing any trouble.

A Nav2 demo

We implemented a little TurtleBot-based Nav2 demo to showcase the dev container. You can get the source here.

After opening the demo in the devcontainer, you'll have to install all dependencies:

scripts/rosdep-install.sh

Then build the packages:

scripts/build.sh

Finally, source the new workspace:

source install/setup.zsh

The demo runs Nav2 on the TurtleBot robot. To try it out, you can execute

ros2 launch turtlebot_nav_sim turtlebot_nav_sim.launch.py send_to_goal:=True

In this case, a TurtleBot is spawned in a house-like environment, shown in the Gazebo GUI. The robot perception and world model are displayed in an RViz2 instance. The Nav2 stack is also fired up. Then, a goal is commanded as part of the same launch, and the robot moves to that destination.

Headless

The same simulation can be executed in headless mode, which runs significantly faster and makes it reusable for testing purposes:

ros2 launch turtlebot_nav_sim turtlebot_nav_sim.launch.py send_to_goal:=True headless:=True

To make sure things are working correctly, you can open a separate RViz to peek at what's going on:

rviz2 -d src/turtlebot_nav_sim/rviz/nav.rviz

You should see the same thing as you did in the previous section.

Turning it into an integration test

This simulation can be set up as an integration test to make sure changes don't break basic navigation.

If you take a look at test_turtlebot_nav_sim_launch.py, you'll see a pytest that uses launch_pytest to launch the simulation, command the robot to go to a certain point, and assert that the navigation system responds as expected in a couple of places.

The test can be run with:

scripts/test.sh

The output is not super fun, but lets you know things were alright:

➜ ws git:(main) colcon test
Starting >>> turtlebot_nav_sim
[Processing: turtlebot_nav_sim]
Finished <<< turtlebot_nav_sim [58.6s]
Summary: 1 package finished [58.7s]

If you want to see more details, you can run the test by calling pytest directly, asking it to output to the console directly (-s):

python3 -m pytest src/turtlebot_nav_sim/test/test_turtlebot_nav_sim_launch.py::test_navigation_system -v -s

You should see something like this:

================ 1 passed in 44.58s ================

Putting it all together in a CI

Once we have all this infrastructure in place, turning it into a continuous integration system (CI) is not difficult.

First, we create a script that performs all the tasks we have done so far, including installing dependencies, building, and running the test. You can run that script locally by doing:

scripts/ci.sh

Then, you add a file that will tell your platform what to run and when. At Focus, we use Bitbucket, and we like to run our CI systems in local runners:

  • It improves execution speed,
  • gives access to local hardware for hardware-in-the-loop testing when needed.

The file bitbucket-pipelines.yml defines what to do to validate a PR:

  • Build the dev container that serves as an execution environment
  • Use it to run ci.sh
  • Fail if anything goes wrong

script:
 # Build the devcontainer Docker image with commit hash
 - docker build -t devcontainer-ci:${BITBUCKET_COMMIT} -f .devcontainer/Dockerfile .
 # Run the CI script inside the devcontainer
 - docker run --rm -v $(pwd):/home/ws -w /home/ws devcontainer-ci:${BITBUCKET_COMMIT} scripts/ci.sh

If everything goes all right, you'll get a successful pipeline. It's also fun to break the system and corroborate that it fails, try it out.

If anything goes wrong with it, it can be reproduced locally and introspected, without any gap between environments.

Conclusions

We have now made our ROS2 Jazzy dev container with Nav2 and Gazebo publicly available. I hope you enjoy it, and pull requests are most welcome!

In this article, we've covered building an integration test based on a simulation. We then plugged it into a CI system. We have seen how dev containers can not only streamline development setup, but also provide a means to implement the CI execution environment. Sharing the same environment means the results are easily reproducible.

I hope you have enjoyed this tutorial and that you can reuse (and even contribute to!) our devcontainer. Let us know in the comments if you're planning to do so - happy to help in case you find any blockers.