32blogby StudioMitsu

dotfilesと環境変数の実践管理ガイド

dotfilesと環境変数をLinux・macOSで効率的に管理する方法。シェル初期化の仕組み、PATH構築、XDG Base Directory、GNU Stow、direnvまで実践的に解説。

22 min read
CLILinuxdotfilesbashdev-environment
目次

dotfilesは、シェル・エディタ・Git・その他ツールの設定を保持する隠しファイル群だ。環境変数は、それらが実行時にセットする値 — PATHEDITOR、APIキー、ロケール設定など。この2つが「開発環境そのもの」であり、きちんと管理すれば、新しいマシンでも数分で同じ環境を再現できる。

dotfilesをGitリポジトリに入れ、GNU StowかchezmoiでシンボリックリンクをはればOK。XDG Base Directoryに従って $HOME を整理し、direnvでプロジェクトごとの環境変数を管理する。この記事ではそれぞれの手順を実例で解説する。

シェル設定を管理すべき理由

ターミナルを開くたびに、シェルは一連の設定ファイルを読み込んで環境を構築する。プロンプトの見た目、使えるコマンド、エイリアス — すべてこのファイルで制御されている。意識せず使い続けていると、Stack Overflowからコピーしたエイリアスや、インストーラが追加したPATHエントリが積み重なって、いつの間にかカオスになる。

具体的に困る場面:

  • 新しいマシンのセットアップ — 記憶を頼りに環境を再構築。半分は忘れている
  • SSHでサーバーに入ったとき — いつものエイリアスも関数もない
  • CIパイプラインのデバッグ — ローカルと環境変数が違っていてビルドが落ちる
  • ペアプロ — 相手のシェルの挙動が全然違ってデバッグしづらい

僕がこれを痛感したのは、32blogの開発環境をメインPC、ノートPC、VPSの3台に構築したときだ。どの .bashrc が「正」なのかわからなくなった。あるマシンには忘れていたエイリアスがあり、別のマシンには毎日使うツールのPATHが通っていなかった。そこからdotfilesの管理を始めた。

シェル初期化:どのファイルがいつ読まれるか

ここで混乱する人が多い。BashとZshはシェルの起動方法によって読み込むファイルが違う。これを間違えると、環境変数がセットされない原因がわからず何時間も無駄にする。

Bashの読み込み順序

Bashは ログインシェル (SSH、TTY、bash --login)と 非ログインインタラクティブシェル (ターミナルエミュレータを開いたとき)を区別する:

bash
# ログインシェルが読むファイル:
# 1. /etc/profile         — システム全体の設定(常に読まれる)
# 次に以下の1つを探す(最初に見つかったもので止まる):
# 2. ~/.bash_profile      — ユーザーのログイン設定
# 3. ~/.bash_login        — .bash_profileがなければこれ
# 4. ~/.profile           — 上の2つもなければこれ

# 非ログインインタラクティブシェルが読むファイル:
# 1. /etc/bash.bashrc     — システム全体(ディストロによる)
# 2. ~/.bashrc            — ユーザーの設定

最大の罠は .bashrc はログインシェルでは読まれない こと。そして .bash_profile は非ログインシェルでは読まれない こと。だからほとんどのdotfilesガイドは .bash_profile にこう書けと言っている:

bash
# ~/.bash_profile
# ログインシェルでも.bashrcの設定を適用する
[[ -f ~/.bashrc ]] && source ~/.bashrc

こうすれば設定は .bashrc に一元化できる。.bash_profile はそれをsourceするだけ。管理ポイントが1箇所になる。

Zshの読み込み順序

Zshのほうがわかりやすい:

bash
# すべてのZshセッション:
# 1. /etc/zshenv      → ~/.zshenv        — 常に読まれる(スクリプトでも)

# ログインシェルはさらに:
# 2. /etc/zprofile    → ~/.zprofile       — ログイン固有
# 3. /etc/zshrc       → ~/.zshrc          — インタラクティブ設定
# 4. /etc/zlogin      → ~/.zlogin         — .zshrcの後

# インタラクティブ非ログインシェル:
# 1. /etc/zshenv      → ~/.zshenv
# 2. /etc/zshrc       → ~/.zshrc

# ログアウト時:
# ~/.zlogout → /etc/zlogout

重要なのは .zshenv は常に読まれる ということ。スクリプト実行でも読まれる。だから環境変数は .zshenv に書くのが正解。.zshrc はエイリアス、プロンプト、キーバインドなどインタラクティブ用。

