見出し画像

NixOSの設定ファイル完全解説—5万字超えで語る僕の最新構成

昨日の夜、リビングのデスクで sudo nixos-rebuild switch --flake /etc/nixos#nixos と打ち込みました。画面を数十行の文字が流れて、しばらくするとプロンプトが戻ってきます。それだけで、僕の自作PCは492個目の「世代」になりました。

492回。それだけの回数、僕はこのマシンを作り直しています。しかも、最初に入れてから一度も再インストールをしていません。

この記事では、2026年8月時点の僕のNixOS構成を、設定ファイル493行のほぼ全項目にわたって「なぜそう書いたか」から説明します。設定の全文をただ貼っても、初めての人には呪文にしか見えません。逆に、1行ずつの意図さえ分かれば、NixOSという少し変わったOSが何を便利にしようとしているのかは、プログラミングの知識がなくても伝わるはずです。

かなり長い記事になります。目次から気になる層だけ拾って読んでいただいても大丈夫なように書きました。



まず地図——4つのファイルと、その分担

/etc/nixos の中身を数えたら、合計493行でした。

内訳はこうなっています。

ポイントは、僕が日常的に指で書き換えているのが configuration.nix の37行ではないという点です。37行はむしろ「二度と触らない場所」で、普段の要望は382行の側に積み上がっていきます。

この分担ができるまでに、僕は何度か構成を作り直しました。以前は1つのファイルに全部書いていて、そのころの設定は過去の記事でも公開しています。今の形にたどり着いた理由は、この記事を読み進めるうちに分かってもらえると思います。

NixOSそのものの入門は 僕が特におすすめしたいLinuxディストリビューション、NixOS のほうが詳しいので、「そもそもNixOSって何」という方はそちらを先にどうぞ。


NixOSの根っこ——「設定した記憶」がいらないOS

WindowsやmacOSで、去年いじった設定を思い出せるでしょうか。僕は無理です。

普通のOSでの環境構築は、手順の積み上げです。ドライバを入れて、設定画面のチェックを外して、ターミナルで何かのコマンドを叩いて。1つずつは覚えていても、半年後には「なぜこのPCだけ調子がいいのか」が誰にも説明できなくなります。会社のPCが人によって微妙に違うのも、だいたいこれが原因です。

NixOSはそこを逆から攻めます。手順を積み上げるのではなく、完成した状態を1枚の設計書に書いて、OSにその通り作らせるのです。料理でいえば「昨日つくったカレーの作業の記憶」ではなく「レシピ」を持つ、という発想の転換になります。

総務の仕事に置き換えると、もっとしっくりきます。備品管理で本当に価値があるのは「発注履歴」ではなく「あるべき備品一覧表」のほうです。一覧表さえ正しければ、棚の中身はそこに合わせればいい。NixOSの設定ファイルは、PCにおけるその一覧表にあたります。

「世代」という安全装置

この作り方から生まれる最大の恩恵が、世代(generation)です。

設定を書き換えて nixos-rebuild switch を叩くたび、NixOSは新しいシステムを丸ごと1つ作り、それを492番目、493番目……と番号を振って積んでいきます。古い世代は消されずに残ります。だから、新しい設定で画面が映らなくなっても、電源を入れ直して起動メニューから1つ前の番号を選べば、何ごともなかったように戻れます。

普通のOSで「ドライバを入れたら起動しなくなった」が怖いのは、戻る道が用意されていないからです。NixOSでは戻る道が常に494本くらい用意されている、というだけの話なのですが、この差は実際に使うと想像以上に効きます。壊す勇気の値段が、非常に安くなるのです。


手で書く37行と、生成される382行

/etc/nixos の中で、僕が自分の指で書いている行数は37行です。

NixOSの記事を読むと、たいてい何百行もの設定ファイルが貼られています。僕自身、過去に「全設定公開」という記事を何度か書きました。でも今の構成は、そこから方向が変わっています。人間が読む場所を、意図的に小さく閉じ込めているのです。

実際の configuration.nix が、これで全部です。

{ config, lib, pkgs, ... }:

{
  imports = [
    ./hardware-configuration.nix
    ./generated.nix
  ];

  # Boot loader (UEFI).
  boot.loader.systemd-boot.enable = lib.mkDefault true;
  boot.loader.efi.canTouchEfiVariables = lib.mkDefault true;

  networking.hostName = lib.mkDefault "nixos";
  networking.networkmanager.enable = lib.mkDefault true;

  users.users.takeshi = {
    isNormalUser = lib.mkDefault true;
    extraGroups = lib.mkDefault [ "wheel" "networkmanager" ];
  };

  nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" ];

  system.stateVersion = lib.mkDefault "25.05";
}

この37行が守っているもの

やっていることは4つだけです。

① UEFIで起動できること。systemd-boot は起動メニューを出してくれるプログラムで、さきほどの「1つ前の世代に戻る」の入口がここです。canTouchEfiVariables は、マザーボード側の起動設定をNixOSが書き換えてよい、という許可になります。

② ネットワークにつながること。networkmanager は無線や有線の接続を管理する定番の仕組みです。

③ 自分のユーザーが存在すること。isNormalUser は管理者ではない普通のユーザーという意味で、extraGroups の wheel が sudo を使える資格にあたります。ここが消えると、ログインはできても何も設定できない人になってしまいます。

④ flakeという新しい書き方を使えること。experimental-features の2つが、後で説明する flake.nix を有効にするスイッチです。

最後の system.stateVersion = "25.05" だけは、少し性格が違います。これはこのマシンに最初にNixOSを入れたときの版を記録しておくもので、バージョンを上げるための数字ではありません。データベースの保存形式のように「一度作ったら勝手に形を変えられては困るもの」の互換性を、この数字が守っています。NixOS初心者がいちばんやりがちな事故が、ここを最新の数字に書き換えてしまうことです。僕の機体は25.05、つまり2025年5月リリースの版でスタートしています。

なぜ2つに分けたのか

imports に並んだ2つのファイルが、残りを担当します。hardware-configuration.nix(60行)はインストール時にPCが自分のディスクやCPUを調べて自動で書いたもの。generated.nix(382行)は、僕の普段の要望をもとに生成させている、日々育つ側の設定です。

このツールでgenerated.nixを生成しています。

生成には自作のこちらのツールを使用しています。

なぜ分けたのか。壊れたら困る部分と、しょっちゅう書き換える部分を、同じファイルに置きたくなかったからです。備品一覧表と今月の発注メモを同じ紙に書かない、というのと同じ理屈になります。

lib.mkDefault——「こちらは譲ります」という宣言

もう1つ、初心者がつまずくポイントを説明させてください。37行のほぼ全部についている lib.mkDefault です。

NixOSでは、同じ項目を2箇所で違う値に設定すると、エラーになって起動しません。会議で2人が別々の日程を主張して、決まらないまま止まるのと同じです。

lib.mkDefault は「この値は仮です。もっと強い指定がどこかにあれば、そちらを優先してください」という印で、最初から譲ると宣言しておく機能にあたります。これを付けておくと、382行の側で同じ項目を設定しても衝突せず、あとから書いたほうが素直に勝ちます

実際、users.users.takeshi.extraGroups は37行の側では wheel と networkmanager の2つだけですが、382行の側では8つに増えています。

users.users.takeshi.extraGroups = [
  "networkmanager" "wheel" "video" "docker" "render" "libvirtd" "kvm" "vboxusers"
];

video と render はGPUを直接触るため、docker はコンテナを管理するため、libvirtd と kvm は仮想マシンを動かすため、vboxusers はVirtualBox用の資格です。手書きの層で最低限を保証しつつ、生成の層で本番用に上書きする——この二層が安全に成立しているのは、lib.mkDefault があるからです。


flake.nix——14行が「時間を止める」

半年前に動いていた設定が、今日そのまま動くとは限りません。ソフトウェアは日々更新されるからです。

その「時間」を扱うのが flake.nix です。これも全文で14行しかありません。

{
  description = "NixOS configuration for nixos";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      modules = [
        { nixpkgs.hostPlatform = "x86_64-linux"; }
        ./configuration.nix
      ];
    };
  };
}

大事なのは4行目だけです。nixos-26.05 と書いてあります。これはソフトウェアの巨大なカタログ(nixpkgs)のどの版を使うかの指定で、僕はいま2026年5月リリース版の系統を使っています。ここを nixos-unstable に変えれば最新を追いかける構成になりますが、そのぶん壊れる頻度も上がります。

hostPlatform = "x86_64-linux" は、この設定がどのCPUの種類向けかの宣言です。同じ設定ファイルからArm版を組み立てることもできる仕組みなので、明示しておく必要があります。

flake.lockが「材料のロット」まで記録する

flake.nix には flake.lock という相棒のファイルが自動でつきます。こちらには、そのカタログのどの瞬間のものを使ったかが固定して記録されます。

いま動いているシステムの版を確認したら 26.05.20260804 でした。つまり8月4日時点のカタログから組み上がっています。

レシピの比喩に戻すと、flake.nix が「このレシピ本の第26.05版を使う」という宣言で、flake.lock は「小麦粉はこの製粉所のこのロットを使った」まで書き留めた買い物メモです。ここまで固定されているから、同じ設定ファイルを別のPCに持っていっても、去年と同じものが今日も焼き上がります

生命保険の仕事をしていると、約款や制度は「いつ時点の版か」で意味が変わるのが当たり前です。契約書に版番号を振るのと、flakeでカタログを固定するのは、僕の中ではほとんど同じ動作に見えています。

ちなみに冒頭のコマンド nixos-rebuild switch --flake /etc/nixos#nixos の #nixos は、flake.nix の中の nixosConfigurations.nixos を指しています。1つのファイルに何台分もの設定を並べて、名前で呼び分けられるということです。デスクトップとサブのノートPCを1つのリポジトリで管理したいなら、この形が効いてきます。


hardware-configuration.nix——PCが自分で書いた60行

このファイルの冒頭には「手で編集するな」と書いてあります。

インストール時に nixos-generate-config が機体を調べて自動生成したもので、この機体でしか意味を持たない事実が並んでいます。

boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
boot.kernelModules = [ "kvm-amd" ];

上の行は、起動のいちばん最初の段階で読み込む部品です。nvme はSSD、ahci はSATA、xhci_pci はUSB。ここにディスクの種類が入っていないと、OS本体を読み出す前に迷子になります。下の kvm-amd は、AMDのCPUに載っている仮想化支援機能を使うための部品で、後半で出てくる仮想マシンの土台になります。

ディスクの指定はこうです。

fileSystems."/" = { device = "/dev/disk/by-uuid/62b288fa-…"; fsType = "ext4"; };
fileSystems."/mnt/steam" = { device = "UUID=1d98c8b4-…"; fsType = "ext4"; options = [ "rw" "nofail" ]; };

sda のような名前ではなくUUID(ディスク固有の背番号)で指定しているのがミソです。SATAケーブルを挿し替えても順番が変わらないので、起動しなくなりません。nofail は「このディスクが見つからなくても起動を止めない」という指定で、増設ディスクには必須です。これが無いと、データ用ディスクを1本外しただけでPCが起動しなくなります

僕の機体はシステム用のほかに、Steam用と、データ用が2本の合計4本構成になっています。

そして生成側で、全ディスクに1つだけ設定を足しています。

fileSystems."/".options = [ "noatime" ];

noatime は「ファイルを読んだだけの時刻を記録しない」という指定です。標準ではファイルを開くたびに「最終アクセス時刻」を書き込むのですが、これはほとんど誰も使わない情報のわりに書き込みが発生します。SSDの寿命と速度のために、僕は4本すべてで切っています。

