Developer Tools

Setting Up a Productive Local Dev Environment From a Blank Laptop

A real step-by-step order for setting up a new machine: dotfiles first, version managers before languages, editor config in the repo, local HTTPS, and one secrets convention.

By Aissam Ait Ahmed Developer Tools 0 comments

Why the order matters more than the tool list

Every “best dev tools” list reads the same: here's a version manager, here's an editor, here's a terminal theme, good luck. What those lists skip is that the order you set things up in determines how much of it survives contact with your first real project. I've rebuilt my setup from a blank machine four times now — twice by choice, twice because a laptop died — and the sequence below is what I've converged on, along with the reasoning for each step, not just the step.

Step one: shell and dotfiles, before you install a single language

The first thing I do on a new machine is clone my dotfiles repo and symlink it into place, before Node, before PHP, before anything project-specific. The reasoning is simple: every tool installed afterward writes configuration somewhere, and if your shell config isn't already under version control, that configuration accumulates in a .bashrc or .zshrc you'll eventually lose.

git clone git@github.com:me/dotfiles.git ~/dotfiles
cd ~/dotfiles
./install.sh   # symlinks .zshrc, .gitconfig, .editorconfig into $HOME

The install script is deliberately dumb — it just creates symlinks and refuses to overwrite anything that isn't already a symlink, so re-running it on a machine that already has real config is safe. This step also means my aliases and the CLI tools I actually rely on (I go through the specific ones in Seven CLI Tools That Save Me Real Time Every Week) are available from the first terminal session, instead of getting reinstalled piecemeal over the following week whenever I remember I'm missing something.

Step two: version managers, never system-installed languages

The second and least optional step: never let the OS package manager install a language runtime directly. Use a version manager (mise, asdf, nvm, rbenv — pick one, mise now covers most languages in a single tool) so each project can pin its own version.

# .mise.toml in a project root
[tools]
node = "20.11.0"
php = "8.3.4"

The specific failure this prevents: I once had two projects on the same machine that needed Node 16 and Node 20 respectively, discovered after the fact because “npm install” on the older project started failing with cryptic native-module build errors. A system-wide Node install has no concept of “this project wants something older.” A version manager reads a per-project file and switches automatically when you cd into the directory, so the mismatch simply can't happen again.

Step three: editor config that travels with the project, not the person

Whichever editor you use — and the honest trade-offs between VS Code and JetBrains IDEs are worth a longer look, which I get into in VS Code vs JetBrains: A Head-to-Head — the setting that matters most isn't the editor choice, it's whether formatting and linting rules live in the repo instead of in personal settings.

  • An .editorconfig file for indentation and line endings, so it applies regardless of which editor a teammate opens the project in.
  • A committed linter config (.eslintrc, pint.json, whatever your stack uses) rather than relying on everyone's personal editor settings matching.
  • A format-on-save hook wired to the project's actual formatter, not the editor's built-in default, so CI and your local save produce identical output.

This matters more than it sounds like it should. On a team project without a committed .editorconfig, I once spent part of a code review arguing about tabs versus spaces on a diff that was 90% whitespace noise from two editors disagreeing about defaults — a problem a two-line config file would have made impossible.

Step four: local HTTPS and real dev domains

Testing everything on localhost:8000 works until you need to test cookies with the Secure flag, third-party OAuth callbacks that reject non-HTTPS redirect URIs, or multiple services that need to talk to each other under realistic hostnames. Setting this up early avoids a scramble later when a payment provider's sandbox flatly refuses to redirect back to an HTTP URL.

brew install mkcert
mkcert -install
mkcert booking.test

# then point booking.test at 127.0.0.1 via /etc/hosts or a local DNS resolver like dnsmasq

mkcert generates a locally-trusted certificate without the browser security warnings a self-signed cert produces, and a .test domain (a TLD reserved for exactly this, so it never collides with a real site) makes every project feel like a real deployed service instead of a numbered port you have to remember.

Step five: a secrets-handling convention, decided once and followed everywhere

