Skip to main content

Dev Containers: From Fundamentals to Implementation


PART 1: Generic Devcontainer Fundamentals

What is a Dev Container?​

A dev container is a Docker container that holds your entire development environmentβ€”programming languages, tools, databases, libraries. Instead of installing everything on your machine, you define it once in files, and anyone can spin up an identical environment.

Simple way to think about it:

  • Your code lives on your computer (files you edit)
  • Your tools live in a container (Docker runs them)
  • VS Code acts as a bridge between them

Result: Everyone on your team has the exact same setup.


Why Use Dev Containers?​

ProblemDev Container Solution
"It works on my machine" but not yoursEveryone gets identical environment
New developers spend 2 hours setting upNew dev: open container, 5 minutes done
Different results on Windows vs Mac vs LinuxContainer runs the same on all OS
Conflicting global installationsAll tools isolated in one container
Hard to reproduce old project environmentsContainer definition is version-controlled

Prerequisites: What You Need​

  1. Docker Desktop (Windows/Mac) or Docker Engine (Linux)

  2. VS Code with the Dev Containers extension

    # Install from VS Code extension marketplace
    # Or command line: code --install-extension ms-vscode-remote.remote-containers
  3. Git (to clone the project)


How Dev Containers Work: The Flow​

User Action                Dev Container Action
─────────────────────────────────────────────────────────
VS Code: "Reopen in container"
↓
Read devcontainer.json
↓
Build/pull Docker image (Dockerfile)
↓
Start services (docker-compose.yml)
↓
Run init script (init-dev-env.sh)
↓
VS Code connects inside container
↓
Ready! Edit code on your machine,
container runs it

Step-by-Step: Building a Dev Container​

Step 1: Create the .devcontainer/ Folder​

This is your dev container configuration directory. All devcontainer files go here.

# From project root
mkdir .devcontainer
cd .devcontainer

File structure so far:

your-project/
└── .devcontainer/ ← All devcontainer files live here

Step 2: Create devcontainer.json​

What it does: This is VS Code's instruction manual. It tells VS Code:

  • Which Docker image/compose file to use
  • Which ports to forward to your machine
  • Which extensions to auto-install
  • Environment variables to inject
  • When to run initialization scripts

Minimal example (generic, works for any project):

{
"name": "My Dev Container",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace/my-project",

"features": {
"ghcr.io/devcontainers/features/common-utils:2": {}
},

"customizations": {
"vscode": {
"extensions": [
"ms-vscode.remote-repositories"
]
}
},

"forwardPorts": [3000, 5000],
"postCreateCommand": "bash .devcontainer/init-dev-env.sh",
"remoteUser": "vscode"
}

What each section means:

