TechQuant Blog

chezmoiでdotfilesを3台同期した話。symlink地獄から抜け出すまで

8分で読める

火曜の夜、新調したM3 MacBook Airでnvimを開いた瞬間に手が止まった。プラグインも、キーバインドも、自分のものじゃない。3年寝かせた.zshrcnvim/init.luaが、新しい筐体には入っていない。

古典的なsymlink + GitHub方式(~/dotfilesを作って各ファイルをln -sする、あの方式)で長くやってきた。Macだけならそれで十分回る。でもRaspberry Pi 5とVPSが加わってから、明らかに破綻していた。

マシンによって入れたいプラグインが違う。Piは省メモリ寄りで、VPSはサーバー寄り、Macはフル装備。同じ.zshrcを投げると条件分岐が肥大化して、しかもsymlinkが時々壊れる。brewのpostinstallが上書きしたり、自分のinstall.shが古かったり。

その夜に思い切って chezmoi に乗り換えた。3週間運用した今、もう戻れない。この記事はその乗り換え記録。

chezmoiは「dotfilesにテンプレートエンジンを足したGit」

chezmoiはGo製のdotfiles管理ツールで、開発者は Tom Payne 氏。公式サイトに英語の濃いドキュメントがある。

競合は yadm / GNU Stow / dotbot / 自作symlinkスクリプト、あたり。chezmoiの売りはざっくり3つ。

  • テンプレート機能(Goのtext/template)でマシンごとの差分を1ファイルで吸収できる
  • シークレット連携が標準(1Password / Bitwarden / pass / age / gopass)
  • chezmoi diffで「適用したらどう変わるか」を事前に確認できる

正直、symlinkでも回るっちゃ回る。ただ「Piのときはこのプラグインを除外」みたいな分岐を毎回shellで書くのが嫌になっていて、テンプレートで宣言的に書ける利点がデカかった。

3台に入れて見えた最低限の流れ

初期化(空のリポジトリから始める場合)

インストールはワンライナーで終わる。Mac / Linux / Pi (arm64) で同じコマンドが通る。

sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply your-github-id

このコマンドは「your-github-idのdotfilesリポジトリをcloneして即適用」までやる。atuinでシェル履歴を同期した時もそうだったが、3台で同じワンライナーが叩けるのは精神衛生上かなり大きい。

既存ファイルから始めるなら、Macで一旦GitHubに空リポジトリを作り、こうする。

chezmoi init --apply
chezmoi add ~/.zshrc ~/.gitconfig ~/.config/nvim
chezmoi cd
git init
git remote add origin git@github.com:your-id/dotfiles.git
git add . && git commit -m "init" && git push -u origin main

chezmoi addを実行すると、対象ファイルが~/.local/share/chezmoi/配下にコピーされる。symlinkじゃない。あくまでコピー、source-of-truthはchezmoiリポジトリ側になる。ここがStow方式と決定的に違うところ。

2台目以降のセットアップ

新しいPiやVPSが増えたら、その上で1コマンド。

chezmoi init --apply git@github.com:your-id/dotfiles.git

SSH鍵さえ通っていれば、これでホームディレクトリに必要なdotfilesが展開される。Pi 5の初期セットアップでこれを叩いた瞬間、シェルが完全に「自分のいつもの環境」になって素直に感動した。

テンプレートでマシン差分を1ファイルに畳む

chezmoiの本領はここから。ファイル名の末尾に.tmplを付けると、適用時にテンプレート展開される。

chezmoi chattr +template ~/.zshrc

これでdot_zshrcdot_zshrc.tmplにリネームされる。中身ではホスト名、OS、アーキテクチャ、ユーザー名などが変数として使える。

{{ if eq .chezmoi.os "darwin" -}}
# Macだけbrewのパス通し
eval "$(/opt/homebrew/bin/brew shellenv)"
{{ end -}}

{{ if eq .chezmoi.hostname "raspi-5" -}}
# Pi 5だけメモリ節約のためhistory短め
export HISTSIZE=1000
{{ else -}}
export HISTSIZE=50000
{{ end -}}

条件分岐は素直にGoテンプレ。chezmoi dataを叩くと「今このマシンで使える変数の一覧」が出る。最初はこれを横に置いて書くといい。

マシン固有の値は .chezmoi.toml.tmpl に集める

マシンごとに違う値(GitHubのemail、社用と個人で分けたい類)は、テンプレ初期化時に対話で聞く。リポジトリのrootに.chezmoi.toml.tmplを置く。

{{- $email := promptStringOnce . "email" "Git email" -}}
[data]
    email = {{ $email | quote }}