最後にスワップです。

swapDevices = [{ device = "/swapfile"; size = 16 * 1024; }];

16GBのスワップファイル。メモリが足りなくなったときの避難先で、16 * 1024 と掛け算のまま書けるのがNixらしいところです。設定ファイルがプログラミング言語なので、計算式をそのまま置いておけるわけです。


generated.nixーーー日々の更新ファイルであり、OSの肉体部分

ハードウェアの層——このPCの性能を引き出す設定

僕のメインPCは、Ryzen 7 5700X3D、Radeon RX 7800 XT、メモリ64GBの自作デスクトップです。リビングのデスクに置いて、27インチのモニターとモバイルモニターの2枚で使っています。

382行のうち、かなりの割合がこの構成の性能を引き出すための設定です。ここがいちばん「なぜその値なのか」の説明しがいがある層になります。

カーネルと起動オプション

boot.kernelPackages = pkgs.linuxPackages_latest;
boot.initrd.kernelModules = [ "amdgpu" ];
boot.kernelParams = [ "preempt=full" "amdgpu.ppfeaturemask=0xffffffff" ];

linuxPackages_latest は、Linuxの中核(カーネル)を最新版にする指定です。確認したら 7.1.6 が動いていました。NixOSの標準はもう少し保守的な版なのですが、新しいGPUやハードウェアのサポートはカーネルの新しさで決まるので、Radeonを積んでいるなら上げておくほうが得です。

initrd.kernelModules に amdgpu を入れているのは、起動のいちばん最初からGPUドライバを読ませるためです。これがないと、起動途中で画面のモードが切り替わってチラつきます。

preempt=full は、カーネルが処理の途中でも別の作業に割り込みを許す設定です。全体の処理量はわずかに落ちますが、マウスやウィンドウの反応が良くなります。サーバーではなくデスクトップとして使う機体なので、応答性を取りました。

amdgpu.ppfeaturemask=0xffffffff が、この2行でいちばん効きます。AMDのGPUドライバは、電力やクロックを細かく制御する機能を標準では封印しています。この値は全ビットを1で埋めた指定で、要するに「封印を全部開けろ」という意味です。後で出てくるGPU調整ツールが実際に効くようになるのは、この1行があるからになります。

メモリまわりの5つの数字

boot.kernel.sysctl = {
  "vm.dirty_background_bytes" = 67108864;   # 64MB
  "vm.dirty_bytes" = 268435456;             # 256MB
  "vm.max_map_count" = 2147483642;
  "vm.swappiness" = 100;
  "vm.vfs_cache_pressure" = 50;
};

呪文に見えますが、1つずつは単純です。

dirty_background_bytes(64MB)と dirty_bytes(256MB)は、書き込みをどれだけメモリに溜めてからディスクへ流すかの設定です。64MB溜まったら裏でこっそり書き始め、256MBまで溜まったらアプリを待たせてでも書き切る、という二段構えになります。動画のような大きなファイルを扱うときに、書き込みが一気に押し寄せて他の操作が固まるのを防ぐ狙いです。

max_map_count(約21億)は、1つのプログラムがメモリ領域をいくつまで細切れに確保できるかの上限です。標準値はずっと小さいのですが、Proton経由でWindowsのゲームを動かすと、この上限に当たって落ちるタイトルが実在します。事実上の上限まで開けてしまうのが、Linuxでゲームをする環境の定番になっています。

swappiness(100)は、メモリが余っていてもどれくらい積極的に退避させるかの度合いです。標準は60前後で、ディスクのスワップが遅かった時代の名残から「なるべく使わない」方向に振られています。ただし次に説明するzramを使う場合は話が逆で、退避先がメモリの中にあって高速なので、遠慮なく使わせたほうが得になります。だから最大の100にしています。

vfs_cache_pressure(50)は、ファイルの場所や名前の情報をどれだけ長くキャッシュに残すかです。標準の100に対して半分にすることで、キャッシュが長生きします。大量のファイルを扱うときの体感が変わる部分です。

zram——メモリを圧縮して使う

zramSwap.enable = true;
zramSwap.algorithm = "zstd";
zramSwap.memoryPercent = 30;
zramSwap.priority = 100;
boot.tmp.useZram = true;

zramは、メモリの一部を圧縮領域として切り出し、そこを退避先にする仕組みです。ディスクへ書きに行くより桁違いに速く、しかも圧縮が効くので実質的にメモリが増えたように振る舞います。zstd は圧縮の方式で、速度と圧縮率のバランスが良い定番です。memoryPercent = 30 は、64GBのうち最大で3割ほどを圧縮領域に充てるという意味になります。

priority = 100 が地味に重要です。さきほどの16GBのスワップファイルと、このzramのどちらを先に使うかの優先順位で、数字が大きいほうが先に選ばれます。ディスクのスワップファイルは標準で -1 なので、この指定で必ずzramが先、ディスクは最後の砦という順番になります。

boot.tmp.useZram は、一時ファイル置き場(/tmp)そのものを圧縮メモリ上に置く指定です。ビルドの中間ファイルがディスクを叩かなくなります。

ただし、正直に書きます。この記事を書いている時点で、この設定はまだ1ミリも効いていません。確認したところ、zramのデバイスはまだ存在しませんでした。理由は単純で、設定ファイルに書き足したあと nixos-rebuild switch をまだ実行していないからです。

これがNixOSの性格をよく表しています。設定ファイルを書くのは「発注書を書く」ことであって、「棚に品物が並ぶ」ことではありません。書いた時点では世界は何も変わらず、rebuildを叩いた瞬間にまとめて変わる。慣れるまでは戸惑いますが、逆にいえば書きかけの状態でPCが半端に壊れることがないという安心でもあります。

GPU——Radeonをフルに使う

services.xserver.videoDrivers = [ "amdgpu" ];
hardware.graphics.enable = true;
hardware.graphics.extraPackages = with pkgs; [ vulkan-tools rocmPackages.rocminfo rocmPackages.clr.icd libva ];
hardware.amdgpu.opencl.enable = true;

amdgpu はAMDの正式なオープンソースドライバです。hardware.graphics.enable は、3D描画や動画の再生支援を有効にする基本のスイッチになります。

extraPackages の中身が、それぞれ別の役割を持っています。vulkan-tools はゲームで使う描画API、rocmPackages.clr.icd は計算用の入口、libva は動画のデコードをGPUに任せるための仕組みです。最後のこれが無いと、YouTubeを再生しただけでCPUが唸ります。

そして、この構成で僕がいちばん引っかかったのがここです。

environment.variables = {
  HSA_OVERRIDE_GFX_VERSION = "11.0.1";
  RUSTICL_ENABLE = "radeonsi";
  TERMINAL = "ghostty";
};

ローカルでAIを動かす話は、たいていNVIDIAのCUDAを前提に語られます。僕の実機はRadeonなので、その前提には乗れません。代わりに使うのがAMD側のROCmという仕組みです。

僕のRX 7800 XTは、内部的には gfx1101 という型番で認識されます(実機で確認しました)。ROCmは対応GPUの一覧を持っていて、そこに載っていない型番だと動作そのものを拒否します。HSA_OVERRIDE_GFX_VERSION = "11.0.1" は「このGPUを 11.0.1 として扱ってくれ」という申告で、要するに受付で門前払いされないための身分証明です。gfx1101 と 11.0.1 が対応している、という覚え方をすると分かりやすいと思います。Radeon環境でローカルAIを触ろうとして最初に詰まるのが、だいたいここです。

RUSTICL_ENABLE = "radeonsi" は、OpenCL(GPUを計算に使う仕組み)の新しい実装をAMD向けに有効化する指定になります。

GPUを触るための道具立て

services.lact.enable = true;

LACTは、AMDのGPUのクロック・電力・ファンをグラフィカルに調整できるツールです。さきほどの ppfeaturemask=0xffffffff で封印を開けてあるので、ここで実際に電力上限を下げたり、ファンカーブを描いたりできます。夏場に「性能を1割落として、静かさと温度を取る」といった調整が、GUIで完結します。

パッケージ側にも、GPU関連を厚めに入れています。amdgpu_top(負荷をリアルタイムで見る)、radeon-profile、radeontools、amdctl、そして rocmPackages 一式。microcode-amd と amd-ucodegen はCPUの不具合修正を当てるためのものです。

hardware.rasdaemon.enable = true;
hardware.rasdaemon.record = true;

これは地味ですが、僕がけっこう気に入っている設定です。rasdaemonは、メモリやCPUで起きた訂正可能なエラーを記録し続ける番人にあたります。自作PCで「たまに落ちる」の原因がメモリなのか他なのかを切り分けるとき、この記録があるかどうかで調査の手間がまるで違います。会社で機器の故障報告を受ける立場をやっていると、ログを取っていない機械の原因究明がどれだけ絶望的かは身に染みています。

電源——3つの管理を切って「常時全開」に倒す

powerManagement.cpuFreqGovernor = "performance";
services.auto-cpufreq.enable = false;
services.power-profiles-daemon.enable = false;

ここは、僕が過去に書いた記事の答え合わせになる部分です。

以前 毎日使用している"ちょっと"マイナーなLinuxアプリケーション4選 Ghostty・auto-cpufreq・GPU Screen Recorder・Librepods という記事で、僕はauto-cpufreqを推していました。CPUの周波数を状況に応じて自動調整してくれるツールです。ところが今の構成では、明示的にオフにしています

理由は単純で、この機体がデスクトップだからです。バッテリー駆動時間を1分でも延ばしたいノートPCなら自動調整に価値がありますが、コンセントに挿しっぱなしのデスクトップでは、周波数を上げ下げする判断そのものが遅延になります。performance は「常に上限で回せ」という指定で、迷わせないほうが速い、という割り切りです。

power-profiles-daemon も同じ役目の別実装なので、こちらも切ってあります。この手のツールは複数動かすと互いに設定を上書きし合って、どれが効いているのか分からなくなるのが定番の事故です。1つに絞る、というのがここでの意思決定になります。

なお、持ち出し用のノートPCは別の設定で運用しているので、あの記事が間違っていたわけではありません。同じ人間の環境でも、機体が違えば正解が反転するという話です。

周辺機器——Logicoolと自作PCの実務

hardware.logitech.wireless.enable = true;
hardware.logitech.wireless.enableGraphical = true;
services.ratbagd.enable = true;
hardware.bluetooth.enable = true;
hardware.bluetooth.powerOnBoot = true;
services.blueman.enable = true;

僕のマウスはLogicool MX MASTER 3S、キーボードはPEBBLE KEYS 2 K380sで、気づけばLogicool率がかなり高い机になっています。hardware.logitech.wireless は、Unifyingレシーバーで複数の機器を1つのUSBポートにまとめるための対応です。enableGraphical を付けると、Solaarという管理画面から電池残量やペアリングを見られるようになります。

ratbagd と、パッケージ側の piper / libratbag は、マウスのDPIやボタン割り当てをLinux側から書き換えるための組です。メーカー製の設定ソフトはたいていWindows専用なので、Linuxではこの系統に頼ります。

Bluetoothは、標準のスイッチに加えて3行が「そのまま通す」印つきで入っています。

hardware.bluetooth.settings.General.Experimental = true;
hardware.bluetooth.settings.General.FastConnectable = true;
hardware.bluetooth.settings.Policy.AutoEnable = true;

Experimental は電池残量の表示などの新しい機能を開ける指定、FastConnectable は再接続を速くする代わりに電力を少し多く使う指定、AutoEnable は起動時にペアリング済みの機器へ自動でつなぐ指定です。イヤホンはAirPods 4を使っているので、この3つが揃っていると机に座った瞬間につながります。

