← All articles
PRODUCTIVITY Developer Workstation Setup: Dotfiles, System Provis... 2026-02-09 · 7 min read · setup · workstation · dotfiles

Developer Workstation Setup: Dotfiles, System Provisioning, and Automation

Productivity 2026-02-09 · 7 min read setup workstation dotfiles automation onboarding

Developer Workstation Setup: Dotfiles, System Provisioning, and Automation

Setting up a new development machine takes anywhere from half a day to a full week depending on how much tribal knowledge lives in your head instead of in automation. The right tooling turns "reinstall OS and spend three days remembering how things were configured" into "run a script and go make coffee." This guide covers dotfiles management, system provisioning, package management, and shell customization.

Dotfiles Managers

Your dotfiles (.bashrc, .gitconfig, .vimrc, .ssh/config, etc.) are the core of your development environment. Managing them manually means copying files between machines, losing track of changes, and inevitably diverging between your laptop and your work machine. A dotfiles manager puts them in version control and handles the symlinks or copies.

chezmoi

chezmoi is the most full-featured dotfiles manager. It stores your dotfiles in a source directory, applies them to your home directory, and handles machine-specific differences through templates.

# Install
sh -c "$(curl -fsLS get.chezmoi.io)"

# Initialize from a new machine
chezmoi init --apply your-github-username

# Add a dotfile
chezmoi add ~/.gitconfig
chezmoi add ~/.config/starship.toml

# Edit and apply
chezmoi edit ~/.gitconfig
chezmoi apply

The killer feature is templates. You can have a single .gitconfig that varies by machine:

# ~/.local/share/chezmoi/dot_gitconfig.tmpl
[user]
    name = "Your Name"
{{ if eq .chezmoi.hostname "work-laptop" }}
    email = "[email protected]"
{{ else }}
    email = "[email protected]"
{{ end }}

[core]
    editor = nvim
    autocrlf = {{ if eq .chezmoi.os "windows" }}true{{ else }}input{{ end }}

chezmoi also supports encrypted secrets (via age or gpg), scripts that run on apply, and one-shot setup scripts for first-time initialization.

Strengths: Templates for machine-specific config, encrypted secrets, works on every OS, excellent documentation.

Weaknesses: More complex than simpler alternatives. The template syntax (Go templates) has a learning curve. Overkill if all your machines are identical.

GNU Stow

GNU Stow takes the opposite approach: no templates, no encryption, no configuration language. It just creates symlinks.

# Structure your dotfiles directory
dotfiles/
  git/
    .gitconfig
  vim/
    .vimrc
    .vim/
      autoload/
  zsh/
    .zshrc
    .zsh/
      aliases.zsh

# Symlink everything
cd ~/dotfiles
stow git vim zsh

# This creates:
# ~/.gitconfig -> ~/dotfiles/git/.gitconfig
# ~/.vimrc -> ~/dotfiles/vim/.vimrc
# ~/.zshrc -> ~/dotfiles/zsh/.zshrc

Each "package" (subdirectory) maps its contents to your home directory. stow git creates a symlink from ~/dotfiles/git/.gitconfig to ~/.gitconfig. That's it.

Strengths: Dead simple. No learning curve beyond understanding symlinks. Your dotfiles directory mirrors your home directory structure. Easy to selectively install packages.

Weaknesses: No templating. No machine-specific configuration. No secret management. If you need different configs on different machines, you're managing branches manually.

yadm

yadm (Yet Another Dotfiles Manager) wraps Git itself. Your home directory becomes the Git working tree, with yadm managing the .git directory elsewhere to keep things clean.

# Install and init
yadm init
yadm add ~/.gitconfig ~/.zshrc ~/.config/starship.toml
yadm commit -m "initial dotfiles"
yadm remote add origin [email protected]:you/dotfiles.git
yadm push -u origin main

# On a new machine
yadm clone [email protected]:you/dotfiles.git

