YOSHIHIKO.TECH

<writing/>/DEV ENV/dotfiles-operation-and-nix

設定ファイルを端末に置いたままにしない — dotfiles 運用のすすめと、その先の Nix

設定ファイルを Git リポジトリで管理する dotfiles 運用の始め方と、その先で効いてくる Nix の話。何を入れて何を入れないか、配線の 3 手法の比較、運用して見えてくる穴、3 端末を 1 つの設定で回すところまでを実例で書く。

  • PUBLISHED 2026.11.04
  • DEV ENV
  • 14 MIN READ
  • 8 SECTIONS
  • 11 CODE BLOCKS
[ CONTENTS ]
  1. 01結論から
  2. 02dotfiles とは何か
  3. 03何を入れて、何を入れないか
  4. 04端末へどう配線するか
  5. 05運用してみると見えてくる穴
  6. 06プラスアルファ: ここで Nix を足す
  7. 07始め方
  8. 08まとめ

設定ファイルを端末に置いたままにしない

結論から

開発環境の設定ファイルを、端末に置いたままにしない。 Git リポジトリに移して、端末側にはリンクを張るだけにする。dotfiles 運用と呼ばれているもので、やることは実質これだけだ。

見返りは分かりやすい。新しい端末で環境を作り直さなくて済む。clone してリンクを張れば元に戻る。設定をいじって壊しても、どこをどう変えたかが git diff で追える。2 台目以降で「あっちにはあるのにこっちには無い」が消える。

ただ、運用が回り始めると穴が見えてくる。設定ファイルは揃うのに、そのファイルを読むツール本体は揃わない。ここを埋めるのが Nix だ。

この記事は前半で dotfiles 運用の始め方を、後半で「その先に Nix を足すとどうなるか」を書く。私は現在 3 台(MacBook Pro / Mac mini / 会社支給 Windows の WSL2)をこの方式で回していて、リポジトリは公開している。


dotfiles とは何か

Unix 系 OS では、設定ファイルの名前が . で始まる慣習がある。.zshrc、.gitconfig、.vimrc のたぐいだ。これらをまとめて dotfiles と呼ぶ。

最近は ~/.config/ 以下に置く形式(XDG Base Directory)が主流になっていて、. で始まらないものも増えた。私のリポジトリで管理している中身はこのあたりになる。

  • シェル: .zshenv / .zprofile / .zshrc と、分割した zsh/ 配下のスクリプト群
  • エディタ: nvim/(Lua の設定一式)
  • ターミナル: wezterm/ ghostty/ tmux/
  • Git: git/(.gitconfig と .gitignore_global)
  • プロンプト: starship/
  • その他: mise/(言語ランタイム管理)、lazygit/、karabiner/(キー配列)、aerospace/(ウィンドウ管理)

新しい端末に移ったときに無いと困るもの。それが全部対象になる。

端末に置いたままだと何が起きるか

セットアップが記憶頼りになる。 前の端末で何を入れて、どう設定したかを思い出しながらやる。移行前の私は「何をどの順で入れるか」が完全に頭の中にしか無かった。しかも順序には依存関係がある。Homebrew の前に Xcode Command Line Tools が要る、みたいなやつだ。それがどこにも書かれていない。

端末間でドリフトする。 2 台目で .zshrc にエイリアスを 1 つ足す。1 台目には無い。3 か月後、1 台目で同じエイリアスを打って「あれ、効かない」となる。台数が増えるほど確実に起きる。

壊したときに戻せない。 設定をいじって動かなくなったとき、どこを触ったか分からない。.zshrc.bak .zshrc.bak2 .zshrc.old が生えていく。私はこれを何度もやった。

全部「バージョン管理されていないから」起きている。だから Git に載せる。


何を入れて、何を入れないか

入れるもの

判断基準はシンプルで、**「新しい端末で手で書き直すのが面倒なもの」**は全部入れる。

私のリポジトリはこういう構成になっている。

TEXT12 LINES
dotfiles/
├── shell/        # zsh 設定(zshenv / zprofile / zshrc + zsh/ 分割)
├── config/       # ~/.config へ配線するもの
│   ├── nvim/     # エディタ
│   ├── git/      # Git
│   ├── tmux/     # ターミナルマルチプレクサ
│   ├── starship/ # プロンプト
│   └── ...       # wezterm, ghostty, mise, lazygit, karabiner, aerospace, gh
├── home/         # $HOME 直下に置く単体ファイル(.editorconfig 等)
├── ssh/          # ~/.ssh/config(後述。鍵は入れない)
├── scripts/      # セットアップ用のシェルスクリプト
└── README.md     # 復元手順のメモ