bash
# ~/.zshenv — 環境変数(常に読まれる)
export EDITOR="nvim"
export LANG="ja_JP.UTF-8"

# ~/.zshrc — インタラクティブ専用(プロンプト、エイリアス、補完)
autoload -Uz compinit && compinit
alias ll='ls -lah'

一覧図

┌──────────────────────────────────────────────────┐
│                  BASH                            │
│                                                  │
│  ログイン:       /etc/profile → ~/.bash_profile  │
│  インタラクティブ: /etc/bash.bashrc → ~/.bashrc  │
│  スクリプト:     (デフォルトではなし)             │
├──────────────────────────────────────────────────┤
│                  ZSH                             │
│                                                  │
│  常に:           /etc/zshenv → ~/.zshenv         │
│  ログイン:       + ~/.zprofile → ~/.zshrc        │
│  インタラクティブ: + ~/.zshrc                     │
│  スクリプト:     ~/.zshenvのみ                    │
└──────────────────────────────────────────────────┘

シェルスクリプトを書くなら、シェルスクリプト実践ガイドでスクリプト実行時のファイル読み込みについてもっと詳しく解説している。

環境変数を使いこなす

環境変数は、子プロセスに継承されるキーと値のペアだ。シェルで export した変数は、その後に実行するすべてのコマンドから読める。

設定とexport

bash
# 変数を設定(このシェルのみ有効)
MY_VAR="hello"

# exportする(子プロセスに継承される)
export MY_VAR="hello"

# 設定とexportを1行で(最も一般的)
export EDITOR="nvim"
export GOPATH="$HOME/go"

# 全環境変数を表示
env
# または
printenv

# 特定の変数を確認
echo $PATH
printenv PATH

setexport の違いを理解していない人は多い。exportしていない変数は現在のシェルにしか存在しない — 実行するコマンドやスクリプトからは見えない。

PATH:最も重要な環境変数

PATH はコロン区切りのディレクトリリスト。git と打つと、シェルは PATH のディレクトリを左から右に検索して最初に見つかった実行ファイルを使う。

bash
# 典型的なLinuxのPATH
echo $PATH
# /home/furuya/.local/bin:/usr/local/bin:/usr/bin:/bin

# PATHにディレクトリを追加(先頭 = 優先度高)
export PATH="$HOME/.local/bin:$PATH"

# 末尾に追加(優先度低)
export PATH="$PATH:$HOME/go/bin"

僕の .bashrc ではこんなパターンを使っている:

bash
# ~/.bashrc — PATH構築
# 存在するディレクトリだけ追加する
add_to_path() {
  [[ -d "$1" ]] && [[ ":$PATH:" != *":$1:"* ]] && export PATH="$1:$PATH"
}

add_to_path "$HOME/.local/bin"
add_to_path "$HOME/.cargo/bin"
add_to_path "$HOME/go/bin"
add_to_path "$HOME/.npm-global/bin"

この add_to_path 関数は重複を防ぎ、存在しないディレクトリはスキップする。.bashrc を何年もいじっていると、同じディレクトリが6回もPATHに入っていた、なんてことが普通に起きる。

よく使う環境変数

変数用途
PATH実行ファイルの検索パス/usr/local/bin:/usr/bin
HOMEホームディレクトリ/home/furuya
EDITORデフォルトエディタnvim
VISUALビジュアルエディタcode --wait
SHELLログインシェル/bin/zsh
LANGロケール設定ja_JP.UTF-8
TERMターミナルタイプxterm-256color
XDG_CONFIG_HOMEユーザー設定ディレクトリ~/.config
PAGERman 等のページャless
GPG_TTYGPG署名用のTTY$(tty)

シークレットと.envファイル

シークレットをシェル設定ファイルに書いてはいけない。 .bashrc.zshrc はdotfilesリポジトリ(Git管理下)に入ることが多い。バージョン管理にシークレットが入ったら事故になる。

プロジェクトのシークレットには .env ファイルを使う:

bash
# .env(Gitにコミットしない)
DATABASE_URL="postgres://user:pass@localhost:5432/32blog_dev"
STRIPE_SECRET_KEY="sk_test_abc123"
NEXTAUTH_SECRET="random-secret-here"

.gitignore.env を入れ、プレースホルダー付きの .env.example をコミットする:

bash
# .env.example(Gitにコミットする — 実際の値は入れない)
DATABASE_URL="postgres://user:pass@localhost:5432/mydb"
STRIPE_SECRET_KEY="sk_test_..."
NEXTAUTH_SECRET="generate-with-openssl-rand"

