Skip to main content

Development Environment as Code

An overview of managing your development environment as code, and the main approaches for doing so. This is the foundational technique for improving reproducibility and portability.

Why codify your development environment?​

Environment setup steps often live only "in someone's head" or in old notes. Codifying them brings the following benefits.

BenefitDescription
ReproducibilityRebuild the same environment in minutes on a new machine or for a new teammate
ReviewableReview configuration changes via pull request, with full history
Disaster recoveryRestore from the repository even if a machine is lost
ConsistencyReduce configuration drift across machines
Knowledge sharingDocument which tools are used, and why

Web development tends to focus on application code, but a developer's local environment is also part of the infrastructure. Think of this as applying Infrastructure as Code (IaC) principles to the developer machine.

The layers of a development environment​

A development environment can be broken down into layers of differing scope.

┌─ dotfiles ──────────────┐ Shell, editor, and Git settings (.zshrc / .gitconfig, etc.)
├─ runtimes & tools ──────┤ Language runtimes, package managers, CLI tools
├─ OS-level settings ─────┤ Timezone, packages, desktop environment
└─ external dependencies ─┘ Cloud resources, tunnels, CI runners (managed in a separate repo)

Each layer is typically managed differently. For example:

  • dotfiles: a dedicated tool (e.g. chezmoi)
  • runtimes & tools: version managers (nvm / mise / uv) plus an install script
  • OS level: a provisioning script or IaC (Ansible / cloud-init / Terraform)
tip

Deciding where to draw the line is important. Keep secrets (SSH keys, tokens) and machine-specific settings out of git, and only commit templates (e.g. *.example) to the repository.

Comparison of common approaches​

ApproachMechanismProsCons
dotfiles + shell scriptGit-track config files, install tools via an idempotent scriptLightweight, low learning curve, works anywhereYou must ensure idempotency yourself
devcontainer / DockerDefine the environment as a container imageFully reproducible, easy to standardize across a teamEditor integration and GUI are constrained
Nix / NixOSDeclaratively define packages and environmentExtremely high reproducibilitySteep learning curve
Ansible and friendsDeclare server state in a playbookStrong for many machines / scaleSomewhat heavyweight for a single machine

For small-to-medium personal or team use, "dotfiles + an idempotent bootstrap script" is the most approachable and a good place to learn. This category explores each underlying technique with that setup in mind.

Example repository layout​

A typical "dotfiles + idempotent script" layout looks like this:

dotfiles/
├── bootstrap.sh # Idempotent setup (apt / nvm / tools / dotfiles)
├── chezmoi.sh # Thin wrapper for operating on dotfiles with chezmoi
├── dotfiles/ # chezmoi source (dot_zshrc / dot_gitconfig, etc.)
└── *.local.example # Templates for machine-specific settings (not git-tracked)

With this layout, the following are intentionally kept out of git:

  • Secrets such as SSH keys and tokens
  • Cloud resources and network settings (managed by IaC in a separate repository)
  • Machine-specific settings (e.g. ~/.zshrc.local)