Devcontainers vs Bare Docker Compose for Team Onboarding
Decide between devcontainers and plain Docker Compose for onboarding a team. Compare startup cost, IDE integration, reproducibility, CI parity, and learning curve.
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 |
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
:cachedmounts 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): pinplatform: linux/amd64only for images lacking arm64 manifests; otherwise prefer native arm64 to avoid emulation in both setups.