Container-based Development: How to Setup Development in Docker with Dev Container

Agoda Engineering
Agoda Engineering & Design
9 min readSep 8, 2023

By Kraiwat Nimudomsuk

At Agoda, we have lots of microservices built on the .NET Core framework. These services are managed by multiple teams and can use different versions of .NET Core. When our engineers work on different projects, they need to follow the documentation for each project to configure their development environments and manage version compatibility on their local machines.

One of these microservices is Gateway, responsible for serving API HTTP requests for the Agoda website and application. Previously, Gateway was utilizing .NET Core 3.1. The idea was to upgrade to version 6.0, but we had to be careful not to break anything. Additionally, a challenge emerged: the need to disseminate this upgrade directive to hundreds of engineers, ensuring they made the necessary updates on their individual machines.

To address these challenges and streamline these processes, we explored the use of Development Containers (Dev Containers) within Visual Studio Code to help us improve processes and manage development environments.

What is Development Container or Dev Container

According to the Development Container website,

A Development Container (or Dev Container for short) allows you to use a container as a full-featured development environment. It can be used to run an application, to separate tools, libraries, or runtimes needed for working with a codebase, and to aid in continuous integration and testing. Dev containers can be run locally or remotely, in a private or public cloud, in a variety of supporting tools and editors.

You can build a container-based development environment, which allows you to have all dependencies, tools, libraries, and applications running inside the container. Each project can configure dependencies and environments to run inside the container. This means you need to set up prerequisites once to run the development container on your machine. Then, you can easily switch projects and environments by connecting to different containers.

We configured our Gateway development container by cloning source code into a container volume and setting up .NET version and environment of this project inside the container. Here is the Gateway architecture with development container.

Gateway Dev Container Stack

Using development containers can significantly enhance the software development process. Here are some benefits of using development containers:

Benefits of Using Development Containers

  • Consistent Development Environment
    Containers encapsulate the development environment, including dependencies, libraries, and files. Engineers will have the same consistent environment across local machines.
  • Isolation
    Each project has its container, which can prevent conflict between different versions of dependencies in different projects. It also ensures that any changes in one project will not affect others.
  • Dependency Management
    All required dependencies are in the container. This eliminates dependency conflicts on each engineer's machine. You can manage all dependencies in one place.
  • Easy Onboarding
    New joiners can quickly start development by spinning up the container. This reduces the time for initial setup and improves the development process.

While development containers offer numerous benefits, they also come with certain challenges that organizations need to be aware of.

Challenges Associated with Development Containers

  • Resource Overhead
    Running a container requires resources such as memory and CPU on the host system. If you are running multiple containers at the same time, it can slow down your machine and development process.
  • Security Concerns
    Container images may contain vulnerabilities or outdated dependencies. Regular updates and proper security practices are required to minimize the risks.
  • Learning Curve
    Using containers requires basic knowledge, command, and tools related to docker. This can slow down adoption and increase the learning curve for engineers.

Let's see an example of setting up a development container for C# .NET Core 6 project.

Setting up a Development Container

There are a few prerequisites that you need to install on your machine to get started with development container.

  • Docker Desktop
  • Visual Studio Code (VS Code)
  • Dev Container Extension on Visual Studio Code

After installing the prerequisites, you can create a development container configuration for your project. A container configuration file describes how to create a container and environment setting. The configuration file is either located under the .devcontainer folder or stored as a .devcontainer.json file in the root of your project. You can update the configuration to do things such as:

  • Install additional tools.
  • Install VS Code extensions.
  • Expose ports from container.
  • Set runtime arguments. etc...

You can use Command Palette in VS Code to get predefined dev container configuration by selecting Dev Containers: Open folder in Container... then select your project folder.

This will show predefined container configuration options. In this case, we will select C#(.NET) from the list.

A .NET version list will be provided, select version 6.0 to match with project settings.

This will provide you with additional features list that you can install in the container. After you click OK, it will generate the dev container configuration file for you.

The new .devcontainer folder and devcontainer.json file are added to the root folder. VS Code will display that the project is opened in the dev container at the bottom left of the screen.

The default configuration will define the name of the container and the docker image to use for the dev container.

Sample default dev container config

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "C# (.NET)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0"

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5000, 5001],
// "portsAttributes": {
// "5001": {
// "protocol": "https"
// }
// }

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "dotnet restore",

