dotfileschezmoiterminalproductivity

Syncing Dotfiles Across Machines with Chezmoi

The Problem

Every time I get a new Linux machine (a work refresh, a personal laptop, a VPS) I end up spending hours setting up my terminal from scratch. Installing zsh, oh-my-zsh, Powerlevel10k, git, configuring SSH keys, setting up aliases and shell functions... it adds up fast.

I tried solving this with a bootstrap script. It worked well enough, it installed everything: oh-my-zsh, p10k, git, docker, you name it. But installing programs is only half the battle. It didn't restore my configuration files. I still had to run p10k configure, set up nvm, sdkman, and redefine all my aliases and functions from scratch every time.

Things got worse when I started building a homelab. My homelab runs headless, so I interact with it entirely over SSH. I started learning Vim and eventually moved to Neovim for its plugin ecosystem: themes, icons, LSP support, and more. But having to reconfigure all those plugins on every new machine felt like Groundhog Day.

The other pain point: when I updated a config on one machine, I had to manually mirror that change on all the others. If I forgot, the machines would quietly drift apart, and I'd end up with different behavior depending on where I was working.

Introducing Chezmoi

Chezmoi is an open-source tool for managing your dotfiles across multiple machines, securely.

The concept is simple: you tell Chezmoi which config files to track, and it stores them in a Git repository that you push to GitHub (or any remote). From there, syncing your config to a new machine is just a matter of cloning and applying.

On a new machine, I either run the one-liner:

chezmoi init --apply https://github.com/yourusername/dotfiles

Or after my bootstrap script installs everything:

chezmoi init
chezmoi apply

When I update a config on one machine:

chezmoi add ~/.zshrc
git commit -m "update zshrc"
git push

Then on any other machine:

git pull --rebase
chezmoi apply

Restart the terminal and it's done. This has been especially useful when spinning up AWS EC2 instances. What used to take hours now takes minutes.

Works on Termux Too

One thing that surprised me: this workflow works on Termux.

Termux is a free, open-source terminal emulator for Android that runs a full Linux environment without root access. Most of the tools I use daily, including Neovim, run on it without issue.

I occasionally SSH into my remote machines from my phone using Termux, and with Chezmoi, I get the exact same terminal environment I have on my other machines. Same aliases, same Neovim config, same everything.

Tip: Don't Put Everything in .zshrc

Early on I stuffed all my aliases, paths, and functions into a single .zshrc. It got big and hard to manage fast. Things got trickier when I started working across different machine architectures (macOS, Linux, and Termux on Android), each with their own quirks.

For machine-specific or sensitive stuff (API tokens, credentials, arch-specific paths), I use ~/.zshrc.local. This file is intentionally not tracked by Chezmoi — it stays local to each machine and never gets pushed to GitHub.

For general aliases and functions, I created ~/.zsh_functions.d/, a folder of .zsh files that .zshrc sources on startup. This folder is tracked by Chezmoi, so any changes I make get synced across all my machines automatically. Each file is named by topic, so it's easy to find things:

~/.zsh_functions.d
├── adb.zsh
├── ai.zsh
├── chezmoi.zsh
├── general.zsh
├── git.zsh
├── navigation.zsh
├── networking.zsh
└── scrcpy.zsh

Here's the snippet in .zshrc that loads them:

# Load all .zsh files from ~/.zsh_functions.d
if [[ -d ~/.zsh_functions.d ]]; then
  for config_file in ~/.zsh_functions.d/*.zsh; do
    [[ -r "$config_file" ]] && source "$config_file"
  done
  unset config_file
fi

# Load local config (sensitive info, machine-specific overrides)
[[ -f ~/.zshrc.local ]] && source ~/.zshrc.local

When a tool behaves differently across architectures, I handle it inside the relevant .zsh file with a simple check:

# fd is packaged as fdfind on Debian/Ubuntu
if [[ "$OSTYPE" != darwin* && "$OSTYPE" != linux-android* ]]; then
  alias fd='fdfind'
fi

I'm sure there are other ways to manage dotfiles, but this is what's been working for me. Let me know how you handle yours.

Did you find this helpful?