シェル環境で使うパーソナルなシークレット(CLIツールのAPIトークンなど)は、OSのキーチェーンか pass(標準的なUnixパスワードマネージャ)を検討しよう:

bash
# シークレットを保存
pass insert dev/github-token

# シェル設定で使う
export GITHUB_TOKEN=$(pass show dev/github-token)

XDG Base Directory:$HOMEを整理する

ls -la ~ を実行してみてほしい。たぶんdotfilesだらけだ。XDG Base Directory仕様 は、ファイルの種類ごとに標準の保存場所を定義してこの問題を解決する:

変数デフォルト用途
XDG_CONFIG_HOME~/.config設定ファイル
XDG_DATA_HOME~/.local/shareアプリケーションデータ
XDG_STATE_HOME~/.local/state状態データ(ログ、履歴)
XDG_CACHE_HOME~/.cacheキャッシュ(削除可能)
XDG_RUNTIME_DIR/run/user/$UIDランタイムファイル

モダンなツールの多くは既にXDGに対応している:

~/.config/git/config       # ~/.gitconfig の代わり
~/.config/htop/htoprc      # htopの設定
~/.config/btop/btop.conf   # btopの設定
~/.config/nvim/init.lua    # Neovimの設定

.zshenv.bash_profile でXDG変数をセットする:

bash
export XDG_CONFIG_HOME="$HOME/.config"
export XDG_DATA_HOME="$HOME/.local/share"
export XDG_STATE_HOME="$HOME/.local/state"
export XDG_CACHE_HOME="$HOME/.cache"

XDGに対応していないツールも、環境変数でリダイレクトできることが多い:

bash
# XDGディレクトリを使うように設定
export HISTFILE="$XDG_STATE_HOME/bash/history"
export LESSHISTFILE="$XDG_STATE_HOME/less/history"
export NPM_CONFIG_USERCONFIG="$XDG_CONFIG_HOME/npm/npmrc"
export DOCKER_CONFIG="$XDG_CONFIG_HOME/docker"
export CARGO_HOME="$XDG_DATA_HOME/cargo"
export GOPATH="$XDG_DATA_HOME/go"
export RUSTUP_HOME="$XDG_DATA_HOME/rustup"

Arch WikiのXDGページ がXDG対応状況と設定方法の最も包括的なリファレンスだ。新しいCLIツールを入れるたびに参照している。

Git + GNU Stowでdotfilesを管理する

dotfilesを整理したら、変更履歴を追跡し、マシン間で共有し、1コマンドでデプロイする仕組みが必要だ。メジャーなアプローチは3つある。

アプローチ1:GNU Stow(シンプルでおすすめ)

GNU Stow はシンボリックリンク管理ツールだ。ホームディレクトリの構造をミラーしたディレクトリにdotfilesを置き、Stowがシンボリックリンクを作成する。

bash
# GNU Stowのインストール(2024年時点で2.4.1)
sudo apt install stow     # Debian/Ubuntu
sudo pacman -S stow        # Arch
brew install stow          # macOS

ディレクトリ構造をセットアップ:

bash
mkdir -p ~/dotfiles
cd ~/dotfiles
git init

# ファイルの配置先をミラーしたディレクトリを作成
mkdir -p bash/.config
mkdir -p git/.config/git
mkdir -p nvim/.config/nvim
mkdir -p tmux/.config/tmux

設定ファイルをStowディレクトリに移動:

bash
# .bashrcをbashパッケージに移動
mv ~/.bashrc ~/dotfiles/bash/

# Git設定を移動
mv ~/.config/git/config ~/dotfiles/git/.config/git/config

# Neovim設定を移動
mv ~/.config/nvim/init.lua ~/dotfiles/nvim/.config/nvim/init.lua

Stowでデプロイ:

bash
cd ~/dotfiles

# 各パッケージをstow — $HOMEにシンボリックリンクが作成される
stow bash       # ~/.bashrc → ~/dotfiles/bash/.bashrc
stow git        # ~/.config/git/config → ~/dotfiles/git/.config/git/config
stow nvim       # ~/.config/nvim/init.lua → ~/dotfiles/nvim/.config/nvim/init.lua
stow tmux

# まとめてstow
stow */

# パッケージのシンボリックリンクを削除
stow -D bash

# 再stow(削除して再作成 — ファイル追加時に便利)
stow -R bash

Stowの美点は透過的なこと — ツール側はシンボリックリンクされたファイルを読んでいることを知らない。そしてdotfilesディレクトリは普通のGitリポジトリだから、フルの変更履歴が残る。

