じゃあ、おうちで学べる

本能を呼び覚ますこのコードに、君は抗えるか

「自分の環境では動く」から解放される Nix Flake

はじめに

「自分の環境では動くんだけど...」という言葉を、何度聞いたことがあるだろうか。手順書に書かれたバージョンが古くなったり、Dockerfileが参照するタグの中身が変わったりすると、同じ手順でも環境がずれる。言語ごとにツールを管理する場合も、どのバージョンを組み合わせたかを残しておきたい。そのために、ビルドの入力を明示して管理する「Nix」と、依存する入力を固定する「Nix Flake」を使う。

これらの課題感については Infrastructure as Code, 3rd Edition が詳しく論じており、参考になる。2025年 俺が愛した本たち 技術書編 で1冊として取り上げられなかったのが悲しいほどよい書籍である。オライリー・ジャパンさん 自分は翻訳の準備できてます!!!

本記事では、Dockerとの使い分けから始め、Nix Flakeで複数言語の開発環境を定義し、CIでも使う方法を紹介する。実際に複数言語のプロジェクトで検証した結果も含めて説明する。

この記事で分かること

  • Nix Flake の基本概念と従来の Nix との違い
  • Docker と Nix の使い分け・組み合わせ方
  • 各プログラミング言語(Rust, Go, Python, TypeScript)での開発環境の構築方法
  • CI/CD との統合方法と direnv による自動環境切り替え

Nix とは何か

純粋関数型パッケージマネージャ

Nixは、ビルドを入力から出力を作る関数のように扱う。ソースコード、ビルド手順、依存パッケージを明示し、同じ入力でビルドを繰り返せるようにする。

  • 再現性: ビルドに使う入力を固定し、共有できる
  • 分離性: パッケージを専用のstoreで管理する
  • 共存性: 異なるバージョンを別々のパスに置ける

ただし、入力を固定するだけで出力のビット単位の一致まで保証されるわけではない。NixOSの再現可能ビルドの説明でも、タイムスタンプなどが出力に混ざる問題は別途取り除く必要があるとしている。

nixos.org

ハッシュベースの依存管理

Nix は全てのパッケージを /nix/store/ 以下にハッシュ付きで保存する。例えば、Node.js 20.10.0 は以下のようなパスに保存される。

/nix/store/abc123...-nodejs-20.10.0/

通常の入力に基づくビルドでは、ソースコードやビルド手順、依存関係がパスのハッシュに反映される。異なる構成を別のパスに置けるため、同じ名前のパッケージを上書きせずに共存させられる。

Nix の核心概念

Nix を理解するには、いくつかの重要な概念を押さえておく必要がある。

Derivation(導出)

Derivationは、ビルドに使う入力、実行するプログラム、対象のプラットフォームなどを記述したレシピだ。sandboxを使うと、宣言していない依存がビルドへ混ざるのを抑えられる。出力が毎回一致するかは、ビルド処理そのものも含めて確かめる必要がある。

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.dev

nix.dev

Nix Flake とは

Flake の基本構造

Nix Flakeは、プロジェクトの入力と出力を決まった形式で定義する仕組みだ。NIX_PATHや<nixpkgs>の参照先がマシンごとに違うと、同じ式でも結果が変わりうる。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に似た役割を持つ。Flakeの入力について、リビジョンや内容のハッシュなどを保存する。アプリが別途取得する依存まで自動で固定するわけではないため、各言語のlockfileも合わせて管理する。

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1702312524,
        "narHash": "sha256-...",
        "rev": "abc123...",
        "type": "github"
      }
    }
  }
}

Flake についての詳細は NixOS Wiki を参照してほしい。

https://nixos.wiki/wiki/Flakesnixos.wiki

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で管理できる - 固定した入力をチームとCIで共有できる - パッケージ単位のキャッシュを利用できる

blog.replit.com

www.devzero.io

Nix + Docker の組み合わせ

両者を組み合わせることで「再現可能なビルド」と「ポータブルなデプロイ」を両立できる。

{
  packages.docker-image = pkgs.dockerTools.buildLayeredImage {
    name = "my-app";
    tag = "latest";
    contents = [ myApp pkgs.cacert ];
    config.Cmd = [ "/bin/my-app" ];
  };
}

依存するstore objectをレイヤーに分けることで、変更されていないレイヤーを再利用しやすくする。ただし、Nixpkgsの説明にあるように、レイヤー数の上限を超える場合は一部のobjectが同じレイヤーにまとめられる。すべての依存が必ず別レイヤーになるわけではない。Dockerfile を書く必要がなく、Nix の宣言的な記述で完結する。

flox.dev

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"; };
    };
  };
}

nixidy.dev

近年、ソフトウェアサプライチェーンのセキュリティが重視されている。ビルドの再現性と依存関係の透明性は「必須」になりつつある。Nix はビルドプロセス全体を宣言的に記述するため、SBOM の自動生成と来歴の追跡が容易だ。

thenewstack.io

実践:複数言語での開発環境構築

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;
        };
      };
    };
}

flake.parts

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
        ];
      };
    };
}

github.com

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 リポジトリで公開されている。

github.com

direnv との連携

direnv とは

direnv は、ディレクトリごとに環境変数を自動で切り替えるツールだ。.envrc ファイルを配置したディレクトリに入ると自動的に環境がロードされ、離れるとアンロードされる。

direnv.net

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

github.com

プロジェクトでの使用

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 の連携について詳しく解説されている。

determinate.systems

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

github.com

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 でのチェック(変更があればエラー)

github.com

トラブルシューティング

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 が見つからない

Gitリポジトリ内のローカルパスをFlakeとして参照する場合、Flake ファイルは Git に追加されている必要がある。その参照方法では、未追跡ファイルは Nix から見えない。

git add flake.nix flake.lock

まとめ

Nix Flakeを使うと、複数言語のツールチェーンを一つの定義にまとめ、チームとCIで共有できる。Dockerイメージのビルドにも、そのパッケージ定義を使える。

Nix言語やStore、Derivationの理解には時間がかかる。それでも、セットアップの手順をnix developへまとめられるのは助かる。ただし、OSやCPUアーキテクチャ、実行時の環境変数、外部サービスまで同一になるわけではない。どこまでをNixで揃え、どの差分をテストで確認するかは残る。

まずは小規模なサイドプロジェクトで試してみてほしい。nix flake init -t github:the-nix-way/dev-templates#rust ですぐに始められる。

このブログが良ければ読者になったり、nwiizoのXやGithubをフォローしてくれると嬉しいです。