Dev Containers
Summary
- A dev container is a full development environment defined in code (
devcontainer.json) and run inside a container, so every engineer gets the same toolchain. - Insight: dev containers move the development environment toward parity, complementing images that already give you runtime parity.
- Key Concepts: devcontainer.json, Dev Container spec (open, CLI-driven), features, lifecycle hooks, VS Code / Cursor Remote.
- When to Use: onboarding, polyglot repos, native toolchains that are painful to install per-laptop, and reproducible CI-equivalent dev shells.
- Limitations: dev containers standardize the dev box, not the production runtime; you still build a separate production image.
Recipe
Add a .devcontainer/devcontainer.json to the repo describing the image or Dockerfile, tools, and setup commands.
Open the folder in VS Code or Cursor and choose "Reopen in Container", or use the devcontainer CLI in CI.
Keep the dev container's base aligned with your production base so the two do not drift.
Working Example
A devcontainer.json built from your own Dockerfile, adding tools via features and installing dependencies on create.
{
"name": "api-dev",
"build": {
"dockerfile": "Dockerfile",
"target": "dev"
},
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"helm": "latest"
}
},
"forwardPorts": [3000, 5432],
"postCreateCommand": "npm ci",
"remoteUser": "node",
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "ms-azuretools.vscode-docker"]
}
}
}For multi-service work, point the dev container at a Compose service instead.
{
"name": "api-dev",
"dockerComposeFile": "../docker-compose.yml",
"service": "api",
"workspaceFolder": "/app",
"postCreateCommand": "npm ci"
}Now the dev container is the api service, and its depends_on databases start alongside it.
Deep Dive
What devcontainer.json actually configures
The file describes the environment: which image or Dockerfile to build, which tools to layer on, which ports to forward, and which user to run as.
remoteUser matters for parity and security. Running as node rather than root mirrors a hardened production image and keeps file ownership sane on bind mounts.
forwardPorts publishes container ports back to your host so the app and its database are reachable from the editor.
Features are composable install units
A feature is a reusable, versioned installer referenced by an OCI ref like ghcr.io/devcontainers/features/kubectl-helm-minikube:1.
Features let you add kubectl, Helm, language runtimes, or cloud CLIs without hand-writing install steps in a Dockerfile.
They compose, so the base image stays small and each team layers only what it needs.
Lifecycle hooks
The spec defines ordered hooks: onCreateCommand, updateContentCommand, postCreateCommand, and postStartCommand.
Use postCreateCommand for one-time setup like installing dependencies, and postStartCommand for things that must run every start.
Keeping setup in these hooks means a fresh clone becomes a working environment with zero manual steps.
Docker-from-inside and the runtime boundary
The docker-outside-of-docker feature mounts the host Docker socket so commands inside the dev container talk to the host daemon.
This is a build/dev convenience. It does not change the fact that on a real cluster, containerd via the CRI runs your pods, not Docker.
The spec is open and tool-agnostic
devcontainer.json is an open specification with a standalone devcontainer CLI, so it is not limited to one editor.
That lets CI build and exercise the exact dev environment engineers use, closing another parity gap.
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . npm testGotchas
Confusing the dev container with the production image is the big one; the dev stage ships compilers and dev dependencies you must not deploy. Build a separate runtime target for prod.
Running as root inside the dev container creates root-owned files on bind mounts, which then break host tooling; set remoteUser.
Pinning features to a floating latest reintroduces drift; pin feature versions like any other dependency.
Large postCreateCommand steps run on every rebuild of the container image; put slow, cacheable work in the Dockerfile instead so it lands in a layer.
Forwarding a port that a Compose service already binds causes conflicts; let one mechanism own each port.
Mounting the Docker socket grants broad host access, so treat it as a privileged action and avoid it on shared or untrusted machines.
Alternatives
Plain Dockerfile + Compose, no dev container spec. Simpler, editor-agnostic, but onboarding is manual and the editor is not preconfigured.
Nix or asdf on the host. Reproducible toolchains without a container, but they do not isolate system libraries the way a container does.
Cloud dev environments (Codespaces-style, self-hosted equivalents). Same devcontainer.json runs on a remote host, removing local resource limits at the cost of network dependence.
Language-native tooling only (virtualenv, nvm). Lightest weight, weakest isolation and parity; fine for single-language, single-service repos.
Choose dev containers when onboarding speed, polyglot toolchains, and dev-environment reproducibility matter; choose lighter options for small, uniform projects.
FAQs
Do dev containers replace my production Dockerfile? No. They configure the development box. You still build and deploy a separate, hardened production image.
Can I share one config across editors? Yes. devcontainer.json is an open spec with a CLI, so VS Code, Cursor, and CI can all consume it.
How do I get kubectl and Helm inside? Add the relevant feature (for example the kubectl-helm-minikube feature) rather than scripting installs by hand.
Where do I install project dependencies? In postCreateCommand for a one-time setup, or bake truly static tooling into the Dockerfile layer for caching.
Is the mounted Docker socket safe? It is convenient but privileged; the container can control the host daemon, so avoid it on untrusted machines.
Related
- Local Dev Basics
- Bind Mounts vs Image Rebuild
- Hot Reload in Containers
- Dev/Prod Parity as a Mental Model
- Local Dev Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).