Embarassingly, until about a year ago I had never heard of dotfiles - I just had a .zshrc that I copied around to different machines as needed. As I’ve built out my infrastructure, keeping everything in a single file was becoming difficult to maintain. After an afternoon plunge into the world of dotfile management, I came up with an approach for managing mine. Here’s what it looks like:

Skip to the technical bits?

This is highly personal

Dotfiles are a personal thing. What they do and how they do it are, and should be, very specific to the person who wrote them. I took a look at what others have done and cherry picked the bits I liked and thought would work best for me. I advise you do the same.

What are dotfiles?

Broadly speaking, dotfiles are files where a user’s configuration for their environment and applications is stored. These files’ names generally start with a dot (.zshrc, .gitconfig, etc). For some, this is just a collection of shell aliases. For others, it’s is an entirely automated environment setup process complete with installers. I went back and forth trying to decide how much I wanted my dotfiles to accomplish - here’s what I settled on:

My dotfiles should automatically handle the text-based configuration of any Mac/Linux environment I use.

Keyword there is configuration. This doesn’t install or rely on any external applications. It also assumes that ZSH (my shell of choice) is installed and set as the default shell.

What about Ansible?

I’ve been riding the Ansible train quite a bit this year, and for good reason. It’s been a definite net-positive addition to the management of my server infrastructure, and I’m already at the point where I can’t believe I used to go without. It can also be used to automate dotfile (and everything else) installation - see Jeff Geerling’s mac-dev-playbook - but I decided to not use Ansible for a couple reasons.

Dependencies: To run an Ansible playbook, you need Ansible installed (duh). I want my dotfiles to be installed first thing after logging into a new machine, without relying on much else. This is really only an issue for my daily driver, but if I’m reprovisioning that, I don’t want to have to mess with Ansible first.

Effort: Ansible playbooks are a bit cumbersome in my opinion. Idempotence (getting the same result at the end of the operation, regardless of if it’s been run once or a thousand times) is the main point of using Ansible as far as I’m concerned, and it’s exactly why I use it for my server infrastructure. But for workstations, I just don’t need that. I’m constantly tweaking settings, and don’t want to fumble around in playbooks every time I want to make a small change.

Most of my Ansible playbooks do install these dotfiles though, which is good enough for me.

How do they work?

My dotfiles live in a git repo that is expected to be cloned to the home directory.

For now, it primarily contains two types of files:

  • .symlink: These files will be symlinked to from the home directory by the bootstrap script. Right now, this is just .zshrc.
  • .zsh: These are mostly aliases and functions sourced by .zshrc.

These files are split into 2 directories:

Common

Inside common there are directories for each program/subject that I want to have dotfiles for. In some cases, these are files with only a few lines each. I like having them broken up like this because it makes things easy to find and simple to understand.

Hosts

Some of the hosts I would install dotfiles on will have configurations that only apply to them. For example, I have a couple commands for my main server that switch Nextcloud in/out of maintenance mode. I also have python scripts I use on my laptop for ingesting files into Paperless and swapping external display orientation. These configs go under hosts, in a subdirectory named to match the machine’s hostname.

Bootstrapper

When the dotfiles are first cloned to a new machine, I run a bootstrap script based off of Zach Holman’s dotfiles installer, which does 2 things:

  • Make sure that the dotfiles repo was cloned into the home directory.
  • Make a symlink from the home directory to any files that end with .symlink.

Right now, this just replaces ~/.zshrc with a symlink to my dotfiles version. That .zshrc file, which is loaded every time a new interactive shell is opened, will:

  • Source all .zsh files under the common directory
  • Check the hostname of the current machine. If a directory under hosts matches this hostname, all .zsh files found there are sourced as well.

This means I can make changes to individual .zsh files, then just reload the session to see it take effect.

What next?

This is very much a work in progress. I have other things I want to add to this config (ssh, git, etc), but I’m still figuring that out. So far, I’ve incorporated this into my playbooks for my servers, and have it deployed on pretty much every machine I’m using, and it’s worked out great. We’ll see how it holds up as I expand on it.