これでchezmoi initのたびに1度だけ聞かれ、~/.config/chezmoi/chezmoi.tomlに保存される。.gitconfig.tmplからは{{ .email }}で参照できる。

個人と仕事を分けたいときの定番パターン。最初は冗長に感じたが、「マシン固有の値は対話で聞く」「テンプレで参照する」の2段構えに慣れると、逆にこれ無しで運用するのが怖くなる。

シークレットと「コミットしたくないファイル」

API keyや.env系をdotfilesに含めたい瞬間が必ず来る。GitHubには絶対に置きたくないが、新マシンには欲しい。

chezmoiの解は外部のシークレットマネージャから引く。1Password CLI / Bitwarden / pass / age / gopass に対応している。

# 1Password CLIから引く例(.envrc.tmpl)
export OPENAI_API_KEY={{ (onepasswordRead "op://Personal/openai/credential") | quote }}

自分はagebox(ageのラッパー)を使っている。鍵をUSBに退避しておけば、新マシンでも復号して展開できる。Pi 5を家サーバー化した時のSSH鍵もこれで運んだ。VPS側に置く前にちゃんと暗号化されているかchezmoi diffで確認するクセは付けた方がいい。

ちなみにVPSはお名前.comの高性能VPSのKVM枠を使っているが、初期化直後にchezmoi init --applyを叩くだけで普段の作業環境に切り替わるのは想像以上に体験が良い。

3週間で固まった運用ルール

chezmoi diffを癖にする

編集はchezmoi側で行う。chezmoi cdでリポジトリに飛び、git pullしてから何かを変える。本物のファイルに反映する前に必ず差分を確認。

chezmoi diff       # 何が変わるか見るだけ
chezmoi apply -v   # 適用(-vで何をしたか出力)
chezmoi git status # cd せずにgit状態見る

これを徹底するだけで、「新マシンで突然.zshrcが壊れる」みたいな事故が消えた。

chezmoi editで間接編集にする

本物の~/.zshrcを直接いじらない。chezmoi edit ~/.zshrcで叩くと、リポジトリ側のdot_zshrc.tmplが自動で開く。閉じた瞬間にchezmoi applyがかかる(設定すれば)。

このフロー、最初の3日は地味に面倒。1週間後には呼吸が変わる。zoxideでcdが書き換わった話と似ていて、最初の摩擦を超えると元に戻れない系のツール。

scripts ディレクトリで brew/apt も合流させる

リポジトリにrun_onchange_install-packages.sh.tmplのような名前のスクリプトを置くと、内容が変わった時だけ実行される。

#!/bin/bash
{{ if eq .chezmoi.os "darwin" -}}
brew bundle --file=- <<EOF
brew "ripgrep"
brew "fzf"
brew "neovim"
EOF
{{ else if eq .chezmoi.os "linux" -}}
sudo apt-get install -y ripgrep fzf neovim
{{ end -}}

「dotfiles取得」と「ツール群インストール」が1ステップになる。新Piを買ってからフル環境が整うまで、いまは8分。

ハマったところ、これからやりたいこと

正直まだ試行錯誤中の部分もある。書き残しておくと、

  • ファイル名のprefix命名(dot_, private_, executable_等)が最初は混乱する。chezmoi addで吸い込めば自動で付く。手で書こうとしない方がいい
  • テンプレ展開のデバッグchezmoi execute-template < ファイル。試行錯誤時の必須コマンド
  • fishとの相性は問題なかったが、nushellのconfig.nuはbinary扱いされてテンプレが効かない。chezmoi chattr +templateで強制している

これからやりたいのは、Macのdefaults write系をchezmoiのrun_onchangeに寄せること。dock位置やキーリピート速度まで再現できれば、新Mac受け取った日から完全に元の環境になる。

結論: 3年放置していたのが惜しかった

chezmoiは1年くらい前に名前は知っていて、symlinkで足りてるしと放置していた。3台体制になった瞬間、その判断が完全に誤りだったと分かった。

git pushすれば3台でchezmoi applyが通る」という静かな安心感は、思っていたより日々の作業のストレスを下げてくれる。dotfilesは「動く設定ファイル群」じゃなくて、「自分の手癖のスナップショット」だ。それを物理マシンに依存させないこと自体が、思考のレイヤーを1段クリアにする。

シェル周りをもう少し詰めたい人は、fzfの実用Tipsatuinの履歴同期もセットで効くので、興味があれば。dotfilesがリポジトリに乗った今、これらの設定を3台に配るのは「chezmoi addしてgit push」だけで終わる。これが地味に効く。