· · 10 min leestijd Beginner

Dotfile management met mise en chezmoi: de perfecte combo

Hoe je dotfiles beheert over meerdere machines met chezmoi, gecombineerd met mise voor tool version management. Een praktische gids voor developers.

Drie identieke koffertjes, de derde vouwt zich op een vreemd bureau tot dezelfde opstelling, een goblin met losse snippers

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 inhoud
  • run_onchange_ draait zodra de inhoud van het script verandert
  • run_ 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:

  1. Kloont mijn dotfiles-repo
  2. Vraagt om de machine-specifieke data, werk of privé, e-mail
  3. Ontsleutelt de secrets, na het vragen om de age-key
  4. Draait de bootstrap-scripts, die mise en packages installeren
  5. 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: