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
- Architecture
- Prerequisites
- Setup
- Adding a Service
- Workspace File Reference
- Debugging
- Testing
- Ngrok & Public Access
- Runbooks
- ADRs
- References
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:
- Start your service (see Adding a Service)
- The service container appears in Remote Explorer (left sidebar, monitor icon)
- Open Run & Debug (Ctrl+Shift+D) β select your service from the dropdown β F5
- Set breakpoints, step through code, inspect variables β all inside the running container
- 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
includetobasic-infrastructure/compose.yaml - [ ] Check Envoy
BackendandHTTPRouteYAML 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.jsonover 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.jsonfiles would fragment the debugging experience across workspace roots. - ADR:
*-servicenaming convention β Directory suffix convention enables wildcard patterns in.gitignoreandsettings.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-workspacein VS Code - [ ] Install recommended extensions when prompted
- [ ]
docker compose up -dinbasic-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-workspaceand debug config tolaunch.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