TL;DR Na zes manieren om dotfiles te syncen bleef er één hangen, chezmoi voor de bestanden en mise voor de toolversies, met de mise-config gewoon in de chezmoi-repo. Templates regelen het verschil tussen werklaptop en privédesktop, age regelt de secrets, en een verse machine is één commando en een minuut of vijf. Van één beheerd bestand tot aan de bootstrap die ik echt draai.
Mijn .zshrc is ouder dan de carrière van sommige collega’s. Geldt ook voor mijn .vimrc en mijn tmux-config. Ze zijn uitgegroeid tot een setup die precies doet wat ik wil, en zonder zou ik verloren zijn. Alleen draai ik meer dan één machine. Een laptop, een desktop, af en toe een wegwerp-VM om iets te testen. Dat in sync houden was jarenlang een stapel hacks waar ik zelf niet op vertrouwde.
Wat ik van dotfiles wil is portability. Elke machine waar ik achter ga zitten moet binnen een paar minuten van mij voelen, en ik wil precies weten wat er waar wordt weggeschreven. Een setup vinden die allebei waarmaakte heeft jaren aan doodlopende wegen gekost.
Alles wat ik eerst heb geprobeerd
Het begon met de klassieker, een bare git-repo in mijn home directory. git init --bare ~/.dotfiles, een paar aliassen, klaar. Werkt, tot de dag dat je git clean op de verkeerde plek draait en je eigen configs wegveegt. Machine-specifieke instellingen? Zoek het zelf maar uit.
Toen vond ik GNU Stow. Het idee is netjes. Je zet je dotfiles in directories en Stow symlinkt ze op hun plek. Dat heb ik best lang gedraaid. Maar Stow weet niet wat een template is, en mijn werklaptop en mijn privédesktop hebben een andere .gitconfig nodig. Dus eindig je met gitconfig-work en gitconfig-personal en een handmatige switch, en vroeg of laat vergeet je die om te zetten.
Op een gegeven moment dacht ik, ik draai al Ansible voor mijn servers, waarom dan niet voor mijn dotfiles? Dus bouwde ik een playbook voor mijn workstation. Roles, templates, handlers, het hele circus. Het was prachtig. Het was ook schromelijk over-engineered. Elke keer ansible-playbook draaien als ik iets in mijn .zshrc aanpaste voelde absurd, en YAML schrijven om één bestand te kopiëren is een pijn op zich.
Onderweg heb ik er nog een paar geprobeerd. yadm is git-based en een stap omhoog vanaf de bare repo, maar de templating voelde erop geplakt. dotbot heeft een nette YAML-config en was niet flexibel genoeg voor wat ik wilde. home-manager op Nix is heel krachtig, en helemaal op Nix overgaan zag ik nog niet zitten.
Niks bleef hangen. Elke tool loste het probleem op waarvoor ik kwam en had ergens anders een gat.
Toen kwam ik uit bij chezmoi en zette er mise naast. Dat is de setup die gebleven is. Hieronder hoe hij in elkaar zit, van één beheerd bestand tot de bootstrap die ik op een verse machine draai.
Wat een kale repo niet kan
Op papier is dotfile-management triviaal. Het zijn maar configbestanden. De ellende begint zodra je een tweede machine hebt. Mijn laptop wil andere instellingen dan mijn desktop. Mijn .gitconfig bevat tokens en mijn .ssh/config hostnames die ik niet in een publieke repo wil hebben. Ik heb Go 1.21 nodig op de ene bak en 1.22 op de andere. En op een verse machine wil ik binnen een paar minuten aan het werk, in plaats van een middag bestanden rondkopiëren.
Een kale git-repo doet niks van dat alles. Je hebt templating nodig, iets voor secrets en een bootstrap die je kunt herhalen, en precies dat gat vult chezmoi.
chezmoi, te beginnen met één bestand
Chezmoi is een dotfile-manager die dat allemaal afdekt. De naam is Frans voor “bij mij thuis”, en dat klopt wel, want hij neemt je home-omgeving mee waar je ook gaat.
Source state en home directory
Chezmoi houdt een “source state” bij in ~/.local/share/chezmoi/ en past die toe op je home directory. Daar kan in zitten:
- Gewone bestanden
- Templates, met variabelen per machine
- Scripts, voor wat je niet als statisch bestand kunt vastleggen
- Versleutelde bestanden, voor secrets
Je kunt precies nalezen wat er waar wordt weggeschreven, en dat is de eigenschap waar het mij om gaat. Geen black box die namens mij beslist.
Installeren en één bestand toevoegen
# Installeren
brew install chezmoi # macOS
sudo pacman -S chezmoi # Arch
sudo apt install chezmoi # Debian/Ubuntu
# Initialiseren
chezmoi init
# Je eerste bestand toevoegen
chezmoi add ~/.zshrc
Meer is het minimum niet. Je .zshrc wordt nu door chezmoi beheerd.
De dagelijkse loop
# Bestand toevoegen aan chezmoi
chezmoi add ~/.config/nvim/init.lua
# Bestand bewerken (opent in $EDITOR)
chezmoi edit ~/.zshrc
# Bekijk wat er zou veranderen
chezmoi diff
# Pas wijzigingen toe op home directory
chezmoi apply
# Push naar git
chezmoi cd
git add -A && git commit -m "Update zshrc" && git push
Templates voor config per machine
Dit is wat Stow niet kon. Met Go-templates render je uit één bronbestand een andere config per machine.
Definieer eerst je data in ~/.config/chezmoi/chezmoi.toml:
[data]
hostname = "laptop"
work = true
email = "tom@example.com"
Gebruik die dan in een template. Een bestand toevoegen met --template geeft het in de source state een .tmpl-suffix:
chezmoi add --template ~/.gitconfig
Nu ~/.local/share/chezmoi/dot_gitconfig.tmpl:
[user]
name = Tom Meurs
email = {{ .email }}
{{ if .work -}}
[url "git@gitlab.work.com:"]
insteadOf = https://gitlab.work.com/
{{- end }}
Op m’n werklaptop wordt de GitLab-URL-rewrite ingerenderd. Op m’n privémachines blijft hij eruit. Eén bestand, twee uitkomsten, en geen switch om te vergeten.
Machinedetectie
Je kunt chezmoi ook zelf machine-eigenschappen laten detecteren:
# ~/.config/chezmoi/chezmoi.toml
[data]
hostname = {{ .chezmoi.hostname | quote }}
os = {{ .chezmoi.os | quote }}
{{ if eq .chezmoi.hostname "work-laptop" -}}
work = true
{{- else -}}
work = false
{{- end }}
Of het bij de eerste setup laten vragen:
# chezmoi.toml.tmpl
[data]
email = {{ promptString "email" | quote }}
work = {{ promptBool "work machine" }}
Secrets, versleuteld met age
Commit nooit secrets in plain text. Chezmoi werkt samen met age, zodat secrets in dezelfde repo kunnen staan zonder te lekken.
# Genereer age key
age-keygen -o ~/.config/chezmoi/key.txt
# Configureer chezmoi om het te gebruiken
chezmoi edit-config
encryption = "age"
[age]
identity = "~/.config/chezmoi/key.txt"
recipient = "age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Versleutel nu de gevoelige bestanden:
chezmoi add --encrypt ~/.ssh/config
Het bestand belandt versleuteld in je repo. Alleen een machine met de age-key kan het ontsleutelen, dus een publieke dotfiles-repo blijft veilig.
Scripts voor wat een bestand niet kan
Soms is een bestand niet het juiste gereedschap en moet er een commando draaien. Daar heeft chezmoi scripts voor:
# ~/.local/share/chezmoi/run_once_install-packages.sh
#!/bin/bash
# Dit draait één keer (bijgehouden via hash)
if command -v brew &> /dev/null; then
brew install ripgrep fd bat fzf
elif command -v apt &> /dev/null; then
sudo apt install -y ripgrep fd-find bat fzf
elif command -v pacman &> /dev/null; then
sudo pacman -S --noconfirm ripgrep fd bat fzf
fi
De prefix bepaalt wanneer een script draait:
run_once_draait één keer en daarna nooit meer, bijgehouden via een hash van de inhoudrun_onchange_draait zodra de inhoud van het script verandertrun_draait elke keer
mise erbij voor de toolversies
Chezmoi regelt de dotfiles, maar zegt niks over welke versie van Go, Node, Python of Terraform je geïnstalleerd hebt. Die wil ik ook op alle machines hetzelfde hebben. Anders ben ik degene die “werkt op mijn laptop” zegt.
Dat is het werk van mise. Een polyglot versiemanager in de geest van asdf, sneller en een stuk prettiger in gebruik.
Waarom mise en niet asdf
Ik heb jaren asdf gedraaid. Het werkt, en het heeft scherpe randjes. Het is traag, want gebouwd op shellscripts. Het plugin-ecosysteem is versnipperd. En er is geen nette manier om z’n config in je dotfiles te houden.
Mise lost alle drie op. Het is in Rust geschreven, dus snel. De meeste tools worden out of the box ondersteund, geen plugins zoeken. En de config staat in ~/.config/mise/config.toml, en die schuift zo in chezmoi.
mise installeren
# Installeren
curl https://mise.run | sh
# Of via package manager
brew install mise
Toevoegen aan je shell:
# ~/.zshrc
eval "$(mise activate zsh)"
Definieer je tools
# ~/.config/mise/config.toml
[tools]
go = "1.22"
node = "lts"
python = "3.12"
terraform = "1.7"
kubectl = "latest"
helm = "latest"
Draai nu:
mise install
Elke tool wordt op de vastgepinde versie geïnstalleerd. Wissel van machine, draai mise install, en je kijkt naar dezelfde omgeving. Dát was waar ik naar zocht.
Versies per project
Je kunt versies ook per project vastpinnen:
# ~/projects/legacy-app/.mise.toml
[tools]
node = "16" # Oud project heeft Node 16 nodig
Mise wisselt zodra je de directory in cd’t. Niks om te onthouden.
Hoe de twee samenkomen
Zo ziet het er in m’n eigen setup uit.
De mise-config gaat in chezmoi
chezmoi add ~/.config/mise/config.toml
Nu reizen mijn toolversies mee met mijn dotfiles. Overal dezelfde versies, bijgehouden in git.
Bootstrap-scripts
# ~/.local/share/chezmoi/run_once_before_10-install-mise.sh
#!/bin/bash
if ! command -v mise &> /dev/null; then
curl https://mise.run | sh
fi
# ~/.local/share/chezmoi/run_once_after_90-mise-install.sh
#!/bin/bash
# Draai nadat dotfiles op hun plek staan
mise install
De before en after in de bestandsnaam bepalen de volgorde, dus mise wordt eerst geïnstalleerd en mise install draait pas als de config er staat.
Shell-integratie
M’n .zshrc.tmpl:
# mise
if command -v mise &> /dev/null; then
eval "$(mise activate zsh)"
fi
# ... rest van config ...
Tools per machine
Verschillende machines hebben verschillende tools nodig, en dezelfde templating regelt dat:
# ~/.config/mise/config.toml.tmpl
[tools]
go = "1.22"
node = "lts"
python = "3.12"
{{ if .work -}}
terraform = "1.7"
kubectl = "1.29"
helm = "3.14"
{{- end }}
{{ if eq .chezmoi.os "darwin" -}}
# macOS-specifieke tools
{{- end }}
Een verse machine in één regel
Dit is de beloning. Op een verse machine komt het hele verhaal neer op één commando:
# 1. Installeer chezmoi en init vanuit repo
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply yourusername
# 2. Dat is het
Het init-script:
- Kloont mijn dotfiles-repo
- Vraagt om de machine-specifieke data, werk of privé, e-mail
- Ontsleutelt de secrets, na het vragen om de age-key
- Draait de bootstrap-scripts, die mise en packages installeren
- Past alle dotfiles toe
Binnen vijf minuten zit ik in een volledig ingerichte omgeving, secrets erbij. In de bare-repo-tijd was ik een uur configs aan het repareren en vond ik een week later alsnog iets wat ontbrak.
Kleinere dingen die je wilt weten
Externe bestanden
Bestanden nodig van een URL of uit een andere git-repo?
# ~/.local/share/chezmoi/.chezmoiexternal.toml
[".oh-my-zsh"]
type = "archive"
url = "https://github.com/ohmyzsh/ohmyzsh/archive/master.tar.gz"
exact = true
stripComponents = 1
refreshPeriod = "168h"
[".config/nvim/lazy-lock.json"]
type = "file"
url = "https://raw.githubusercontent.com/you/nvim-config/main/lazy-lock.json"
refreshPeriod = "24h"
Bestanden negeren
# ~/.local/share/chezmoi/.chezmoiignore
# Sync deze nooit
.DS_Store
*.swp
.zsh_history
# Machine-specifieke ignores
{{ if not .work }}
.config/work-vpn/
{{ end }}
Re-add na direct bewerken
Als je een bestand rechtstreeks bewerkt in plaats van via chezmoi edit:
# Re-add om source state te updaten
chezmoi re-add
Checken op drift
Wil je weten of je home directory is afgedreven van wat chezmoi verwacht?
chezmoi verify
Waar dit me brengt
Na een bare git-repo, GNU Stow, een Ansible-playbook en een handvol andere tools is chezmoi plus mise de setup die gebleven is. De eerste avond gaat op aan de templates en de age-key. Daarna is een nieuwe machine één commando, en een nieuwe toolversie een regel in config.toml, een push, en een pull op de andere machines.
Overkill als je maar één machine aanraakt? Waarschijnlijk. Spin je VM’s op, herinstalleer je vaak, of wil je je config gewoon versioned op een plek die van jou is, dan is het die avond waard:
chezmoi init --apply yourusername
Resources:
- chezmoi.io - officiële documentatie
- mise.jdx.dev - mise documentatie
- age encryption - simpele, moderne encryptie