README.md を軽視しないでほしい。数か月後に読み返すための手順書になる。私はここに「まっさらな Mac から復元する手順」を書いていて、リポジトリの中で一番効いている資産はこれだと思っている。設定ファイルそのものより効いている。

絶対に入れてはいけないもの

秘密情報はコミットしない。 ここが dotfiles 運用で唯一、事故ると本当に痛いポイントになる。

  • SSH の秘密鍵(~/.ssh/id_ed25519 など)
  • API トークン、アクセスキー
  • パスワードを含む設定ファイル
  • .env ファイル

公開リポジトリでなくても入れない。 private でも、後で public に切り替えたときや、コラボレータを追加したときに漏れる。一度コミットしたものは履歴に残るので、消すのも面倒だ。

私は ~/.ssh/config を入れているが、これは中身が接続先のホスト名とポート番号だけだからだ。鍵そのものは入れていない。

トークン類をどうしても管理下に置きたい場合は、暗号化したままコミットする仕組み(sops / git-crypt / age など)を使う。私は将来のために sops-nix の仕組みだけ用意してあるが、現時点で管理対象はゼロにしている。必要になるまで入れない、というのも判断のうちだ。

迷うもの

  • 端末固有の設定(ディスプレイ解像度依存のもの、会社 PC 専用の proxy 設定など)→ 入れてよい。ただし後述の「端末差分」の扱いを決めてから
  • アプリが自動生成する設定(起動のたびに書き換わるもの)→ 入れると差分ノイズになる。.gitignore するか、必要な部分だけ切り出す
  • 巨大なバイナリ(フォント、プラグインのキャッシュ)→ 入れない。リポジトリが重くなるだけで、再取得できる

端末へどう配線するか

リポジトリを作ったら、次は ~/.zshrc からリポジトリの中身を参照させる作業に入る。やり方は主に 3 つで、後になるほど強力で、後になるほど重い。

方法 1: 手書きのシェルスクリプト

ln -s を並べたスクリプトを書く。

SH7 LINES
#!/usr/bin/env bash
DOTFILES="$HOME/ghq/github.com/yoshihiko555/dotfiles"

ln -sfn "$DOTFILES/shell/zshrc"     "$HOME/.zshrc"
ln -sfn "$DOTFILES/config/nvim"     "$HOME/.config/nvim"
ln -sfn "$DOTFILES/config/starship" "$HOME/.config/starship"
# ... 以下続く

最初はこれで十分だと思う。 依存が何もないし、何が起きているか全部読める。10 個や 20 個ならこれで回る。

弱点は、消すときの処理を手で書く必要があること(配線をやめたファイルのリンクが残り続ける)と、行数が増えると管理しづらくなることだ。

方法 2: GNU stow

stow はディレクトリ構造をそのまま ~/ に symlink として展開するツールで、dotfiles 界隈では長く定番だった。

SH2 LINES
# dotfiles/nvim/.config/nvim/init.lua という構造にしておくと
stow nvim   # → ~/.config/nvim/init.lua へリンクが張られる

リポジトリ側を「$HOME からの相対パス」の形に整えておく必要があり、そのぶんディレクトリが 1 階層深くなる。私も以前は stow を使っていた。

stow の限界は「張らない」を表現できないことにある。 ヘッドレスのサーバーに GUI アプリの設定を配りたくない、という状況で、stow にできるのは「そのコマンドを打たない」だけだ。配らないという意思がリポジトリのどこにも書かれない。

方法 3: Nix(home-manager)

後半で詳しく書くが、配線を宣言として書く方式になる。

NIX2 LINES
home.file.".zshrc".source = mkLink "shell/zshrc";
xdg.configFile."nvim".source = mkLink "config/nvim";

見た目は方法 1 とあまり変わらない。違うのは、宣言から外したものが自動で消えることと、同じ仕組みでパッケージも管理できることだ。

どれを選ぶか

手書きスクリプト GNU stow Nix (home-manager)
導入コスト ほぼゼロ 低(brew install stow) 高(Nix の学習が要る)
リポジトリ構造の制約 なし あり($HOME 相対の形) なし
配線をやめたときの後始末 手で書く stow -D 自動
「配らない」の表現 不可 不可 可能
パッケージ管理 別途 別途 同じ仕組みで可能

まだ dotfiles を運用していないなら、方法 1 から始めてほしい。 最初から Nix に行くと、dotfiles 運用そのものの学習と Nix の学習が同時に来て、たいてい途中で止まる。私自身、stow で 1 年以上運用してから Nix に移った。


運用してみると見えてくる穴

dotfiles 運用が回り始めると、これでは解決しない問題が残っているのに気づく。私が実際にぶつかった順に書く。

設定ファイルは揃うが、ツール本体は揃わない

