Skip to content

VS Code Workspace β€” Multi-Root Development Environment

Single VS Code instance managing all platform repositories, with centralized debugger launch configs, container-attached development, and parallelized test execution. From empty machine to hitting a breakpoint inside a running container.

πŸ“ Type: Workspace Configuration
πŸ‘€ Owner: Ktwenty Threel
🎯 Outcome: Understand the Workspace Configuration Process


Table of Contents


Overview

The workspace is a single Git repository (engineering-infrastructure-hub) that contains the VS Code configuration, the documentation vault, and references to all other repositories. It does not contain service code β€” each service and the base infrastructure are their own Git repositories, cloned into this directory and git-ignored. VS Code sees them as separate workspace roots.

Repository Layout

engineering-infrastructure-hub/
β”œβ”€β”€ eih.code-workspace             # Multi-root workspace definition
β”œβ”€β”€ .vscode/
β”‚   β”œβ”€β”€ launch.json                 # Centralized debugger configs (all services)
β”‚   └── settings.json               # Repo exclusions (prevents double entries)
β”œβ”€β”€ docs/                           # Obsidian documentation vault
β”œβ”€β”€ .gitignore
β”œβ”€β”€ README.md
β”œβ”€β”€ LICENSE
β”œβ”€β”€ basic-infrastructure/           # Cloned, git-ignored
β”œβ”€β”€ django-service-template/        # Cloned, git-ignored
└── *-service/                      # Generated services, git-ignored by wildcard

What VS Code Shows

πŸ“ πŸ› οΈ Setup Workspace                  ← This repo (workspace config, docs)
πŸ“ πŸ“¦ Basic Infrastructure             ← Envoy Gateway, Keycloak, PostgreSQL, etc.
   ──────────────────────────────
πŸ“ πŸ“š Django Service Template          ← Copier template for generating services
   ──────────────────────────────
πŸ“ πŸ’½ Backend Service                  ← Generated service (example)
πŸ“ πŸ’½ Billing Service                  ← Generated service (example)

Delimiter lines are visual separators created by empty delimiter-* directories, hidden from Explorer via files.exclude. Cloned repos are excluded from the Setup Workspace root so they don't appear twice.


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        VS Code Workspace                            β”‚
β”‚                                                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ eih.code-workspace                                           β”‚  β”‚
β”‚   β”‚  Folder roots β†’ emoji-prefixed, per-repo                     β”‚  β”‚
β”‚   β”‚  Settings β†’ file exclusions, git, python analysis            β”‚  β”‚
β”‚   β”‚  Extensions β†’ recommended on first open                      β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚   β”‚ .vscode/         β”‚    β”‚ Run & Debug (Ctrl+Shift+D)         β”‚    β”‚
β”‚   β”‚  launch.json     │───▢│  πŸ’½ Backend Service  β†’ port 5678   β”‚    β”‚
β”‚   β”‚  (all services)  β”‚    β”‚  πŸ’½ Billing Service  β†’ port 5679   β”‚    β”‚
β”‚   β”‚                  β”‚    β”‚  πŸ’½ Analytics Service β†’ port 5680  β”‚    β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                           β”‚ debugpy attach          β”‚
β”‚                                           β–Ό                         β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚ Docker Containers (via Remote Explorer)                      β”‚  β”‚
β”‚   β”‚                                                              β”‚  β”‚
β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚  β”‚
β”‚   β”‚  β”‚ Django A β”‚ β”‚ Django B β”‚ β”‚ Keycloak β”‚ β”‚ Postgres β”‚  ...    β”‚  β”‚
β”‚   β”‚  β”‚ debugpy  β”‚ β”‚ debugpy  β”‚ β”‚          β”‚ β”‚          β”‚         β”‚  β”‚
β”‚   β”‚  β”‚ :5678    β”‚ β”‚ :5679    β”‚ β”‚          β”‚ β”‚          β”‚         β”‚  β”‚
β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚  β”‚
β”‚   β”‚                                                              β”‚  β”‚
β”‚   β”‚  Test Explorer β†’ discovers tests inside attached container   β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The key principle: you develop inside the same containers that run your application. VS Code attaches to the running container via Remote Explorer, the debugger connects to debugpy over a mapped port, and the Test Explorer discovers tests from inside the container. What works here works in production.


Prerequisites

Requirement Version Verify
Docker Engine 24.0+ docker --version
Docker Compose v2.34.0+ docker compose version
Python 3.11 python3 --version
VS Code Latest code --version
Git 2.30+ git --version
Ngrok Latest ngrok version

Setup

1. Clone Workspace

git clone git@github.com:yourorg/engineering-infrastructure-hub.git
cd engineering-infrastructure-hub

