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.
| Benefit | Description |
|---|---|
| Reproducibility | Rebuild the same environment in minutes on a new machine or for a new teammate |
| Reviewable | Review configuration changes via pull request, with full history |
| Disaster recovery | Restore from the repository even if a machine is lost |
| Consistency | Reduce configuration drift across machines |
| Knowledge sharing | Document 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)
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
| Approach | Mechanism | Pros | Cons |
|---|---|---|---|
| dotfiles + shell script | Git-track config files, install tools via an idempotent script | Lightweight, low learning curve, works anywhere | You must ensure idempotency yourself |
| devcontainer / Docker | Define the environment as a container image | Fully reproducible, easy to standardize across a team | Editor integration and GUI are constrained |
| Nix / NixOS | Declaratively define packages and environment | Extremely high reproducibility | Steep learning curve |
| Ansible and friends | Declare server state in a playbook | Strong for many machines / scale | Somewhat 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)