はじめに
「自分の環境では動くんだけど...」という言葉を、何度聞いたことがあるだろうか。開発環境の差異は、これまで「手順書」「Docker」「asdf/anyenv」で解決を試みてきたが、いずれも時間経過で破綻する。手順書は陳腐化し、Dockerfileのベースイメージは変わり、asdfは言語ごとにツールが分散する。問題の本質は「環境の固定」ではなく「依存関係の完全な追跡」にあった。これを根本から解決するのが、純粋関数型パッケージマネージャ「Nix」と、その最新機能「Nix Flake」だ。
これらの課題感については Infrastructure as Code, 3rd Edition が詳しく論じており、参考になる。2025年 俺が愛した本たち 技術書編 に入れれていなくて悲しいほどよい書籍である。オライリー・ジャパンさん 自分は翻訳の準備できてます!!!
本記事では、Nix Flake を使った開発環境の統一について、Docker との比較を交えながら包括的に解説する。実際に複数言語のプロジェクトで検証した結果も含めて、実践的な導入方法を紹介する。
この記事で分かること
- Nix Flake の基本概念と従来の Nix との違い
- Docker と Nix の使い分け・組み合わせ方
- 各プログラミング言語(Rust, Go, Python, TypeScript)での開発環境の構築方法
- CI/CD との統合方法と direnv による自動環境切り替え
Nix とは何か
純粋関数型パッケージマネージャ
Nix は、従来のパッケージマネージャ(apt, brew, npm など)とは根本的に異なるアプローチを取る。その核心は「純粋関数型」(入力が同じなら出力も必ず同じになる仕組み)という概念にある。
数学の関数と同様に、Nix では「同じ入力からは常に同じ出力が得られる」。パッケージのビルドに必要な全ての依存関係を明示的に指定し、外部環境に依存しない閉じた環境でビルドを行う。この仕組みにより、以下が保証される。
- 再現性: 誰がいつどこでビルドしても、同じ結果が得られる
- 分離性: システムの既存環境を汚さない
- 共存性: 同じパッケージの異なるバージョンが同時に存在できる
ハッシュベースの依存管理
Nix は全てのパッケージを /nix/store/ 以下にハッシュ付きで保存する。例えば、Node.js 20.10.0 は以下のようなパスに保存される。
/nix/store/abc123...-nodejs-20.10.0/
このハッシュは、パッケージのソースコード、ビルドスクリプト、全ての依存関係から計算される。つまり、依存関係が少しでも異なれば、異なるハッシュ(異なるパス)になる。これにより、バージョン競合が原理的に発生しない。
Nix の核心概念
Nix を理解するには、いくつかの重要な概念を押さえておく必要がある。
Derivation(導出)
Derivation はビルドレシピのようなもので、Nix の中核概念だ。「既存の store object から新しい store object を生成する純粋関数」と捉えれば理解しやすい。ビルドは sandboxed プロセスとして実行され、指定された入力のみを読み込み、決定論的に出力を生成する。
Store(ストア)
Store は /nix/store/ に存在するオブジェクトの集合だ。全てのパッケージ、ビルド成果物、依存関係がここに保存される。Store は不変(immutable)であり、一度書き込まれたオブジェクトは変更されない。
Store Path
Store path は store object の一意な識別子だ。例えば以下のような形式になる。
/nix/store/a040m110amc4h71lds2jmr8qrkj2jhxd-git-2.38.1
この長い文字列(a040m110...)は、パッケージの全ての入力から計算されたハッシュだ。入力が変われば、パスも変わる。これが Nix の再現性を支える基盤となっている。
Realise(実現化)
Realise は derivation を実際にビルドし、store path を valid な状態にすることだ。既にキャッシュにあればダウンロードされ、なければビルドが実行される。
これらの概念については、公式マニュアルと用語集で詳しく解説されている。
Nix Flake とは
Flake の基本構造
Nix Flake は、Nix の最新機能であり、プロジェクトの依存関係を宣言的に管理する仕組みだ。従来の Nix には2つの問題があった。(1) NIX_PATH や <nixpkgs> などグローバルな状態に依存し、マシンごとに異なる結果を生む可能性があった。(2) 依存関係のバージョンを固定する標準的な方法を欠いていた。nix-channel の更新で環境が変わってしまうのだ。Flake は flake.lock でこれらを解決する。
project/ ├── flake.nix # プロジェクト定義 ├── flake.lock # 依存関係のロックファイル └── src/ # ソースコード
flake.nix は以下の構造を持つ。
{
description = "プロジェクトの説明";
inputs = {
# 依存する外部 Flake を定義
nixpkgs.url = "github:nixos/nixpkgs?ref=nixpkgs-unstable";
};
outputs = { self, nixpkgs }: {
# 出力(devShells, packages, etc.)を定義
};
}
flake.lock による再現性
flake.lock は npm の package-lock.json や Rust の Cargo.lock に相当する。全ての依存関係のコミットハッシュが固定されるため、時間が経っても同じ環境を再現できる。
{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1702312524, "narHash": "sha256-...", "rev": "abc123...", "type": "github" } } } }
Flake についての詳細は NixOS Wiki を参照してほしい。
Docker / コンテナエコシステムとの比較
Nix と Docker は競合ではなく補完関係にある。Nix は「ビルド時の再現性」を、Docker は「ランタイムの分離とデプロイ」を担う。
各ツールとの関係
| ツール | 役割 | Nix との関係 |
|---|---|---|
| Dockerfile | イメージビルド | Nix で置き換え可能(より再現性が高い) |
| Docker Compose | マルチコンテナ構成 | devenv/process-compose で補完 |
| Kubernetes | コンテナオーケストレーション | Nixidy/kubenix で統合可能 |
| Helm | K8s パッケージ管理 | nix-helm で Nix から利用可能 |
| Skaffold | 開発ワークフロー自動化 | ビルドフェーズで Nix を使用可能 |
Dockerfile の課題と Nix の解決策
Dockerfile は広く普及しているが、再現性に課題がある。
# Dockerfile: 再現性の問題 FROM python:3.12 # タグは可変 RUN apt-get update && apt-get install -y curl # バージョン固定なし RUN pip install requests # バージョン固定なし
# Nix: 完全な再現性
{
packages.docker-image = pkgs.dockerTools.buildImage {
name = "my-app";
copyToRoot = pkgs.buildEnv {
name = "image-root";
paths = [ pkgs.python312 pkgs.curl pkgs.python312Packages.requests ];
};
};
}
Nix の優位点: - ビット単位で同一の結果を保証 - 全ての依存を明示的に管理(暗黙の依存が混入しない) - パッケージ単位の効率的なキャッシュ - SBOM(Software Bill of Materials)の自動生成
Nix + Docker の組み合わせ
両者を組み合わせることで「再現可能なビルド」と「ポータブルなデプロイ」を両立できる。
{
packages.docker-image = pkgs.dockerTools.buildLayeredImage {
name = "my-app";
tag = "latest";
contents = [ myApp pkgs.cacert ];
config.Cmd = [ "/bin/my-app" ];
};
}
各依存パッケージが独立したレイヤーになるため、パッケージAを更新してもパッケージBのレイヤーは再利用される。Dockerfile を書く必要がなく、Nix の宣言的な記述で完結する。
Kubernetes との統合: Nixidy
Nixidy は Nix と Argo CD を組み合わせた GitOps ツールで、クラスター全体を NixOS のように管理できる。
{
applications.nginx = {
namespace = "default";
helm.releases.nginx = {
chart = inputs.nixhelm.chartsDerivations.nginx;
values = { replicaCount = 3; service.type = "LoadBalancer"; };
};
};
}
近年、ソフトウェアサプライチェーンのセキュリティが重視されている。ビルドの再現性と依存関係の透明性は「必須」になりつつある。Nix はビルドプロセス全体を宣言的に記述するため、SBOM の自動生成と来歴の追跡が容易だ。
実践:複数言語での開発環境構築
flake-parts によるモジュール化
複雑な Flake を管理しやすくするために、flake-parts を使う。これは NixOS モジュールシステムの考え方を Flake に適用したもので、設定を複数ファイルに分割できる。
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
outputs = { flake-parts, ... }@inputs:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ inputs.treefmt-nix.flakeModule ];
systems = [ "aarch64-darwin" "aarch64-linux" "x86_64-linux" ];
perSystem = { config, pkgs, ... }: {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
nodejs_22
config.treefmt.build.wrapper
];
};
treefmt = {
projectRootFile = "flake.nix";
programs.prettier.enable = true;
programs.nixfmt.enable = true;
};
};
};
}
Rust 開発環境
Rust プロジェクトでは、rust-overlay を使う。rustupなしで stable/nightly を切り替えられる。rust-analyzer や clippy も flake.nix で宣言的に管理できる。
{
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
perSystem = { pkgs, system, ... }:
let
overlayPkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.rust-overlay.overlays.default ];
};
rustToolchain = overlayPkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" "clippy" ];
};
in {
devShells.default = pkgs.mkShell {
packages = [
rustToolchain
pkgs.cargo-watch
pkgs.cargo-edit
];
};
};
}
Go 開発環境
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
golangci-lint
gopls
delve
];
env = {
CGO_ENABLED = "0";
};
};
}
Python 開発環境
Python では uv との組み合わせを推奨する。Nix で Python 本体と uv を提供し、パッケージ管理は uv に任せる。pyenv/venv/pip の組み合わせより高速で、依存解決も確実だ。
{
devShells.default = pkgs.mkShell {
packages = with pkgs; [
python312
uv
ruff
pyright
];
env = {
UV_PROJECT_ENVIRONMENT = ".venv";
};
};
}
マルチ言語プロジェクト
1つの Flake で複数の開発環境を提供できる。
{
devShells = {
default = pkgs.mkShell {
packages = [ rustToolchain pkgs.go pkgs.nodejs_22 ];
};
rust = pkgs.mkShell { packages = [ rustToolchain ]; };
go = pkgs.mkShell { packages = [ pkgs.go ]; };
nodejs = pkgs.mkShell { packages = [ pkgs.nodejs_22 ]; };
};
}
使用時は以下のように選択できる。
nix develop # デフォルト(全言語) nix develop .#rust # Rust のみ nix develop .#go # Go のみ
様々な言語向けのテンプレートが dev-templates リポジトリで公開されている。
direnv との連携
direnv とは
direnv は、ディレクトリごとに環境変数を自動で切り替えるツールだ。.envrc ファイルを配置したディレクトリに入ると自動的に環境がロードされ、離れるとアンロードされる。
nix-direnv のセットアップ
Nix Flake と direnv を連携させるには、nix-direnv が必要だ。実際にセットアップした手順を紹介する。
1. nix-direnv のインストール
# Nix profile でインストール nix profile install nixpkgs#nix-direnv # インストール確認 ls ~/.nix-profile/share/nix-direnv/ # direnvrc が存在することを確認
2. direnvrc の設定
~/.config/direnv/direnvrc に以下を追加する。
# nix-direnv を使用して Nix Flake 環境を高速にロード # キャッシュにより、シェル起動時の遅延を大幅に削減 if [ -f "$HOME/.nix-profile/share/nix-direnv/direnvrc" ]; then source "$HOME/.nix-profile/share/nix-direnv/direnvrc" elif [ -f "/nix/var/nix/profiles/default/share/nix-direnv/direnvrc" ]; then source "/nix/var/nix/profiles/default/share/nix-direnv/direnvrc" elif [ -f "/run/current-system/sw/share/nix-direnv/direnvrc" ]; then source "/run/current-system/sw/share/nix-direnv/direnvrc" fi
3. シェルへの hook 追加
使用しているシェルに応じて設定を追加する。
# bash (~/.bashrc) eval "$(direnv hook bash)" # zsh (~/.zshrc) eval "$(direnv hook zsh)" # fish (~/.config/fish/config.fish) direnv hook fish | source
プロジェクトでの使用
1. .envrc ファイルの作成
プロジェクトルートに .envrc を作成する。
# .envrc - 基本的な使い方
use flake
より詳細な設定も可能だ。
# .envrc - 詳細な設定例 # nix-direnv を使用(高速・キャッシュ対応) use flake # 特定の devShell を使用する場合 # use flake .#rust # 追加の環境変数 export EDITOR="nvim" export MY_PROJECT_ENV="development"
2. direnv の許可
セキュリティのため、初回は明示的に許可が必要だ。
cd my-project
direnv allow
動作確認
実際に動作を確認した結果を示す。
# direnv のステータス確認 $ direnv status direnv exec path /opt/homebrew/bin/direnv DIRENV_CONFIG /Users/nwiizo/.config/direnv Found RC path /path/to/project/.envrc Found RC allowed 0 Found RC allowPath /Users/nwiizo/.local/share/direnv/allow/...
nix-direnv のキャッシュ機構
nix-direnv は .direnv/ ディレクトリにキャッシュを作成する。実際のキャッシュ構造は以下のようになる。
.direnv/ ├── bin/ # 一時的なバイナリラッパー ├── flake-inputs/ # 入力 Flake のキャッシュ ├── flake-profile-* # Nix Store へのシンボリックリンク └── flake-profile-*.rc # 環境変数のキャッシュ(約86KB)
キャッシュの効果
flake-profile-*は Nix Store の実際のパッケージを指す- 例:
/nix/store/l5rhpr6i98h3kvydy6gww5cvszmqi05a-nix-shell-env - 2回目以降のロードは数ミリ秒で完了
nix-collect-garbageでもキャッシュは保護される
nix-direnv vs 標準 direnv
| 観点 | nix-direnv | 標準 direnv + use nix |
|---|---|---|
| 初回ロード | 同等(ビルドが必要) | 同等 |
| 2回目以降 | 数ミリ秒 | 数秒〜数十秒 |
| GC 耐性 | 保護される | 削除される可能性 |
| Flake 対応 | ネイティブ | 追加設定が必要 |
| キャッシュサイズ | 〜100KB/プロジェクト | なし |
マルチ言語プロジェクトでの設定
複数の devShell を持つプロジェクトでは、以下のように使い分けられる。
# .envrc # デフォルトで全言語環境をロード use flake # または特定の言語環境のみロードする場合: # use flake .#rust # use flake .#go # use flake .#python # use flake .#nodejs
トラブルシューティング
direnv が反応しない
# シェルフックが設定されているか確認 which direnv direnv status # 許可されているか確認 direnv allow
環境がロードされない
# .envrc の構文エラーをチェック direnv edit # キャッシュをクリアして再構築 rm -rf .direnv direnv allow
Flake が見つからない
# flake.nix が Git に追加されているか確認 git status flake.nix git add flake.nix flake.lock
Determinate Systems のブログでは、direnv と Nix の連携について詳しく解説されている。
CI/CD との統合
GitHub Actions での使用
name: CI with Nix Flake on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Nix インストール(Determinate Systems 推奨) - uses: DeterminateSystems/nix-installer-action@main # Magic Nix Cache でビルドを高速化 - uses: DeterminateSystems/magic-nix-cache-action@main # Flake のチェック - run: nix flake check # フォーマットチェック - run: nix develop --command treefmt --ci # ビルド - run: nix build
Cachix によるバイナリキャッシュ
CI でビルドした成果物を Cachix にプッシュすると、他の開発者やCI環境ではビルド済みバイナリをダウンロードするだけで済む。ビルド時間が大幅に短縮される。
- uses: cachix/cachix-action@v15 with: name: your-cache authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
overlay によるカスタマイズ
パッケージのカスタマイズ
overlay を使うと、既存のパッケージをカスタマイズしたり、独自のパッケージを追加したりできる。
{
customOverlay = final: prev: {
# 既存パッケージをラップ
myGit = prev.writeShellScriptBin "git" ''
exec ${prev.git}/bin/git -c init.defaultBranch=main "$@"
'';
# カスタムスクリプト
project-init = prev.writeShellScriptBin "project-init" ''
echo "Initializing project..."
${prev.git}/bin/git init
echo "# New Project" > README.md
'';
};
}
treefmt による統一フォーマット
複数言語のフォーマッターを1つのコマンドで実行できる。
{
treefmt = {
projectRootFile = "flake.nix";
programs = {
nixfmt.enable = true;
rustfmt.enable = true;
gofmt.enable = true;
prettier.enable = true;
ruff-format.enable = true;
};
};
}
treefmt # 全ファイルをフォーマット treefmt --ci # CI でのチェック(変更があればエラー)
トラブルシューティング
experimental-features エラー
error: experimental Nix feature 'nix-command' is disabled
~/.config/nix/nix.conf に以下を追加する。
experimental-features = nix-command flakes
nix develop が遅い
初回は依存関係のダウンロードとビルドに時間がかかる。2回目以降はキャッシュが効くため高速だ。Cachix を使うとより高速化できる。
direnv が無限ループする
Fish shell を使っている場合、shellHook で exec fish を呼ばないように注意する。
Flake が見つからない
Flake ファイルは Git に追加されている必要がある。未追跡ファイルは Nix から見えない。
git add flake.nix flake.lock
まとめ
Nix Flake を導入することで、開発環境の「再現性」「分離性」「共有性」を根本から改善できる。Docker とは競合ではなく補完関係にあり、両者を組み合わせることで「再現可能なビルド」と「ポータブルなデプロイ」を両立できる。
導入の主なメリットをまとめる。
- 開発環境のセットアップが
nix developの1コマンドに - チーム全員が同じツールバージョンを使用
- CI と開発環境の乖離がなくなる
- フォーマットの一貫性を自動で保証
- Docker イメージのビルドも再現可能に
学習コストは確かに高い。Nix言語の習得やStore/Derivationの概念理解には時間がかかる。しかし一度導入すれば、環境構築が1コマンドで完了する。「環境差異によるバグ」が原理的になくなり、CIと開発環境が同一になる。特に複数言語プロジェクトでは、rustup/pyenv/nvm/goenvの個別管理から解放され、単一のflake.nixで全ての言語ツールチェーンを統一できる。
まずは小規模なサイドプロジェクトで試してみてほしい。nix flake init -t github:the-nix-way/dev-templates#rust ですぐに始められる。