2. Clone Infrastructure & Template

git clone git@github.com:yourorg/basic-infrastructure.git
git clone git@github.com:yourorg/django-service-template.git

These repos are listed in .gitignore β€” the workspace tracks references to them, not their contents.

3. Install VS Code Extensions

Open the workspace and VS Code prompts to install recommended extensions automatically.

Extension ID Required? Purpose
Python ms-python.python Required Language support, IntelliSense (Pylance), debugging (debugpy), linting, testing
Dev Containers ms-vscode-remote.remote-containers Required Remote Explorer for attaching to running containers, in-container debugging and test execution
Docker ms-azuretools.vscode-docker Required Container management, log viewing, image inspection
Black Formatter ms-python.black-formatter Recommended Auto-formatting on save, matches pre-commit hooks in the service template
GitLens eamodio.gitlens Optional Enhanced git blame, history, and diff β€” helpful in multi-repo workspaces

4. Open Workspace

code eih.code-workspace

5. Start Infrastructure Only

cd basic-infrastructure
cp .env.example .env
docker compose up -d

This starts the base platform:

Container Purpose Startup
eih-envoy-gateway TLS termination, path-based routing Immediate
eih-envoy-certs Control plane certificate generation Runs once, exits
eih-keycloak OIDC / SSO identity provider 60–90s
eih-postgres Shared database, per-service schemas Immediate
eih-maildev Email capture with web UI Immediate
eih-echo Request mirror for route debugging Immediate

Wait for all services to report healthy: docker compose ps.

Keycloak startup

Keycloak takes 60–90 seconds. The health check has start_period: 90s. It will show unhealthy briefly β€” this is normal.

6. Start Ngrok

ngrok http 8080

Copy the generated URL β€” you'll need it as DOMAIN in environment files and Keycloak redirect URIs. See Ngrok & Public Access for details on free vs. paid accounts.

7. Develop Inside Containers

Once infrastructure is running:

  1. Start your service (see Adding a Service)
  2. The service container appears in Remote Explorer (left sidebar, monitor icon)
  3. Open Run & Debug (Ctrl+Shift+D) β†’ select your service from the dropdown β†’ F5
  4. Set breakpoints, step through code, inspect variables β€” all inside the running container
  5. Test Explorer (Ctrl+Shift+T) shows all tests with parallelized per-test execution

Adding a Service

Generating a new service and wiring it into the platform takes a service repo, infrastructure configuration, and a workspace entry.

Step 1: Generate from Template

copier copy --trust gh:yourorg/django-service-template backend-service
cd backend-service
make setup
nano .env

Naming convention

Always name service directories with a -service suffix (e.g. backend-service, billing-service). The workspace .gitignore and .vscode/settings.json use *-service wildcards β€” following this convention means zero manual changes to workspace configuration files.

The generated service ships with API, authentication, authorization, and core apps. See the Django Service Template for the full configuration checklist and port allocation.

Step 2: Register with Infrastructure

Two files in basic-infrastructure/ need updating:

Add compose include β€” edit basic-infrastructure/compose.yaml:

include:
  - ../backend-service/compose.yaml

Envoy settings for backend-service route β€” three pre-configured YAML files (for dev you just need the two files in common):

basic-infrastructure/services_configs/envoy_config/
β”œβ”€β”€ common/backends/backend-service.yaml
β”œβ”€β”€ common/routes/backend-service.yaml
└── prod/routes/backend-service.yaml

Works with Django Service Template out of the box should you need to make changes or add services go to gateway integration section and either adjust the service name and port or copy and dajust the files.

Step 3: Register with Workspace

Add a folder entry to eih.code-workspace:

{
  "name": "πŸ’Ύ Backend Service",
  "path": "backend-service/"
}

Check the debug configuration in .vscode/launch.json:

{
  "name": "Backend: Remote Attach",
  "type": "debugpy",
  "request": "attach",
  "connect": {
      "host": "localhost",
      "port": 5678
  },
  "pathMappings": [
      {
          "localRoot": "${workspaceFolder:πŸ’Ύ Backend Service}/src",
          "remoteRoot": "/code"
      }
  ],
  "justMyCode": false
}

Because you named the directory with a -service suffix, the .gitignore wildcard and .vscode/settings.json exclusion already cover it. No other files need editing.

Step 4: Start & Verify

cd basic-infrastructure
docker compose up -d
curl https://${DOMAIN}/backend-service/health/

Quick Checklist

  • [ ] Generate service from template (name it *-service)
  • [ ] Configure .env (slug, ports, client secret)
  • [ ] Add include to basic-infrastructure/compose.yaml
  • [ ] Check Envoy Backend and HTTPRoute YAML files
  • [ ] Add folder entry to eih.code-workspace
  • [ ] Check debug config in .vscode/launch.json
  • [ ] docker compose up -d β†’ verify health endpoint
  • [ ] Select service in Run & Debug β†’ F5 β†’ hit a breakpoint

