$ cat ~/notes/devcontainers.md

Dev Containers

A practical note on why dev containers are useful, what belongs in one, and how to keep them fast enough to enjoy.

devcontainersdockertooling

Dev containers are a way to put the development environment next to the codebase. The goal is simple: cloning a repo should get you close to a working shell without asking every contributor to rediscover the right Node version, package manager, system libraries, CLI tools, and environment quirks.

I think of a dev container as a project-local workstation. It should describe the boring parts of the machine well enough that the interesting parts of the work can start quickly.

What Goes In

A good dev container usually owns the tools that are specific to the project:

  • language runtimes and package managers
  • system packages needed to build or test
  • editor extensions that are genuinely useful for the repo
  • shell setup that makes common commands discoverable
  • ports, volumes, and environment defaults

It should not become a second production platform. Production images care about runtime size, attack surface, and deployment constraints. Dev containers care about fast feedback, readable failures, and giving the developer a comfortable place to work.

The Core Files

Most setups start with .devcontainer/devcontainer.json. That file tells the editor or CLI how to open the workspace, which image or Dockerfile to use, which features to install, and which commands should run after the container is created.

For small projects, an image plus a few features can be enough. For heavier projects, I prefer a dedicated Dockerfile because it gives more control over layer order and build caching.

{
  "name": "project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "lts"
    }
  },
  "postCreateCommand": "npm install",
  "forwardPorts": [4321]
}

That is the useful baseline: name the environment, install the runtime, run the first setup command, and expose the development server.

Keep It Fast

The moment a dev container feels slow, people route around it. The fix is usually boring and mechanical:

  • put slow, rarely changing installs earlier in the Dockerfile
  • keep dependency installation separate from application source copies
  • use named volumes for large dependency directories when it makes sense
  • avoid running expensive setup on every attach
  • make rebuilds predictable with a small number of clear layers

The best dev container is one you forget about because it gets out of the way.

What To Automate

I like postCreateCommand for one-time dependency setup and postStartCommand for cheap checks that should happen when the container starts. Anything long-running should usually be explicit. A development environment should help without surprising you.

Useful automation includes:

  • installing project dependencies
  • preparing git hooks
  • printing the most common commands
  • verifying that required CLIs are available
  • starting optional services only when the project really expects them

Why It Matters

Dev containers turn setup knowledge into code. That makes projects easier to return to, easier to share, and easier to debug when someone says “it works on my machine.”

They also make tool building nicer. If the project is a terminal app, a CLI, or a Linux-heavy workflow, the dev container can become the exact environment where the tool is meant to live. That is a much better feedback loop than carrying a hidden checklist in your head.