Dev Containers - Part 5: Multiple Projects & Shared Container Configuration
Tags: devcontainer
Published: 2024-06-21 06:46:15

Welcome to the fifth guide in the Dev Container series:

- [Dev Containers - Why You Need Them](https://dev.to/graezykev/dev-containers-part-0-why-you-need-them-4727)
- [Part 1: Quick Start - Basic Setup and Usage](https://dev.to/graezykev/dev-containers-part-1-quick-start-basic-setup-and-usage-51l6)
- [Part 2: Image, Features, Workspace, Environment Variables](https://dev.to/graezykev/dev-containers-part-2-image-features-workspace-environment-variables-375o)
- [Part 3: Full Stack Dev - Docker Compose & Database](https://dev.to/graezykev/dev-containers-part-3-full-stack-dev-docker-compose-database-5f5o)
- [Part 4: Remote Dev - Develop on a Remote Docker Host](https://dev.to/graezykev/dev-containers-part-4-remote-dev-develop-on-a-remote-docker-host-440l)
- Part 5: Multiple Projects & Shared Container Configuration

To get started, you can clone [my demo project](https://github.com/graezykev/dev-container/tree/part-5-shared-configure-for-multiple-projects) using the following command:

```sh
git clone -b part-5-shared-configure-for-multiple-projects https://github.com/graezykev/dev-container.git
```

## Introduction

In [Part 3](https://dev.to/graezykev/dev-containers-part-3-full-stack-dev-docker-compose-database-5f5o), we learned about using Docker Compose in Dev Containers to build containers for `Node.js` applications and databases.

Currently, you can only connect to one container per Visual Studio Code window. But what if you have multiple projects using different tech stacks like `Node.js`, `Python`, `Go`, etc., and need to create Dev Containers for each?

One option is to place a `.devcontainer` folder under each project:

```plaintext
.
└── path
 └── to
    ├── project-a-node-js
    │   └── .devcontainer
    │       ├── docker-compose.yml
    │       ├── ...
    │       └── devcontainer.json
    ├── project-b-node-js
    │   └── .devcontainer
    │       ├── ...
    │       └── devcontainer.json
    ├── project-c-python
    │   └── .devcontainer
    │       ├── ...
    │       └── devcontainer.json
    ├── project-d-go-lang
    │   └── .devcontainer
    │       ├── ...
    │       └── devcontainer.json
    └── project-...
```

If these applications need to share the same database, you must ensure they all use the same database container in their `docker-compose.yml` and the same volume:

```yaml
services:

  app-name-...
    ...

  postgres:
    image: postgres:latest
    ...

...

volumes:
  postgres-data:
```

However, this can result in overlapping configurations across multiple projects, making maintenance tedious. A better approach is to share a common `docker-compose.yml`:

```plaintext
.
└── path
 └── to
  └── dev-container
      │
      ├── .devcontainer
      │   │
      │   ├── .env
      │   ├── docker-compose.yml
      │   ├── ...
      │   │
      │   ├── project-a-node-js
      │   │   └── devcontainer.json
      │   │
      │   ├── project-b-node-js
      │   │   └── devcontainer.json
      │   │
      │   ├── project-c-python
      │   │   └── devcontainer.json
      │   │
      │   ├── project-d-go-lang
      │   │   └── devcontainer.json
      │   │
      │   └── project-e-...
      │       └── devcontainer.json
      │
      ├── project-a-node-js
      │       └── index.js
      │
      ├── project-b-node-js
      │
      ├── project-c-python
      │       └── hello.py
      │
      ├── project-d-go-lang
      └── project-e-...
```

All projects and the `.devcontainer` folder share a common root-level folder, with each project having its own configuration folder under `.devcontainer`.

This setup allows you to define multiple Dev Containers (and a container for the database) in a common `docker-compose.yml`, and create a `devcontainer.json` for each project to reference the shared `docker-compose.yml`. This approach also helps manage each project's features and lifecycle scripts, avoiding configuration conflicts.

## Common Docker Compose File

First, create a common `docker-compose.yml` inside the root-level `.devcontainer`:

```yaml
services:
  project-a-node-js:
    image: graezykev/dev-container-base-image:latest
    volumes:
 - ..:/workspaces:cached
    ports:
 - 8001:8000
    depends_on:
 - postgres
    command: /bin/zsh -c "while sleep 1000; do :; done"
  
  project-b-node-js:
    ...
    volumes:
 - ..:/workspaces:cached
    ports:
 - 8002:8000
  
  project-c-python:
    ...

  project-d-go-lang:
    ...

  project-e-...

  postgres:
    ...

volumes:
  postgres-data:  
```

See the complete file in [my demo](https://github.com/graezykev/dev-container/blob/part-5-shared-configure-for-multiple-projects/.devcontainer/docker-compose.yml).

All projects share the same database container `postgres`.

We have learned most of the concepts in [Part 3](https://dev.to/graezykev/dev-containers-part-3-full-stack-dev-docker-compose-database-5f5o#1-define-raw-services-endraw-) but there are a few things we need to pay attention to.

### Ports

Notice the port mappings: `8001:8000`, `8002:8000`, etc. Each project uses port `8000` within its own Dev Container, mapped to different ports on the host machine. This setup avoids port conflicts and allows access to each project's server via distinct ports (e.g., `8001`, `8002`, etc.).

> See [demo preveiw](#demo-preview) below.

### Volumes & Workspace

Using `..:/workspaces` for the `volumes` sections mounts the entire root-level folder (`dev-container`) on the host machine to `/workspaces` in the containers.

In each `devcontainer.json`, specify the project folder within the workspace:

```json
"workspaceFolder": "/workspaces/project-b-node-js"
```

![set workspaceFolder for the right dev container](https://raw.githubusercontent.com/graezykev/dev-container/main/read-me-images/part-5/workspacefolder.png)

### Service

In each `devcontainer.json`, reference the Docker Compose file `docker-compose.yml` and specify the service name:

```json
{
  "name": "Dev Container",
  "dockerComposeFile": [
    "../docker-compose.yml"
  ],
  "service": "project-a-node-js",
  "shutdownAction": "none",
  "workspaceFolder": "/workspaces/project-a-node-js"
}
```

```json
{
  "service": "project-b-node-js",
  "workspaceFolder": "/workspaces/project-b-node-js"
}
```

```json
{
  "service": "project-c-python",
  "workspaceFolder": "/workspaces/project-c-python"
}
```

The `"shutdownAction": "none"` option will leave the containers running when VS Code closes -- which prevents you from accidentally shutting down both containers by closing one window (or switching containers).

## Build and Switch Dev Containers

In VS Code, open the root-level folder (`dev-container` in my demo). Run `Dev Containers: Reopen in Container` from the Command Palette and select the project to build.

![choose dev container to build](https://raw.githubusercontent.com/graezykev/dev-container/main/read-me-images/part-5/choose-dev-container.png)

This triggers the Dev Container build for the selected project.

To switch between projects, use `Dev Containers: Switch Container` from the Command Palette and select the desired project.

After each Dev Container is built, we can use this `Switch Container` command to switch between projects, the current VS Code window will reload each time and connect to the selected Dev Container.

## Demo Preview

In [my demo](https://github.com/graezykev/dev-container/tree/part-5-shared-configure-for-multiple-projects), I have three projects: two `Node.js` projects and one `Python` project, each starts an HTTP server with a web page.

When visited, the projects write "visiting records" to the shared database and display all records on the page.

![preview](https://raw.githubusercontent.com/graezykev/dev-container/main/read-me-images/part-5/preview.png)

And remember the [port mapping](#ports) we mentioned before?

![visiting records](https://raw.githubusercontent.com/graezykev/dev-container/main/read-me-images/part-5/visiting-record.png)