Workspace File Reference

eih.code-workspace

Defines folder roots, workspace settings, and extension recommendations.

Folder roots β€” each cloned repo appears as a separate root with emoji prefixes:

Prefix Meaning
πŸ› οΈ Workspace setup (this repo)
πŸ“¦ Base infrastructure
πŸ“š Template reference
πŸ’Ύ or πŸ’½ Generated service
──── Visual delimiter (hidden delimiter-* dirs)

Workspace settings:

Setting Purpose
files.exclude Hides __pycache__, .git, .pyc, .pytest_cache, delimiter dirs
scm.repositories.visible: 15 Shows all Git repos in Source Control (default too low for multi-repo)
git.openRepositoryInParentFolders: "always" Auto-detects Git repos in all workspace roots
python.analysis.diagnosticSeverityOverrides Unused imports = warning, missing imports = error

Exclusion split

Repo exclusions (basic-infrastructure, *-service, etc.) live in .vscode/settings.json, not in the workspace file. Workspace files.exclude applies to all roots β€” putting repo exclusions there would hide them everywhere, not just from the Setup Workspace root.

.vscode/settings.json

Prevents cloned repos from appearing as subfolders of Setup Workspace. Without this, every repo shows twice β€” once as its own workspace root and once as a subfolder under πŸ› οΈ Setup Workspace.

{
  "files.exclude": {
    "**/basic-infrastructure": true,
    "**/django-service-template": true,
    "**/*-service": true
  }
}

.vscode/launch.json

All debug configurations in one central file. Every service appears in the Run & Debug dropdown (Ctrl+Shift+D).

.gitignore

Every cloned repo is git-ignored. The *-service/ wildcard automatically catches any service following the naming convention:

basic-infrastructure/
django-service-template/
*-service/
.DS_Store
.AppleDouble
.LSOverride

Debugging

All debug configurations live in engineering-infrastructure-hub/.vscode/launch.json. Each service exposes a debugpy port starting at 5678, incrementing per service:

Service Debug Port
Service 01 5678
Service 02 5679
Service 03 5680
... +1 per service

A configuration entry:

{
  "name": "Backend: Remote Attach",
  "type": "debugpy",
  "request": "attach",
  "connect": {
      "host": "localhost",
      "port": 5678
  },
  "pathMappings": [
      {
          "localRoot": "${workspaceFolder:πŸ’Ύ Backend Service}/src",
          "remoteRoot": "/code"
      }
  ],
  "justMyCode": false
}

The workspaceFolder name must match the folder name in eih.code-workspace exactly (including the emoji prefix). The pathMapping maps the container's /code directory to your local source so breakpoints resolve correctly. The port must match the service's DEBUG_PORT in its .env.


Testing

When attached to a container via Remote Explorer, the Python extension runs inside the container and discovers tests automatically. Test Explorer (Ctrl+Shift+T) shows all tests with per-test execution and parallelized runs.

The workflow: attach to container via Remote Explorer β†’ open Test Explorer β†’ tests appear β†’ run individually or in bulk. Test output, coverage, and failures all display inline.


Ngrok & Public Access

Ngrok tunnels a public HTTPS URL to your local Envoy Gateway on port 8080. This enables OAuth redirect flows (Keycloak needs a public URL for OIDC callbacks), mobile testing against the local stack, and remote demos.

Free accounts generate a new random URL every session. This means updating DOMAIN in environment files and Keycloak redirect URIs each restart. Paid accounts support a stable custom domain, removing this friction.

For solo development, the free tier works β€” just update the domain after each Ngrok restart.


Runbooks

Keycloak shows unhealthy after startup

Expected during the first 60–90 seconds. Wait and re-check: docker compose ps. If still unhealthy after 2 minutes: docker compose logs keycloak.

Ngrok URL doesn't reach services

Verify Envoy Gateway is running: docker compose ps gateway. Confirm Ngrok tunnels to port 8080. Check that the Ngrok URL matches DOMAIN in environment files and Keycloak hostname configuration.

"Port already in use"

Another process holds a required port. Find it: sudo lsof -i :8080. Kill it or adjust port mappings. Most common: a previous Ngrok session or stale container β€” docker compose down --remove-orphans.

Service folder appears twice in Explorer

The service directory name doesn't end in -service, so the wildcard in .vscode/settings.json didn't catch it. Rename to follow the convention or add an explicit exclusion.

Debugger won't attach

