systemd-nspawnコンテナの駆け足ツアー
A whirlwind tour of systemd-nspawn containers (quantum5.ca)
要約
この記事は、Linuxの軽量コンテナ技術である`systemd-nspawn`について解説しています。Dockerとは異なり、`systemd-nspawn`は書き込み可能なルートファイルシステムを使用し、仮想マシンに近い感覚で利用できます。特に、ネストされた仮想化が利用できないVPSや、GPUなどのハードウェア共有が必要な場合に有用です。
全文翻訳
systemd-nspawnコンテナの駆け足ツアー
前回の年次アップデートで、Ollamaで実行しているセルフホストLLMやOpen WebUIをsystemd-nspawnコンテナで分離することについて話しましたが、その際にブログ記事にすると約束しました。しかし、そのブログ記事を執筆中に、Dockerではなくsystemd-nspawnを使用する理由についての脚注が、誤って独立したブログ記事になってしまいました。こちらが、systemd-nspawnに関する実際の記事です。
基本的に、systemd-nspawnは軽量なLinuxネームスペースベースのコンテナ技術であり、Dockerと大きくは異なりません。主な違いはイメージ管理にあります。Dockerfileを使ってイメージをビルドする方法を記述し、実行準備の整ったソフトウェアを含む読み取り専用の事前ビルド済みイメージを配布するのではなく、systemd-nspawnは通常、書き込み可能なルートファイルシステムで使用され、仮想マシンにより近い形で機能します。異なるLinuxディストリビューションでソフトウェアを実行するためにchrootを使用していたことを覚えている方にとっては、chrootを強化したものと説明することもできます。
私は特に以下のシナリオでsystemd-nspawnが有用だと感じています:
- VPS上で、ネストされた仮想化が利用できないためにフル仮想マシンを作成できない場合に、ある程度の分離でソフトウェアを実行したいとき1;
- GPUのようなハードウェアへのアクセスを共有する必要があるとき(私がLLMを
systemd-nspawnで実行する理由です); - 仮想化のオーバーヘッドを避けたいとき;
virtiofsに頼ることなく、ホストシステムのファイルに直接アクセスしたいとき;および- 通常はDockerを使用するが、できない、またはしたくないとき。理由については、脚注になったブログ記事を参照してください。
この記事では、systemd-nspawnコンテナの設定プロセスと、一般的なシナリオでの使用方法について説明します。
更新 (2025-05-19): より安全なAlpineコンテナ作成手順と、Alpine Linuxコンテナの設定に関する追加のガイダンスを追加しました。
chrootから始める
systemd-nspawnは、他のすべてのコンテナと同様に、ディレクトリ内に別のrootfsを必要とします。イメージをビルドするためのイメージマネージャーが付属していないため、新しいrootfsを作成するには、従来のUnixアプローチであるchrootを使用する必要があります。結局のところ、これは強化されたchrootなのです。
systemd-nspawnでは、これらのrootfsを/var/lib/machines/[hostname]の下に配置するのが一般的です。ここで、hostnameはコンテナのホスト名です。この演習では、OSの名前をホスト名として使用します。
各Linuxディストリビューションには独自のrootfs作成方法があるため、より一般的なものをいくつか見ていきます。私はDebianでこれらすべてをテストしていますが、他のディストリビューションでも同等のパッケージに置き換えることで、これらすべてを実行できない理由はありません。それでも、私はおそらくDebianについてより深く掘り下げるでしょう。なぜなら、私はそれに最も慣れているからです。
Debianファミリーのdebootstrap
debootstrapは、このブログで何度も取り上げられており、主にレスキューシステムから手動でDebianをインストールするようなクレイジーなことを行う際に使用しました。Debianベースのディストリビューション(UbuntuやDebian自体など)では、rootfsを作成するためのツールです。
DebianベースのOSで、sudo apt install debootstrapでインストールします。他のディストリビューションでもパッケージ化されている場合があります。または、Debianのリポジトリからダウンロードすることもできます。
$ curl -L https://salsa.debian.org/installer-team/debootstrap/-/archive/master/debootstrap-master.tar.gz | tar xz
...
$ cd debootstrap-master/
$ ./debootstrap
I: usage: [OPTION]... <suite> <target> [<mirror> [<script>]]
...
どちらの方法でdebootstrapをインストールした場合でも、/var/lib/machines/debianにrootfsを作成しましょう。
$ sudo debootstrap --include=systemd,dbus,locales,bash-completion,curl bookworm /var/lib/machines/debian
I: Target architecture can be executed
I: Retrieving InRelease
...
I: Base system installed successfully.
ここではbookwormをインストールしましたが、最新のDebianリリースに自由に置き換えてください。些細な問題を避けるために、systemdとdbusがインストールされていることを確認しました。また、aptでの奇妙な警告を避けるためにlocales、手でコマンドを入力する際に生活が嫌にならないようにbash-completion、後でファイルをダウンロードできるようにcurlも追加しました。これらのうちいくつかは後で追加の設定が必要になりますが、コンテナにブートしたらそれを行います。
dnfを使ったRHEL派生のインストール
まず、他のディストリビューション(例: Debian)で実行している場合は、dnfとrpmをインストールします。
$ sudo apt install dnf rpm
...
Setting up dnf (4.14.0-3+deb12u1) ...
Setting up rpm (4.18.0+dfsg-1+deb12u1) ...
...
$ sudo mkdir -p /var/lib/rpm
$ sudo rpm --initdb
次に、例としてFedora 41のchrootを作成します。
$ sudo mkdir -p /var/lib/machines/fedora/etc/dnf
$ sudo tee /var/lib/machines/fedora/etc/dnf/dnf.conf > /dev/null
[fedora]
name=Fedora $releasever - $basearch
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
gpgkey=https://getfedora.org/static/fedora.gpg
[updates]
name=Fedora $releasever - $basearch - Updates
metalink=https://mirrors.fedoraproject.org/metalink?repo=updates-released-f$releasever&arch=$basearch
gpgkey=https://getfedora.org/static/fedora.gpg
$ sudo dnf --releasever=41 --best --setopt=install_weak_deps=False --repo=fedora --repo=updates --installroot=/var/lib/machines/fedora install dnf fedora-release glibc glibc-langpack-en iproute iputils less ncurses passwd systemd systemd-networkd systemd-resolved util-linux vim-default-editor
...
Transaction Summary
================================================================================
Install 137 Packages
Total download size: 62 M
Installed size: 206 M
Is this ok [y/N]: y
...
Complete!
alpine-nspawn-installを使ったAlpine Linuxのインストール
Alpine Linuxは、その軽量さからDockerイメージで一般的に使用されているため、Alpine Linuxのchrootを作成してみることにしました。この種の作業の通常のツールはalpine-chroot-installですが、そのデフォルトの動作は、私たちのユースケースにとって恐ろしいものです。例えば:
- カレントディレクトリが
/home内にある場合、「互換性の理由」からそれをchrootにバインドしますが、警告なしです。rm -rfをchrootで実行した際に、私のホームディレクトリ全体を誤って削除するところでした… /dev、/proc、/sysもchrootにマウントします。/devの場合、コンテナのUID分離を利用するためにコンテナのchownを試みるたびに、ホストシステムのすべてのデバイスもchownされることになり、幸いにも再起動で修正できた大惨事につながります。
alpine-chroot-installの後で、以下のようなクレイジーなumountコマンドを実行してクリーンアップすることは可能です。
mount | grep /var/lib/machines/alpine | cut -d' ' -f3 | sort -r | while read -r path; do sudo umount "$path"; done
しかし、これは誰かの顔に爆発するのを待っている時限爆弾だと判断したため、代わりにスクリプトをフォークしてalpine-nspawn-installを作成しました。私のバージョンは、マウント、chrootヘルパースクリプト、qemuサポートなど、systemd-nspawnには意味のないalpine-chroot-installのクレイジーな「機能」をすべて削除しています2。その代わりに、デフォルトでより健全なパッケージリストをインストールします。以下のように使用できます。
$ wget https://raw.githubusercontent.com/quantum5/alpine-nspawn-install/refs/heads/master/alpine-nspawn-install
...
2025-05-19 22:23:27 (47.6 MB/s) - ‘alpine-nspawn-install’ saved [15897/15897]
$ sudo bash alpine-nspawn-install -d /var/lib/machines/alpine
...
---
Alpine container installation is complete.
これでAlpineがその評判に応えているか確認してみましょう。
$ sudo du -sh /var/lib/machines/debian
338M /var/lib/machines/debian
$ sudo du -sh /var/lib/machines/fedora
507M /var/lib/machines/fedora
$ sudo du -sh /var/lib/machines/alpine
16M /var/lib/machines/alpine
これは確かに印象的です… Alpineは他の2つと比較して1桁小さく、Fedoraはインストールしたパッケージリストでかなり肥大化しています。私はFedoraユーザーではなく、Arch Linux wikiのsystemd-nspawnガイドからパッケージリストをコピーしたので、肥大化したものをインストールしたからかどうかはわかりませんが、どれも pretty necessary のようです… dnfが主張していたサイズよりも確かに大きいので、確認したところ、dnfは/var/cache/dnfの下に260 MiBのものを生成していました。
Debianにも公平を期すために、より快適な体験のためにlocalesのような「肥大化」をいくつかインストールしました。「肥大化」を削除すると305 MiBになり、わずかに小さくなります。
systemd-nspawnでの初期ブート
まず、sudo apt install systemd-containerまたはお使いのディストリビューションの同等のコマンドでsystemd-nspawnをインストールします。
次に、以下のコマンドを実行してコンテナ内でrootシェルを取得できます。
$ sudo systemd-nspawn -D /var/lib/machines/[name] -M [name] --private-users=pick --private-users-ownership=chown
各引数の意味は以下の通りです。
-D(--directory): rootfsのディレクトリを指定します。-M(--machine): マシン名を指定します。これはコンテナ内のホスト名としても設定されます。--private-users=pick: UIDとGIDの名前空間を有効にし、ホストシステム上の未使用ブロックから65536のUIDとGIDの範囲を選択します。範囲は65536の倍数で、524288から1878982656の間です3。代替オプションには、yes(rootfsの所有者のUIDとGIDから始まる範囲を選択します。65536の倍数である必要があります)、no(UID名前空間を無効にします)、整数(ブロック65536の開始UIDとして機能します)、またはa:b形式の文字列(aは開始UID、コンテナに割り当てるUID数)があります。--private-users-ownership=chown: rootfsの所有権を、選択されたUIDとGIDに変更します。これにより、将来のブート時に--private-users=yesで同じUIDとGIDが使用され続けます。
例としてDebianをブートしてみましょう。
$ sudo systemd-nspawn -D /var/lib/machines/debian -M debian --private-users=pick --private-users-ownership=chown
Spawning container debian on /var/lib/machines/debian.
Press ^] three times within 1s to kill container.
Selected user namespace base 1766326272 and range 65536.
root@debian:~#
新しく作成されたDebianコンテナに入りました。これは単なるrootシェルであり、コンテナを長期的にデプロイするための適切な方法ではないことに注意してください。initプロセスすらありません。
Debian固有の設定ヒント
rootシェルをかなりの時間使用し、コマンド全体を入力するのを避けるためにオートコンプリートを使用したい場合は、以下のようにします。
root@debian:~# cp /etc/skel/.bashrc .
root@debian:~# . ~/.bashrc
コンテナを単一のアプリケーションを実行するためだけにしか使用しない場合、ユーザー管理やsudoの管理は非常に面倒になります。そのため、私はrootとして管理を行うことを選択し、機能する.bashrcがあると非常に役立ちます。これは技術的にはベストプラクティスではありませんが、単一アプリケーションコンテナ内でrootを必要としないコマンドを実質的に実行しないため、実際には違いはありません。
debootstrapはホストのホスト名も/etc/hostnameにコピーしました。これを修正しましょう。
root@debian:~# hostname > /etc/hostname
コンテナに戻る
何らかの理由でrootシェルを終了した場合、以下のようにして戻ることができます。
$ sudo systemd-nspawn -D /var/lib/machines/debian -M debian --private-users=yes
Spawning container debian on /var/lib/machines/debian.
Press ^] three times within 1s to kill container.
Selected user namespace base 1766326272 and range 65536.
root@debian:~#
コンテナの準備
このようにコンテナを実行するのは長期的な解決策ではありません。おそらく、バックグラウンドで適切に実行できるように設定したいでしょう。しかし、コンテナへのアクセス方法という問題が残ります。systemdベースのディストリビューションを使用している場合、いくつかの選択肢があります。
-
ホストマシンからコンテナにアクセスしたいだけであれば、何もする必要はありません。
machinectlで直接シェルアクセスを取得する方法を見ていきます。また、以下のようにすることもできます。 -
passwdでrootパスワードを設定すると、ターミナルからログインできるようになります。root@debian:~# passwd New password: Retype new password: passwd: password updated successfully -
openssh-serverをインストールし、SSHキーを設定してリモート接続します(後述するネットワーク設定も必要になります。そうしないと接続できません)。root@debian:~# apt install openssh-server ... Created symlink /etc/systemd/system/sshd.service → /lib/systemd/system/ssh.service. Created symlink /etc/systemd/system/multi-user.target.wants/ssh.service → /lib/systemd/system/ssh.service. Processing triggers for libc-bin (2.36-9+deb12u10) ... root@debian:~# curl -q https://ssh.qt.ax -o ~/.ssh/authorized_keys % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 243 100 243 0 0 3934 0 --:--:-- --:--:-- --:--:-- 3983現在rootシェルしかなく、適切なブートではないため、
systemd(または他のinitシステム)は実際には実行されていないことに注意してください。そのため、現時点ではsshdを起動できるとは期待しないでください。次のステップでコンテナを適切にブートします。 -
ユーザーを作成し、
sudoなどを設定してそのユーザーとしてログインします。これは読者への演習とします。
machinectl経由のコンソールおよびシェルアクセスには、コンテナ内でsystemdが実行されている必要があります。これは、Alpineのような非systemdディストリビューションを使用している場合には不可能です。Alpineを使用している場合は、openssh-serverのルートを使用する必要があります。
Alpine Linuxの注意点
alpine-chroot-installによって作成されるデフォルトのAlpine Linuxインストールは、chrootの使用専用に設定されており、機能するinitシステムさえ含まれていません。systemd-nspawnコンテナで適切に実行するには、initシステムであるopenrcとopenssh-serverをインストールする必要があります。
alpine:~# apk add openrc openssh-server
...
alpine:~# rc-update add sshd
alpine:~# rc-update add networking
私のalpine-nspawn-installスクリプトを使用している場合は、これらすべてが完了しています。行う必要があるのは、いくつかの基本的な設定だけです。
alpine:~# hostname > /etc/hostname
alpine:~# mkdir -p ~/.ssh
alpine:~# wget https://ssh.qt.ax -O ~/.ssh/authorized_keys
...
Alpineはネットワーク設定にifupdown-ngを使用しており、構文はDebianのものと互換性があるはずなので、以下の例をコピーするだけで済みます。ssh経由でログインできるようにするには、コンテナを開始する前にネットワーク設定を行う必要があります。
machinectlの使用
/etc/systemd/nspawn/[name].nspawnを作成して、machinectlでコンテナを管理できます。例: machinectl start [name]。/etc/systemd/nspawnディレクトリはデフォルトでは作成されないため、以下のようにする必要があるかもしれません。
$ sudo mkdir -p /etc/systemd/nspawn
.nspawnファイルはINI形式のテキストファイルで、systemd-nspawnのコマンドライン引数に相当するものが含まれています。基本的には、--private-users=yesに相当します。
[Exec]
PrivateUsers=yes
ネットワーク、リソース制限、ホストシステムからコンテナへのディレクトリのバインドマウントなど、他にも多くのオプションがあります。これらは後でカバーします。
Alpineのような非systemdディストリビューションの場合、コンテナが停止したときにinitが適切に終了することを保証するために、[Exec]セクションにKillSignal=SIGTERMを追加します。デフォルトのシグナルSIGRTMIN+3はsystemdのみが理解しますが、SIGTERMはより広く理解されます。
ここでは、コンテナをバックグラウンドで、完全なinitシステムで起動してみましょう。
$ sudo tee /etc/systemd/nspawn/debian.nspawn > /dev/null
[Exec]
PrivateUsers=yes
$ sudo machinectl start debian
$ sudo machinectl login debian
Connected to machine debian. Press ^] three times within 1s to exit session.
Debian GNU/Linux 12 debian pts/1
debian login: root
Password:
Linux debian 6.1.0-32-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.129-1 (2025-03-06) x86_64
The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law.
Last login: Wed Mar 19 22:59:57 EDT 2025 on pts/1
root@debian:~#
ログインできました!終了するには、machinectlが示唆するように、Ctrl+ ] を3回押してコンソールから切断します。
machinectl shellで直接シェルを取得することもできます。
$ sudo machinectl shell root@debian
Connected to machine debian. Press ^] three times within 1s to exit session.
root@debian:~#
このコンテナを停止するには、sudo machinectl stopを使用します。非systemdディストリビューションの場合は、sudo systemctl stop systemd-nspawn@[name]を使用します。
このコンテナを起動時に実行するには、単に以下を実行します。
$ sudo machinectl enable debian
元に戻すには、sudo machinectl disableを使用します。
machinectlは、基盤となるsystemdユニットsystemd-nspawn@[name].serviceを制御するだけです。これは後で重要になります。ユニットに追加オプションを設定するには、sudo systemd edit systemd-nspawn@[name].serviceを実行し、以下の行を追加します。
[Service]
# Note that a CPU quota > 100% means allowing more than one core,
# so this means two full cores are allowed:
CPUQuota=200%
MemoryMax=1G
MemorySwapMax=512M
# Limit I/O to 100 MB/s
IOReadBandwidthMax=100M
IOWriteBandwidthMax=100M
詳細については、systemdのリソース制御に関するドキュメントを参照してください。
結論
そして、これがsystemd-nspawnの駆け足ツアーでした。多くのことを扱いましたが、それは主に設定方法が数多くあったためです。systemd-nspawnを使用すると、完全に制御しながら、好みに合わせて設定を自由に構成できます。結局のところ、それは仮想マシンと同じです。好きなように何でもできます!
気に入った設定が見つかったら、あとは名前とIPアドレスの割り当てがあれば、最初のブート時にイメージを準備するためのいくつかのコマンドを実行するだけです。それ以外のすべては、.nspawnファイルを編集するだけで済みます。すべてを自動化するための独自のスクリプトを作成することさえできます。もちろん、それを行うことは読者への演習とします。
注釈
- ネストされた仮想化が利用可能であっても、通常は奇妙な制限や安定性の問題があります。本番環境での実行は通常推奨されません。↩
- 削除されたその他のクレイジーな機能には、
sudoを実行しているユーザーをchrootにコピーする方法、ホストシステム上のパッケージをランダムにaptでインストールする方法、コンテナ内のsudoルールを台無しにする方法などがあります。↩ - バイナリで考える人のために、これは実質的にコンテナ内のUID/GIDに下位16ビットを使用し、上位16ビットを
0x8から0x6fffの間の値に設定しています。↩ - VPSプロバイダーが、ユーザー自身の使用のために/64(またはそれ以上のブロック、ただしこれには/64しか必要ありません)をデフォルトで、または要求に応じて委譲しない場合、IPv6を適切に展開できなかったことになり、恥じるべきです。初心者向けに説明すると、IPv6はNATを使用せず、代わりにネットワーク上のすべてのホストにグローバルに一意なIPアドレスを割り当てます。このため、SLAACを容易にするために、すべてのL2ネットワークには/64ブロックを割り当てる必要があります。優れたISPはこのことを知っており、要求に応じてそのようなブロックを割り当てるか、あるいはより大きなブロック(/56または/48)を配布して、常にそれらに連絡する必要がないようにします。↩
- マッピングできないUIDはコンテナ内では
nobodyとして表示されますが、nobodyユーザーがそれらのファイルに実際にアクセスできるわけではありません。ホストから見た実際のUIDがアクセス権限のチェックに使用されます。rootはホスト上のUID xxにマッピングされることを忘れないでください。UID xxにアクセス権がない場合、コンテナ内のrootユーザーでさえアクセスが拒否されます。↩