devcontainer for ROS2 project
Published: 2022-08-26 14:42:01

# Summary
In this article, I want to share how to create your local development environment for ROS2 projects. Recent code development like the one with ROS2 framework has become very complex and heavily depending on OSS packages. VS Code devcontainer makes it much easier to create your local development environment.

# TOC
- [Overview](#overview)
- [devcontainer.json](#devcontainer)
- [Dockerfile](#dockerfile)
- [Directory structure](#directory)
  - [How to run the docker container development environment](#how)
  - [Where is the project directory?](#where)
    - [Experiment 1](#experiment1)
    - [Experiment 2](#experiment2)
    - [Experiment 3](#experiment3)

# Overview <a name="overview"></a>

**devcontainer** is one of Visual Studio Code Remote Development features that helps create local develop environment. Personally I love Windows OS and Visual Studio when developing .NET C# applications, but when in development with some other languages like Python, Typescript etc., VS Code devcontainer is really powerful. Here is my personal opinion of whe it is so useful.

- **Independent of your machine's operating system**: According to statistica.com, Windows has around 75 percent of the global market share of operating system in 2022. Combined with WSL(Windows Subsystem for Linux) technology, VS Code devcontainer enables you to run Linux development environment in your Windows machine.

- **Package verion combinations**: In general, you want to divide development environments depending on projects. For example, you can use `pyenv`, `virtualenv`, `conda` or whatever virtual environment you prefer for Python. However, when you want to combine the Python environment with other framework, for example ROS2, it is going to be more complex. VS Code devcontainer solves this problem by creating a just in time docker container for each development environment.

- **No need to install all dependencies from scratch**: To create your just in time development environment, you do not need to install all dependencies from scratch. You can utilize publicly available images from the well-known container registry. For example, I am using `ros:foxy-ros-base-focal` image for my ROS2 developenvironment - [Dockerfile](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer/Dockerfile)

- **Debugger**: Visual Studio Code supports debuggers for different languages. For this exmaple - [devcontainer.json](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer/devcontainer.json), it uses `ms-python` VS Code extension with which you can run a Python debugger in your local machine.

- **VS Code support and extensions**: VS Code has built-in support and so many extensions for different languages. You can use those support even in the docker container development environment of devcontainer.

# .devcontainer/devcontainer.json <a name="devcontainer"></a>

The sample [devcontainer.json](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer/devcontainer.json) looks like below.

- **dockerfile**: This points to `Dockerfile` located in the same folder as `devcontainer.json` under `.devcontainer` directory.
- **workspaceFolder**: The directory in the docker container where `.devcontainer` directory exists.
- **extensions**: VS Code extensions you want to use in the docker contaienr development environment.

```
{
  "name": "ROS devcontainer",
  "dockerFile": "Dockerfile",
  "workspaceFolder": "/workspaces/EdgeIntegrationTest/src/apps/FileGenerator",
  "settings": {},
  "extensions": [
		"ms-python.python"
	]
}
```

# .devcontainer/Dockerfile <a name="dockerfile"></a>

The sample [Dockerfile](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer/Dockerfile) under `.devcontainer` directory looks like below.

- `FROM ros:foxy-ros-base-focal`: In this sample, it uses that ros image as the base image. It bases on [ros:foxy-ros-core-focal](https://github.com/osrf/docker_images/blob/3f4fbca923d80f834f3a89b5960bad5582652519/ros/foxy/ubuntu/focal/ros-core/Dockerfile), which bases on [ubuntu:focal](https://github.com/tianon/docker-brew-ubuntu-core/blob/570d5970a8b18bc772ad2c3eb1ce8fd0887d991a/focal/Dockerfile), which is the same as `ubuntu:20.04`.
- `COPY requirements.txt ./`: Copy `requirements.txt` to `/app` in the docker container so the docker container can run `pip install` to set up necessary Python dependencies.
- `RUN echo 'source /opt/ros/foxy/setup.bash' >> /root/.bashrc`: According to ROS2 documentation [Configuring environment](https://docs.ros.org/en/foxy/Tutorials/Beginner-CLI-Tools/Configuring-ROS2-Environment.html#configuring-environment), in order to set up the docker container to access ROS2 commands, you need to source the set up files in the docker container - `/opt/ros/foxy/setup.bash`. To avoid running the source command every time, it adds the source command to shell startup script.

```
FROM ros:foxy-ros-base-focal

SHELL ["/bin/bash", "-c"]

WORKDIR /app

COPY requirements.txt ./
RUN apt update && apt install -y \
    python3-pip \
    python3-colcon-common-extensions
RUN pip install -r requirements.txt

RUN echo 'source /opt/ros/foxy/setup.bash' >> /root/.bashrc
```

**The location of `setup.bash` file in the docker container**

```
/opt
└── ros
    └── foxy
        ├── _local_setup_util.py
        ├── bin
        ├── cmake
        ├── include
        ├── lib
        ├── local_setup.bash
        ├── local_setup.sh
        ├── local_setup.zsh
        ├── opt
        ├── setup.bash
        ├── setup.sh
        ├── setup.zsh
        ├── share
        ├── src
        └── tools
```

# Directory structure <a name="directory"></a>
## How to run the docker container development environment <a name="how"></a>

The step to open the development environment is like below.

1. Go to your terminal and change your directory to where `.devcontainer` folder exists.
2. Run `code .` in your terminal, and you will see VS Code is coming up.
3. Go to `Open a Remote Window` and select `Reopen in Container`, and you will see VS Code is reopened with the one running in the docker container.

## Where is the project directory? <a name="where"></a>

In this sample - [.devcontainer](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer), when the docker container VS Code is open, you will see your directory in the docker container is `/workspaces/EdgeIntegrationTest/src/apps/FileGenerator`. You opened the devcontainer at `FileGenerator` in your local machine directory. However, the docker container copied `EdgeIntegrationTest` directory in your local machine to `/workspaces` in the docker container.

```
EdgeIntegrationTest
└── src
    └── apps
        └── FileGenerator
              ├── .devcontainer
              |   ├── Dockerfile
              |   ├── devcontainer.json
              |   └── requirements.txt
              ├── Dockerfile
              ├── main.py
              └── requirements.txt
```

Is this because your articulate `"workspaceFolder": "/workspaces/EdgeIntegrationTest/src/apps/FileGenerator"` in [devcontainer.json](https://github.com/koheikawata/azure-iot-edge-integration-test-template/blob/main/src/apps/IothubConnector/.devcontainer/devcontainer.json)? No. If you change `workspaceFolder` in `devcontainer.json` to `"workspaceFolder": "/workspaces/FileGenerator"`, you will see `EdgeIntegrationTest` directory is copied under `/workspaces` in the docker container and see the error below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jp3rd6kcqxa2o6ohosnl.png)

It seems like the VS Code Remote Development extension copies the directory where **one** `.git` exists over your current directory in which you `Reopen in Container`. I tried three experiments below.

### Experiment 1 <a name="experiment1"></a>

**Reopen in Container at `FileGenerator` in your local machine**

```
C:.
└── FileGenerator
    ├── .devcontainer
    |   ├── Dockerfile
    |   ├── devcontainer.json
    |   └── requirements.txt
    ├── Dockerfile
    ├── main.py
    └── requirements.txt

```

**Your directory in the docker container**

```
root@xxxxxxx:/workspaces/FileGenerator# 
```

### Experiment 2 <a name="experiment2"></a>

**Reopen in Container at `FileGenerator` in your local machine**

```
C:.
└── ProjectFolder1
    ├── .git
    └── FileGenerator
        ├── .devcontainer
        |   ├── Dockerfile
        |   ├── devcontainer.json
        |   └── requirements.txt
        ├── Dockerfile
        ├── main.py
        └── requirements.txt

```

**Your directory in the docker container**

```
root@xxxxxxx:/workspaces/ProjectFolder1/FileGenerator# 
```

### Experiment 3 <a name="experiment3"></a>

**Reopen in Container at `FileGenerator` in your local machine**

```
C:.
└── ProjectFolder2
    ├── .git
    └── ProjectFolder1
        ├── .git
        └── FileGenerator
            ├── .devcontainer
            |   ├── Dockerfile
            |   ├── devcontainer.json
            |   └── requirements.txt
            ├── Dockerfile
            ├── main.py
            └── requirements.txt

```

**Your directory in the docker container**

```
root@xxxxxxx:/workspaces/ProjectFolder1/FileGenerator# 
```