パッケージには librepods も入れています。これはLinuxでAirPodsのノイズキャンセリング切り替えなどを扱うためのもので、AppleのOS以外では諦めがちな機能を取り戻すツールです。


日本語という、Linux最大の関門

Linuxで日本語が打てない、ゲームの文字が全部四角になる。この2つは、Linuxを使ってきた4年間で僕がいちばん多く踏んだトラブルです。

日本語環境は、実は3つの別々の層でできています。ここを分けて理解すると、詰まったときの原因究明が一気に楽になります。

第1層:ロケール(表示と書式)

i18n.defaultLocale = "ja_JP.UTF-8";
i18n.extraLocaleSettings = {
  LC_TIME = "ja_JP.UTF-8";
  LC_MONETARY = "ja_JP.UTF-8";
  LC_NUMERIC = "ja_JP.UTF-8";
  LC_PAPER = "ja_JP.UTF-8";
  # …ほか5項目
};

ロケールは「どの国の言語と書式でものを表示するか」の設定です。メニューが日本語になるだけでなく、日付の並び順、通貨記号、そして LC_PAPER に至っては印刷の用紙サイズがA4になる(アメリカのレターサイズではなく)という設定まで含みます。9項目を個別に並べているのは、「表示は日本語だけど数値の区切りは英語式」といった混在を避けるためです。

time.timeZone = "Asia/Tokyo";
networking.timeServers = [ "ntp.nict.jp" ];

時刻は東京、時刻合わせの参照先は情報通信研究機構(NICT)の公開サーバーにしています。日本国内から使うなら、海外の標準サーバーより素直に近くて速いです。

第2層:入力メソッド(日本語を打つ)

i18n.inputMethod.type = "fcitx5";
i18n.inputMethod.enable = true;
i18n.inputMethod.fcitx5.addons = [ pkgs.fcitx5-mozc pkgs.fcitx5-gtk ];
i18n.inputMethod.fcitx5.waylandFrontend = false;

environment.sessionVariables = {
  GTK_IM_MODULE = "fcitx";
  QT_IM_MODULE = "fcitx";
  XMODIFIERS = "@im=fcitx";
};

fcitx5が入力の仕組みの本体、mozcがかな漢字変換のエンジン(Google日本語入力のオープンソース版にあたるもの)です。この2つを入れるだけでは足りません。

下の3つの環境変数が、アプリに対して「日本語を打つときはこの窓口を使え」と案内する看板の役割をします。GTK_IM_MODULE はGTK系のアプリ向け、QT_IM_MODULE はQt系のアプリ向け、XMODIFIERS はそれ以外向け。Linuxのアプリは大きく2つの流派に分かれていて、それぞれ別の看板を見に行くので、両方立てる必要があります。「ブラウザでは日本語が打てるのに、特定のアプリだけ打てない」という現象は、ほぼこの看板の立て忘れです。

waylandFrontend = false は、僕の環境がX11という従来方式のデスクトップで動いていることの表明です。新しいWayland向けの入り口を切って、X11側の入り口を使わせています。

第3層:フォント

fonts.fontconfig.enable = true;
fonts.fontDir.enable = true;
fonts.fontconfig.defaultFonts.monospace = [ "Migu 1M" ];
fonts.fontconfig.defaultFonts.sansSerif = [ "Noto Sans CJK JP" ];
fonts.fontconfig.defaultFonts.serif = [ "Noto Sans CJK JP" ];
fonts.packages = with pkgs; [ migu noto-fonts-cjk-sans noto-fonts-cjk-serif noto-fonts-color-emoji ipafont ];

フォントは「入れる」と「使わせる」が別の作業です。fonts.packages が入れるほう、defaultFonts が使わせるほうにあたります。日本語が四角(いわゆる豆腐)になるのは、たいてい入れたのに指名していないか、そもそも入っていないかのどちらかです。

僕は等幅にMigu 1Mを指定しています。プログラミング用に読みやすく、日本語と英数字の幅がきれいに揃うのが理由です。noto-fonts-color-emoji は絵文字、ipafont はPDFや古い文書の互換用に入れてあります。

正直に書くと、serif(明朝体)にもゴシック体の Noto Sans を指定してあるので、明朝が出るべき場面でもゴシックが出ます。実害を感じていないので放置していますが、こういう「昔書いてそのままの行」が382行の中には確実に混ざっています。設定ファイルが全部見える状態というのは、こういう恥ずかしい行も一緒に見えてしまうということでもあります。

キーボード配列

services.xserver.xkb.layout = "jp";
services.xserver.xkb.model = "jp106";

日本語キーボードの配列指定です。これが英語配列のままだと、記号の位置がすべてズレます。


デスクトップの層——XFCEをどう組んでいるか

電源ボタンを押してからデスクトップが出るまでの数秒に、いくつの設定が働いているでしょうか。

流れを順番に追ってみます。

services.xserver.enable = true;
services.xserver.displayManager.lightdm.enable = true;
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "takeshi";
services.displayManager.defaultSession = "xfce";
services.xserver.desktopManager.xfce.enable = true;
services.xserver.desktopManager.xterm.enable = false;

xserver が画面表示の土台、lightdm がログイン画面、autoLogin はそのログイン画面を素通りする設定です。自宅のリビングに置いた個人のデスクトップなので、電源を入れたら即デスクトップが出るようにしています。当然ながら、持ち出す機体や他人が触る機体では入れてはいけない設定です。

xterm.enable = false は、標準でくっついてくる古典的なターミナルを外す指定になります。ゴミが残らないのが気持ちいい、というだけの理由です。

デスクトップ環境にXFCEを選んでいるのは、軽くて、壊れにくくて、部品を好きに足せるからです。以前はCOSMICという新しい環境を使っていた時期もありました。

パネルとランチャー

XFCEのパネル(画面上下のバー)に、プラグインをかなり足しています。

xfce4-cpugraph-plugin      # CPU使用率のグラフ
xfce4-systemload-plugin    # 負荷とメモリ
xfce4-fsguard-plugin       # ディスク残量の見張り
xfce4-genmon-plugin        # 好きなコマンドの結果をパネルに出す
xfce4-weather-plugin       # 天気
xfce4-pulseaudio-plugin    # 音量
xfce4-xkb-plugin           # キーボード配列の表示
xfce4-whiskermenu-plugin   # 検索できるスタートメニュー
xfce4-screenshooter        # スクリーンショット
xfce4-appfinder / xfdashboard / xfce4-dict

genmon が個人的な当たりで、任意のコマンドの出力をパネルに表示できます。GPU温度でも、未読件数でも、書きようによっては何でも出せます。fsguard は残量が減ると警告を出してくれるので、175GBを食っているこの機体には実利があります。

services.xserver.displayManager.sessionCommands = pkgs.plank + "/bin/plank &\n";

これはログインしたらplank(Dockバー)を起動するという指定です。macOSの下にあるようなアイコンの並びを足しています。& を付けて裏で起動させ、改行まで含めて文字列で書いてあるあたりが、いかにも「動けばいい」で書いた行です。

copyq はクリップボードの履歴管理、kdePackages.ark は書庫の展開、kdePackages.discover はアプリ管理の画面です。

ファイルマネージャまわり

programs.thunar.enable = true;
programs.thunar.plugins = [
  pkgs.thunar-vcs-plugin        # Git等の状態表示
  pkgs.thunar-shares-plugin     # 共有フォルダ設定
  pkgs.thunar-archive-plugin    # 右クリックで圧縮・展開
  pkgs.thunar-dropbox-plugin    # Dropbox連携
  pkgs.thunar-media-tags-plugin # 音楽ファイルのタグ編集
  pkgs.thunar-volman            # USBメモリの自動認識
];
services.tumbler.enable = true;
services.gvfs.enable = true;
programs.fuse.userAllowOther = true;
xdg.mime.defaultApplications = { "inode/directory" = [ "thunar.desktop" ];};

Thunar本体だけでは、USBメモリを挿しても何も起きず、サムネイルも出ません。thunar-volman が自動認識を、tumbler がサムネイル生成を担当します。gvfs はネットワーク上の共有フォルダやスマホをファイルマネージャから開くための仕組みで、fuse.userAllowOther はマウントしたものを他のプログラムからも見えるようにする許可です。

最後の xdg.mime は「フォルダを開けと言われたらThunarを使え」という指定で、これが無いと別のアプリが割り込んでくることがあります。パッケージ側には catfish(ファイル検索)、gigolo(ネットワークドライブ接続)、mousepad(テキスト)、ristretto(画像)、parole(動画)、orage(カレンダー)、xfburn(ディスク書き込み)と、XFCE純正の小道具を一通り入れています。

見た目

kora-icon-theme    # アイコン
nordic             # 暗い青系のテーマ
whitesur-gtk-theme # macOS風のテーマ

このあたりは完全に趣味です。ただテーマもパッケージとして宣言できるという点は、NixOSらしさが出るところだと思っています。別のPCに移っても、同じ見た目が同じ手順で再現されます。


ゲームの層——Steamを日本語で快適に動かす

Steamの設定は、382行の中でいちばん長い1行になっています。

programs.steam.enable = true;
programs.steam.package = pkgs.steam.override {
  extraEnv = {
    AMD_VULKAN_ICD = "RADV";
    FONTCONFIG_PATH = "/etc/fonts";
    LANG = "ja_JP.UTF-8";
    LC_ALL = "ja_JP.UTF-8";
    GTK_IM_MODULE = "fcitx";
    QT_IM_MODULE = "fcitx";
    XMODIFIERS = "@im=fcitx";
    XCURSOR_THEME = "Adwaita";
  };
  extraPkgs = (pkgs: with pkgs; [
    migu noto-fonts-cjk-sans gnome-themes-extra adwaita-icon-theme
    mangohud gamemode gamescope fcitx5 fcitx5-gtk fcitx5-mozc kdePackages.fcitx5-qt
  ]);
};

なぜSteamにだけ、日本語設定をもう一度渡すのか

さきほど日本語入力の看板を立てたはずなのに、ここでもう一度同じことをしています。理由は、SteamがLinux上で半分独立した箱の中で動いているからです。

Steamは、どのLinuxディストリビューションでも同じように動くよう、自前の実行環境を抱えています。おかげで互換性は高いのですが、代わりにシステム側のフォントや日本語入力の設定が、その箱の中まで自動では届きません。だから箱の中にもフォント(Migu、Noto)を入れ直し、日本語入力(fcitx5一式)を入れ直し、環境変数で看板を立て直しています。

「Steamの日本語が全部豆腐になる」「Steamのチャットで日本語が打てない」というLinuxでの定番トラブルは、この構造を知っていれば原因が一発で分かります。

AMD_VULKAN_ICD = "RADV" は、AMDの2種類あるVulkanドライバのうち、オープンソース版のRADVを使うという指名です。ゲーム用途では一般にこちらが速く、安定しています。

性能まわりの道具

programs.steam.extraCompatPackages = [ pkgs.proton-ge-bin ];
programs.steam.protontricks.enable = true;
programs.gamescope.enable = true;
programs.gamescope.capSysNice = true;
programs.gamemode.enable = true;
programs.steam.fontPackages = [ pkgs.migu ];
hardware.steam-hardware.enable = true;

Protonは、WindowsのゲームをLinuxで動かすための互換レイヤーです。proton-ge-bin はその有志改造版で、公式版がまだ対応していないタイトルが動くことがあります。protontricks は、ゲームごとに必要な追加部品を入れるための道具です。

