自宅のWindows 11機をUbuntu 26.04 LTSにクリーンインストールして、実験サーバに仕立て直した。RTX 2070(VRAM 8GB)が載っている。この記事は、その初日にやったこと全部の記録である。SSHの鍵設定から始まり、勝手に落ちるサーバ、Docker、RDP、そしてローカルLLMまで。
作業はほぼすべてMacのClaude Codeにやらせた。私がやったのは、パスワードを打つことと、画面のエラーを貼ることだけだ。読みどころはRDPの章で、「正しいパスワードを入れているのに弾かれる」現象を、サーバ側のログから追い詰めていく。
環境を先に固定しておく。
| 項目 | 内容 |
|---|---|
| サーバ | Ubuntu 26.04.1 LTS(resolute)、GNOME、OpenSSH 10.2 |
| GPU | NVIDIA GeForce RTX 2070 8GB、ドライバ 595.91、CUDA 13.2 |
| クライアント | macOS、Windows App 11.4.1(旧 Microsoft Remote Desktop) |
| RDPサーバ | gnome-remote-desktop 50.2、FreeRDP 3.31 |
| LLM | Ollama 0.34.2、qwen3:8b |
SSHをパスワードなしにする
最初の依頼は「ssh hal@192.168.10.250 でパスワードを求められないようにしたい」だった。
やることは決まっていて、Macの公開鍵をサーバの ~/.ssh/authorized_keys に入れるだけである。ところが確認してみると、鍵認証はすでに通っていた。Ubuntuのインストーラで鍵を取り込んでいたらしい。パスワードを聞かれていたのは、鍵が使われていないのではなく、ssh hal という短縮名で入ろうとして ~/.ssh/config にエントリが無かったからだ。
これを足せば終わりなのだが、ここで一つ事故った。Claude Codeが用意した追記コマンドを、私はサーバにSSHしたままのターミナルで実行してしまった。結果、サーバ側の ~/.ssh/config に Host hal が書かれ、Mac側は何も変わらない。
Claude Codeはこれを、Macで ssh -G hal を実行して見抜いた。-G はsshが最終的に採用する設定値を出力するオプションで、hostname hal user yoshiharu.ishii のままだったので「Mac側のconfigには入っていない」と即断できた。プロンプトが hal@hal-Pocraft-Server になっている貼り付けを見せた時点で確定である。ターミナルのプロンプトは意外と重要な情報源だ。
pingが通らないのにインターネットは見える
途中、ping 192.168.10.250 が全滅した。Macはインターネットに出られる。Windowsなら「ファイアウォールがICMPを落としているだけ」がまず疑われるが、Linuxはデフォルトでpingに応答する。しかもSSHのポート22も無応答。つまりファイアウォールではなく、そもそも届いていない。
原因はサスペンドだった。GNOMEデスクトップ入りのUbuntuは、ログイン画面(GDM)で誰もログインしないまま20分たつと、ユーザーの電源設定とは無関係にマシンをサスペンドする。サーバとして置いておくと、これに必ず刺される。
対処はsystemdのレベルでスリープ系ターゲットを封じるのが一番確実だ。GNOMEの設定を一つ一つ潰すより、根元を止める。
masked になっていれば、GDMだろうとGNOMEだろうと、どこから要求されてもスリープしない。再起動しても効く。
sudoとDocker
実験機なので sudo もパスワードなしにした。/etc/sudoers.d/ にファイルを一つ置く方式だと、本体の sudoers に触らずに済み、壊しても消せば戻る。
これ以降、Claude Codeは ssh hal 'sudo ...' で直接root作業ができるようになり、私の出番はほぼ無くなった。
Dockerは公式リポジトリから入れた。Ubuntu 26.04は新しいので公式aptリポジトリが対応しているか不安だったが、https://download.docker.com/linux/ubuntu/dists/resolute/Release が200を返したので問題なし。Docker Engine 29.8、Compose v5.5が入った。ついでに、素のUbuntu 26.04には curl が入っていない。確認スクリプトが curl: command not found で止まって気づいた。
RDP: 正しいパスワードが弾かれる
ここからが本題である。GUIも触りたいのでRDPを有効にした。UbuntuのGNOMEには gnome-remote-desktop が標準で入っていて、「Remote Login」モードなら誰もログインしていない状態でもRDPで接続し、ログイン画面から入れる。サーバ用途にはこちらが向く。
罠1: TLS証明書は自分で用意する
GUIの設定画面からRemote Loginを有効にすると証明書は自動生成されるのだが、grdctl --system rdp enable でCLIから有効にした場合は生成されない。サービスは起動するがログに RDP TLS certificate and key not yet configured properly と出て、3389番を待ち受けない。
自己署名で作って登録した。
罠2: 資格情報を設定してもデーモンが読まない
RDP用のユーザー名とパスワードを grdctl --system rdp set-credentials で登録して接続すると、Windows Appは 0x204(接続できない)を返した。grdctl --system status では Username: (hidden) と出ていて登録済みに見える。
ログを見ると一発だった。
デーモンは起動時に資格情報を読み、その後の変更を拾わない。systemctl restart gnome-remote-desktop で解決。地味だが、GUIから設定した場合はサービスの再起動が裏で走るので、CLI派だけが踏む罠である。
罠3: パスワードは合っているのに「資格情報が違う」
再起動後に接続すると、今度は Windows App が「The Credentials Did Not Work」と言ってパスワードを聞き直してきた。正しいパスワードを入れても同じ。ここで普通なら「パスワードを間違えて登録したのでは」と疑うところだが、Claude Codeはパスワードを扱わないので、登録したパスワードの文字数だけを確認した(9文字、想定どおり)。
サーバ側の journal を時系列で並べると、話が見えてくる。
まず Sending server redirection が出ている。これは認証が通った後に出るメッセージだ。つまりパスワードは合っている。
GNOMEのRemote Loginは二段構えになっている。最初にシステム常駐のデーモンが、登録した資格情報でクライアントを認証する。通ると、ログイン画面用のヘッドレスセッションを起動し、その中で動く別のデーモン(上のログの 27666)にクライアントを引き継ぐ。引き継ぎは「サーバーリダイレクト」という仕組みで、クライアントに使い捨ての資格情報を渡して「これでつなぎ直せ」と指示する。
問題はそのつなぎ直しで起きている。MIC verification failed はNTLM認証メッセージの整合性チェック失敗で、クライアントが渡された使い捨て資格情報を正しく使えていないことを示す。macOSのWindows Appは、この段階で資格情報を壊して送るという既知のバグがある。GNOME側の issue #215 に報告されていて、開発者の回答は「クライアントの挙動が完全に仕様外。Microsoftが直すべき」。
その後の Could not find user in SAM database は、私が「パスワードを聞き直された」ダイアログに hal のパスワードを入れた時のログだ。引き継ぎ先のデーモンは使い捨て資格情報しか知らないので、hal というユーザー自体を知らない。だから何を入れても弾かれる。エラーダイアログの文言は正しかったが、指しているものが違った、というのがこの謎の答えである。
回避策
コミュニティで知られている回避策は、.rdp ファイルにリダイレクト時のサーバー名の扱いを指示する一行を入れることだ。
このファイルをダブルクリックすればWindows Appが開く。Windows Appの画面から手で登録したエントリではこの設定が付かないので、ファイル経由で接続する必要がある。
(執筆時点の注記: この回避策はまだ試していない。結果は追記する。)
もう一つの逃げ道は、リダイレクトを使わない「Desktop Sharing」モード(本体でログイン中の画面をそのまま映す)に切り替えることだ。こちらは一段目の認証で終わるので、上のバグを踏まない。
RTX 2070でOllama
本命のローカルLLMである。Ollamaの公式インストーラはNVIDIAドライバを検出して勝手にCUDA版を入れてくれる。
ログに library=CUDA compute=7.5 name="NVIDIA GeForce RTX 2070" total="7.6 GiB" と出て認識完了。Macからも叩きたいので、systemdのoverrideでLAN側に開いた。
モデルは qwen3:8b(4bit量子化、5.6GB)。VRAM 8GBに丸ごと載る。ollama ps で 100% GPU を確認した。
GPUとCPUで同じ質問を投げる
「結構速いな、GPUのおかげ?」と聞かれたので、同じモデル・同じプロンプトで num_gpu: 0 を付けてCPUだけでも回した。
| 実行 | 生成速度 | 所要時間 |
|---|---|---|
| RTX 2070 | 62.0 トークン/秒 | 9.3 秒 |
| CPU のみ(8コア) | 4.5 トークン/秒 | 96.4 秒 |
約14倍。CPUの4.5トークン/秒は人が読む速度より遅く、実用にならない。
差の理由は帯域だ。LLMの生成は1トークンごとにモデルの重み全部(ここでは5.6GB)をメモリから読み直す処理で、計算量よりメモリ帯域で律速される。RTX 2070のGDDR6は448GB/s、DDR4のデュアルチャネルは40GB/s台なので、そのまま10倍以上の差になる。だからVRAMに収まるかどうかがすべてで、8GBからはみ出した分はCPU側の速度に引きずられる。8GBなら8Bの4bit量子化までが目安である。
なお、CPU実行したあとモデルがCPU側に載ったままになる。ollama stop qwen3:8b で一度降ろさないと、次の呼び出しも遅いままだった。
Open WebUIをDockerで
GUIで遊びたいので Open WebUI をDockerで立てた。Ollamaはホストのsystemdサービスなので、コンテナから host.docker.internal で見る。
起動直後は healthcheck が unhealthy を返すが、1〜2分待てば healthy になる。Macのブラウザから http://192.168.10.250:3000 を開き、モデル一覧に qwen3:8b が出れば完了。
まとめ
一日でやったことを並べると、SSH config、サスペンド無効化、sudo、Docker、RDP、Ollama、Open WebUI。この中で本当に手こずったのはRDPだけで、しかもサーバ側は正しく動いていて、クライアント側のバグだった。
ログを時系列で読むと「認証は通っている」「引き継ぎで落ちている」「ユーザーが再入力したパスワードは別のデーモンに届いている」と順番に分かる。エラーダイアログの文言だけ見ていたら、パスワードを何度も登録し直して終わっていたと思う。
Claude Codeとの分担は、私がパスワードを打ち、エラー画面を貼り、AIがログを引いて仮説を立てる、という形に落ち着いた。人はセンサー、AIはトレーサー。この構図は前回の記事から変わっていない。
ローカルLLMの使い道はこれから考える。常時起動でAPIが叩けてタダ、という条件は、クラウドのAPIでは気軽にできなかった「毎日・大量に・データを外に出さず」に回す類の仕事に向いている。候補は日次コミットの要約とか、ブログ原稿の下読みとか。試したらまた書く。


コメント