~/.config/nvim/init.lua をリンクしても、Neovim 本体が入っていなければ意味がない。 .tmux.conf があっても tmux が無ければ動かない。

つまり dotfiles リポジトリだけでは、新端末で

SH1 LINE
brew install neovim tmux ripgrep fd fzf ghq lazygit starship zoxide ...

を手で打つ工程が残る。しかも「どれを入れたか」はリポジトリのどこにも書かれていない。Brewfile を置く手はあるが、今度は Brewfile と実態がずれていく(消したはずのツールが Brewfile に残る、逆もある)。

端末差分がシェルスクリプトの if に溜まる

2 台目、3 台目と増えると、こういう記述が .zshrc に生えてくる。

SH7 LINES
if [[ "$OSTYPE" == darwin* ]]; then
  eval "$(/opt/homebrew/bin/brew shellenv)"
fi

if [[ "$(hostname)" == "work-mac" ]]; then
  export HTTP_PROXY="http://proxy.example.com:8080"
fi

1 つ 2 つなら問題ない。ただし減ることはまずない。増える一方で、しかも「この端末では何が有効なのか」を知るには全部読むしかない。

手順の順序が、結局まだ暗黙のまま

README に手順を書いても、それは実行されない文章でしかない。書き忘れれば抜ける。更新を忘れれば古くなる。手順が正しいことを保証する仕組みがどこにもない。


プラスアルファ: ここで Nix を足す

この 3 つは、どれも「設定ファイル以外のものも宣言したい」という同じ要求に行き着く。Nix はそこを埋める道具だ。

Nix が dotfiles に足すもの

パッケージがリポジトリに書かれる。 私は CLI を 1 ファイルに宣言している。

NIX8 LINES
# home/packages.nix — 3 台共通の CLI
{ pkgs, ... }:
{
  home.packages = with pkgs; [
    fd fzf gh ghq git glow lazygit mise
    neovim ripgrep starship tmux tree yazi zoxide d2
  ];
}

このファイルがそのまま「この環境に入っているものの一覧」になる。バージョンは flake.lock で固定されるので、3 台が同じところに揃う。

「入れない」が書ける。 ヘッドレスの Mac mini には GUI アプリを配らない、という意思を宣言として残せる。stow との決定的な差はここだ。私の場合、GUI アプリ(cask)は MacBook Pro に 13 個、Mac mini に 7 個で宣言が分かれている。

端末差分が宣言に移る。 シェルスクリプトの if から、ホストごとのファイルへ移る。私は 3 層に分けた。

TEXT4 LINES
config/nix/
├── darwin/   # Mac 2 台共通のシステム設定(WSL2 は通らない)
├── home/     # 3 台すべてで共有するユーザー設定
└── hosts/    # ホスト固有(macbook / hermes / wsl)

ルールは 2 つだけにしている。新規はまず、使うホストの hosts/ に書く。2 台以上で使い始めたら共通層へ昇格させる。 「将来使うかもしれないから共通層へ」をやると、共通層が条件分岐の塊に戻ってしまう。

設定ファイルは書き直さなくていい。 ここが一番言いたいところで、Nix を入れても既存の dotfiles を捨てる必要はない。

home-manager には設定内容を Nix の属性として書く流儀(programs.git.userName = "..." のような書き方)もあるが、私はそれを採らなかった。代わりに mkOutOfStoreSymlink を使って、リポジトリの実体を指すリンクを張っている。

NIX17 LINES
# home/dotfiles.nix
{ config, hostSpec, ... }:
let
  mkLink = path: config.lib.file.mkOutOfStoreSymlink "${hostSpec.dotfilesDir}/${path}";
in
{
  home.file = {
    ".zshenv".source = mkLink "shell/zshenv";
    ".zshrc".source  = mkLink "shell/zshrc";
  };

  xdg.configFile = {
    "nvim".source     = mkLink "config/nvim";
    "git".source      = mkLink "config/git";
    "starship".source = mkLink "config/starship";
  };
}

.zshrc は .zshrc のまま、Git 管理下の素のシェルスクリプトとして残る。リポジトリを編集した瞬間に反映されるので、1 文字直すたびに再ビルド、ということもない。

Nix の導入は、これまでの dotfiles 運用を捨てて乗り換えるのではなく、その上に載せる形にできる。手書きスクリプトや stow で積んできた資産はそのまま生きる。

3 台を 1 つの設定で回す

私の場合、最終的にこうなっている。

ホスト 用途 Nix の適用範囲
MacBook Pro メイン機 CLI + dotfiles + GUI アプリ
Mac mini ヘッドレスの常時稼働機 ほぼ全体(常駐デーモンの定義まで)
会社 Windows の WSL2 予備 WSL2 内部のみ(Windows 本体は対象外)