gamescopeは、ゲームを専用の小さな画面環境の中で動かす仕組みで、解像度の変換やフレームレート上限をゲーム側の設定に依存せず制御できます。capSysNice は、gamescopeに処理の優先度を上げる権限を与える指定です。

gamemodeは、ゲームを起動している間だけ全体の設定を切り替える仕組みで、細かい調整が入っています。

programs.gamemode.settings.general.renice = 10;
programs.gamemode.settings.general.ioprio = 0;
programs.gamemode.settings.general.softrealtime = "auto";
programs.gamemode.settings.general.inhibit_screensaver = 1;
programs.gamemode.settings.gpu.apply_gpu_optimisations = "accept-responsibility";
programs.gamemode.settings.gpu.gpu_device = 1;
programs.gamemode.settings.gpu.amd_performance_level = "high";

renice はCPUの優先度を上げる、ioprio はディスク読み書きの優先度を最上位にする、softrealtime は準リアルタイム的な扱いにする、inhibit_screensaver はコントローラーで遊んでいる最中に画面が消えるのを防ぐ設定です。GPU側は、amd_performance_level = "high" でゲーム中だけクロックを高く張り付かせています。

apply_gpu_optimisations = "accept-responsibility" という値が面白くて、直訳すると「責任を引き受ける」です。GPUの動作設定を勝手に変えるのは本来リスクがあるので、この文字列を自分で書くことが同意の代わりになっています。設定ファイルが同意書を兼ねているわけです。

hardware.steam-hardware はSteamコントローラーなどを認識させるための設定、mangohud と goverlay はゲーム画面にフレームレートや温度を重ねて表示するツールとその設定画面、lsfg-vk はフレーム生成を後付けする仕組み、protonup-qt はProtonの版を管理する画面、steam-run はSteam環境を借りて他のプログラムを動かす道具です。

自宅の外から、このPCで遊ぶ

services.sunshine.enable = true;
services.sunshine.autoStart = true;
services.sunshine.capSysAdmin = true;
services.sunshine.openFirewall = true;
programs.steam.remotePlay.openFirewall = true;
programs.steam.localNetworkGameTransfers.openFirewall = true;

Sunshineは、このPCの画面と音を配信し、手元の操作を送り返してもらうためのサーバーです。受け取る側のアプリがパッケージにある moonlight-qt になります。

僕はROG AllyにSteamOSを入れて使っているので、ダイニングテーブルでハンドヘルド機を持ちながら、実際に動いているのはリビングのRadeonという遊び方ができます。もともとSteam Deckを使っていたのですが、スペックが足りずに手放してROG Allyへ乗り換えました。その判断がこの構成で活きている形です。

capSysAdmin はSunshineが画面全体を取り込むために必要な権限、openFirewall は必要なポートを自動で開ける指定になります。localNetworkGameTransfers は、同じ家のPC同士でゲームのデータを直接コピーする機能で、100GB級のタイトルを再ダウンロードせずに済みます。


ネットワークの層——外と中をどうつなぐか

自宅のPCを外から触れるようにするのは、便利さと危なさが直結する部分です。

networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [
  53317                          # LocalSend
  2049                           # NFS(ファイル共有)
  8080
  config.services.tailscale.port
];
networking.firewall.trustedInterfaces = [ "tailscale0" ];

まずファイアウォールは有効にしたうえで、必要な入口だけを番号で開けています。53317はLocalSend(スマホとPCの間でファイルを送り合うアプリ)が使う番号で、実際に確認したら今この瞬間もその番号で待ち受けていました。2049はNFSという昔からあるファイル共有の番号です。

面白いのは4番目で、config.services.tailscale.port と設定ファイル自身を参照しています。ポート番号を数字で書き写すのではなく、「Tailscaleが使う番号を、そのときの設定から取ってこい」と書いてあるわけです。設定ファイルがプログラミング言語である利点が、いちばん素直に出ている行だと思います。数字を2箇所に書いてズレる、という事故が構造的に起きません

services.tailscale.enable = true;
services.tailscale.extraUpFlags = [ "--ssh" "--operator=takeshi" ];

Tailscaleは、自分の持ち物同士だけをつなぐ小さな専用線のようなサービスです。ルーターのポートを外向けに開けなくても、自宅のPCとノートPCとスマホが同じネットワークにいるかのように通信できます。--ssh はTailscale経由のSSH接続を許可する指定、--operator=takeshi は管理者権限なしで自分のユーザーからTailscaleを操作できるようにする指定です。

trustedInterfaces = [ "tailscale0" ] が効いていて、Tailscale経由で入ってくる通信については、ファイアウォールの番号チェックを免除しています。「外の世界には固く閉じるが、自分の持ち物同士は素通し」という設計です。

services.openssh.enable = true;
services.openssh.settings.Macs = [
  "hmac-sha2-512-etm@openssh.com" "hmac-sha2-256-etm@openssh.com"
  "umac-128-etm@openssh.com" "hmac-sha2-512" "hmac-sha2-256"
];

SSHは有効にしたうえで、通信の改ざん検出に使う方式を、強いものだけに絞って明示しています。標準のままでも弱い方式はあらかた外されていますが、こうして書いておくと何を許しているのかが設定ファイルを読むだけで分かります。ここは総務職として社内システムの設定を眺めてきた経験が効いていて、「安全側の既定に頼る」より「明示して記録に残す」ほうが、後から人に説明できると思っています。

programs.localsend.enable = true;
programs.localsend.openFirewall = true;
programs.kdeconnect.enable = true;
systemd.services.ModemManager.enable = false;
systemd.services.NetworkManager-wait-online.enable = true;

LocalSendはAirDropのような近距離ファイル送信、KDE Connectはスマホとの通知連携やリモート操作です。この2つがあるおかげで、Pixel 10aとこのPCの行き来はかなり快適になりました。

ModemManager は携帯回線のモデムを扱うサービスで、このデスクトップには存在しないので明示的に切っています。NetworkManager-wait-online は逆に、ネットワークがつながるまで起動処理を待たせる指定です。起動直後にネットワークを必要とするサービスが空振りするのを防ぎます。


仮想化の層——1台の中に別のPCを立てる

「これはWindowsでしか動きません」。仕事でも趣味でも、年に何度かこの壁にぶつかります。

そのたびに別のPCを引っ張り出すのは面倒なので、この機体の中に仮想のPCを立てられるようにしてあります。

virtualisation.libvirtd.enable = true;
virtualisation.libvirtd.qemu.package = pkgs.qemu_kvm;
virtualisation.libvirtd.qemu.swtpm.enable = true;
virtualisation.libvirtd.qemu.vhostUserPackages = [ pkgs.virtiofsd ];
virtualisation.spiceUSBRedirection.enable = true;
programs.virt-manager.enable = true;

libvirtd と qemu_kvm が仮想マシンの本体、virt-manager がそれを操作する画面です。さきほど hardware-configuration.nix にあった kvm-amd が、ここで効いてきます。

swtpm が地味に重要で、これはTPM 2.0というセキュリティチップをソフトウェアで再現する仕組みです。Windows 11はTPM 2.0を要求するので、これが無いと仮想マシンにWindows 11をインストールできません。virtiofsd はホストとゲストでフォルダを高速に共有する仕組み、spiceUSBRedirection はUSB機器を仮想マシン側へ差し込む機能です。

systemd.tmpfiles.rules = [
  "d /usr/share/OVMF 0755 root root - -"
  ("L+ /usr/share/OVMF/OVMF_CODE_4M.fd - - - - " + pkgs.OVMF.firmware)
  ("L+ /usr/share/OVMF/OVMF_CODE.fd - - - - " + pkgs.OVMF.firmware)
];

これはNixOSの特殊さが牙を剥く場面への対処です。

NixOSでは、あらゆるプログラムが /nix/store の中の長いハッシュ付きのパスに置かれます。/usr/share/ のような伝統的な場所には、基本的に何もありません。ところが仮想マシンの管理ソフトは、UEFIファームウェア(OVMF)が /usr/share/OVMF/ にある前提で探しに行きます

そこでこの3行は、/usr/share/OVMF というフォルダを作り、そこから実体へのショートカットを張っています。L+ は「既にあっても上書きして張り直す」という意味です。他のディストリビューションの常識と、NixOSの流儀の橋渡しをしているわけで、こういう「翻訳」が必要になるのがNixOSを使う上での現実的なコストになります。

virtualisation.docker.enable = true;
virtualisation.docker.daemon.settings.default-address-pools = [ { base = "172.30.0.0/16"; size = 24; } ];
virtualisation.docker.daemon.settings.experimental = true;
virtualisation.virtualbox.host.enable = true;
virtualisation.virtualbox.host.enableExtensionPack = true;

Dockerの default-address-pools は、コンテナ同士をつなぐ内部ネットワークに使うアドレスの範囲を指定するものです。標準の範囲をそのまま使うと、勤め先のVPNや自宅のルーターと番号がぶつかって「Dockerを起動した瞬間に社内システムへ届かなくなる」という事故が起きます。あらかじめ使っていない範囲へ寄せておく、という予防策です。

VirtualBoxも別途入れていて、enableExtensionPack でUSB 3.0対応などを足しています。libvirtとVirtualBoxを両方入れているのは、扱いたい仮想マシンのイメージ形式が違う場面があるためです。

programs.nix-ld も、この流れで説明しておきます。

programs.nix-ld.enable = true;

これは、NixOS向けにビルドされていないプログラムを動かすための救済措置です。VS Codeの拡張機能が裏で落としてくるツールなど、「普通のLinux」を前提としたバイナリはそのままでは動きません。nix-ldは、そうしたプログラムが探しに行く場所に必要な部品を用意してくれます。これを知らないと「NixOSでは動かない」と結論して諦めがちな場面が、かなり救われます


音と映像の層

「Linux 音が出ない」で検索した回数は、たぶん日本語入力の次に多いと思います。

原因の大半は、同じ役目の仕組みを2つ同時に動かしてしまっていることです。だからこの層は、有効にする行と同じくらい「切る行」が重要になります。

services.pipewire.enable = true;
services.pipewire.alsa.enable = true;
services.pipewire.alsa.support32Bit = true;
services.pipewire.pulse.enable = true;
services.pulseaudio.enable = false;
security.rtkit.enable = true;

PipeWireは、Linuxの音と映像の交通整理を担う比較的新しい仕組みで、従来のPulseAudioを置き換えます。だから pulseaudio.enable = false で古いほうを明示的に切っています。この2つを同時に有効にすると音が出なくなる、というのがよくある事故です。

alsa.enable と pulse.enable は、古い方式を前提に作られたアプリに対して、その方式のふりをして受け付ける互換の窓口になります。support32Bit は32ビットのアプリ向けで、これが無いと古いゲームだけ音が出ないという状態になります。

security.rtkit は、PipeWireが音の処理に高い優先度を取るための許可です。これが無いと、CPUが忙しいときに音がプツプツ途切れます。

programs.obs-studio.enable = true;
environment.systemPackages = with pkgs; [
  obs-studio-plugins.wlrobs
  obs-studio-plugins.obs-backgroundremoval
  obs-studio-plugins.obs-pipewire-audio-capture
  obs-studio-plugins.obs-vaapi
  obs-studio-plugins.obs-gstreamer
  obs-studio-plugins.obs-vkcapture
];
programs.gpu-screen-recorder.enable = true;

OBSは配信・録画の定番ソフトです。プラグインでは、obs-vaapi が録画のエンコードをRadeonに任せるもの、obs-vkcapture がゲーム画面を効率よく取り込むもの、obs-pipewire-audio-capture がアプリごとに音を分けて録るもの、obs-backgroundremoval が背景を消すものになります。

