Devcontainers vs Bare Docker Compose for Team Onboarding

You need a new engineer productive on day one, and you are deciding whether to ship a .devcontainer/devcontainer.json that wraps the stack inside the editor or a plain docker-compose.yml they run from the terminal. Both build on the same image and orchestration layer covered in Devcontainer Configuration Standards and Multi-Service Orchestration with Compose; the choice is about where the developer's tools live, not whether you use containers at all.

The two options in one sentence each

Bare Docker Compose runs your services in containers, but the developer's editor, language server, linters, and debugger run on the host and connect to exposed ports. Devcontainers put the editor's backend (extensions, language servers, terminals) inside a container too, so the toolchain itself is version-controlled and identical for everyone.

A devcontainer almost always consumes a Compose file via dockerComposeFile. So this is not "Compose or devcontainers" at the infrastructure layer — it is "do we also containerize the IDE backend."

Decision criteria

Criterion Bare Docker Compose Devcontainers
First-run startup cost Low — docker compose up; host tools already installed Higher — pulls/builds a dev image, installs features and extensions on first open
IDE integration Manual — host editor connects to forwarded ports; each dev installs extensions themselves Automatic — customizations.vscode.extensions and settings provision the editor for everyone
Reproducibility of the toolchain Partial — services reproducible, but host Node/Python/linters drift per machine High — language runtimes, CLIs, and extensions pinned in the container
CI parity Good for service behavior; build steps may differ from host tooling Strong — the same image can back local dev and CI runners
Learning curve Lowest — engineers already know docker compose Moderate — devcontainer.json lifecycle, features, and the CLI are new to most
Editor lock-in None — any editor works Best in VS Code; JetBrains and devcontainer CLI support exists but is less polished
Offline / air-gapped friendliness Easier — fewer registries to reach Harder — features and extensions pull from ghcr.io and the marketplace
Where the toolchain runs Two stacks compared: bare Compose keeps the editor on the host while devcontainers move the editor backend into a container. Where the Toolchain Runs Bare Docker Compose Host: editor + linters + LSP Containers: app, db, cache via forwarded ports Devcontainer Container: editor backend + extensions + LSP + app, db, cache host runs only the UI

Choose bare Docker Compose when

  • Your team uses mixed editors (Vim, Emacs, JetBrains, VS Code) and you cannot mandate one.
  • Onboarding speed on a known machine matters more than toolchain reproducibility — engineers already have Node/Python/Go installed and consistent.
  • You run in constrained or air-gapped networks where pulling features and marketplace extensions is unreliable.
  • The stack is small (1–3 services) and the toolchain rarely drifts.

A minimal Compose-first setup looks like this:

# docker-compose.yml
services:
  app:
    build:
      context: .
      target: dev
    ports:
      - "${APP_PORT:-3000}:3000"
    volumes:
      - ./src:/app/src:cached
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 3s
      timeout: 5s
      retries: 5
#!/usr/bin/env bash
# bin/up.sh — bare Compose onboarding
set -euo pipefail
cp -n .env.example .env || true
docker compose up -d --wait
echo "Stack ready on http://localhost:${APP_PORT:-3000}"

Choose devcontainers when

  • "Works on my machine" failures trace back to host tool versions, not service config.
  • Most of the team is on VS Code and you want extensions, formatters, and settings provisioned automatically.
  • You want one image to back both local development and CI, maximizing the kind of parity described in automating runtime parity checks between local and staging.
  • New hires should not need to install a single language runtime locally.

The devcontainer wraps the same Compose file and adds editor provisioning:

// .devcontainer/devcontainer.json
{
  "name": "Platform Baseline",
  "dockerComposeFile": ["../docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/app",
  "features": {
    "ghcr.io/devcontainers/features/node:1": { "version": "20" }
  },
  "customizations": {
    "vscode": {
      "extensions": ["[email protected]", "esbenp.prettier-vscode"],
      "settings": { "editor.formatOnSave": true }
    }
  },
  "postCreateCommand": "npm ci"
}

Don't pick once and freeze it

A pragmatic path is to keep docker-compose.yml as the source of truth and layer a devcontainer.json on top. Terminal-only engineers run docker compose up; VS Code users open the folder in the container. Both consume the same services, so you avoid maintaining two divergent stacks. Keep extensions and settings shareable as described in sharing VS Code extensions and settings across a team.

Validating the decision

Whichever you pick, measure it. Time a clean clone-to-running-app on a fresh machine and track the first-run success rate, the same metric onboarding teams use in how to measure developer onboarding time in distributed teams.

#!/usr/bin/env bash
# bin/time-onboarding.sh — clone-to-ready timing
set -euo pipefail
start=$(date +%s)
docker compose up -d --wait
end=$(date +%s)
echo "Clone-to-ready: $((end - start))s"

macOS (Docker Desktop): devcontainer first-open is slower because image build and bind-mount warmup run inside the Linux VM; use :cached mounts to soften it. WSL2: keep the repo on the Linux filesystem (~/code, not /mnt/c) for both options, or file-watch and devcontainer attach degrade. Apple Silicon (ARM64): pin platform: linux/amd64 only for images lacking arm64 manifests; otherwise prefer native arm64 to avoid emulation in both setups.