// Configure tool-specific properties.
// "customizations": {}

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

You can modify the configuration file to customize the container to match your requirements. If you already have docker compose file in your project, you can update the configuration to use your docker compose file. Let's say we have a docker-compose.yml file in the .devcontainer folder.

Sample docker compose file

version: '3.1'

services:
dotnet-container:
image: mcr.microsoft.com/devcontainers/dotnet:0-6.0
command: sleep infinity

The configuration file will be updated to use dockerComposeFile instead of the image parameter, and a few parameters will be added to the configuration.

Sample default dev container config

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "C# (.NET)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"dockerComposeFile": "docker-compose.yml",
"service": "dotnet-container",
"workspaceFolder": "/workspaces",
"shutdownAction": "stopCompose"

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5000, 5001],
// "portsAttributes": {
// "5001": {
// "protocol": "https"
// }
// }

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "dotnet restore",

// Configure tool-specific properties.
// "customizations": {}

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

You can define the VS Code extensions you want to install in the development container in the customizations section. Here is an example of setting up two extensions - C# dotnet tools and GitLens.

Sample config extensions

"customizations": {
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-dotnettools.csharp",
"eamodio.gitlens"
]
}
}

This is the final configuration file.

Sample final dev container config

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "C# (.NET)",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"dockerComposeFile": "docker-compose.yml",
"service": "dotnet-container",
"workspaceFolder": "/workspaces",
"shutdownAction": "stopCompose",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5000, 5001],
// "portsAttributes": {
// "5001": {
// "protocol": "https"
// }
// }
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "dotnet restore",
// Configure tool-specific properties.
"customizations": {
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-dotnettools.csharp",
"eamodio.gitlens"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

Note: You can check the configuration specification for customizing your configuration file from the development container website

After updating the configuration file, push the configuration file to the master branch of your repository. This will ensure we get the latest configuration when we clone source code in container volume.

To open your project in container volume, you can use Command Palette and select Dev Containers: Clone Repository in Container Volume... or use the following link to open it.

vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=<your git repository url>

This command or link will cause VS Code to automatically:

  1. Clone your repository into a container volume. It may ask you to input your credentials for cloning the repository.
  2. Spin up a development container.
  3. Install VS Code extensions which are configured in your dev container configuration file.

Wait until they are finished and ready in VS Code

You can open Docker Desktop to see the container and volume of your project.

That’s it! You have an isolated development environment. All files and dependencies are in a container volume. You can run and debug your application inside the container. When other engineers are onboarded on the project, they can install prerequisites and open the repository in container volume on their machine. They will have a consistent development environment in the container.

Upgrading .NET Core

When you want to upgrade to .NET Core 7, you can update the configuration file to use .NET 7.0 image and rebuild the dev container on your machine. Then, you can upgrade your application to .NET 7 and run it in the dev container. Other engineers can easily pull the latest configuration file from you and update the development environment on their machines.

image: mcr.microsoft.com/devcontainers/dotnet:0-7.0

Engineers Feedback

We sent out a survey to collect feedback from engineers who use the Gateway development container. We wanted to compare the development experience between local machine and dev container. These are the survey results.

  • Our engineers mainly use JetBrain Rider for .NET IDE for local environment.
  • We asked engineers to switch to Visual Studio Code for using dev container. We saw better satisfaction score for the dev container compared to local environment.
  • Setup time was also reduced from 10–15 minutes to 5–10 minutes.
  • Most of them had a better experience with the dev container.
  • Some engineers were not familiar with Visual Studio Code and had slightly worse experiences with the IDE.

Engineers also saw the following benefits of using dev containers.

  • Simplified setup and configuration​
  • Consistent development environment​
  • Reduced dependency conflicts

Conclusion

With dev container, you can configure and manage dependencies for each project separately to prevent conflict between projects. It ensures that all engineers work in the same consistent development environment regardless of their local setups. New team members can quickly onboard your project by spinning up the container. These can lead to improved collaboration, faster development cycles, and more reliable software releases.

We recommend you give dev container a try. You can also explore dev containers on the cloud, such as Github Codespaces, Gitpod, or JetBrains Spaces. It can solve local resource problems, and engineers can code from any device.

References

--

--

Agoda Engineering
Agoda Engineering & Design

Learn more about how we build products at Agoda and what is being done under the hood to provide users with a seamless experience at agoda.com.