gpu-screen-recorder は、さきほどの記事でも紹介した負荷の軽い録画ツールです。あの記事で挙げた4本のうち、Ghostty・GPU Screen Recorder・Librepodsの3本は今も現役で、auto-cpufreqだけが構成から外れた、という状態になっています。

映像編集まわりは davinci-resolve、kdePackages.kdenlive、handbrake、audacity、ffmpeg-full、vlc。3Dは blender と freecad を入れていて、さらに1行あります。

services.blendfarm.enable = true;
services.blendfarm.blenderPackage = pkgs.blender;

BlendFarmは、Blenderのレンダリングを複数のPCで分担するための仕組みです。1台で数時間かかる計算を、家にある機体で分けて回すためのものになります。


AIツールの層——設定ファイルに増えた、新しい住人

AI関連のパッケージだけで、7項目が並んでいます。

claude-code
opencode
opencode-desktop
opencode-claude-auth
claude-monitor
lmstudio
(pkgs.callPackage ./pkgs/claude-desktop.nix { })

382行を眺めていて、いちばん顔ぶれが新しいのがこの一角です。数年前のLinux環境の記事を書いていたころには、そもそも存在していなかった種類のソフトが並んでいます。

claude-code はターミナルで動くAIコーディング支援、claude-monitor は使用量を見るツール、opencode 系は同種の別実装になります。lmstudio はAIモデルを自分のPCの中だけで動かすためのアプリで、さきほどのROCm設定(HSA_OVERRIDE_GFX_VERSION)が効いてくる先がここです。ネットに投げたくない文章を扱うときに使っています。

そして、VS Codeの書き方が個人的にお気に入りです。

(vscode-with-extensions.override {
  vscodeExtensions = with vscode-extensions; [
    ms-ceintl.vscode-language-pack-ja
    bbenoist.nix
    anthropic.claude-code
  ];
})

エディタ本体だけでなく、拡張機能まで設定ファイルに書いてあります。日本語化パック、Nixの構文サポート、AI支援の3つ。これの何が嬉しいかというと、別のPCで同じ設定を使えば、拡張機能まで含めて完全に同じエディタが立ち上がることです。「新しいPCで作業を始めたら、拡張機能を入れ直すところから」という時間が消えます。

なお、claude-desktop だけは自前で包み直しています。理由は後半で説明します。


シェルと日々の道具

ターミナルを開いて最初に出てくるのは、bashではなくfishです。

programs.fish.enable = true;
users.users.takeshi.shell = pkgs.fish;
environment.variables.TERMINAL = "ghostty";

シェルにはfishを使っています。設定なしで補完や履歴の候補が出てくるのが理由で、スクリプトの互換性より日々の打ちやすさを優先しました。TERMINAL 変数は、「ターミナルを開け」と言われたアプリがGhosttyを開くようにするための指定です。

programs.tmux.enable = true;
programs.tmux.keyMode = "vi";
programs.tmux.baseIndex = 1;
programs.tmux.clock24 = true;
programs.tmux.historyLimit = 10000;
programs.tmux.terminal = "tmux-256color";
programs.tmux.plugins = with pkgs.tmuxPlugins; [ sensible resurrect continuum ];

tmuxは、1つのターミナルの中で画面を分割したり、作業を残したまま離席したりするための道具です。baseIndex = 1 は、画面の番号を0ではなく1から始める指定で、キーボードの数字の並びと一致させるためのものになります。

プラグインの3つが効いていて、resurrect が開いていた画面の構成を保存・復元、continuum がそれを自動で定期実行します。つまり、PCを再起動しても作業していた画面の並びがそのまま戻ってきます。

programs.gnupg.agent.enable = true;
programs.gnupg.agent.enableSSHSupport = true;
programs.gnome-disks.enable = true;
services.flatpak.enable = true;

gnupg.agent は暗号鍵の管理で、enableSSHSupport を付けるとSSHの鍵もこのエージェントに預けられます。鍵の管理場所が1つにまとまるのが利点です。

flatpak は、NixOSのカタログに無いアプリを別ルートで入れるための逃げ道として入れてあります。宣言的な管理からは外れるので多用はしませんが、どうしても必要なときの保険です。

そのほか、neovim(エディタ)、git と gh(バージョン管理とGitHub操作)、htop(プロセス監視)、fastfetch(構成の表示)、gparted(パーティション操作)、lshw(ハードウェア一覧)、pciutils、bash-language-server、そして rustc / cargo / gcc / clang / perl といった開発用の基本セットが並んでいます。libinput-gestures はタッチパッドのジェスチャー、remmina はリモートデスクトップ、libreoffice と obsidian と thunderbird は日常の文書・メモ・メールです。

Dropboxは、パッケージを入れるだけでなく常駐サービスとして定義しています。

systemd.user.services.dropbox = {
  description = "Dropbox Service";
  environment = { QT_QPA_PLATFORM = "xcb"; };
  serviceConfig = {
    ExecStart = pkgs.dropbox + "/bin/dropbox";
    Restart = "on-failure";
    RestartSec = "10s";
  };
  wantedBy = [ "graphical-session.target" ];
};

wantedBy = graphical-session.target は「デスクトップにログインしたら一緒に起動する」という意味、Restart = "on-failure" は落ちたら10秒後に自動で立ち上げ直すという指定です。QT_QPA_PLATFORM = "xcb" は、DropboxのアイコンをX11方式で描かせるための調整になります。「自動起動アプリ」の設定画面をポチポチする代わりに、こう書いてあるわけです。

Spotifyにも小細工があります。

(pkgs.symlinkJoin {
  name = "spotify";
  paths = [ pkgs.spotify ];
  buildInputs = [ pkgs.makeWrapper ];
  postBuild = ''wrapProgram $out/bin/spotify --add-flags "--no-sandbox"'';
})

これは既存のパッケージを包み直して、起動時のオプションを1つ足しているだけの記述です。パッケージそのものを作り直さずに、起動の仕方だけ変えられる。この「包み直し」の考え方が、次の章につながります。


掃除と保守——175GBの現実

/nix/store の容量を測ったら、175GBありました。

NixOSは「前の状態に戻れる」ことを売りにしていますが、その正体は過去のバージョンを消さずに全部取ってあるからです。便利さの裏には、素直にディスク容量という代金がついてきます。

nix.gc.automatic = true;
nix.gc.dates = "weekly";
nix.gc.options = "--delete-older-than 30d";
nix.optimise.automatic = true;
nix.optimise.dates = [ "weekly" ];
nix.settings.auto-optimise-store = false;

gc(ガベージコレクション)が週に1回、30日より古い世代を捨てる設定です。492という世代番号を見て「そんなに残しているのか」と思われたかもしれませんが、番号は増え続けても実体は30日ぶんしか残っていません。

optimise は、中身がまったく同じファイルを1つにまとめて、残りをショートカットに置き換える作業です。バージョン違いのソフト同士でも共通のファイルは多いので、これがけっこう効きます。

面白いのが最後の auto-optimise-store = false で、これは「保存するたびに毎回まとめる」機能をオフにしているという意味になります。毎回やると書き込みのたびに待たされるので、まとめて週1回やるほうを選んだわけです。オンとオフが両方書いてあると混乱しますが、「常時オンをやめて、定期実行に寄せた」と読めば筋が通ります。

つまり、NixOSは容量を食うOSではなく、「捨てる指示を書き忘れると容量を食うOS」というのが正確なところです。

services.journald.extraConfig = ''
  SyncIntervalSec=1s
  SystemMaxUse=500M
  SystemMaxFileSize=50M
'';

ログの設定も入れています。SyncIntervalSec=1s は1秒ごとにログをディスクへ確実に書く指定で、これが効くのは「フリーズして強制電源断したとき」です。標準では書き込みをまとめるので、いちばん知りたい直前のログが消えているという悲しいことが起きます。SystemMaxUse=500M は、ログ全体で500MBを超えたら古いものから消すという上限です。


欲しいものが無いときは、自分で作る

「公式サポートはDebianとUbuntuのみ」。Claude Desktopの案内には、そう書いてあります。

NixOSのカタログは10万個以上のソフトを揃えていますが、それでも載っていないものはあります。そういうとき、NixOSでは自分で"包み直す"ことができます。僕の /etc/nixos/pkgs/ には、そうやって作った自前パッケージが2つ、合計269行ぶん入っています。

claude-desktop.nix——公式のDebian版を包み直す

やっていることは、こういう流れです。

src = fetchurl {
  url = "https://downloads.claude.ai/…/claude-desktop_${finalAttrs.version}_amd64.deb";
  hash = "sha256-mcS89eP30OxEpJ+/JNfWWfLqRuKcfsYcd8cphSL1fnY=";
};

公式が配っているDebian用のファイルを、そのまま取ってくるところから始まります。hash は取得したファイルの指紋で、中身が1バイトでも違えばビルドが止まります。配布元が差し替わったことに気づかず古い前提で動き続ける、という事故が起きません。

問題はここからで、Debian用のプログラムは /usr/lib/ のような伝統的な場所にライブラリがある前提で作られています。NixOSにはその場所がありません。そこで、プログラムが必要とするライブラリを40個以上列挙して、/nix/store の中の正しい場所へつなぎ直しています。引っ越し先の間取りに合わせて、家具の配線をやり直すような作業です。

ファイルの中には、僕がハマった跡がコメントとして残っています。ラッパーの種類を変えないと環境変数が展開されないまま渡ってしまう、という趣旨のメモです。こういう「なぜこう書いたか」がファイルの中に残っているのが、設定をコードで管理することの本当の価値だと思っています。半年後の僕は、確実に理由を忘れています。

更新手順もコメントに書いてあります。最新版の番号とハッシュを調べて、2箇所を書き換えるだけ。実際、git の履歴には claude-desktop: 1.24012.9 -> 1.24012.11 という更新が残っています。

qtscrcpy.nix——真っ黒な画面の原因を、バイト単位で追う

こちらのほうが、僕には楽しい経験でした。QtScrcpyはPixel 10aの画面をPCに映して操作するためのソフトなのですが、カタログ版を入れると画面が真っ黒のまま何も映りません。

原因を追うために、スマホとPCの間で流れているデータの先頭を実際に覗いてみました。ファイルにその実測結果を残してあります。

server 3.3.3 (77バイト):
  [0] 予備 / [1..64] 端末名 / [65..68] "h264" / [69..72] 幅 / [73..76] 高さ
server 4.0   (81バイト):
  [0] 予備 / [1..64] 端末名 / [65..68] "h264"
  / [69..72] 0x80000000 ← 4.0で新規追加 / [73..76] 幅 / [77..80] 高さ

新しい側は途中に4バイトの項目が挿し込まれていて、古い側のソフトはそれを画面の横幅だと読み違えていたわけです。幅が「0x80000000」という現実にはありえない数字になり、映像の準備が失敗して真っ黒になる。バージョン違いの部品を組み合わせた結果の、寸法の読み間違いでした。

分かってしまえば対処は簡単で、ソフトが本来想定しているバージョンの相方を公式配布から取ってきて、組み合わせを揃えた自前版を作りました。それが今も動いています。

中学から高校のころ、僕はPSPにカスタムファームウェアを入れて、レトロゲームのエミュレータを動かすのに夢中でした。メーカーが決めた範囲の外側に手を伸ばして、動かなかったものを動かす。あのときの感触と、この作業はまったく同じものです。僕がNixOSから離れられない理由は、たぶん機能の便利さより、こちら側にあります。


