4 minute read

Stir Trek 2025

This month at Stir Trek 2025, I presented on Dev Containers and GitHub Codespaces, demonstrating how these tools streamline both local and cloud-based development workflows. The session covered the essentials of creating portable development environments, customizing containers with features and extensions, and launching Codespaces directly from your repository. A lively Q&A followed, with attendees asking about strategies for running and working with multiple containers. Below, I’ve distilled those discussions and provided a deeper dive into shared container configurations across multiple projects-including folder structures, Docker Compose setups, VS Code workflows, and advanced tips you can apply in your own work.

Recap: Dev Containers and Codespaces

Dev Containers

Dev Containers are Docker-based environments enriched with development-specific tooling, settings, and startup tasks as defined in a devcontainer.json file. They enable you to use a container as a full-featured development environment-isolating dependencies, standardizing tool versions, and enabling reproducible setups locally or remotely (Dev Containers).

GitHub Codespaces builds on Dev Containers by providing cloud-hosted environments that spin up in seconds with configurable CPU, memory, and storage. Codespaces leverages the same open specification as Dev Containers, making your devcontainer.json a first-class citizen whether you connect via VS Code, IntelliJ, or directly in the browser (GitHub Docs).

Q&A: Running Multiple Containers

Can I connect to multiple containers at once?

By default, VS Code allows only one container per window, but you can open additional windows and attach each to a different container to work on multiple services in parallel (Visual Studio Code).

What about using a single window for multiple containers?

Docker Compose

If you use Docker Compose, define multiple services in your docker-compose.yml and create separate devcontainer.json configurations for each service-each referencing the common Compose file. VS Code will then list each configuration in its Dev Container picker, letting you reopen the current window to connect to a different service without duplicating your Compose setup (Dev Containers).

How do I configure separate containers for multiple projects?

To maintain isolation and clarity, place each container configuration in its own subdirectory under .devcontainer, following the pattern .devcontainer/<project>/devcontainer.json. Tools supporting the spec recognize this layout and list all found configurations in the Codespaces or VS Code Dev Container dropdown (containers.dev, GitHub Docs).

Shared .devcontainer Folder Structure

Centralize your container configurations in a single root directory:

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-a-node-js/
├─ project-b-node-js/
├─ project-c-python/
└─ project-d-go-lang/

This layout lets all projects share a single Compose definition and environment variables-reducing duplication and easing updates (containers.dev).

Common Docker Compose File

In .devcontainer/docker-compose.yml, define every project service alongside shared dependencies:

services:
  project-a-node-js:
    image: mcr.microsoft.com/devcontainers/base:latest
    volumes:
      - ..:/workspaces:cached
    ports:
      - "8001:8000"
    command: sleep infinity

  project-b-node-js:
    image: mcr.microsoft.com/devcontainers/base:latest
    volumes:
      - ..:/workspaces:cached
    ports:
      - "8002:8000"
    depends_on:
      - postgres

  project-c-python:
    image: mcr.microsoft.com/devcontainers/base:latest
    volumes:
      - ..:/workspaces:cached
    ports:
      - "8003:8000"
    depends_on:
      - postgres

  project-d-go-lang:
    image: mcr.microsoft.com/devcontainers/base:latest
    volumes:
      - ..:/workspaces:cached
    ports:
      - "8004:8000"

  postgres:
    image: postgres:latest
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  postgres-data:

Each service listens on port 8000 internally and is mapped to a unique host port (8001-8004) to prevent collisions (Visual Studio Code).

Workspace Mounts & Folder Mapping

Mounting the root-level folder into /workspaces in each container gives uniform access to all projects. In each devcontainer.json, point workspaceFolder at the specific subdirectory:

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

This ensures your editor is scoped appropriately when connected (Dev Containers).

Per-Project devcontainer.json

Each project’s configuration references the shared Compose file and specifies its service:

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

Using "shutdownAction": "none" keeps all containers running when you close one window, so you don’t inadvertently tear down shared services (Dev Containers).

Building & Switching Between Containers

  1. Open the root folder (dev-container/) in VS Code.
  2. Reopen in Container: Run Dev Containers: Reopen in Container and select your desired project.
  3. Switch Container: Later, use Dev Containers: Switch Container to hop to another project without restarting the Docker stack (Visual Studio Code, GitHub Docs).

Advanced Multi-Project Strategies

Environment-Specific Overrides

Layer additional Compose files for environment-specific tweaks:

docker-compose -f docker-compose.yml \
  -f docker-compose.override.yml \
  -f docker-compose.dev.yml up -d

Overrides can redefine images, ports, mounts, or feature flags per environment (Visual Studio Code).

Isolated Networks & Namespaces

Define separate Docker networks to segment traffic:

networks:
  dev-a: {}
  dev-b: {}

services:
  project-a-node-js:
    networks: [dev-a]
  project-b-node-js:
    networks: [dev-b]
  postgres:
    networks: [dev-a, dev-b]

This prevents unintended inter-service communication between project environments (Visual Studio Code).

GitHub Codespaces Integration

Codespaces recognizes the same .devcontainer layout:

  • Configuration Dropdown: Multiple devcontainer.json files under .devcontainer/ are automatically listed when creating a Codespace (GitHub Docs).
  • Port Forwarding: Host-mapped ports (8001-8004) surface as forwarded ports in the Codespaces UI.
  • Pre-builds & Secrets: Enable pre-builds in devcontainer.json and leverage repository or organization secrets instead of a local .env file (The GitHub Blog).

Lifecycle Customization

Use Dev Container lifecycle hooks per project to automate setup:

"postCreateCommand": "cd /workspaces/project-a-node-js && npm ci",
"postStartCommand": "npm run migrate",
"initializeCommand": "git submodule update --init"

These commands ensure each container is fully prepared for development immediately (Dev Containers).

Troubleshooting Tips

  • Stuck at “Rebuilding container…“: Clear the Docker build cache or raise VS Code’s Docker logging level.
  • Ports not forwarding: Verify forwardPorts in devcontainer.json or check Codespaces port settings.
  • Volume performance issues: On macOS/Windows, consider isolating cache directories (e.g., node_modules) in named volumes to speed up I/O (Some Natalie’s corner of the internet, pamela fox’s blog).

Conclusion

By centralizing Dev Container configurations and sharing a unified docker-compose.yml, you eliminate duplication, streamline dependency management, and enable seamless switching between multiple projects-both locally and in GitHub Codespaces. This pattern scales from a handful of services to extensive microservice landscapes, delivering consistent, reproducible developer environments across your entire workspace.