Verify the service container is running and debugpy is listening on the expected port. Confirm the port in launch.json matches the service's DEBUG_PORT. Confirm pathMapping uses the correct workspaceFolder name (must match the emoji-prefixed name exactly). Restart the service container.

Login redirect fails after Keycloak restart

If the realm was reimported (docker compose down -v), verify KEYCLOAK_CLIENT_SECRET matches between Keycloak and your service .env. See Keycloak OIDC.

Tests not appearing in Test Explorer

Ensure you're attached to the container via Remote Explorer, not running VS Code locally. The Python extension must be running inside the container to discover tests. Check the bottom-left corner of VS Code β€” it should show the container name.


Clean Slate Files

Reference copies of workspace configuration files before any services have been generated. Use to reset if needed.

eih.code-workspace (clean slate)

{
  "folders": [
    { "name": "πŸ› οΈ Setup Workspace", "path": "./" },
    { "name": "πŸ“¦ Basic Infrastructure", "path": "basic-infrastructure/" },
    { "name": "--------------------", "path": "delimiter-0/" },
    { "name": "πŸ“š Django Service Template", "path": "django-service-template/" },
    { "name": "--------------------", "path": "delimiter-1/" }
  ],
  "settings": {
    "scm.repositories.visible": 15,
    "git.openRepositoryInParentFolders": "always",
    "files.exclude": {
      "**/node_modules": true,
      "**/.git": true,
      "**/.DS_Store": true,
      "**/__pycache__": true,
      "**/*.pyc": true,
      "**/.pytest_cache": true,
      "**/delimiter-*": true
    },
    "python.terminal.activateEnvironment": true,
    "python.analysis.diagnosticSeverityOverrides": {
      "reportUnusedImport": "warning",
      "reportMissingImports": "error"
    }
  },
  "extensions": {
    "recommendations": [
      "ms-python.python",
      "ms-vscode-remote.remote-containers",
      "ms-azuretools.vscode-docker",
      "ms-python.black-formatter",
      "eamodio.gitlens"
    ]
  }
}

.vscode/launch.json (clean slate)

{
  "version": "0.2.0",
  "configurations": []
}

.vscode/settings.json (clean slate)

{
  "files.exclude": {
    "**/basic-infrastructure": true,
    "**/django-service-template": true,
    "**/*-service": true
  }
}

.gitignore (clean slate)

basic-infrastructure/
django-service-template/
*-service/
.DS_Store
.AppleDouble
.LSOverride

ADRs

  • ADR: Multi-root workspace over monorepo β€” Each service is its own Git repository, cloned into the workspace directory and git-ignored. This preserves independent version history per service while providing a unified VS Code experience. The alternative (monorepo) would simplify cloning but complicate CI/CD, code ownership, and per-service deployment.
  • ADR: Centralized launch.json over per-service configs β€” All debug configurations live in one file in the workspace root. This puts every service in the Run & Debug dropdown without switching context. Per-service .vscode/launch.json files would fragment the debugging experience across workspace roots.
  • ADR: *-service naming convention β€” Directory suffix convention enables wildcard patterns in .gitignore and settings.json. New services require zero changes to workspace configuration files β€” only entries in the workspace folder list and launch config.
  • ADR: Container-attached development over local virtualenvs β€” Developers work inside the same containers that run the application. The Python interpreter, dependencies, and environment are identical to production. Local virtualenvs would introduce environment drift.
  • ADR: Emoji-prefixed folder names β€” Visual hierarchy in the Explorer sidebar. Infrastructure, template, and services are immediately distinguishable. The delimiter pattern provides grouping without nested folders.

Onboarding Checklist

  • [ ] Install prerequisites (Docker, Compose, Python, VS Code, Git, Ngrok)
  • [ ] Clone workspace: git clone git@github.com:yourorg/engineering-infrastructure-hub.git
  • [ ] Clone infrastructure and template into workspace directory
  • [ ] Open eih.code-workspace in VS Code
  • [ ] Install recommended extensions when prompted
  • [ ] docker compose up -d in basic-infrastructure/
  • [ ] Start Ngrok: ngrok http 8080
  • [ ] Verify Keycloak admin console at https://{ngrok-url}/auth/admin/
  • [ ] Verify MailDev web UI loads
  • [ ] Generate a service from the template (name it *-service)
  • [ ] Add service to compose include and Envoy route
  • [ ] Add folder entry to eih.code-workspace and debug config to launch.json
  • [ ] docker compose up -d β†’ verify health endpoint through gateway
  • [ ] Select service in Run & Debug β†’ F5 β†’ hit a breakpoint
  • [ ] Attach to container via Remote Explorer β†’ run tests via Test Explorer
  • [ ] Open docs/ in Obsidian and browse the Engineering Hub

References