How to Set Up a Development Environment From Scratch
A step-by-step guide to setting up a reproducible development environment on Mac, Windows or Linux, covering editor, terminal, Git, version managers and containers.
To set up a development environment, install a package manager, Git with an SSH key, a code editor such as VS Code and a good terminal. Then install languages per project with version managers, run databases in containers, and commit config files such as .tool-versions and devcontainer.json so anyone can reproduce the setup.
Photo: Unsplash
On this page
“It works on my machine” is the most expensive sentence in software. It usually means 2 developers have different versions of a language, a missing environment variable or a database configured by hand 18 months ago. The result is lost afternoons, flaky onboarding and bugs that only appear in production.
A good development environment fixes that. It’s fast to set up, the same for everyone on the team and easy to rebuild when your laptop dies. This guide walks through setting one up from scratch on macOS, Windows or Linux, in the order that avoids rework.
What a development environment includes
A development environment is the set of tools and configuration you use to write, run, test and debug code locally. The main layers are:
| Layer | What it does | Common examples |
|---|---|---|
| Operating system and package manager | Installs and updates your tools | Homebrew (macOS), winget (Windows), apt or dnf (Linux) |
| Terminal and shell | Where you run commands | Windows Terminal, iTerm2, the built-in macOS Terminal; zsh, bash, fish |
| Version control | Tracks code changes | Git, plus GitHub, GitLab or Bitbucket |
| Code editor or IDE | Where you write and debug | VS Code, JetBrains IDEs, Neovim |
| Language runtimes and version managers | Run your code at the right version | mise, asdf, nvm, pyenv, uv, rustup, SDKMAN! |
| Containers | Run databases, services and whole environments consistently | Docker Desktop, Podman, Dev Containers |
| Project config | Makes setups reproducible | .tool-versions, .env.example, devcontainer.json, Makefile |
The guiding principle: install as little as possible globally, and capture everything project-specific in files that live in the repository.
Set up the basics: OS, terminal, Git and editor
These 4 steps are installed once per machine and shared by every project you work on.
Step 1: Prepare your operating system
macOS: Install the Xcode Command Line Tools (xcode-select --install), then Homebrew, the most common package manager for macOS. Use Homebrew for Git, your shell tools and anything else you need system-wide.
Windows: For most web and backend work, install the Windows Subsystem for Linux (WSL). Microsoft’s docs say running wsl --install in an administrator PowerShell enables the required features, sets WSL 2 as the default and installs Ubuntu, and it requires Windows 10 version 2004 or later, or Windows 11. Keep your code inside the Linux file system for better performance, and use winget for Windows apps such as your editor and terminal.
Linux: Update your system packages, then install build essentials (compilers and headers) through your distribution’s package manager.
If you’re still choosing hardware, our guide on how to choose a laptop covers the trade-offs.
Step 2: Set up your terminal and shell
You’ll spend hours a day here, so make it comfortable:
- Pick a modern terminal: Windows Terminal on Windows, iTerm2 or the built-in Terminal on macOS, or your distribution’s default on Linux.
- Choose a shell: zsh is the default on macOS; bash is common on Linux. Either is fine.
- Add a few productivity tools: a better prompt that shows your Git branch, fuzzy search for command history, and modern replacements for common commands if you like them.
- Keep your dotfiles in a Git repository so you can restore your shell config, aliases and editor settings on a new machine in minutes.
Don’t over-customize on day 1. Add tools when you feel a specific pain.
Step 3: Install and configure Git
Install Git through your package manager, then set your identity:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main
Next, create an SSH key for your Git host. GitHub’s documentation recommends generating an Ed25519 key with ssh-keygen -t ed25519 -C "you@example.com", setting a passphrase, adding it to the ssh-agent and then adding the public key to your account. Turn on multi-factor authentication for your Git hosting account too.
New to Git? Our guide to Git for beginners covers the commands you’ll use daily.
Key takeaway: Install only the basics globally: package manager, terminal, Git and editor. Everything that differs between projects, such as language versions and databases, belongs in per-project config so it can be reproduced.
Step 4: Install and configure your editor
VS Code and JetBrains IDEs are the most common choices; Neovim and others are popular with developers who prefer the keyboard.
Set up these essentials:
- Language extensions for the languages you use (syntax, IntelliSense, debugging).
- Formatter and linter integration that runs on save, using the project’s config, not your personal preferences.
- EditorConfig support so indentation and line endings follow the repository’s
.editorconfig. - Settings sync through your editor account or dotfiles.
- Remote development support if you use WSL, SSH hosts or containers. VS Code’s Dev Containers extension, for example, opens a project inside a container defined by the repository.
- An AI coding assistant, if your company allows one. See our guide to AI coding assistants for where they help and where to be careful.
Install languages with version managers
This is the step that prevents most “works on my machine” problems. Never rely on a system-installed language version for project work. Use a version manager so each project can pin its own version.
Options:
- Multi-language managers such as mise or asdf read a .tool-versions file (or their own config) and install the right versions of Node.js, Python, Ruby, Go, Java and more per project.
- Language-specific managers such as nvm for Node.js, pyenv or uv for Python, rustup for Rust and SDKMAN! for Java.
Good practice:
- Commit the version file (for example .tool-versions, .nvmrc or .python-version) to the repository.
- Use long-term support releases for production work. The Node.js project, for example, says production applications should use only Active LTS or Maintenance LTS releases.
- Use isolated dependency environments: project-local node_modules, Python virtual environments, and lock files committed to the repository.
Choosing between languages? See our Python vs JavaScript comparison.
Use containers for services and full environments
Containers make the rest of your stack reproducible.
Level 1: Services in containers. Run databases, caches and queues (PostgreSQL, Redis and similar) with Docker Compose instead of installing them on your machine. A compose.yaml file in the repository means everyone runs the same versions with one command. If containers are new to you, start with what Docker is.
Level 2: The whole environment in a container. The Development Container specification uses a devcontainer.json file to describe a complete development environment: base image, language tools, extensions, ports and setup commands. Tools that support it include the VS Code Dev Containers extension, the open-source dev container CLI and GitHub Codespaces. New teammates open the repository and get a ready environment, without installing runtimes locally.
| Approach | Setup effort | Reproducibility | Best for |
|---|---|---|---|
| Everything installed locally | Low at first, high over time | Low | Solo projects, quick experiments |
| Version managers plus local services | Medium | Medium | Small teams, simple stacks |
| Version managers plus Docker Compose for services | Medium | High | Most web and backend teams |
| Full dev containers or cloud dev environments | Higher upfront | Very high | Larger teams, complex stacks, fast onboarding |
Make the project reproducible
Your environment is only reproducible if the repository captures it. Commit:
- Tool version files (
.tool-versions,.nvmrc,.python-version). - Lock files for dependencies.
.env.examplelisting every environment variable with safe placeholder values. Never commit real secrets; keep them in.envfiles listed in.gitignoreor in a secrets manager.compose.yamlfor local services anddevcontainer.jsonif you use dev containers.- A single setup command such as
make setupor ascripts/setupfile that installs dependencies, seeds the database and runs a smoke test. - Formatter, linter and pre-commit hook configs so code quality checks run the same for everyone.
- A README quick start that works on a clean machine. Our guide to writing technical documentation includes a README template.
Test it: ask a new teammate to set up from the README alone, and fix every place they get stuck.
Security basics for your dev machine
- Turn on full-disk encryption and a screen lock.
- Use a password manager and multi-factor authentication for Git hosting, cloud consoles and package registries. Passkeys are worth enabling where supported.
- Keep secrets out of code, shell history and screenshots.
- Install packages and editor extensions only from trusted publishers.
- Keep your OS, editor and tools updated.
Our guide to secure coding best practices covers the code side.
Where to start: a 1-day setup plan
- Morning: OS updates, package manager, terminal, shell and dotfiles repository.
- Late morning: Git, SSH key, Git host login with multi-factor authentication.
- Lunch break: Editor, extensions and settings sync.
- Afternoon: Version manager and the language versions your first project needs.
- Late afternoon: Docker or another container runtime, then clone the project and run its setup command.
- End of day: Run the tests. Anything you had to figure out manually goes into the README or setup script.
Conclusion
A good development environment is boring in the best way: it’s quick to set up, matches your teammates’ and can be rebuilt from scratch without drama. Install the basics globally, pin language versions per project with version managers, run services in containers and commit the configuration that makes it all reproducible. When the setup is captured in the repository, onboarding takes hours instead of days, and “works on my machine” stops being an excuse. Teams that want to go further can build on this with platform engineering. For more developer terms, see our glossary.
Sources
- Microsoft Learn: Install WSL — wsl –install behavior and Windows version requirements
- GitHub Docs: Generating a new SSH key and adding it to the ssh-agent — Ed25519 key generation
- Development Containers: Supporting tools and services — devcontainer.json specification and supporting tools
- VS Code Docs: Developing inside a Container — Dev Containers extension
- Node.js: Previous releases — guidance to use LTS releases in production
Frequently asked questions
What do I need to set up a development environment?
You need a package manager for your operating system, a terminal and shell, Git with an SSH key, a code editor or IDE, the language runtimes for your project installed through version managers, and usually a container runtime such as Docker for databases and services. Add project config files such as tool version files, lock files and an .env.example so the setup is reproducible.
Should I use WSL for development on Windows?
For most web, backend and cloud development, yes. WSL runs a real Linux environment on Windows, so tools and scripts behave like they do on Linux servers and Macs. Microsoft's `wsl --install` command sets it up with Ubuntu by default on Windows 10 version 2004 or later, or Windows 11. Native Windows remains a better fit for .NET desktop and some game development work.
What is a version manager and why should I use one?
A version manager installs and switches between multiple versions of a language, such as Node.js or Python, per project. Instead of one system-wide version, each project pins its version in a file such as .tool-versions or .nvmrc. That prevents conflicts between projects and makes sure everyone on the team runs the same version. Common choices include mise, asdf, nvm, pyenv and uv.
Are dev containers worth it?
Dev containers are worth it for teams with complex stacks, frequent onboarding or many projects. A devcontainer.json file in the repository describes the full environment, so a new developer can open the project and start working without installing runtimes locally. For a small project, version managers plus Docker Compose for services are often enough and add less overhead.
What is the best code editor for beginners?
VS Code is a common starting point because it's free, runs on macOS, Windows and Linux, and has extensions for nearly every language. JetBrains IDEs offer deeper built-in tooling for specific languages, with free and paid editions. The best choice is one your team supports and you're comfortable using daily. Learn its debugger and keyboard shortcuts early.
How do I make my development setup reproducible?
Commit everything project-specific to the repository: tool version files, dependency lock files, an .env.example with placeholder values, a compose.yaml for services, optional devcontainer.json, formatter and linter configs, and a single setup command. Then test it by having someone set up the project from the README on a clean machine and fixing every step where they got stuck.
Should I install databases locally or in Docker?
For most development work, run databases in Docker or another container runtime. Containers let each project use its own database version, keep your machine clean, and make it easy to reset data. A compose.yaml file in the repository means teammates start the same services with one command. Local installs can still make sense for performance testing or tools that don't containerize well.
Was this guide useful
No reviews yet. Share your experience and help other readers.