設定ごと、GitHubに置いている

/etc/nixos の中で git log を叩くと、12個のコミットが並びます。

b2a1f0e Raise vm.swappiness to 100 for zram
abf6901 Raise zramSwap priority to 100
375a63e Enable zram swap and zram-backed /tmp
b637844 claude-desktop: 1.24012.9 -> 1.24012.11
…

設定ファイルがテキストなので、そのままバージョン管理できます。しかも僕はこれをGitHubのプライベートリポジトリに置いているので、PCのOS設定そのものがクラウドにバックアップされている状態です。

これがどれくらい強いかというと、もしこのPCが今日壊れても、新しい機体にNixOSを最小構成で入れて、このリポジトリを持ってきて、nixos-rebuild switch を1回叩けば、ほぼ元通りになります。日本語入力もゲーム環境もAIツールも、全部この493行の中にあるからです(ディスクのUUIDを書いた hardware-configuration.nix だけは機体ごとに作り直しになります)。

.gitignore には2つだけ書いてあります。

.claude/settings.local.json
result
result-*

上はマシン固有の許可リストなので共有しない、下は nix build が作る一時的なショートカットなので追跡しない、という判断です。

そして世代番号とコミット数の差が、この運用の性格をよく表しています。世代は492、コミットは12。つまり僕は普段、生成させた設定でどんどん試して、「これは残す」と決めたものだけを記録に残しているわけです。試行の回数と、記録に値する変更の回数は、まったく別の数字でいい。


正直に書く、5つの欠点

175GBという数字を、もう一度出します。

ここまで良いことを書いてきたので、実際に困っている点も同じ分量で書きます。

① 容量を食う。175GBという数字がすべてです。掃除の設定を書き忘れると、これは際限なく増えます。

② 書いても、叩くまで何も起きない。zramの話がまさにそれでした。設定を直して、直ったつもりで数十分過ごしてから、rebuildを忘れていたと気づく。いまだに、たまにやります。

③ 情報が少なく、エラーが読みにくい。NixOSの設定は独自のプログラミング言語で書かれていて、書き間違えると数十行のエラーが吐き出されます。UbuntuやMintに比べれば、日本語の情報量は桁で違います。もっとも僕は最近、この読解を生成AIにかなり手伝ってもらっていて、そこがLinuxを始めたころとはいちばん変わったところかもしれません。

④ 他のディストリビューションの常識が通じない。OVMFのショートカットを手で張った話や、nix-ldを入れないと市販のバイナリが動かない話がそれです。「Linuxでの一般的な手順」がそのまま使えない場面は、確実にあります。

⑤ 設定が全部見えるぶん、放置した行も全部見える。明朝体にゴシックを指定したままの行の話をしましたが、他にもあります。たとえば services.qemuGuest や services.spice-vdagentd は、このPC自身が仮想マシンとして動くときに効く設定です。母艦として使っている今の構成では、出番があるはずのない行になります。

こういう行が残るのは、たぶんNixOSの欠点ではなく、382行という規模になった設定を人間が棚卸ししていないだけの話です。ただ、少なくとも「棚卸しすべき行がどこにあるか」がファイルを開けば分かる、という点は普通のOSより恵まれています。Windowsの設定画面には、5年前に付けたチェックの一覧なんて出てきません

Linuxそのものの不便さについては、下記に本音を書いています。


向いてない人・向いてる人

初めてメインPCにLinuxを入れた週、僕がやったことは日本語入力を直すことだけでした。

その経験から、先に向いていない人を正直に書きます。

  • PCを道具として使いたいだけの人。設定を書く工程が最初に挟まる時点で、ふつうは損です

  • 今日インストールして明日から本気で使いたい人。最初の1週間は、たぶん日本語入力と格闘して終わります

  • 検索してすぐ答えが出ないと不安な人。UbuntuやMintに比べると、日本語の記事は桁で違います

  • ディスク容量に余裕がない人。100GB程度の空きでは、すぐ苦しくなります

そのうえで、向いている人です。

  • 同じ環境をもう1台にそのまま作りたい人。ノートPCとデスクトップを揃えたい人に、これ以上の道具を僕は知りません

  • 設定をいじって壊すのが好きな人。起動メニューから前の世代を選べば戻れるので、壊す勇気の値段がとても安いです

  • 「なぜこのPCはこう動くのか」を全部説明できる状態が気持ちいい人。僕の環境の理由は、いま読んでいただいた通り、全部ファイルに書いてあります

  • AIに設定を手伝わせたい人。設定がテキスト1枚に集約されているので、「この症状を直したい」と相談する相手がAIでも、話が早いです


おわりに

昨夜の492個目の世代は、今朝もそのまま動いています。書き足したzramは、次にrebuildを叩いた瞬間に493個目として世界に出てきます。

492回。数字だけ見ると、ずいぶん落ち着かないPCに見えるかもしれません。でも実際は逆で、いつでも作り直せると分かっているから、僕はこのPCに対してずいぶん落ち着いていられるのだと思います。壊れたら戻せばいい。移りたくなったら、493行を持っていけばいい。

この記事を書くために、僕は自分の設定を1行ずつ読み直しました。そうしたら、明朝体の指定を間違えたままの行や、もう出番のない仮想マシン用の行が出てきました。自分の道具の中身を、他人に説明できる形で読み返す機会というのは、なかなかありません。

設定ファイルというのは、PCの取扱説明書ではなく、その人が道具にどう向き合ってきたかの記録なのだと思います。あなたのPCが今そう動いている理由は、いま誰かに説明できる状態でしょうか。

関連記事:

ここまで読んでくださって、ありがとうございました。この記事が面白かったら、スキとフォローをいただけると、次の世代を作る励みになります。


補足:この記事の数値と前提について

  • 世代番号(492)・/nix/store の容量(175GB)・GPUの識別名(gfx1101)・稼働中のカーネル(7.1.6)・搭載メモリ(62GiB表示)は、いずれも2026年8月6日時点の実機で確認した値です

  • 世代の数は現在のマシンにNixOSを入れて以降の累計で、Linux自体の使用歴(4年)とは別の数字です。system.stateVersion = "25.05" から、この機体の導入は2025年5月リリース版以降になります

  • 設定ファイルの行数は、configuration.nix 37行/flake.nix 14行/hardware-configuration.nix 60行/generated.nix 382行/pkgs/ 269行(同日時点)

  • zramの設定は同日に追記したもので、記事執筆時点では未適用です(zramctl にデバイスが無いことを確認済み。本文中に明記のとおり)

  • 8080番ポートを開けている理由は、この記事のために設定を読み返した時点で僕自身も特定できませんでした。用途を裏取りできない設定について、それらしい理由を後付けするのはやめておきます

  • Claude Desktopの公式サポート対象がDebian/Ubuntuである点は、Anthropicの公式ドキュメント(Desktop for Linux の案内)に基づきます

  • 一部の項目には、生成時に「この版のオプション一覧では確認できなかったため、書かれたまま通した」という印が付いています(nixpkgs.config.allowUnfree やBluetoothの詳細設定など)。動いてはいますが、将来の版で書き方が変わる可能性がある行という位置づけです


設定ファイル全文

flake.nix

{
  description = "NixOS configuration for nixos";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { self, nixpkgs }: {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      modules = [
        { nixpkgs.hostPlatform = "x86_64-linux"; }
        ./configuration.nix
      ];
    };
  };
}

configuration.nix

{ config, lib, pkgs, ... }:

{
  imports = [
    ./hardware-configuration.nix
    ./generated.nix
  ];

  # Every definition below uses lib.mkDefault, so anything you also set in
  # nixgen takes precedence instead of colliding with it.

  # Boot loader (UEFI).
  boot.loader.systemd-boot.enable = lib.mkDefault true;
  boot.loader.efi.canTouchEfiVariables = lib.mkDefault true;

  networking.hostName = lib.mkDefault "nixos";
  networking.networkmanager.enable = lib.mkDefault true;

  users.users.takeshi = {
    isNormalUser = lib.mkDefault true;
    extraGroups = lib.mkDefault [ "wheel" "networkmanager" ];
  };

  # Needed before `nixos-rebuild --flake` will work.
  nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" ];

  # The release you first installed. Do not raise it to match a newer NixOS
  # unless you have read the release notes — it exists to keep stateful data
  # readable across upgrades.
  system.stateVersion = lib.mkDefault "25.05";
}

generated.nix

# Generated by nixgen — do not edit by hand.
# channel: nixos-26.05   generated: 2026-08-07 20:22
#
# Import it:  imports = [ ./generated.nix ];
# Or use it in place of your configuration.nix.
# Either way keep it beside ./hardware-configuration.nix.

{ config, lib, pkgs, ... }:

