Reproducible Dev Environments & Shells #
A walkthrough of how I approach reproducible developer environments across different team scales and projects — from Nix shells for personal and platform toolchains to containerized dev environments for large engineering teams.
❄️ 1. Declarative CLI Toolchains (Nix Shell) #
When jumping between multiple projects (e.g., Kubernetes manifests, OpenTofu modules, and Hugo sites), installing different CLI versions globally quickly leads to version conflicts. I use shell.nix files per repository to provide isolated, instant developer environments.
Example: Platform & IaC Dev Shell (shell.nix)
#
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "platform-dev-shell";
buildInputs = with pkgs; [
# Infrastructure as Code
opentofu
terraform-docs
tflint
# Kubernetes & GitOps
kubectl
kubernetes-helm
k9s
argocd
# Utilities
jq
yq-go
git
age
sops
];
shellHook = ''
echo "⚡ Loaded platform engineering toolchain"
echo "OpenTofu: $(tofu version | head -n1)"
echo "Kubectl: $(kubectl version --client -o yaml | grep gitVersion | head -n1)"
'';
}
Usage: Simply running nix-shell inside the directory enters a pinned subshell with every required tool available in $PATH, leaving the host OS clean.
🐳 2. Team-Wide Devcontainers (VS Code / Dev Containers) #
For engineering groups working across Windows, macOS, and Linux, local toolchain drift is common. I’ve built reusable Dev Container templates that standardise dependencies, linters, and extensions across teams.
Example: Multi-Language Service Template (.devcontainer/devcontainer.json)
#
{
"name": "Cloud Microservice Devcontainer",
"build": {
"dockerfile": "Dockerfile",
"args": {
"NODE_VERSION": "20",
"JAVA_VERSION": "21"
}
},
"customizations": {
"vscode": {
"extensions": [
"hashicorp.terraform",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker",
"esbenp.prettier-vscode",
"tsandall.opa"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "zsh",
"editor.formatOnSave": true
}
}
},
"remoteUser": "vscode",
"postCreateCommand": "make bootstrap"
}
Benefits:
- Onboarding a new developer drops from a multi-day guide to opening the repository in VS Code.
- Every developer runs the exact same compiler, linter, and runtime versions.
🛠️ 3. Makefile Wrappers for Local Testing #
To keep developer interaction fast and uniform without requiring deep Docker/container CLI knowledge, I pair container tools with self-documenting Makefiles:
.DEFAULT_GOAL := help
.PHONY: help bootstrap up down test lint
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
bootstrap: ## Install local dependencies and pre-commit hooks
@echo "Bootstrapping environment..."
@pre-commit install
up: ## Spin up local mock dependencies and services
@docker compose up -d
down: ## Tear down local services
@docker compose down -v
test: ## Run unit and component test suites
@npm test
Running make help displays a formatted menu of available actions, providing a single entry point for any repository.