DevContainer上でNix Flake環境を構築する
背景
チーム開発でDevContainerを使っているプロジェクトがあり、 CIや開発環境を整える作業を任されました。
私はVSCodeを一切使わないので当然DevContainerも使わないのですが。
自分はNix以外使う気があまりしないので、 Nix Flakesで管理したくなりました。既にCI環境とかはNixを使って管理していましたし。
DockerfileやDevContainer Featuresだけで開発環境を管理すると、再現性が低くなって私がメンバーの問題を再現して解決するのが面倒だったり、 DockerのキャッシュはNixよりレイヤー依存で切れがちなので、環境構築に時間がかかる問題があります。
なのでNix環境を簡単にバラまければ良いんじゃないかと思いました。自分がNixの環境を管理すれば他の人はnix flake checkとか実行するだけですからね。流石にコマンドを実行するだけなら難しくもなんともないはず。
なのでNixをDevContainerの上で動かせば良いじゃんと思ったので、許可を取ってやりました。
これでNix Flakes環境を変更するだけでDevContainer上の環境も自動で変わるはずです。
DockerfileでNixをインストールしてもボリュームマウントで消える
問題
最初はDockerfileでNixをインストールしようとしました。しかしNixの/nixディレクトリをボリュームマウントで永続化すると、コンテナ起動時に空のボリュームがマウントされて、
Dockerfileでインストールした内容が見えなくなってしまいます。
ボリュームマウントしないという選択肢もありますが、そうするとコンテナを再作成するたびにNixのビルドキャッシュが消えてしまいます。キャッシュサーバがあるとは言え、キャッシュがないとダウンロードやビルドに時間がかかるので、ボリュームマウントは必須と考えました。
解決
コンテナ作成後に実行されるpostCreateCommandでNixをインストールすることにしました。ボリュームマウント後に実行されるので、インストールした内容がちゃんと永続化されます。
実装
devcontainer.json
{
"$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json",
"name": "My Project",
"dockerComposeFile": "docker-compose.yml",
"service": "dev",
"workspaceFolder": "/workspaces/my-project",
"mounts": [
"source=nix-store-${devcontainerId},target=/nix,type=volume"
],
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh",
"customizations": {
"vscode": {
"settings": {
"direnv.restart.automatic": true,
"nix.enableLanguageServer": true,
"nix.serverPath": "nil",
"nix.serverSettings": {
"nil": {
"formatting": {
"command": ["nixfmt"]
}
}
}
},
"extensions": [
"jnoortheen.nix-ide",
"mkhl.direnv"
]
}
},
"remoteUser": "vscode"
}
/nixをボリュームマウントしてNixストアを永続化しています。
${devcontainerId}を使うことでプロジェクトごとに独立したボリュームになります。
VS Code拡張機能としてjnoortheen.nix-ideとmkhl.direnvを追加しています。
direnv拡張機能はdirenv.restart.automaticを有効にすることで、
.envrcの変更時に自動で環境を再読み込みしてくれます。
もしかしたらdirenvは拡張機能より、 devcontainers-community/features-direnv: 👩💻 Installs direnv ベースのほうがスムーズなのかもしれません。
docker-compose.yml
services:
dev:
image: mcr.microsoft.com/devcontainers/base:ubuntu-24.04
user: vscode
command: sleep infinity
env_file:
- path: ../.env.local
required: false
volumes:
- ..:/workspaces/my-project:cached
ベースイメージはMicrosoftの公式DevContainerイメージを使っています。 Dockerfileを書かずにシンプルに済ませています。
どうせスクリプトでnixなどをインストールするならDockerfileで細かなカスタムをする必要はありません。 nixのイメージと言えば、 nixos/nix - Docker Image もありますが、 DevContainer向けの設定がされてそうな方を選びました。
gitignoreしている.env.localから環境変数を読み込むようにしています。これはCachixの認証トークンを渡すために使います。
required: falseにすることでファイルがなくても起動できるようにしています。ホストの方にnixがインストールされていればcachixもインストールしてもらうだけですが、今回はホストの方にはインストールしなくていいという前提で進めたのでファイルにトークンだけ書き込んでもらう形にしました。
postCreateCommand.sh
#!/usr/bin/env bash
set -euo pipefail
# cachixのトークンは秘密なのでデバッグログを明示的に有効にしない
set +x
# Nixのインストール
# `/nix`がボリュームマウントされているため、
# コンテナ作成後にインストールが必要
NIX_VERSION=2.31.2
NIX_INSTALL_SHA256=078e2ffeddf6a9c1f22adf41458ccc46a58bb26911a9e01579645314f9982994
if ! command -v nix >/dev/null 2>&1; then
echo "Nix not found. Installing Nix ${NIX_VERSION}..."
curl -L "https://releases.nixos.org/nix/nix-${NIX_VERSION}/install" -o /tmp/nix-install.sh
echo "${NIX_INSTALL_SHA256} /tmp/nix-install.sh" | sha256sum -c -
sh /tmp/nix-install.sh --no-daemon
rm /tmp/nix-install.sh
echo "Nix installation complete."
fi
# Nixのユーザグローバル設定
if [ ! -f ~/.config/nix/nix.conf ] || ! grep -q "experimental-features" ~/.config/nix/nix.conf; then
echo "Configuring Nix..."
mkdir -p ~/.config/nix
cat >~/.config/nix/nix.conf <<'NIXCONF'
experimental-features = nix-command flakes
accept-flake-config = true
substituters = https://cache.nixos.org https://nix-community.cachix.org https://your-cache.cachix.org
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= your-cache.cachix.org-1:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX=
NIXCONF
echo "Nix configuration complete."
fi
# Nixプロファイルを読み込む
if [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then
# shellcheck source=/dev/null
. ~/.nix-profile/etc/profile.d/nix.sh
fi
# Nixプロファイルの読み込みを.bashrcに追加
# VS Codeのターミナルは非ログインシェルなので.profileは読み込まれない
# shellcheck disable=SC2016
if ! grep -q "nix-profile/etc/profile.d/nix.sh" ~/.bashrc; then
echo "Configuring Nix profile for bash..."
echo '[ -e "$HOME/.nix-profile/etc/profile.d/nix.sh" ] && . "$HOME/.nix-profile/etc/profile.d/nix.sh"' >>~/.bashrc
fi
# direnvをグローバルにインストール
# direnvはdirenv自身を読み込む必要があるのでdevShellsに入れるだけでは不十分
echo "Installing global packages via Nix profiles..."
if ! command -v direnv >/dev/null 2>&1; then
nix profile add .#direnv .#nix-direnv
fi
# nilはVS CodeのNix拡張機能がうまくdirenvの環境を認識できないのでグローバルに入れる
if ! command -v nil >/dev/null 2>&1; then
nix profile add .#nil
fi
echo "Global package installation complete."
# nix-direnvの設定
if [ ! -f ~/.config/direnv/direnvrc ] || ! grep -q "nix-direnv" ~/.config/direnv/direnvrc; then
echo "Configuring nix-direnv..."
mkdir -p ~/.config/direnv
cat >>~/.config/direnv/direnvrc <<'DIRENVRC'
# nix-direnvを使用してキャッシュを有効化
source $HOME/.nix-profile/share/nix-direnv/direnvrc
DIRENVRC
echo "nix-direnv configuration complete."
fi
# direnvフックをシェルごとに設定
# shellcheck disable=SC2016
if ! grep -q "direnv hook bash" ~/.bashrc; then
echo "Configuring direnv hook for bash..."
echo 'eval "$(direnv hook bash)"' >>~/.bashrc
fi
# Cachix認証設定
if [ -n "${CACHIX_AUTH_TOKEN:-}" ]; then
echo "Setting up Cachix authentication..."
mkdir -p ~/.config/nix
# netrcファイルを作成
# umaskで最初から600で作成(一瞬でも緩い権限にしない)
(
umask 077
cat >~/.config/nix/netrc <<EOF
machine your-cache.cachix.org
password $CACHIX_AUTH_TOKEN
EOF
)
# nix.confにnetrcファイルの場所を指定
if [ ! -f ~/.config/nix/nix.conf ] || ! grep -q "netrc-file" ~/.config/nix/nix.conf; then
echo "netrc-file = $HOME/.config/nix/netrc" >>~/.config/nix/nix.conf
fi
echo "Cachix netrc configured."
else
echo "CACHIX_AUTH_TOKEN not set. Skipping Cachix authentication."
fi
# Nix開発環境をビルドして現在のシェルにエクスポート
echo "Building and exporting Nix development environment..."
eval "$(nix print-dev-env)"
# direnvを許可して新しいシェルで自動的に環境がロードされるように
echo "Allowing direnv for future shell sessions..."
direnv allow
echo "Setup complete!"
ポイント
シングルユーザーモードでインストール
--no-daemonオプションでシングルユーザーモードでインストールしています。
DevContainerではsystemdが動いていないので、デーモンモードでインストールすると正常に動作しません。
バージョン固定とハッシュ検証
Nixのインストールスクリプトをバージョン固定してハッシュ検証しています。これにより再現性が向上し、サプライチェーン攻撃のリスクも軽減できます。
Remove GPG-signing of releases by edolstra · Pull Request #7411 · NixOS/nix で誰も気にしないからとGPG署名が廃止されましたが、 Claude Code Reviewは気にしているそうです。
VS Codeの非ログインシェル問題
VS Codeのターミナルは非ログインシェルとして起動するので、
.profileや.bash_profileは読み込まれません。そのため.bashrcにNixプロファイルの読み込みを追加しています。
direnvとnix-direnvのグローバルインストール
direnvはdevShells.defaultに入れるだけでは不十分です。
direnvが有効になる前にdirenvが存在しないといけないので、
nix profileでグローバルにインストールしています。
nix-direnvを使うことで、
direnv allow後の環境ロードが高速になります。純粋なdirenvだと毎回nix develop相当の処理が走りますが、
nix-direnvはキャッシュを効かせてくれます。
Cachix認証のnetrc方式
Cachixのプライベートキャッシュを使う場合、認証が必要になります。
netrcファイルを使うことで、
Nixが自動的に認証情報を使ってくれます。
netrcファイルはumask 077で作成して、一瞬でも緩い権限にならないようにしています。
flake.nixでグローバルインストール用のパッケージを公開
packages = {
# グローバルインストール用
# flake.lockに従った再現可能なインストールが可能
direnv = pkgs.direnv;
nil = pkgs.nil;
nix-direnv = pkgs.nix-direnv;
};
nix profile add .#direnvのように、
flakeから直接パッケージをインストールできるようにしています。
flake.lockに従ってバージョンが固定されるので再現性があります。
.envrc
#!/usr/bin/env bash
# 開発時に使う共有されている環境変数群がもしあれば読み込む。
dotenv_if_exists .env.deve
# 存在していればローカルなカスタムを先に優先して読み込んで、ハックしやすいようにする。
dotenv_if_exists .env.local
# Nix Flakesのロード。
use flake . --accept-flake-config
プロジェクトルートに.envrcを置いておきます。
direnv allow後はディレクトリに入るだけで自動的に開発環境が有効になります。
試行錯誤の記録
DevContainer FeaturesでのNixインストール
最初は features/src/nix at main · devcontainers/features というDevContainer Featuresを使おうとしました。
しかしどうも設定方法が中途半端に制約されていてやりにくそうで、実際試してみても何故かうまくいかなかったので、素直にスクリプトでNixをインストールするように変更しました。
Determinate Systems Installerの問題
最初は公式インストーラの代わりに、 Determinate Systems Installer を試しました。 flakeとかが最初から有効化されているのが魅力的だったので。
しかしこれはsystemdが動いていない環境でどうもうまく動作しませんでした。一応systemdなしを選ぶオプションはあるのですが、公式インストーラの方が慣れているし、結局公式インストーラをシングルユーザーモード(--no-daemon)で使うことにしました。
docker-composeでの環境変数読み込み
最初はdevcontainer.jsonのrunArgsで.env.localを指定しようとしました。
"runArgs": ["--env-file", ".env.local"]
しかしdocker-compose.ymlを使っている場合、
runArgsの設定は無視されるようです。
docker-compose.ymlのenv_fileで指定する必要がありました。
VS CodeのNix拡張機能がNixのLSPを見つけられない
nix-ideはdirenvの環境をうまく認識できないことがあります。 nilをグローバルにインストールすることで回避できます。
nix profile add .#nil
hatena-bookmark