Windows が混ざっているが、WSL2 の中身は Linux なので同じ仕組みが使える。構成の作り方は macOS 側と変わる(macOS は nix-darwin、WSL2 は home-manager 単体)。それでも共通のユーザー設定層は 3 台でそのまま共有できている。ここは一番不安だった部分だが、やってみたら一番あっさり通った。

正直に言うと、Nix で楽にならないこと

新端末構築は「1 コマンド」にはならない。 私の設計目標は「現状 8 手順・順序が暗黙 → 1 コマンド」だったが、実際にまっさらな Mac から復元する手順は今もこうなっている。

  1. Xcode Command Line Tools を入れる
  2. Homebrew 本体を入れる(Nix はこれを入れてくれない)
  3. GitHub 認証を設定する
  4. Nix 本体を入れる
  5. dotfiles を clone する
  6. 反映コマンドを 1 回打つ

前提手順が 5 つ残っている。手順数でいえば 8 が 6 になった程度だ。

短縮の実体は手順数ではない。変わったのは、この手順と順序がリポジトリに書いてあることだ。移行前は私の記憶の中にしか無かった。思い出しながらやる作業が、読みながらやる作業になった。実感としてはそれが一番大きい。

他にも正直なところを書いておく。

  • Nix 言語の学習コストはある。 設定内容を書き直さない方針にしても、flake の外枠は書けるようにならないと始まらない
  • ロールバックは設定の中身を戻さない。 世代を戻してもリンク先はリポジトリの現在の内容なので、設定を戻すのは git の仕事になる
  • ディスクを食う。 世代を溜めると /nix/store が膨らむので、定期的な GC の設定が要る

始め方

今日(30 分)

1. リポジトリを作る。 dotfiles という名前で GitHub に作る。public でいい(秘密情報を入れなければ、の話だが)。

2. 一番よく触るファイルを 1 つだけ入れる。 .zshrc でも .gitconfig でもいい。

SH3 LINES
mkdir -p ~/ghq/github.com/<user>/dotfiles/shell
mv ~/.zshrc ~/ghq/github.com/<user>/dotfiles/shell/zshrc
ln -sfn ~/ghq/github.com/<user>/dotfiles/shell/zshrc ~/.zshrc

新しいシェルを開いて、今までどおり動くことを確認する。動いたらコミットする。

3. .gitignore を書く。 秘密情報を入れないための最初の防御になる。

GITIGNORE5 LINES
*.key
*.pem
.env
id_*
!id_*.pub

今週

  • 残りの設定ファイルを順に移す。一度に全部やらない。 1 つ移してはシェルを開き直す、を繰り返す
  • リンクを張るスクリプト(前述の方法 1)を書いて、scripts/link.sh に置く
  • README に「新端末での復元手順」を書き始める。この時点では不完全でいい

今月

  • 2 台目の端末に clone して、実際に復元してみる。ここで README の穴が全部見つかる
  • 端末固有の設定が出てきたら、その分離方法を決める(zshrc.local を .gitignore する、など)

その先

  • ツール本体の管理が面倒になってきたら、Brewfile か Nix を検討する
  • 端末が 3 台以上になった、あるいはシェルスクリプトの if が増えてきたら、Nix を検討するタイミングだと思う

端末が 1 台で if も生えていないなら、Nix はまだ要らない。 dotfiles 運用だけで得られるものが十分に大きいので、そこで止めていい。


まとめ

設定ファイルを端末に置かず、Git リポジトリに移してリンクを張る。これだけで新端末での復元・変更履歴・端末間の統一が同時に手に入る。秘密情報を入れないことと、配線は手書きスクリプトから始めていいこと。最初に押さえるのはこのくらいでいい。

それでも穴は残る。設定ファイルは揃ってもツール本体は揃わず、端末差分はシェルの if に溜まり、手順は暗黙のままになる。Nix はそこを埋める。既存の dotfiles を書き直さずに載せられるのも大きい。ただし「1 コマンドで新端末構築」には届かない。変わるのは手順の数ではなく、手順が記録されているかどうかだ。

正直に言うと、Nix を入れて一番効いたのは技術的な部分ではなかった気がしている。「なぜこうしたか」を書き残す習慣がついたことの方が大きい。設定そのものより、判断の履歴の方が後で効いてくる。もっともこれが Nix のおかげなのか、単に 3 台目で必要に迫られただけなのかは、まだ私自身、切り分けられていない。

私の構成は yoshihiko555/dotfiles で公開している。Nix の部分は config/nix/ 以下にあり、判断の経緯は同ディレクトリの docs/ に ADR として残してある。