開発チームが大きくなるにつれ、開発環境の差異に起因するトラブルは増加の一途をたどります。「自分のマシンでは動くのに」という言葉を聞いたことがないエンジニアはいないでしょう。Dockerはこの問題を大きく改善しましたが、Dockerfileのメンテナンスやビルド時間の長さに悩むチームも少なくありません。
そこで注目されているのがNixです。Nixは純粋関数型のパッケージマネージャでして、再現可能なビルドと開発環境構築を実現します。本記事では、Nixの基本概念からDocker代替としての実用性まで、実務経験を踏まえて解説します。
Nixの根幹にあるのは「純粋性」と「再現性」です。すべてのパッケージはハッシュで管理され、同じ入力からは必ず同じ出力が得られます。従来のパッケージマネージャのようにグローバルな状態を変更することはありません。
Nixは独自の関数型言語(Nix Expression Language)を使ってパッケージや環境を定義します。
# 基本的なNix式の例
let
pkgs = import <nixpkgs> {};
in
pkgs.mkShell {
buildInputs = [
pkgs.nodejs_20
pkgs.python311
pkgs.postgresql_15
];
shellHook = ''
echo "開発環境が準備されました"
export DATABASE_URL="postgresql://localhost/myapp_dev"
'';
}
この設定をshell.nixとして保存し、nix-shellコマンドを実行するだけで、Node.js 20、Python 3.11、PostgreSQL 15が揃った環境が立ち上がります。チーム全員が完全に同一のバージョンを使うことが保証されます。
Nix Flakesは、Nixの再現性をさらに向上させる実験的機能です。ロックファイルにより依存関係のバージョンを完全に固定できます。
# flake.nix
{
description = "My project development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
packages = [
pkgs.go_1_22
pkgs.gopls
pkgs.golangci-lint
pkgs.docker-compose
];
};
}
);
}
nix developコマンドを実行すると、flake.lockに記録されたリビジョンのパッケージが使われます。半年後に新しいメンバーが参加しても、全く同じ環境を再現できるのは大きなメリットです。
実際の開発ではnix-direnvとの連携が非常に便利です。プロジェクトディレクトリに入ると自動的にNix環境が有効化されます。
# .envrc
use flake
たったこの一行を.envrcに書くだけです。cdでプロジェクトに移動すると自動的に環境が切り替わり、離れると元に戻ります。複数プロジェクトを掛け持ちするエンジニアにとって、これは非常に快適な体験です。
NixはDockerの完全な代替になるかというと、正直なところケースバイケースです。開発環境の構築に関しては、Nixの方が優れている場面が多いと感じます。
筆者のチームではDockerとNixを併用しています。データベースやメッセージキューなどのミドルウェアはDocker Composeで管理し、開発ツール(言語ランタイム、リンター、フォーマッタなど)はNixで管理するハイブリッド構成です。
Nixの最大の障壁は学習曲線の急さです。Nix言語の独特な構文やエラーメッセージの分かりにくさは、導入初期に多くのチームが苦戦するポイントです。まずはシンプルなshell.nixから始め、チームが慣れてきたらFlakesに移行するステップを踏むことをおすすめします。
再現可能な開発環境は、長期的なプロジェクトの生産性を確実に向上させます。NixはDockerを置き換えるものではなく、補完するツールとして活用するのが現時点での最善策でしょう。