Dev Shells & Environments

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.