{
  boot.initrd.kernelModules = [ "amdgpu" ];
  boot.kernel.sysctl = {
    "vm.dirty_background_bytes" = 67108864;
    "vm.dirty_bytes" = 268435456;
    "vm.max_map_count" = 2147483642;
    "vm.swappiness" = 100;
    "vm.vfs_cache_pressure" = 50;
  };
  boot.kernelPackages = pkgs.linuxPackages_latest;
  boot.kernelParams = [
    "preempt=full"
    "amdgpu.ppfeaturemask=0xffffffff"
  ];
  boot.loader.efi.efiSysMountPoint = "/boot";
  boot.loader.systemd-boot.configurationLimit = 10;
  boot.tmp.useZram = true;
  environment.sessionVariables = {
    GTK_IM_MODULE = "fcitx";
    QT_IM_MODULE = "fcitx";
    XMODIFIERS = "@im=fcitx";
  };
  environment.systemPackages = with pkgs; [
    bash-language-server
    neovim
    wget
    git
    gh
    firewalld
    fastfetch
    htop
    flatpak-builder
    flatpak-xdg-utils
    boost
    ghostty
    libinput-gestures
    pciutils
    ((vscode-with-extensions.override { vscodeExtensions = (with vscode-extensions; [ ms-ceintl.vscode-language-pack-ja vscode-extensions.bbenoist.nix anthropic.claude-code ]); }))
    kora-icon-theme
    nordic
    whitesur-gtk-theme
    ristretto
    tumbler
    mousepad
    parole
    catfish
    gigolo
    orage
    xfburn
    xfce4-appfinder
    xfce4-cpugraph-plugin
    xfce4-dict
    xfce4-fsguard-plugin
    xfce4-genmon-plugin
    xfce4-panel
    xfce4-pulseaudio-plugin
    xfce4-systemload-plugin
    xfce4-weather-plugin
    xfce4-whiskermenu-plugin
    xfce4-xkb-plugin
    xfce4-screenshooter
    xfdashboard
    copyq
    plank
    kdePackages.ark
    bazaar
    firefox
    google-chrome
    thunderbird
    discord
    dropbox
    ((pkgs.symlinkJoin { buildInputs = [ pkgs.makeWrapper ]; name = "spotify"; paths = [ pkgs.spotify ]; postBuild = "wrapProgram $out/bin/spotify --add-flags \"--no-sandbox\"\n"; }))
    remmina
    libreoffice
    obsidian
    tradingview
    freecad
    wqy_zenhei
    ffmpeg-full
    vlc
    audacity
    handbrake
    davinci-resolve
    obs-studio-plugins.wlrobs
    obs-studio-plugins.obs-backgroundremoval
    obs-studio-plugins.obs-pipewire-audio-capture
    obs-studio-plugins.obs-vaapi
    obs-studio-plugins.obs-gstreamer
    obs-studio-plugins.obs-vkcapture
    gimp-with-plugins
    blender
    kdePackages.kdenlive
    steam-run
    protonup-qt
    lsfg-vk
    lsfg-vk-ui
    moonlight-qt
    mangohud
    goverlay
    mesa
    libvdpau-va-gl
    xf86-video-amdgpu
    amdgpu_top
    amdenc
    amdctl
    amd-blis
    amdfwtool
    amd-ucodegen
    microcode-amd
    radeon-profile
    radeontools
    rocmPackages.clr
    rocmPackages.rpp
    rocmPackages.rocminfo
    rustc
    cargo
    perl
    gcc
    clang
    gparted
    piper
    libratbag
    solaar
    librepods
    ((pkgs.callPackage ./pkgs/qtscrcpy.nix { }))
    lshw
    virt-viewer
    qemu_kvm
    vde2
    libguestfs
    lmstudio
    claude-code
    ((pkgs.callPackage ./pkgs/claude-desktop.nix { }))
    claude-monitor
    opencode
    opencode-desktop
    opencode-claude-auth
    gpu-screen-recorder-gtk
    networkmanagerapplet
  ];  # verbatim from your configuration.nix
  environment.variables = {
    HSA_OVERRIDE_GFX_VERSION = "11.0.1";
    RUSTICL_ENABLE = "radeonsi";
    TERMINAL = "ghostty";
  };
  fileSystems."/".options = [ "noatime" ];
  fileSystems."/mnt/deta1".options = [ "noatime" ];
  fileSystems."/mnt/deta2".options = [ "noatime" ];
  fileSystems."/mnt/steam".options = [ "noatime" ];
  fonts.fontDir.enable = true;
  fonts.fontconfig.defaultFonts.monospace = [ "Migu 1M" ];
  fonts.fontconfig.defaultFonts.sansSerif = [ "Noto Sans CJK JP" ];
  fonts.fontconfig.defaultFonts.serif = [ "Noto Sans CJK JP" ];
  fonts.fontconfig.enable = true;
  fonts.packages = with pkgs; [
    migu
    noto-fonts-cjk-sans
    noto-fonts-cjk-serif
    noto-fonts-color-emoji
    ipafont
  ];  # verbatim from your configuration.nix
  hardware.amdgpu.opencl.enable = true;
  hardware.bluetooth.enable = true;
  hardware.bluetooth.package = pkgs.bluez;
  hardware.bluetooth.powerOnBoot = true;
  hardware.bluetooth.settings.General.Experimental = true;  # verbatim — not an option in this release
  hardware.bluetooth.settings.General.FastConnectable = true;  # verbatim — not an option in this release
  hardware.bluetooth.settings.Policy.AutoEnable = true;  # verbatim — not an option in this release
  hardware.graphics.enable = true;
  hardware.graphics.extraPackages = with pkgs; [
    vulkan-tools
    rocmPackages.rocminfo
    rocmPackages.clr.icd
    libva
  ];  # verbatim from your configuration.nix
  hardware.logitech.wireless.enable = true;
  hardware.logitech.wireless.enableGraphical = true;
  hardware.rasdaemon.enable = true;
  hardware.rasdaemon.record = true;
  hardware.steam-hardware.enable = true;
  i18n.defaultLocale = "ja_JP.UTF-8";
  i18n.extraLocaleSettings = {
    LC_ADDRESS = "ja_JP.UTF-8";
    LC_IDENTIFICATION = "ja_JP.UTF-8";
    LC_MEASUREMENT = "ja_JP.UTF-8";
    LC_MONETARY = "ja_JP.UTF-8";
    LC_NAME = "ja_JP.UTF-8";
    LC_NUMERIC = "ja_JP.UTF-8";
    LC_PAPER = "ja_JP.UTF-8";
    LC_TELEPHONE = "ja_JP.UTF-8";
    LC_TIME = "ja_JP.UTF-8";
  };
  i18n.inputMethod.enable = true;
  i18n.inputMethod.fcitx5.addons = [
    pkgs.fcitx5-mozc
    pkgs.fcitx5-gtk
  ];
  i18n.inputMethod.fcitx5.waylandFrontend = false;
  i18n.inputMethod.type = "fcitx5";
  networking.firewall.allowedTCPPorts = [
    53317
    2049
    8080
    config.services.tailscale.port
  ];  # verbatim from your configuration.nix
  networking.firewall.enable = true;
  networking.firewall.trustedInterfaces = [ "tailscale0" ];
  networking.timeServers = [ "ntp.nict.jp" ];
  nix.gc.automatic = true;
  nix.gc.dates = "weekly";
  nix.gc.options = "--delete-older-than 30d";
  nix.optimise.automatic = true;
  nix.optimise.dates = [ "weekly" ];
  nix.settings = {
    experimental-features = [ "nix-command" "flakes" ];
  };
  nix.settings.auto-optimise-store = false;
  nixpkgs.config.allowUnfree = true;  # verbatim — not an option in this release
  powerManagement.cpuFreqGovernor = "performance";
  programs.fish.enable = true;
  programs.fuse.userAllowOther = true;
  programs.gamemode.enable = true;
  programs.gamemode.settings.general.inhibit_screensaver = 1;  # verbatim — not an option in this release
  programs.gamemode.settings.general.ioprio = 0;  # verbatim — not an option in this release
  programs.gamemode.settings.general.renice = 10;  # verbatim — not an option in this release
  programs.gamemode.settings.general.softrealtime = "auto";  # verbatim — not an option in this release
  programs.gamemode.settings.gpu.amd_performance_level = "high";  # verbatim — not an option in this release
  programs.gamemode.settings.gpu.apply_gpu_optimisations = "accept-responsibility";  # verbatim — not an option in this release
  programs.gamemode.settings.gpu.gpu_device = 1;  # verbatim — not an option in this release
  programs.gamescope.capSysNice = true;
  programs.gamescope.enable = true;
  programs.gnome-disks.enable = true;
  programs.gnupg.agent.enable = true;
  programs.gnupg.agent.enableSSHSupport = true;
  programs.gpu-screen-recorder.enable = true;
  programs.kdeconnect.enable = true;
  programs.localsend.enable = true;
  programs.localsend.openFirewall = true;
  programs.nix-ld.enable = true;
  programs.nm-applet.enable = true;
  programs.nm-applet.package = pkgs.networkmanagerapplet;
  programs.obs-studio.enable = true;
  programs.steam.enable = true;
  programs.steam.extraCompatPackages = [ pkgs.proton-ge-bin ];
  programs.steam.fontPackages = [ pkgs.migu ];
  programs.steam.localNetworkGameTransfers.openFirewall = true;
  programs.steam.package = pkgs.steam.override { extraEnv = { AMD_VULKAN_ICD = "RADV"; FONTCONFIG_PATH = "/etc/fonts"; GTK_IM_MODULE = "fcitx"; LANG = "ja_JP.UTF-8"; LANGUAGE = "ja_JP.UTF-8"; LC_ALL = "ja_JP.UTF-8"; QT_IM_MODULE = "fcitx"; XCURSOR_THEME = "Adwaita"; XMODIFIERS = "@im=fcitx"; }; extraPkgs = (pkgs: (with pkgs; [ migu noto-fonts-cjk-sans gnome-themes-extra adwaita-icon-theme mangohud gamemode gamescope fcitx5 fcitx5-gtk fcitx5-mozc kdePackages.fcitx5-qt ])); };  # verbatim from your configuration.nix
  programs.steam.protontricks.enable = true;
  programs.steam.remotePlay.openFirewall = true;
  programs.thunar.enable = true;
  programs.thunar.plugins = [
    pkgs.thunar-vcs-plugin
    pkgs.thunar-shares-plugin
    pkgs.thunar-archive-plugin
    pkgs.thunar-dropbox-plugin
    pkgs.thunar-media-tags-plugin
    pkgs.thunar-volman
  ];
  programs.tmux.baseIndex = 1;
  programs.tmux.clock24 = true;
  programs.tmux.enable = true;
  programs.tmux.historyLimit = 10000;
  programs.tmux.keyMode = "vi";
  programs.tmux.plugins = with pkgs.tmuxPlugins; [ sensible resurrect continuum ];  # verbatim from your configuration.nix
  programs.tmux.terminal = "tmux-256color";
  programs.virt-manager.enable = true;
  security.rtkit.enable = true;
  services.blendfarm.blenderPackage = pkgs.blender;
  services.blendfarm.enable = true;
  services.blueman.enable = true;
  services.displayManager.autoLogin.enable = true;
  services.displayManager.autoLogin.user = "takeshi";
  services.displayManager.defaultSession = "xfce";
  services.flatpak.enable = true;
  services.gvfs.enable = true;
  services.journald.extraConfig = ''
    SyncIntervalSec=1s
    SystemMaxUse=500M
    SystemMaxFileSize=50M
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
  '';
  services.lact.enable = true;
  services.openssh.enable = true;
  services.openssh.settings.Macs = [
    "hmac-sha2-512-etm@openssh.com"
    "hmac-sha2-256-etm@openssh.com"
    "umac-128-etm@openssh.com"
    "hmac-sha2-512"
    "hmac-sha2-256"
  ];
  services.pipewire.alsa.enable = true;
  services.pipewire.alsa.support32Bit = true;
  services.pipewire.enable = true;
  services.pipewire.pulse.enable = true;
  services.power-profiles-daemon.enable = false;
  services.pulseaudio.enable = false;
  services.ratbagd.enable = true;
  services.ratbagd.package = pkgs.libratbag;
  services.spice-autorandr.enable = true;
  services.sunshine.autoStart = true;
  services.sunshine.capSysAdmin = true;
  services.sunshine.enable = true;
  services.sunshine.openFirewall = true;
  services.sunshine.package = pkgs.sunshine;
  services.tailscale.enable = true;
  services.tailscale.extraUpFlags = [ "--ssh" "--operator=takeshi" ];
  services.tumbler.enable = true;
  services.xserver.desktopManager.xfce.enable = true;
  services.xserver.desktopManager.xterm.enable = false;
  services.xserver.displayManager.lightdm.enable = true;
  services.xserver.displayManager.sessionCommands = pkgs.plank + "/bin/plank &\n";  # verbatim from your configuration.nix
  services.xserver.enable = true;
  services.xserver.videoDrivers = [ "amdgpu" ];
  services.xserver.xkb.layout = "jp";
  services.xserver.xkb.model = "jp106";
  systemd.services.NetworkManager-wait-online.enable = true;
  systemd.tmpfiles.rules = [
    "d /usr/share/OVMF 0755 root root - -"
    (("L+ /usr/share/OVMF/OVMF_CODE_4M.fd - - - - " + pkgs.OVMF.firmware))
    (("L+ /usr/share/OVMF/OVMF_CODE.fd - - - - " + pkgs.OVMF.firmware))
  ];  # verbatim from your configuration.nix
  systemd.user.services.copyq.after = [
    "graphical-session.target"
  ];
  systemd.user.services.copyq.description = "CopyQ clipboard manager";
  systemd.user.services.copyq.partOf = [
    "graphical-session.target"
  ];
  systemd.user.services.copyq.serviceConfig = {
    ExecStart = pkgs.copyq + "/bin/copyq";
    Restart = "on-failure";
    RestartSec = "5s";
  };
  systemd.user.services.copyq.wantedBy = [
    "graphical-session.target"
  ];
  systemd.user.services.dropbox.description = "Dropbox Service";
  systemd.user.services.dropbox.environment = {
    QT_QPA_PLATFORM = "xcb";
  };
  systemd.user.services.dropbox.serviceConfig = {
    ExecStart = pkgs.dropbox + "/bin/dropbox";
    Restart = "on-failure";
    RestartSec = "10s";
  };
  systemd.user.services.dropbox.wantedBy = [
    "graphical-session.target"
  ];
  time.timeZone = "Asia/Tokyo";
  users.users.takeshi.description = "takeshi";
  users.users.takeshi.extraGroups = [
    "networkmanager"
    "wheel"
    "video"
    "docker"
    "render"
    "libvirtd"
    "kvm"
    "vboxusers"
  ];
  users.users.takeshi.isNormalUser = true;
  users.users.takeshi.shell = pkgs.fish;
  virtualisation.docker.daemon.settings.default-address-pools = [ ({ base = "172.30.0.0/16"; size = 24; }) ];  # verbatim — not an option in this release
  virtualisation.docker.daemon.settings.experimental = true;  # verbatim — not an option in this release
  virtualisation.docker.enable = true;
  virtualisation.libvirtd.enable = true;
  virtualisation.libvirtd.qemu.package = pkgs.qemu_kvm;
  virtualisation.libvirtd.qemu.swtpm.enable = true;
  virtualisation.libvirtd.qemu.vhostUserPackages = [ pkgs.virtiofsd ];
  virtualisation.spiceUSBRedirection.enable = true;
  virtualisation.virtualbox.host.enable = true;
  virtualisation.virtualbox.host.enableExtensionPack = true;
  xdg.mime.defaultApplications = {
    "inode/directory" = [ "thunar.desktop" ];
  };
  zramSwap.algorithm = "zstd";
  zramSwap.enable = true;
  zramSwap.memoryPercent = 30;
  zramSwap.priority = 100;
}