yadm supports alternate files for machine-specific configs (files named .gitconfig##os.Linux vs .gitconfig##os.Darwin), encrypted files via GPG, and bootstrap scripts.

Strengths: Familiar Git workflow. No separate "source" directory -- files live where they're used. Alternate files handle machine differences without templates.

Weaknesses: Having your home directory be a Git repo (even a bare one) feels conceptually messy. File alternates are less flexible than chezmoi's templates.

Dotfiles Manager Comparison

Feature chezmoi GNU Stow yadm
Machine-specific config Templates Manual branches Alternate files
Encrypted secrets age/gpg No gpg
Learning curve Moderate Low Low
Mechanism Copy/template Symlinks Git in $HOME
Cross-platform Excellent Linux/macOS Linux/macOS
Dependencies Single binary perl git

System Provisioning

Dotfiles handle configuration files, but setting up a machine also means installing packages, configuring system services, setting up SSH keys, and tuning OS settings. Provisioning tools automate this layer.

Ansible

Ansible is overkill for a single laptop -- but it works, and the skills transfer to server management. Write playbooks in YAML, run them locally.

# workstation.yml
---
- hosts: localhost
  connection: local
  become: true
  tasks:
    - name: Install development packages (Fedora)
      dnf:
        name:
          - git
          - neovim
          - ripgrep
          - fd-find
          - tmux
          - zsh
          - jq
          - htop
          - docker
        state: present
      when: ansible_distribution == "Fedora"

    - name: Install development packages (Ubuntu)
      apt:
        name:
          - git
          - neovim
          - ripgrep
          - fd-find
          - tmux
          - zsh
          - jq
          - htop
          - docker.io
        state: present
      when: ansible_distribution == "Ubuntu"

    - name: Set default shell to zsh
      user:
        name: "{{ ansible_user_id }}"
        shell: /usr/bin/zsh

    - name: Enable Docker service
      systemd:
        name: docker
        enabled: true
        state: started
ansible-playbook workstation.yml --ask-become-pass

Strengths: Declarative (describe desired state, not steps), idempotent (safe to run repeatedly), handles OS differences, massive module library.

Weaknesses: Slow for simple tasks. YAML playbooks get verbose. Python dependency. The abstraction layer can be frustrating when you just need to run a shell command.

Nix (Home Manager)

Nix takes a radically different approach: every package is installed in an isolated store (/nix/store/), and your environment is a declarative specification. Home Manager extends this to manage your entire user environment -- packages, dotfiles, and services.

# home.nix
{ config, pkgs, ... }:

{
  home.username = "dev";
  home.homeDirectory = "/home/dev";

  home.packages = with pkgs; [
    ripgrep
    fd
    jq
    bat
    eza
    delta
    starship
    lazygit
  ];

  programs.git = {
    enable = true;
    userName = "Your Name";
    userEmail = "[email protected]";
    delta.enable = true;
    extraConfig = {
      init.defaultBranch = "main";
      push.autoSetupRemote = true;
    };
  };

  programs.zsh = {
    enable = true;
    autosuggestion.enable = true;
    syntaxHighlighting.enable = true;
    shellAliases = {
      ll = "eza -la";
      gs = "git status";
      gd = "git diff";
    };
  };

  programs.starship.enable = true;
  programs.bat.enable = true;
  programs.fzf.enable = true;
}
home-manager switch

Strengths: Truly reproducible environments. Roll back to any previous generation. Multiple versions of the same package coexist. Declarative configuration of programs and their dotfiles in one place.

Weaknesses: Steep learning curve. The Nix language is unique and non-obvious. Build times can be long. Nix's store model conflicts with some software that expects traditional paths. Documentation is notoriously scattered.

Simple Shell Scripts

For many developers, a well-organized shell script is the right answer:

#!/bin/bash
set -euo pipefail

# setup.sh - Developer workstation setup

OS="$(uname -s)"

echo "=== Installing packages ==="
if [ "$OS" = "Darwin" ]; then
    brew install git neovim ripgrep fd tmux zsh jq htop starship
elif [ -f /etc/fedora-release ]; then
    sudo dnf install -y git neovim ripgrep fd-find tmux zsh jq htop
elif [ -f /etc/debian_version ]; then
    sudo apt install -y git neovim ripgrep fd-find tmux zsh jq htop
fi

echo "=== Installing Rust toolchain ==="
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

echo "=== Installing Bun ==="
curl -fsSL https://bun.sh/install | bash

echo "=== Setting up dotfiles ==="
if [ ! -d ~/dotfiles ]; then
    git clone [email protected]:you/dotfiles.git ~/dotfiles
fi
cd ~/dotfiles && stow git vim zsh

echo "=== Changing shell to zsh ==="
chsh -s "$(which zsh)"

echo "Done! Restart your terminal."

Strengths: No dependencies beyond bash. Easy to understand and modify. Version-controlled in your dotfiles repo.

Weaknesses: Not idempotent without careful scripting. No built-in rollback. OS detection gets messy.

Package Management Across Operating Systems

The biggest friction in cross-platform setup is package management. Strategies for handling it:

Homebrew works on both macOS and Linux. Using it everywhere simplifies scripts but adds a dependency on Linux where native package managers work fine.

Bundlefile approach -- list desired packages per manager:

# Brewfile (macOS)
brew "git"
brew "neovim"
brew "ripgrep"
brew "fd"
brew "starship"
cask "wezterm"
cask "firefox"
cask "docker"
brew bundle install

Flatpak for GUI apps on Linux avoids distribution-specific packaging:

flatpak install flathub com.visualstudio.code
flatpak install flathub org.mozilla.firefox

Language-specific version managers: Use mise (formerly rtx) to manage Node, Python, Ruby, Go, and Java versions in one tool:

# .mise.toml (project-level or global)
[tools]
node = "22"
python = "3.12"
go = "1.23"
bun = "latest"
mise install  # Installs all specified versions

Shell Customization

Starship Prompt

Starship is a cross-shell prompt that works with bash, zsh, fish, and PowerShell. It's fast (written in Rust) and configurable:

# ~/.config/starship.toml
[directory]
truncation_length = 3

[git_branch]
symbol = " "

[git_status]
conflicted = " "
ahead = "⇡ "
behind = "⇣ "

[nodejs]
symbol = " "
detect_files = ["package.json"]

[python]
symbol = " "

[cmd_duration]
min_time = 2000
show_milliseconds = false

Essential Shell Plugins

For zsh, install these with your plugin manager of choice (zinit, zsh-autosuggestions, or oh-my-zsh):

# ~/.zshrc (with zinit)
zinit light zsh-users/zsh-autosuggestions
zinit light zsh-users/zsh-syntax-highlighting

Recommendations

Starting from scratch: Use chezmoi for dotfiles and a shell script for package installation. This combination handles 90% of developer needs without over-engineering.

Multiple identical machines: GNU Stow plus a Brewfile or shell script. Keep it simple when there's no machine-specific variation.

Reproducibility matters: Nix with Home Manager. The learning curve is steep but the payoff is real -- your environment is fully declared and reproducible.

Team onboarding: Write a setup script that runs in under 10 minutes. New developers should be able to clone one repo, run one command, and have a working environment. Test the script on a clean VM quarterly.

General principles: