The machine that builds my machine
A new dev box used to cost me the better part of a day of yak-shaving. Now a bare Linux install becomes my exact workstation, unattended, in minutes — because the setup is a program, not a checklist.
- Automation
- Linux
- Reproducibility
- Tooling
A fresh development machine used to cost me the better part of a day: install the editor, the language toolchains, the CLIs, the fonts, the services, the dotfiles — then discover three weeks later that the old machine had one tweak I never wrote down. The setup was tribal knowledge living in my head and a README that was wrong the moment I saved it.
So I stopped treating provisioning as a chore and started treating it as a program. dev-boost takes a bare Linux box and turns it into my exact workstation, unattended, in minutes.
Why a setup script isn’t enough
The obvious answer is “write a bash script.” I did, twice. It rots, because a script that runs top-to-bottom once is not the same thing as a system you can trust to converge.
| A top-to-bottom script | What you actually want |
|---|---|
| Re-running it re-does everything (or breaks) | Re-running is a safe no-op |
| A failure leaves the machine half-built | A failure is resumable from where it stopped |
| ”What’s installed?” lives in your head | The desired state is a file you can read and diff |
| Adding a tool means editing control flow | Adding a tool means adding one entry |
The gap between those two columns is the whole point. The first is automation; the second is convergence.
Two rules make it converge
Separate the engine from the data. The logic that installs knows nothing about what to install. Tools, fonts, services, and dotfiles are declarative data; the engine just executes it. A new tool is one record, not a new branch.
Make every step idempotent and verify-guarded. Each step declares how to check whether it’s already satisfied and how to make it so. The engine runs the check first and only acts on a miss — so a second run does nothing, and a failed run is safe to re-run.
# A unit is satisfied when `verify` exits 0; otherwise `install` runs and verify must then pass.unit "ripgrep" \ --verify 'command -v rg' \ --install 'sudo dnf install -y ripgrep'
unit "git config: pull.rebase" \ --verify 'test "$(git config --global pull.rebase)" = "true"' \ --install 'git config --global pull.rebase true'Because every unit is guarded, the engine is forward-only and reproducible: you never hand-patch state, you change the data and re-run, and the machine moves toward the declared state from wherever it is.
What a run looks like
Point it at a fresh Fedora install and the whole environment materializes — packages, language runtimes, the editor and its config, services, fonts — the same way every time. Run it again next month after you’ve added three tools, and it installs exactly those three.
The payoff isn’t only speed (a lost afternoon becomes a coffee). It’s that my environment is now legible: its definition is a file I can review, and the thing that builds everything else is held to the same bar as everything else — engine/data separation, idempotence, a written constitution, and a few hundred tests.
The tradeoff I accepted
Declarative convergence costs more up front than apt install in a terminal. You write the verify alongside the install, and you resist the urge to “just do it manually this once.” The return is that the cost is paid once and amortized across every machine, every reinstall, and every teammate who clones it.
That’s the pattern under most of what I build: don’t do the work twice — build the thing that does the work, and make it reproducible.