/etc/nixos/pkgs 配下の特殊ファイル

claude-desktop.nix

###############################################################################
# Claude Desktop (Linux beta) — Anthropic 公式 .deb を NixOS 向けに再パッケージ
#
# 公式サポートは Debian / Ubuntu のみ (https://code.claude.com/docs/en/desktop-linux)
# のため、apt リポジトリで配布されている公式ビルドをそのまま取得し、
# autoPatchelf 相当の処理で Nix store 上のライブラリへ解決させている。
#
# 更新手順:
#   1. 最新版を確認
#      curl -s https://downloads.claude.ai/claude-desktop/apt/stable/dists/stable/main/binary-amd64/Packages \
#        | grep -E '^(Version|SHA256):'
#   2. 下の version / hash を書き換える (hash は nix hash convert --hash-algo sha256 --to sri <SHA256>)
###############################################################################
{
  lib,
  stdenv,
  fetchurl,
  dpkg,
  makeShellWrapper,
  wrapGAppsHook3,

  alsa-lib,
  at-spi2-atk,
  at-spi2-core,
  atk,
  cairo,
  cups,
  curl,
  dbus,
  expat,
  fontconfig,
  freetype,
  gdk-pixbuf,
  glib,
  gtk3,
  libGL,
  libappindicator-gtk3,
  libcap_ng,
  libdrm,
  libgbm,
  libnotify,
  libpulseaudio,
  libseccomp,
  libsecret,
  libuuid,
  libxcb,
  libxkbcommon,
  nspr,
  nss,
  pango,
  pipewire,
  systemd,
  wayland,

  libx11,
  libxcomposite,
  libxcursor,
  libxdamage,
  libxext,
  libxfixes,
  libxi,
  libxkbfile,
  libxrandr,
  libxrender,
  libxscrnsaver,
  libxshmfence,
  libxtst,

  # 実行時に PATH から探されるもの
  xdg-utils,
  qemu_kvm,
}:

stdenv.mkDerivation (finalAttrs: {
  pname = "claude-desktop";
  version = "1.24012.11";

  src = fetchurl {
    url = "https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop/claude-desktop_${finalAttrs.version}_amd64.deb";
    hash = "sha256-mcS89eP30OxEpJ+/JNfWWfLqRuKcfsYcd8cphSL1fnY=";
  };

  nativeBuildInputs = [
    dpkg
    # NIXOS_OZONE_WL / WAYLAND_DISPLAY を --add-flags 内でシェル展開するため、
    # バイナリラッパ (makeCWrapper) ではなくシェルラッパを使う。
    # wrapGAppsHook3 は makeBinaryWrapper を伝播するので上書きしないと
    # そちらが勝ってしまい、"${NIXOS_OZONE_WL:+...}" が未展開のまま
    # Electron の引数として渡される。
    makeShellWrapper
    (wrapGAppsHook3.override { makeWrapper = makeShellWrapper; })
  ];

  buildInputs = [
    gtk3 # GSETTINGS_SCHEMAS_PATH のため
    glib
  ];

  # Electron 同梱バイナリは自前で patchelf する
  dontUnpack = true;
  dontBuild = true;
  dontPatchELF = true;
  dontWrapGApps = true; # ラッパーは自前で作り、gappsWrapperArgs だけ借りる

  rpath =
    lib.makeLibraryPath [
      alsa-lib
      at-spi2-atk
      at-spi2-core
      atk
      cairo
      cups
      curl
      dbus
      expat
      fontconfig
      freetype
      gdk-pixbuf
      glib
      gtk3
      libGL
      libappindicator-gtk3
      libcap_ng # 同梱 virtiofsd (Cowork VM) が必要とする
      libdrm
      libgbm
      libnotify
      libpulseaudio
      libseccomp # 同上
      libsecret
      libuuid
      libxcb
      libxkbcommon
      nspr
      nss
      pango
      pipewire
      stdenv.cc.cc
      systemd
      wayland

      libx11
      libxcomposite
      libxcursor
      libxdamage
      libxext
      libxfixes
      libxi
      libxkbfile
      libxrandr
      libxrender
      libxscrnsaver
      libxshmfence
      libxtst
    ]
    + ":${lib.getLib stdenv.cc.cc}/lib64";

  installPhase = ''
    runHook preInstall

    # .deb に setuid バイナリ (chrome-sandbox) が含まれるため dpkg -x は使えない
    dpkg --fsys-tarfile $src | tar --extract
    rm -rf usr/share/lintian usr/share/doc

    mkdir -p $out
    mv usr/* $out

    chmod -R g-w $out

    # 同梱バイナリ (Electron 本体 / virtiofsd / cowork-linux-helper /
    # chrome-native-host / *.node) を Nix store のライブラリに向ける
    for file in $(find $out -type f \( -perm /0111 -o -name \*.so\* -o -name \*.node \) ); do
      patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$file" 2>/dev/null || true
      patchelf --set-rpath "${finalAttrs.rpath}:$out/lib/claude-desktop" "$file" 2>/dev/null || true
    done

    # Nix store 上では setuid を付けられないので SUID sandbox ヘルパは使えない。
    # 削除して Electron を namespace sandbox にフォールバックさせる
    # (NixOS は非特権 user namespace が有効なので sandbox は維持される)。
    rm -f $out/lib/claude-desktop/chrome-sandbox

    # /usr/bin/claude-desktop の壊れた symlink を起動ラッパで置き換える
    rm -f $out/bin/claude-desktop
    makeWrapper $out/lib/claude-desktop/claude-desktop $out/bin/claude-desktop \
      "''${gappsWrapperArgs[@]}" \
      --prefix XDG_DATA_DIRS : "$GSETTINGS_SCHEMAS_PATH" \
      --suffix PATH : ${lib.makeBinPath [ xdg-utils qemu_kvm ]} \
      --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}"

    # .desktop の Exec を絶対パスへ (メインエントリと Desktop Action の両方)
    substituteInPlace $out/share/applications/com.anthropic.Claude.desktop \
      --replace-fail "Exec=claude-desktop" "Exec=$out/bin/claude-desktop"

    runHook postInstall
  '';

  meta = {
    description = "Desktop application for Claude.ai (official Linux beta build)";
    homepage = "https://claude.ai";
    downloadPage = "https://code.claude.com/docs/en/desktop-linux";
    sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
    license = lib.licenses.unfree;
    platforms = [ "x86_64-linux" ];
    mainProgram = "claude-desktop";
  };
})

qtsqcrcpy.nix

###############################################################################
# QtScrcpy — nixpkgs 版のサーバー非互換を修正したラッパー
#
# 【問題】
#   nixpkgs の qtscrcpy は scrcpy-server を nixpkgs の scrcpy パッケージから
#   流用し、さらに postPatch で serverVersion を scrcpy.version へ強制置換する。
#   現在 scrcpy が 4.0 のため、QtScrcpy 3.3.3 に scrcpy-server 4.0 が
#   組み合わされるが、両者はビデオヘッダーの形式が非互換。
#
#   実測したビデオソケットのヘッダー (max_size=720, Pixel 10a):
#     server 3.3.3 (77 bytes):
#       [0] dummy / [1..64] device name / [65..68] "h264"
#       / [69..72] width=320 / [73..76] height=720
#     server 4.0   (81 bytes):
#       [0] dummy / [1..64] device name / [65..68] "h264"
#       / [69..72] 0x80000000 ← 4.0 で新規追加 / [73..76] width / [77..80] height
#
#   QtScrcpy 3.3.3 は 3.x 形式で読むため width を 0x80000000 と誤読し、
#   デコーダの初期化に失敗して画面が一切表示されない。
#
# 【対処】
#   QtScrcpy 3.3.3 が本来対応する scrcpy-server v3.3.3 を公式リリースから取得し、
#   serverVersion の書き換えも行わない (上流のまま "3.3.3" を使う)。
#
# 更新手順:
#   nixpkgs の qtscrcpy が上がったら version を合わせた scrcpy-server を
#   下の serverVersion / hash に設定する。hash は次で取得:
#     nix-prefetch-url https://github.com/Genymobile/scrcpy/releases/download/v<VER>/scrcpy-server-v<VER>
###############################################################################
{
  lib,
  qtscrcpy,
  fetchurl,
  android-tools,
}:

let
  # QtScrcpy 3.3.3 が想定する scrcpy-server のバージョン
  serverVersion = "3.3.3";

  scrcpyServer = fetchurl {
    url = "https://github.com/Genymobile/scrcpy/releases/download/v${serverVersion}/scrcpy-server-v${serverVersion}";
    hash = "sha256-fnAyO6fyWWSd1KzOl6xP77roECssbZHi575hP9U1S+A=";
  };
in
qtscrcpy.overrideAttrs (_: {
  # 上流の postPatch から serverVersion の置換だけを取り除いたもの。
  # (sndcpy 側のパス埋め込みは nixpkgs と同じ内容を維持)
  postPatch = ''
    substituteInPlace QtScrcpy/audio/audiooutput.cpp \
      --replace-fail 'sndcpy.sh' "$out/share/qtscrcpy/sndcpy.sh"
    substituteInPlace QtScrcpy/sndcpy/sndcpy.sh \
      --replace-fail 'ADB=./adb' "ADB=${lib.getExe' android-tools "adb"}" \
      --replace-fail 'SNDCPY_APK=sndcpy.apk' "SNDCPY_APK=$out/share/qtscrcpy/sndcpy.apk"
  '';

  # scrcpy パッケージ同梱の 4.0 ではなく、対応バージョンのサーバーを指す
  preFixup = ''
    qtWrapperArgs+=(
      --set QTSCRCPY_ADB_PATH ${lib.getExe' android-tools "adb"}
      --set QTSCRCPY_SERVER_PATH ${scrcpyServer}
    )
  '';
})


いいなと思ったら応援しよう!

はたけっち|テック&ガジェット雑学 よろしければ応援お願いします! いただいたチップはクリエイターとしての活動費に使わせていただきます!