bash
cd ~/dotfiles
git add -A
git commit -m "feat: add initial dotfiles"
git remote add origin git@github.com:your-username/dotfiles.git
git push -u origin main

新しいマシンのセットアップはこれだけ:

bash
git clone git@github.com:your-username/dotfiles.git ~/dotfiles
cd ~/dotfiles
stow */

アプローチ2:chezmoi(複雑なセットアップ向け)

chezmoi(v2.70.0)はGo製のdotfile管理ツール。テンプレート、シークレット管理、マルチマシン対応が組み込まれている。Stowより複雑だが、Stowでは対応できないシナリオを処理できる:

bash
# chezmoiをインストール
sh -c "$(curl -fsLS get.chezmoi.io)"

# 既存のdotfilesリポから初期化
chezmoi init --apply your-username

# ファイルをchezmoiの管理下に追加
chezmoi add ~/.bashrc

# 管理対象ファイルを編集
chezmoi edit ~/.bashrc

# 変更内容を確認
chezmoi diff

# 変更を適用
chezmoi apply

chezmoiの強力な機能は テンプレート だ。1つの .bashrc テンプレートからマシンごとに異なる出力を生成できる:

bash
# ~/.local/share/chezmoi/dot_bashrc.tmpl
export EDITOR="nvim"

{{ if eq .chezmoi.hostname "work-laptop" }}
export HTTP_PROXY="http://proxy.corp.example.com:8080"
{{ end }}

{{ if eq .chezmoi.os "darwin" }}
eval "$(/opt/homebrew/bin/brew shellenv)"
{{ end }}

アプローチ3:ベアGitリポジトリ

ベアリポ方式はGitを直接使い、追加ツール不要:

bash
# 初期化
git init --bare $HOME/.dotfiles

# エイリアスを作成
alias dotfiles='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'

# 未追跡ファイルを非表示
dotfiles config --local status.showUntrackedFiles no

# 通常のgitと同じように使う
dotfiles add ~/.bashrc
dotfiles commit -m "add bashrc"
dotfiles push

最も軽いアプローチ — Git以外の依存がない。ただしファイルの除外やマルチマシン管理はちょっと面倒になる。

どれを選ぶか?

機能GNU Stowchezmoiベアリポ
依存Perl(プリインストール)GoバイナリGitのみ
テンプレートなしあり(Goテンプレート)なし
シークレット管理なしあり(age, gpg, キーリング)なし
マルチマシン手動組み込み手動
学習コスト
ロールバックGit履歴組み込みdiff/applyGit履歴

僕のおすすめ: まずGNU Stowから始めること。1つの仕事(シンボリックリンク管理)をきっちりやるツールで、テンプレートやシークレット管理が必要になったらchezmoiに移行すればいい。個人のdotfilesでStowを2年使っているが、それ以上が必要になったことはない。

direnv:プロジェクトごとの環境変数

direnv(v2.37.1)は、プロジェクトディレクトリに cd すると環境変数を自動ロード・アンロードするツールだ。シェル設定に export を散りばめる代わりに、プロジェクトごとに .envrc ファイルを持つ。

インストールとセットアップ

bash
# direnvをインストール
sudo apt install direnv     # Debian/Ubuntu
sudo pacman -S direnv       # Arch
brew install direnv          # macOS

# シェルにフックを設定(.bashrcまたは.zshrcに追加)
eval "$(direnv hook bash)"    # Bash用
eval "$(direnv hook zsh)"     # Zsh用

基本的な使い方

bash
# プロジェクトに.envrcを作成
cd ~/projects/32blog
echo 'export NODE_ENV="development"' > .envrc

# direnvはセキュリティ上、承認するまでブロックする
direnv: error /home/furuya/projects/32blog/.envrc is blocked.
Run `direnv allow` to approve its content.

# 承認する
direnv allow

# このディレクトリにいる間、NODE_ENVがセットされる
echo $NODE_ENV
# development

# ディレクトリを出ると自動的にアンロードされる
cd ~
echo $NODE_ENV
# (空)

実戦的な.envrcパターン

bash
# ~/projects/32blog/.envrc

# .envファイルを読み込む(よくあるパターン)
dotenv

# プロジェクトローカルのバイナリをPATHに追加
PATH_add node_modules/.bin
PATH_add .bin

# プロジェクト固有のツールバージョンを設定
export NODE_VERSION="22.14.0"
use node

# チーム共有の設定をsource
source_env ../.shared-env

# このプロジェクト用のAWSプロファイル
export AWS_PROFILE="32blog-prod"

dotenv コマンドは .env ファイルを自動で読み込む。つまり .env(シークレットあり、コミットしない)と .envrc(シークレットなし、コミットする)を組み合わせて使える:

bash
# .envrc(Gitにコミットする)
dotenv
PATH_add node_modules/.bin
export NODE_ENV="development"

# .env(コミットしない — .gitignoreに追加)
DATABASE_URL="postgres://..."
API_SECRET="..."

Node.jsプロジェクトでのdirenv

32blog(Next.jsプロジェクト)の .envrc はこう書いている:

bash
# ~/projects/32blog/.envrc
dotenv                         # .envを読み込む
PATH_add node_modules/.bin     # ローカルのeslint、prettierなどを使う
export NEXT_TELEMETRY_DISABLED=1

これで npx なしで next deveslint . を実行できる。ローカルの node_modules/.bin がPATHに入るのはこのプロジェクトディレクトリにいるときだけ。

FAQ

.bashrcと.bash_profileの違いは?

.bash_profileログインシェル (SSH、TTY、bash --login)で読まれる。.bashrc非ログインインタラクティブシェル (ターミナルエミュレータを開いたとき)で読まれる。.bash_profile から .bashrc をsourceして、設定を1箇所に集約するのが定石。

BashとZshどちらを使うべき?

どちらでもいい。Zshは補完、グロブ、プラグインサポートが最初から優れている。macOSはCatalina(2019年)以降Zshがデフォルト。BashはLinuxの標準シェルで可搬性が高い。他の人が使うスクリプトはBash向けに書き、個人の対話シェルは好みで選べばいい。スクリプトの可搬性についてはシェルスクリプト実践ガイドを参照。

OS間でdotfilesを共有するには?

chezmoiのテンプレートでOS固有の設定を条件分岐する。GNU Stowなら別パッケージ(bash-linuxbash-macos)を作り、対象のものだけstowする。もう1つの方法は .bashrcif 文を使うこと:

bash
if [[ "$(uname)" == "Darwin" ]]; then
  eval "$(/opt/homebrew/bin/brew shellenv)"
fi

direnvはDockerと一緒に使える?

direnvはホストシェルで動くので、コンテナ内では動かない。ただし、direnvの dotenv で読み込む .env ファイルはDocker Composeの env_file と同じフォーマットなので共用できる:

yaml
# docker-compose.yml
services:
  app:
    env_file: .env

PATHの追加はどこに書くべき?

Zshなら .zshenv(スクリプトを含む全シェルタイプで読まれる)。Bashなら .bashrc.bash_profile.bashrc をsourceする前提)。複数のファイルにPATH設定を散らすと重複して、デバッグが難しくなる。

dotfilesリポからシークレットを除外するには?

.gitignore.env やシークレットファイルを除外する。シェル環境で必要なシークレットには pass やOSのキーチェーンを使う。chezmoiには age暗号化、1Password、Bitwardenなどのシークレットマネージャとの統合が組み込まれている。

XDGとは何か?必要か?

XDG Base Directoryはfreedesktop.orgの仕様で、設定・データ・キャッシュ・状態ファイルの置き場所を標準化する。必須ではないが、従えば $HOME がきれいに保てて、dotfilesの管理がラクになる。まずXDG変数をセットして、少しずつツールを移行していくのがおすすめ。

.gitconfigや.vimrcの管理は?

dotfilesリポ内の適切なStowパッケージに移動する。Git固有のXDG準拠パスは ~/.config/git/config。tmuxは ~/.config/tmux/tmux.conf(tmux 3.2以降)。各ツールのXDG対応状況はArch WikiのXDGページが最も詳しいリファレンス。

まとめ

dotfilesと環境変数は開発環境の土台だ。きちんと整理しておけば、新マシンのセットアップ、CIパイプラインのデバッグ、チームメンバーのオンボーディングのたびに報われる。

まずはシンプルに始めよう:.bashrc.zshrc.gitconfig をGitリポに入れ、GNU Stowでシンボリックリンクをはり、direnvでプロジェクト固有の変数を管理する。これで効果の90%は得られる。テンプレートやマルチOS対応が必要になったらchezmoiに移行すればいい。

シェル初期化の順序は覚える価値がある — 「環境変数がセットされない」問題のほとんどはここが原因だ。XDG Base Directoryも少しずつ取り入れれば、きれいな ls ~ の出力を見るたびに気分がいい。

関連トピックとして、エイリアス実践ガイドでシェルエイリアスの整理術、tmux実践ガイドでターミナルマルチプレクサの設定、シェルスクリプト実践ガイドでこれらの規約に従ったポータブルスクリプトの書き方を解説している。