“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
- Remote Team Git Hooks Standardization Guide
- How to Set Up Portainer for Docker Management
-
How to Automate Dev Environment Setup: A Practical Guide
Related Articles