The recurring mistake I see — and made myself, more than once — is treating secrets management as an afterthought per-project instead of a standing convention. Decide the pattern once: .env files that are gitignored by default in every new project, a .env.example with placeholder values committed instead, and a tool like direnv to load environment variables automatically per-directory.

# .envrc, loaded automatically by direnv when you cd into the project
export DB_PASSWORD=$(cat ~/.secrets/booking-db-password)
export STRIPE_TEST_KEY=$(cat ~/.secrets/stripe-test-key)

For anything that needs an actual generated credential — a local database password, a webhook signing secret you're rotating for testing — I stopped typing “something memorable” and just generate a real random one with a proper password generator, store it in the local secrets file, and never think about it again. It costs nothing and removes an entire category of “wait, what was that password” friction.

Where Docker fits into this order

None of the five steps above assume you're avoiding containers, and it's worth being explicit about where Docker sits in the sequence for projects that use it. A version manager and a container aren't competing solutions to the same problem — the version manager is still what I use for tools I want available globally across every project (the CLI tools I lean on daily, for instance), while Docker Compose handles a specific project's exact runtime plus its database, cache, and any other service it depends on. Skipping the version manager because “everything's in Docker anyway” tends to backfire the first time you need to run a quick script outside the container, or debug something with a tool that isn't installed inside the image.

The local HTTPS setup from step four still applies with containers in the picture — mkcert-generated certificates get mounted into the container the same way they'd be referenced by a bare-metal server, and the .test domain in /etc/hosts still points at 127.0.0.1 regardless of what's actually listening on the other end of that port. The one adjustment worth making is mapping container ports explicitly and consistently in docker-compose.yml rather than letting Docker assign random host ports, since a random port breaks the whole point of a memorable, stable dev domain.

What I'd do differently next time

The mistake in my second rebuild was skipping step three — I told myself editor config could wait until “the project settles down” — and ended up retrofitting an .editorconfig and formatter config onto a repo with six months of inconsistent formatting already committed. Retrofitting is strictly more work than starting with it, because you either accept a giant reformat-everything commit that pollutes git blame, or you live with the inconsistency indefinitely. Do it on day one, even for a throwaway project you don't expect to last — the habit is worth more, over a career, than any single specific project ever will be.

What actually happens when you skip a step

It's worth being concrete about the failure mode for each step, because “do this or bad things might happen eventually” is easy to deprioritize on a busy first day with a new machine. Skip dotfiles first, and you end up hand-configuring the same shell aliases twice across two machines, usually noticing the drift months later when a script that works on one laptop mysteriously fails on the other. Skip version managers, and the Node 16 versus Node 20 collision from earlier in this post is exactly the kind of afternoon you lose. Skip local HTTPS until you need it, and you're setting up mkcert for the first time under actual deadline pressure from a payment provider's sandbox rejecting your callback URL, which is a worse time to be learning a new tool than an unhurried first day on a new machine.

None of these failures are catastrophic on their own. What they have in common is that each one costs more to fix after the fact than it would have cost to set up correctly from the start, which is really the whole argument for doing the five steps in order rather than deferring the ones that don't feel urgent yet on day one of a new machine.

The checklist, in order

  1. Dotfiles cloned and symlinked before installing any language
  2. Version manager installed, languages pinned per-project, never system-wide
  3. Editor config and linter rules committed to the repo, not left in personal settings
  4. Local HTTPS and a real dev domain set up before you need it for an OAuth callback
  5. One secrets convention, decided once, applied to every new project from its first commit

None of these five steps takes more than twenty minutes individually. What makes them worth doing in this order is that each one prevents a specific, real category of pain from the step after it — skip step one and step three has nowhere consistent to live; skip step two and step four's domain setup fights with whatever the OS installed globally. The order isn't dogma, it's just what happens if you trace each problem back to what would have prevented it.

Comments

Join the conversation on this article.

Comments are rendered server-side so the discussion stays visible to readers without relying on a separate widget or client-side app.

No comments yet.

Be the first visitor to add a thoughtful comment on this article.

Leave a comment

Share a useful thought, question, or response.

Be constructive, stay on topic, and avoid posting personal or sensitive information.

Back to Blog More in Developer Tools Free Resources Explore Tools