The new hire spends their first day installing Node 20 instead of 22, Postgres 15 instead of 17, and a globally installed CLI that silently shadows the project’s own. Nobody notices until the test suite fails on their machine and passes everywhere else. “Works on my machine” is not a personality flaw — it is a missing environment contract.
Two tools claim to fix it: Dev Containers, which put the entire toolchain inside a container the editor attaches to, and Docker Compose, which has been describing multi-service stacks since 2014 and now ships a file-watching dev loop. They overlap just enough to be confusing, and choosing wrong costs you either flexibility or weeks of onboarding friction.
TL;DR: The 30-Second Verdict
- One language, one toolchain, editor-integrated → Dev Containers.
devcontainer.jsonplus apostCreateCommandgives every developer and every CI runner the same shell, the same extensions, the same versions. - Multi-service application with a database, queue and worker → Docker Compose, optionally inside a Dev Container via
dockerComposeFile. - You want the dev environment to be the deployment artifact → Compose. The image you test is the image you ship.
- You want a fully disposable machine, rebuilt in one click, for contractors or rotating contributors → Dev Containers.
- You already have a working
compose.yamland no onboarding pain → do not migrate. Adddevelop.watchinstead; it solves the edit-reload loop with about eight lines.
Quick Comparison
| Dimension | Dev Containers | Docker Compose |
|---|---|---|
| Specification | devcontainers/spec — 5.8k stars, frozen API since 2022 | docker/compose — 38.6k stars, actively developed |
| Reference CLI | devcontainers/cli — 3.0k stars, active | docker compose plugin |
| Unit of configuration | devcontainer.json per workspace | compose.yaml per stack |
| Editor integration | Native in VS Code, JetBrains, Codespaces, DevPod | None — you attach manually or use a plugin |
| Multi-service support | Via dockerComposeFile pointing at a Compose file | Native |
| Reproducibility | Very high — image + features + lifecycle commands | High — image tags and env files, but host state leaks in |
| Onboarding | Open folder → “Reopen in Container” | Install Docker → docker compose up |
| Dev loop | Bind mount or volume, editor handles sync | Bind mounts, or develop.watch with sync/rebuild |
| CI usage | devcontainer up + devcontainer exec in CI | docker compose run --rm |
| Learning curve | Medium — JSON schema plus lifecycle hooks | Low |
| Best fit | Language toolchains, disposable workspaces | Application stacks, staging-parity testing |
Decision Matrix: Pick in 10 Seconds
| Your situation | Recommendation | Reason |
|---|---|---|
| 12 developers, 3 languages, constant version drift | Dev Containers | One JSON file ends the drift |
| Monolith + Postgres + Redis + worker | Compose (optionally as a Dev Container) | Compose models the stack; the container models the toolchain |
| CI must mirror local exactly | Dev Containers | devcontainer exec runs the same image as the editor |
| You need GPU access for local model work | Either, with explicit --gpus / runArgs | Both support it; Compose is easier to audit |
| Contractors on unknown laptops | Dev Containers | Nothing installed on the host but Docker |
| You want the fastest possible iteration loop | Compose + develop.watch | Sub-second sync, no editor dependency |
Dev Containers: The Toolchain as a Contract
The model is simple: your editor runs on the host, your code lives on the host (or in a volume), and every tool runs inside a container the editor attaches to. The container is described by a devcontainer.json file committed to the repository. Here is a real one from the official template starter:
| |
Three properties make this more than a fancy Dockerfile:
featuresare composable installers. Instead of hand-writing apt lines for Docker, Go, kubectl or AWS CLI, you reference versioned OCI artifacts.docker-in-docker:2above is a single line that gives the container its own daemon.- Lifecycle hooks are explicit.
onCreateCommandruns once when the container is created,postCreateCommandafter it is assigned to the workspace,postStartCommandon every start. Dependency installs belong inpostCreateCommand; long builds belong in the image. - The editor configuration travels with the code. Extensions, settings and forwarded ports are part of the repository, so a fresh clone behaves like an experienced developer’s machine.
Run it without an editor at all — this is how you use the same environment in CI:
| |
That pair of commands is the underrated part of the story: the environment your editor uses and the environment your pipeline uses are the same artifact, not two YAML files that drift apart.
When the project needs services
Pure Dev Containers solve the toolchain, not the stack. For a database and a queue, point devcontainer.json at an existing Compose file instead of duplicating it:
| |
With dockerComposeFile, the editor starts your real stack and attaches to the app service — you get Compose’s multi-service model plus Dev Containers’ editor integration. This is the configuration most teams should end up with.
Docker Compose: The Stack You Already Have
Compose’s dev story improved decisively with develop.watch. Instead of bind-mounting your source tree and hoping inotify behaves on macOS, you declare what should happen when files change:
| |
| |
Three actions cover nearly every workflow: sync copies changed files into the container, sync+restart restarts the service after syncing (for config that is read at boot), and rebuild rebuilds the image when dependencies change. In practice this gets you most of the inner-loop speed of a native install without touching the host.
Where Compose still wins outright:
- Parity with production. The same
compose.yamlcan run on the server. That is not true ofdevcontainer.json. - Multi-service default. Services, networks, volumes, health checks and startup order are first-class.
- No editor dependency. Any terminal works; nothing needs to attach.
- Profiles for optional services.
--profile debugbrings up Mailhog and a debug proxy only when you ask.
Where it quietly fails as a dev-environment contract: the host still owns the toolchain. node, python, make and the shell itself run outside the container unless you carefully wrap every command in docker compose exec. Two developers with different host versions can still get different results — the exact problem Dev Containers was built to eliminate.
Mixing Both Without Regret
The pragmatic architecture in 2026:
| Layer | Tool | Why |
|---|---|---|
| Editor, shell, linters, language runtime | Dev Container | Identical for everyone, disposable |
| Application services (db, cache, queue) | Compose file referenced by dockerComposeFile | Real topology, health checks, one source of truth |
| Production deployment | Compose (or an orchestrator) | Same images, same env contracts |
| CI | devcontainer exec or compose run | Whichever artifact you trust more |
If you go this route, keep the Compose file free of dev-only bind mounts and put host-specific overrides in compose.override.yaml. The Dev Container then consumes the base file, and production consumes base plus a server-side override — one topology, three entry points.
For the management layer around these stacks, our Portainer vs Dockge vs Coolify comparison covers UI options, and the Compose vs podman-compose vs nerdctl comparison covers which CLI actually implements the spec. If your dev containers need to run rootless, the rootless container runtimes guide explains the UID-mapping mechanics.
Pitfalls: The Ones That Cost Days
- File ownership drift. Containers write as root, your host user cannot edit the files, and suddenly the repo needs
sudo. Fix it withremoteUser, a matching UID/GID build argument, or"updateRemoteUserUID": trueindevcontainer.json. node_moduleson a bind mount. Thousands of tiny files over a network filesystem is the slowest possible configuration. Put dependency directories in a named volume instead.- Docker socket exposure. Mounting
/var/run/docker.sockgives the container root-equivalent control of the host. Preferdocker-in-dockerwith its own daemon, or a rootless socket, and never do it on a shared machine. postCreateCommanddoing too much. Every rebuild of the container re-runs it. Compile-heavy steps belong in the image (build.dockerfile+features), not in the hook.- Compose watch syncing build output. If
dist/is inside the watched path, the sync triggers a rebuild which regeneratesdist/, which triggers the sync. Exclude build directories explicitly. - Architecture mismatch on ARM laptops. An
amd64-only base image under emulation is slow enough to feel broken. Choose multi-arch base images and test thelinux/arm64path. - Secrets in
containerEnv. Committed.envfiles leak. Use the CLI’s secret support or a local, gitignored mount.
FAQ
Are Dev Containers and Docker Compose the same thing?
No. Compose describes a multi-container application; Dev Containers describe a development environment attached to an editor. You can use them together by pointing dockerComposeFile at a Compose file, which is the recommended setup for anything with a database.
Do I need VS Code to use Dev Containers?
No. The @devcontainers/cli runs the same specification headlessly — devcontainer up and devcontainer exec work in any terminal and in CI. VS Code, JetBrains IDEs, Codespaces and DevPod are clients of the same spec.
Which one is faster for day-to-day coding?
For a single-language project, a Dev Container with a warm image is as fast as native for editing and faster for onboarding. For edit-reload cycles on a multi-service stack, Compose’s develop.watch with action: sync is usually snappier than a full container restart, because it only copies changed files.
Can I use Compose watch and Dev Containers at the same time?
Yes, but pick one owner for the dev loop. If the editor attaches to the app service, let the editor handle file sync and keep develop.watch for the services the editor does not manage. Running both on the same paths causes duplicate rebuilds.
How do I keep the dev environment from drifting from production?
Build both from the same base image and the same lockfiles, and pin image digests. Treat compose.yaml as the topology contract and devcontainer.json as the toolchain contract — when either changes, both CI and production should rebuild from the same commit.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com