Creating a Development Environment with DevContainer, Dockerfile and Docker-Compose

Learn how to set up a scalable development environment with DevContainer, evolving from simple configurations to multi-service orchestration with Docker-Compose.

Development environments are often complex configurations of tools, dependency versions, and specific systems. This can make transitioning from one project to another or onboarding new team members difficult. This is where DevContainer comes in, offering a solution that allows you to define and share portable, reproducible development environments.

In this article, I will show you how to get started with DevContainer and how to evolve it based on your needs by going through three stages:

  • A basic DevContainer
  • A DevContainer with Dockerfile for more fine-tuned customization
  • A DevContainer with Docker-Compose to manage multiple services in a development environment

Each step will be introduced by adding a new feature to an example project, showing why and when to evolve the configuration.

In the following examples, the index.html file is located in the app folder to illustrate a simple web application.

Step 1: Setting up a Basic DevContainer

The first step is to create a development environment within a container using a very simple configuration. This helps unify environments across a development team without requiring each developer to customize their setup.

Basic Configuration

The simplest configuration for a DevContainer is a devcontainer.jsonfile, which defines the Docker image to be used for the development container. Here's a minimal devcontainer.jsonfile to get started:

{
  "name": "Basic DevContainer",
  "image": "mcr.microsoft.com/vscode/devcontainers/base:ubuntu"
}

With this configuration, any developer working on this project can simply open the project in a compatible editor (such as VSCode) and benefit from a ready-to-use Linux environment, without needing to configure anything locally.

Project Structure for a Basic DevContainer:

/my-project
  ├── .devcontainer
  │   └── devcontainer.json
  └── app
      └── index.html

When to Use This Configuration?

This type of configuration is ideal for simple projects or standard environments where no customization is necessary. If your project only requires a common development environment based on an existing Docker image, this approach is quick and efficient.

Step 2: Adding a Dockerfile for More Control

As development progresses, it often becomes necessary to add specific tools or libraries to the environment. This is where a Dockerfile comes in handy.

Customization with Dockerfile

The second step involves introducing a Dockerfile that allows you to further customize the environment by adding specific tools or configurations to the project. Here's an example of a devcontainer.json with an associated Dockerfile:

{
  "name": "Custom DevContainer",
  "build": {
    "dockerfile": "Dockerfile"
  }
}

The Dockerfile allows you to add instructions to install additional packages:

# Same image as in the minimal devcontainer.json
FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu

# Install additional tools
RUN apt-get update && apt-get install -y curl

Project Structure with Dockerfile Added:

/my-project
  ├── .devcontainer
  │   ├── devcontainer.json
  │   └── Dockerfile
  └── app
      └── index.html

When to Use a Dockerfile?

This configuration is useful when the project has specific needs, such as particular versions of development tools or additional dependencies that the base Docker image doesn’t provide. For example, if you are developing an application that requires certain tools not included in the base image, the Dockerfile allows you to add them.

Step 3: Using Docker-Compose for a Multi-Service Environment

When the project becomes more complex, it may be necessary to orchestrate multiple services, such as a web application and a database. This is where Docker-Compose comes into play.

Managing Multiple Services with Docker-Compose

With Docker-Compose, you can easily orchestrate multiple containers. This allows, for example, running both an API and a database for local development. Here's how to transition to a docker-compose.yml setup for a multi-service environment:

{
  "name": "DevContainer with Docker-Compose",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app"
}

Le fichier docker-compose.yml pourrait ressembler à ceci :

services:
  app:
    build: .
    volumes:
      - .:/workspace
    ports:
      - "8080:8080"
  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb

With this configuration, you can develop and test features that require multiple services in a consistent environment.

Project Structure with Docker-Compose:

/my-project
  ├── .devcontainer
  │   ├── devcontainer.json
  │   └── Dockerfile
  ├── docker-compose.yml
  └── app
      └── index.html

When to Use Docker-Compose?

Docker-Compose is essential when working with complex systems that require multiple services (such as an API and a database). It allows you to replicate environments similar to production while keeping everything manageable in a local development setting.

Conclusion

As your project's needs evolve, it becomes necessary to adapt your development environment. DevContainer offers great flexibility for this, starting with a simple configuration and evolving into more complex environments using Dockerfile and Docker-Compose.

  • Basic DevContainer: Quick and efficient for simple projects.
  • DevContainer with Dockerfile: Allows you to customize the tools and configurations in your environment.
  • DevContainer with Docker-Compose: Manages multiple services for optimized local development and testing.

I encourage you to try these configurations and adapt them to your specific needs. Using DevContainers can greatly simplify the process of managing development environments, especially in teams where consistency is crucial.