FieldPurposeExample
nameDisplay name in VS Code"My Dev Container"
dockerComposeFileWhich compose file to use"docker-compose.yml"
serviceWhich service to attach VS Code to"app"
workspaceFolderWhere your code is inside container"/workspace/my-project"
featuresPre-built environment templatesghcr.io/devcontainers/features/*
forwardPortsPorts to forward to your machine[3000, 5000]
postCreateCommandScript to run after container starts"./init-dev-env.sh"
remoteUserUser to run commands as"vscode"

File structure so far:

your-project/
└── .devcontainer/
└── devcontainer.json ← VS Code config

Step 3: Create Dockerfile​

What it does: A recipe that builds the Docker image. It specifies:

  • Base image (starting point)
  • System packages to install
  • Environment variables
  • User to run as

Minimal example (generic Node.js project):

FROM node:20

# Install system packages
RUN apt-get update && apt-get install -y \
curl \
git \
&& rm -rf /var/lib/apt/lists/*

# Set working directory
WORKDIR /workspace

# Run as non-root user
USER node

What each line means:

LinePurpose
FROM node:20Start with Node.js 20 image
RUN apt-get update...Install system packages
WORKDIR /workspaceSet default directory inside container
USER nodeRun as node user (not root)

Key principle: Your Dockerfile should install everything needed to develop. A new developer runs one command and has all tools ready.

File structure so far:

your-project/
└── .devcontainer/
β”œβ”€β”€ devcontainer.json
└── Dockerfile ← Container recipe

Step 4: Create docker-compose.yml​

What it does: Defines all the services that make up your development environment. Instead of running one container, you might run:

  • Your app container (where VS Code connects)
  • A database container
  • A cache/message queue
  • A mock API server

Minimal example (app + database):

services:

# Main development container (where VS Code connects)
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ../..:/workspace:cached # Your code lives here
command: sleep infinity # Keep container running
environment:
NODE_ENV: development
ports:
- "3000:3000" # Forward port 3000 to your machine
depends_on:
db:
condition: service_started

# Database service
db:
image: postgres:15
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass
POSTGRES_DB: myapp_db
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432"

# Named volumes persist data between container restarts
volumes:
postgres-data:

What each section means:

SectionPurpose
services:Define all containers
app:Your main development container
build:Build from Dockerfile in current directory
volumes:Mount directories from your machine into container
ports:Forward ports from container to your machine
depends_on:Wait for other services before starting
db:A PostgreSQL database service
environment:Environment variables passed to container

Key principle: Each service is a container. They can talk to each other by service name (db, app, etc.). They can also be reached from your machine via forwarded ports.

File structure so far:

your-project/
└── .devcontainer/
β”œβ”€β”€ devcontainer.json
β”œβ”€β”€ Dockerfile
└── docker-compose.yml ← Service definitions

What it does: One-time setup script that runs after the container starts. Common tasks:

  • Install development tools globally
  • Create/migrate databases
  • Verify tool versions
  • Download certificates

Minimal example (generic):

#!/bin/bash
set -e

echo "πŸ”§ Initializing dev container..."

# Verify tools are installed
echo "πŸ“¦ Checking tools..."
node --version
npm --version

# Install project dependencies
echo "πŸ“¦ Installing dependencies..."
cd /workspace/my-project
npm install

echo "βœ… Dev container ready!"

Key sections:

LinePurpose
#!/bin/bashThis is a bash script
set -eExit if any command fails
echo "..."Print messages (guides user)
npm installInstall dependencies

Important: Make the script executable:

chmod +x .devcontainer/init-dev-env.sh

File structure so far:

your-project/
└── .devcontainer/
β”œβ”€β”€ devcontainer.json
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ docker-compose.yml
└── init-dev-env.sh ← Setup script

Step 6: Test It​

You now have a working devcontainer. Test it:

  1. Open VS Code in your project directory
  2. Press F1 (or Cmd+Shift+P on Mac)
  3. Type: Dev Containers: Reopen in Container
  4. Wait for container to build (first time: 2–5 minutes)
  5. Open a terminal inside VS Code (Ctrl+ backtick)
  6. Verify tools:
    node --version
    npm --version

If you see the expected versions, it works! βœ…


Key Concepts to Remember​

Volumes​

Volumes link your machine to the container. Two types:

  • Bind mount (../..:/workspace:cached) β€” Your code on your machine is visible inside container. Edit on machine β†’ code updates inside container.
  • Named volume (postgres-data:) β€” Docker manages storage. Used for databases or build artifacts.

Ports​

Ports forward from container to your machine so you can access services.

Example: "3000:3000" means:

  • Container port 3000 β†’ Machine port 3000
  • Access from your browser: http://localhost:3000

Service Names​

Inside the container, services talk to each other by name:

# Inside container
curl http://db:5432 # Reaches the 'db' service
curl http://app:3000 # Reaches the 'app' service

From your machine:

# From your machine
curl http://localhost:5432 # Reaches forwarded port

The depends_on Rule​

If one service needs another, list it:

app:
depends_on:
db:
condition: service_healthy # Wait until db is healthy

This ensures the database starts before your app tries to connect.


PART 2: SIZO Implementation

How We Applied the Blueprint to SIZO​

The SIZO project is a full-stack application (backend + frontend + database + mocking). We built a devcontainer that:

  1. Provides a complete dev environment (.NET 8, Node.js, PostgreSQL)
  2. Manages multiple services (backend API, frontend dev server, database, mock APIs)
  3. Handles corporate network requirements (SEED Cloudflare TLS inspection)
  4. Enables offline development (mock APIs instead of real external services)
  5. Works with and without the container (dual-mode design)

SIZO Folder & File Structure​

sizo-application/
β”œβ”€β”€ Backend/ # .NET 8 backend
β”‚ β”œβ”€β”€ Properties/launchSettings.json # Two profiles: Backend, Backend-Container
β”‚ β”œβ”€β”€ appsettings.Development.json # Dev config (uses http://wiremock:8080)
β”‚ └── ...
β”œβ”€β”€ frontend/ # React frontend
β”‚ β”œβ”€β”€ package.json # Two scripts: start, start:container
β”‚ └── ...
β”œβ”€β”€ api-mocking/
β”‚ β”œβ”€β”€ mappings/ # WireMock mock definitions (JSON)
β”‚ β”‚ └── proxy-to-sizo-api.json # Proxies to localhost (bare-metal) or app (container)
β”‚ └── files/ # Mock response files
└── .devcontainer/ # ← Dev container config
β”œβ”€β”€ devcontainer.json
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ init-dev-env.sh
β”œβ”€β”€ wiremock/
β”‚ └── proxy-to-sizo-api.json # Container override (targets app:*)
└── README.md

Why this structure?

  • .devcontainer/ = all container config (safe to version-control)
  • Backend/, frontend/, api-mocking/ = shared (work with or without container)
  • Container-specific files override shared files (e.g., WireMock proxy mapping)

Step-by-Step: SIZO's Dev Container Files​

File 1: devcontainer.json (SIZO Version)​

What we added beyond the generic version:

{
"name": "Sizo Backend",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace/sizo-application",

"features": {
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/node:1": {
"version": "lts"
}
},

"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csdevkit",
"humao.rest-client",
"dbaeumer.vscode-eslint"
],
"settings": {
"csharp-dev-tools.roslyn.projectOrSolutionFiles": [
"/workspace/sizo-application/sizo-application.sln"
]
}
}
},

"forwardPorts": [3000, 5001],
"postCreateCommand": "bash /workspace/sizo-application/.devcontainer/init-dev-env.sh",
"remoteUser": "vscode",
"remoteEnv": {
"DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER": "0",
"NODE_TLS_REJECT_UNAUTHORIZED": "0",
"DOTNET_ENVIRONMENT": "Development",
"ASPNETCORE_ENVIRONMENT": "Development",
"SIZO_DB_CONN_STRING": "Host=local-db;Port=5432;Database=sizo_db;Username=sizouser;Password=sizoPassword"
}
}

Why these additions?

AdditionWhy
features.node:1Need Node.js for frontend development
extensionsAuto-install C# Dev Kit, REST Client, ESLint for our stack
roslyn.projectOrSolutionFilesTell C# Dev Kit where the solution file is
forwardPorts: [3000, 5001]Frontend (3000) and backend (5001) ports
remoteEnv variablesConfigure .NET for SEED network and container database

Key difference from generic: We added specific extensions and environment variables for our tech stack (.NET, React, PostgreSQL).


File 2: Dockerfile (SIZO Version)​

What we added beyond the generic version:

# SEED-compliant dev container with .NET 8
# Includes Cloudflare CA certificate for TLS inspection
# Node.js is installed via devcontainer feature (see devcontainer.json)

FROM mcr.microsoft.com/devcontainers/dotnet:8.0

# Install additional system dependencies for C# Dev Kit
RUN apt-get update && apt-get install -y \
libicu-dev \
&& rm -rf /var/lib/apt/lists/*

# Install Cloudflare CA certificate (SEED requirement)
RUN curl -fsSk -o /usr/local/share/ca-certificates/Cloudflare_CA.crt \
https://seed-general-public-files.s3.ap-southeast-1.amazonaws.com/seed-cloudflare-root-certs/Cloudflare_CA.pem \
&& update-ca-certificates

# Set CA bundle environment variables for various tools
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \
AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

USER vscode

Why these additions?

AdditionWhy
FROM mcr.microsoft.com/devcontainers/dotnet:8.0Base image with .NET 8 pre-installed
libicu-devRequired by C# Dev Kit for language support
Cloudflare CA certificateSEED network uses Cloudflare TLS inspection; we need to trust it
ENV variablesTell all tools (.NET, Node, Python, AWS CLI) to trust the SEED certificate

Why this matters: Without the Cloudflare certificate, any HTTPS request inside the container fails. This is a SEED-specific requirement.


File 3: docker-compose.yml (SIZO Version)​

What we defined:

services:

# Main app container (where VS Code connects)
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ../..:/workspace:cached
- frontend-node-modules:/workspace/sizo-application/frontend/node_modules
command: sleep infinity
environment:
ASPNETCORE_ENVIRONMENT: Development
DOTNET_ENVIRONMENT: Development
SIZO_DB_CONN_STRING: "Host=local-db;Port=5432;Database=sizo_db;Username=sizouser;Password=sizoPassword"
depends_on:
local-db:
condition: service_healthy
wiremock:
condition: service_started

# PostgreSQL database
local-db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_DB: sizo_db
POSTGRES_USER: sizouser
POSTGRES_PASSWORD: sizoPassword
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sizouser -d sizo_db"]
interval: 5s
timeout: 5s
retries: 10

# WireMock β€” Mock API server
wiremock:
image: wiremock/wiremock:2.33.2-alpine
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ../api-mocking/mappings:/home/wiremock/mappings:ro
- ../api-mocking/files:/home/wiremock/__files:ro
# Container override: use Docker Compose service names (app:*) instead of localhost
- ./wiremock/proxy-to-sizo-api.json:/home/wiremock/mappings/proxy-to-sizo-api.json:ro
command: ["--global-response-templating", "--verbose"]

volumes:
postgres-data:
frontend-node-modules:

Three services explained:

ServicePurposePortWhy
appYour backend & frontend dev environment5001, 3000Where VS Code connects; where you run code
local-dbPostgreSQL 17 database5432Stores app data; initialized automatically
wiremockMock API server8080Simulates external APIs; pre-loaded with mock responses

Key design choices:

  1. depends_on with service_healthy β€” App waits for database to be fully ready before starting
  2. frontend-node-modules volume β€” Stores node_modules on Docker volume (faster than bind mount on Windows)
  3. postgres-data volume β€” Persists database between restarts
  4. WireMock overlay mount β€” .devcontainer/wiremock/proxy-to-sizo-api.json overrides the committed file so container and bare-metal use different host names

File 4: init-dev-env.sh (SIZO Version)​

What the setup script does:

#!/bin/bash
set -e

echo "πŸ”§ Initializing SEED dev container..."

# Trust the workspace repo (fixes git UID mismatch errors)
git config --global --add safe.directory /workspace/sizo-application

# Fix symlinks and line endings (Windows host issue)
git config core.symlinks true
git checkout HEAD -- Backend/DocumentGenerator/QualifyingActivities 2>/dev/null || true
git config core.autocrlf input
git add --renormalize . 2>/dev/null || true

# Verify tools
echo "πŸ“¦ Verifying tools..."
echo " .NET: $(dotnet --version)"
echo " Node: $(node --version)"
echo " npm: $(npm --version)"

# Verify CA certificate works
echo "πŸ”’ Verifying CA certificate..."
if curl -fsSI https://www.npmjs.com/ > /dev/null; then
echo " βœ… CA certificate working"
else
echo " ⚠️ Warning: Cannot reach npmjs.com"
fi

# Generate HTTPS dev cert
echo "πŸ”’ Generating HTTPS dev certificate..."
if ! dotnet dev-certs https --check --trust 2>/dev/null; then
dotnet dev-certs https --trust 2>/dev/null || true
fi

# Install EF Core tools
echo "πŸ”’ Installing EF tools..."
if ! dotnet ef --version > /dev/null 2>&1; then
dotnet tool install --global dotnet-ef --version 8.0.* 2>/dev/null || true
export PATH="$PATH:$HOME/.dotnet/tools"
fi

# Apply database migrations
echo "πŸ”’ Applying database migrations..."
cd /workspace/sizo-application/Backend
export SIZO_DB_CONN_STRING="Host=local-db;Port=5432;Database=sizo_db;Username=sizouser;Password=sizoPassword"
dotnet ef database update || echo "⚠️ Migration may already be up to date"

# Fix node_modules ownership
echo "πŸ”’ Fixing node_modules volume ownership..."
sudo chown -R vscode:vscode /workspace/sizo-application/frontend/node_modules 2>/dev/null || true

echo "βœ… Dev container ready!"

What each section does:

SectionPurposeWhy SIZO Specific
git config safe.directoryFixes "dubious ownership" errorContainer UID differs from host UID on Linux
Fix symlinks/CRLFWindows hosts mount with wrong line endingsWindows issue
Verify toolsConfirm .NET, Node, npm installedTech stack verification
Verify CA certTest SEED certificate worksSEED network requirement
HTTPS dev certGenerate self-signed cert for local HTTPSBackend runs HTTPS by default
EF Core toolsInstall database migration tool.NET backend uses EF migrations
Database migrationsCreate/update database schemaAutomatic setup for new developers
node_modules ownershipFix Docker volume permissionsContainer runs as root, app runs as vscode

Understanding SIZO's Dual-Mode Design​

This is the key innovation: files work with or without the container.

How It Works​

FileWithout Container (localhost)Inside Container (Docker service names)
Backend/launchSettings.jsonBackend profile β†’ localhost:5432Backend-Container profile β†’ local-db:5432
frontend/package.jsonnpm start β†’ HTTPS, localhostnpm run start:container β†’ HTTP, Docker DNS
api-mocking/mappings/proxy-to-sizo-api.jsonProxies to http://localhost:5001Overridden by .devcontainer/wiremock/... β†’ http://app:5001

Why This Matters​

Without dual-mode:

  • Developers using the container get a working setup
  • Developers without the container break (hard-coded local-db doesn't exist locally)
  • Force everyone onto containers (not flexible)

With dual-mode:

  • Container users: dotnet run --launch-profile Backend-Container
  • Bare-metal users: dotnet run (default profile)
  • Both work; both happy; flexible choice

Issues We Faced & How We Solved Them​

Issue 1: TLS/SSL Certificate Errors in SEED Network​

First: What is TLS/SSL and Why Do We Need It?​

TLS/SSL = Secure encryption for the internet

Think of the internet like a postal service:

  • Without TLS: You send a postcard. Anyone handling it can read it. Anyone could change it.
  • With TLS: You send an encrypted letter. Only the intended recipient can open it. Tampering is detected.

TLS = Transport Layer Security (the newer name)
SSL = Secure Sockets Layer (older name, mostly replaced by TLS but people still say "SSL")

Together they do two things:

WhatWhy
Encrypt trafficProtect data traveling over the network from being read by others
Verify identityProve the server you're talking to is actually who it claims to be (not a fake)

How TLS/SSL Works (The Flow)​

1. You (client)                    Server
"Can I talk to example.com?"
↓
Server sends back: "Here's my certificate (proof I'm example.com)"
←

2. You check:
"Is this certificate signed by someone I trust?"
(Look in your certificate store)
↓

3. Certificate is trusted βœ…
"Encrypted connection established"
↓ ↑ (now all communication is encrypted)

What's in a certificate?

Certificate = ID + Signature
β”œβ”€ Domain name: "example.com"
β”œβ”€ Public key: (math key for encryption)
β”œβ”€ Issued by: "VeriSign" (a trusted authority)
β”œβ”€ Valid from: 2024-01-01
β”œβ”€ Valid until: 2025-01-01
└─ Digital signature: (proof this is real, not forged)

Where certificates come from:

  • Trusted authorities (VeriSign, Let's Encrypt, etc.) issue them
  • Your computer has a certificate store β€” a list of authorities you trust
  • When a server sends a certificate, you check: "Was this signed by someone in my list?"
  • If yes β†’ trusted β†’ encrypted connection βœ…
  • If no β†’ untrusted β†’ connection blocked ❌

Why SEED Network Causes Certificate Errors​

Normal setup:

Your computer β†’ [internet] β†’ Real server (npmjs.com)
sends: "I'm npmjs.com, signed by trusted authority"
you: βœ… "I trust that authority, proceed"

SEED network setup (with corporate firewall/proxy):

Your computer β†’ [SEED proxy - Cloudflare] β†’ [internet] β†’ Real server
↑
The proxy intercepts ALL HTTPS traffic

The problem:

  1. You try to connect to npmjs.com
  2. Cloudflare proxy intercepts (inspects/logs the traffic for security)
  3. Cloudflare re-encrypts and forwards your request
  4. But your computer sees Cloudflare's certificate, not npmjs.com's
  5. Your certificate store doesn't include Cloudflare's certificate
  6. You reject it: ❌ "I don't trust this certificate!"

Result: Connection fails

Error: unable to get local issuer certificate
(means: "I don't recognize who signed this certificate")

The Solution: Trust Cloudflare's Certificate​

We add Cloudflare's certificate to the container's certificate store:

# Download Cloudflare's certificate
RUN curl -fsSk -o /usr/local/share/ca-certificates/Cloudflare_CA.crt \
https://seed-general-public-files.s3.ap-southeast-1.amazonaws.com/seed-cloudflare-root-certs/Cloudflare_CA.pem

# Add it to the system's trusted certificates
RUN update-ca-certificates

# Tell all tools where the trusted certificates are
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt

What this does:

  1. Download Cloudflare's certificate (public file, not secret)
  2. Install it into the system certificate store
  3. Tell all tools where to find the certificate file

Now when the container connects:

Container β†’ [SEED proxy] β†’ [internet]
↓
Sends: "I'm Cloudflare, signed by..."
Container checks certificate store
"I recognize you! Here's the Cloudflare cert I installed"
βœ… Connection established and encrypted

Why This Works​

ComponentWhat It Does
DockerfileInstalls Cloudflare certificate into container
Environment variablesTell .NET, Node.js, Python where to find trusted certificates
init-dev-env.shVerifies certificate is working (test curl to npmjs.com)

All three work together: Install β†’ Configure β†’ Verify


What You Need to Know​

  • βœ… TLS/SSL = encryption + identity verification
  • βœ… Certificates prove a server is who it claims to be
  • βœ… Your computer has a certificate store (list of trusted authorities)
  • βœ… SEED network uses Cloudflare proxy (intercepts HTTPS traffic)
  • βœ… Container doesn't know about Cloudflare by default
  • βœ… Solution: Add Cloudflare's certificate to the container
  • βœ… Already done in Dockerfile; just needs rebuild if errors occur

Symptom:

Error: unable to get local issuer certificate
UNABLE_TO_VERIFY_LEAF_SIGNATURE

Root Cause: SEED network uses Cloudflare TLS inspection. Any HTTPS request goes through Cloudflare's proxy, which needs a custom CA certificate.

Solution: Dockerfile now:

  1. Downloads Cloudflare CA certificate
  2. Installs it into the system certificate store
  3. Tells all tools to use it (via environment variables)
# Download and install certificate
RUN curl -fsSk -o /usr/local/share/ca-certificates/Cloudflare_CA.crt ...
RUN update-ca-certificates

# Tell all tools to trust it
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# ... etc

Result: Certificate errors fixed; npm install, dotnet restore etc. work inside container.


Issue 2: Windows File System Performance​

Symptom:

  • Container startup takes 10+ minutes
  • File operations feel sluggish
  • Rebuilds are slow

Root Cause: Docker Desktop on Windows uses Hyper-V, which has poor performance with bind mounts (/mnt/c/...).

Solution:

  1. Switch Docker to WSL 2 backend (faster)
  2. Store project in WSL file system (not /mnt/c/)
  3. Allocate more resources (8GB RAM, 4+ CPUs)

Result: Container startup: 5 min (first time), 30 sec (subsequent).


Issue 3: Git "Dubious Ownership" Error​

Symptom:

fatal: detected dubious ownership in repository at '...'

Root Cause: Container runs with different UID than host. Git refuses to operate when ownership doesn't match.

Solution: In init-dev-env.sh:

git config --global --add safe.directory /workspace/sizo-application

This tells Git to trust the workspace regardless of UID mismatch.

Result: Git commands work inside container.


Issue 4: Database Migration Fails on First Run​

Symptom:

Failed to apply migration 'InitialCreate'
Connection timeout

Root Cause: App tries to run migrations before PostgreSQL is fully ready.

Solution: In docker-compose.yml:

app:
depends_on:
local-db:
condition: service_healthy # ← Wait for health check to pass

PostgreSQL includes a healthcheck that confirms it's accepting connections.

Result: Database is guaranteed ready before migrations run.


Issue 5: WireMock Mappings Don't Load or Wrong Host Names​

Symptom:

curl http://127.0.0.1:8080/__admin/mappings
# Returns no mappings, or maps to localhost instead of app

Root Cause: Two environments (bare-metal and container) need different proxy targets:

  • Bare-metal: proxy to localhost:5001
  • Container: proxy to app:5001 (Docker Compose service name)

Solution:

  1. Committed file: api-mocking/mappings/proxy-to-sizo-api.json β†’ targets localhost
  2. Container override: .devcontainer/wiremock/proxy-to-sizo-api.json β†’ targets app
  3. Docker Compose overlay mounts the container version on top
wiremock:
volumes:
- ../api-mocking/mappings:/home/wiremock/mappings:ro # Base
- ./wiremock/proxy-to-sizo-api.json:/home/wiremock/mappings/proxy-to-sizo-api.json:ro # Override

Result: Both environments work; WireMock proxies to correct host.


Issue 6: Node Modules Permission Errors​

Symptom:

EACCES: permission denied, open '/workspace/sizo-application/frontend/node_modules/...'
npm ERR! code EACCES

Root Cause: frontend-node-modules volume is created by Docker as root. App runs as vscode user. Permission mismatch.

Solution: In init-dev-env.sh:

sudo chown -R vscode:vscode /workspace/sizo-application/frontend/node_modules

Change ownership from root to vscode.

Result: npm install and builds work without permission errors.


Quick Start: Using SIZO Dev Container​

1. Open in Container​

F1 β†’ "Dev Containers: Reopen in Container"

2. Wait for Initialization​

Watch for: βœ… Dev container ready!

3. Start Backend (Terminal 1)​

cd Backend
dotnet run --launch-profile Backend-Container

4. Start Frontend (Terminal 2)​

cd frontend
npm install # first time only
npm run start:container

5. Open App​

Navigate to: http://127.0.0.1:8080/index.html


Key Takeaways​

Generic Principles (Apply to Any Project)​

  • βœ… .devcontainer/ folder holds all configuration
  • βœ… devcontainer.json tells VS Code how to connect
  • βœ… Dockerfile defines the base image and installs tools
  • βœ… docker-compose.yml orchestrates multiple services
  • βœ… init-dev-env.sh runs one-time setup
  • βœ… Volumes link your code to the container
  • βœ… Ports forward services from container to your machine
  • βœ… Service names allow containers to talk to each other

SIZO-Specific Implementation​

  • βœ… Three services: app (dev environment), local-db (PostgreSQL), wiremock (mock APIs)
  • βœ… Dual-mode design: works with container and without container
  • βœ… SEED certificate handling: Dockerfile installs Cloudflare CA cert
  • βœ… WireMock overlay: container and bare-metal use different proxy targets
  • βœ… Setup script: automates database migrations, tool installations, permissions
  • βœ… Always use Backend-Container profile inside container; plain Backend profile without container

Common Issues & Fixes​

Container won't start​

  • First build is slow (5–10 min). Subsequent builds: 30 sec.
  • Check Docker Desktop has enough resources (8GB RAM, 4 CPUs minimum).

SSL/TLS errors​

  • SEED network requires Cloudflare certificate. Already installed in Dockerfile.
  • If errors persist: F1 β†’ "Dev Containers: Rebuild Container"

Database errors​

  • Wait for βœ… Dev container ready! before running backend.
  • Reset database: docker compose -f .devcontainer/docker-compose.yml down -v

WireMock not working​

  • Restart WireMock: curl -X POST http://127.0.0.1:8080/__admin/shutdown
  • Container restart policy brings it back automatically.

Port already in use​

  • Stop conflicting process: lsof -ti:5432 | xargs kill -9 (Mac/Linux)
  • Or change port in docker-compose.yml

Permission denied errors​

  • Already fixed in init-dev-env.sh (owns node_modules to vscode user)

Where to Go From Here​

  • More detail: See .devcontainer/README.md in the repository
  • Technical deep-dive: See .devcontainer/IMPLEMENTATION-NOTES.md
  • Have questions? Ask your team lead or senior developer