Last updated: March 22, 2026

“It works on my machine” becomes “it works on your machine too” when dev environments are codified. Remote teams are especially exposed to environment drift. developers are on different OS versions, different tool versions, and different configurations. A template that runs consistently on day one means less async debugging and faster onboarding.


Option 1 - Dev Containers (VS Code / JetBrains)

Dev Containers run your entire development environment inside a Docker container. VS Code and JetBrains connect to it ; the developer’s local machine is just a display layer.

Create .devcontainer/devcontainer.json in your repo:

{
  "name": "Project Dev Environment",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  "runArgs": [
    "--cap-add=SYS_PTRACE",
    "--security-opt", "seccomp=unconfined"
  ],
  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly",
    "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
  ],
  "forwardPorts": [3000, 5432, 6379, 8080],
  "postCreateCommand": "make setup",
  "customizations": {
    "vscode": {
      "extensions": [
        "golang.go",
        "esbenp.prettier-vscode",
        "dbaeumer.vscode-eslint",
        "bradlc.vscode-tailwindcss",
        "ms-azuretools.vscode-docker"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "[go]": {
          "editor.defaultFormatter": "golang.go"
        }
      }
    }
  },
  "remoteUser": "vscode"
}

Create .devcontainer/Dockerfile:

FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04

Install language runtimes
RUN apt-get update && apt-get install -y \
    curl wget git build-essential \
    postgresql-client redis-tools \
    && rm -rf /var/lib/apt/lists/*

Go
ARG GO_VERSION=1.22.3
RUN curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
    | tar -C /usr/local -xz
ENV PATH="/usr/local/go/bin:${PATH}"

Node.js via nvm
ARG NODE_VERSION=20
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
    && apt-get install -y nodejs

Tools
RUN go install github.com/air-verse/air@latest \
    && go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest

Set up non-root user
USER vscode
RUN curl -fsSL https://get.pnpm.io/install.sh | sh -

For teams using Docker Compose (multiple services):

{
  "name": "Full Stack Dev",
  "dockerComposeFile": [
    "../docker-compose.yml",
    "docker-compose.devcontainer.yml"
  ],
  "service": "app",
  "workspaceFolder": "/workspace",
  "postCreateCommand": "make setup"
}
.devcontainer/docker-compose.devcontainer.yml
version: "3.8"
services:
  app:
    volumes:
      - ../..:/workspace:cached
    command: sleep infinity  # Keep container alive

Option 2 - Nix Flakes (Reproducible Across All OS)

Nix flakes provide bit-for-bit reproducible environments. The same flake.nix produces identical tool versions on macOS, Linux, and in CI.

Install Nix (macOS/Linux)
curl --proto '=https' --tlsv1.2 -sSf https://install.determinate.systems/nix | sh -s -- install

Create flake.nix in your repo root:

{
  description = "Project development environment";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            # Go toolchain
            go_1_22
            gopls
            golangci-lint
            air

            # Node.js
            nodejs_20
            nodePackages.pnpm

            # Database tools
            postgresql_16
            redis

            # Infrastructure
            terraform
            kubectl
            helm
            awscli2

            # Dev tools
            git
            gnumake
            jq
            yq
          ];

          shellHook = ''
            echo "Dev environment loaded"
            echo "Go: $(go version)"
            echo "Node: $(node --version)"

            # Set up local environment variables
            export GOPATH="$HOME/.local/share/go"
            export PATH="$GOPATH/bin:$PATH"

            # Load .env.local if it exists
            if [ -f .env.local ]; then
              set -a
              source .env.local
              set +a
            fi
          '';
        };
      }
    );
}

Enter the dev shell:

nix develop
Or with direnv auto-activation:
echo "use flake" > .envrc
direnv allow

Option 3 - Makefile Bootstrap (Universal)

For teams where Nix and Docker are too opinionated, a Makefile with a setup target provides a documented, repeatable setup that works anywhere:

Makefile
.DEFAULT_GOAL := help
SHELL := /bin/bash

Tool versions
GO_VERSION := 1.22.3
NODE_VERSION := 20
TERRAFORM_VERSION := 1.8.5

.PHONY: help
help: ## Show this help
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

.PHONY: setup
setup: check-deps install-tools setup-hooks setup-env ## Full dev environment setup
	@echo "Dev environment ready"

.PHONY: check-deps
check-deps: ## Check required system dependencies
	@command -v git  >/dev/null || (echo "ERROR: git not found" && exit 1)
	@command -v docker >/dev/null || (echo "ERROR: docker not found" && exit 1)
	@command -v make >/dev/null || (echo "ERROR: make not found" && exit 1)
	@echo "System dependencies OK"

.PHONY: install-tools
install-tools: ## Install project-specific tools
	@./scripts/install-tools.sh

.PHONY: setup-hooks
setup-hooks: ## Install git hooks
	@which pre-commit > /dev/null || pip install pre-commit
	pre-commit install
	pre-commit install --hook-type commit-msg
	@echo "Git hooks installed"

.PHONY: setup-env
setup-env: ## Set up local environment file
	@if [ ! -f .env.local ]; then \
		cp .env.example .env.local; \
		echo ".env.local created from .env.example. fill in your values"; \
	else \
		echo ".env.local already exists"; \
	fi

.PHONY: dev
dev: ## Start development services
	docker-compose -f docker-compose.dev.yml up -d
	air  # Or: npm run dev, etc.

.PHONY: test
test: ## Run tests
	go test ./...

.PHONY: clean
clean: ## Stop and clean development services
	docker-compose -f docker-compose.dev.yml down -v
#!/bin/bash
scripts/install-tools.sh
set -euo pipefail

install_go() {
  if go version 2>/dev/null | grep -q "go${GO_VERSION:-1.22}"; then
    echo "Go already installed: $(go version)"
    return
  fi
  OS=$(uname -s | tr '[:upper:]' '[:lower:]')
  ARCH=$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
  curl -fsSL "https://go.dev/dl/go${GO_VERSION:-1.22.3}.${OS}-${ARCH}.tar.gz" \
    | sudo tar -C /usr/local -xz
  echo "Go ${GO_VERSION:-1.22.3} installed"
}

install_golangci_lint() {
  curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
    | sh -s -- -b "$(go env GOPATH)/bin" latest
}

install_go
install_golangci_lint

Documenting the Template

Every repo should have an ONBOARDING.md that fits on one screen:

Getting Started

Prerequisites
- macOS 13+ / Ubuntu 22.04+ / Windows 11 + WSL2
- Docker Desktop 4.x
- VS Code with Dev Containers extension (recommended)
- Git 2.40+

Setup (5 minutes)

```bash
git clone git@github.com:your-org/your-repo.git
cd your-repo

Option A - Dev Container (recommended)
code . # VS Code prompts to reopen in container

Option B - Local setup
make setup

Running the Project

make dev # Start all services
open http://localhost:3000

Environment Variables Copy .env.example to .env.local and fill in values. Ask in #dev-setup for secrets. ```


Related Reading

Related Articles