個人用途で、ホストを極力汚さず、メンテナンス性と安全性を両立させるには、LXDを基盤としてVMやDocker(LXDコンテナ内)を運用する「ネスト構造」が便利です。そして、ホストOSの環境は「Timeshift」を導入しておけば、アップデートの失敗などによってOSが起動しなくなった時の復旧にも対応しやすくなります。
構成のイメージ
1. ホストOSの構成
- ファイルシステム: Btrfs
- 理由:
- Timeshift との相性が抜群で、ホストの設定(LXDのインストール状態など)をいつでもロールバックできる。
- ホスト側で透過圧縮(zstd)を効かせれば、LXDのイメージや仮想ディスクの容量を節約できる。
2. LXDのストレージプール構成
LXDのデフォルトプールには 「Btrfsバックエンド」 を推奨。
- バックエンド: Btrfs(ホストのBtrfs上にサブボリュームとして作成)
- 理由:
lxc copyやlxc snapshotが一瞬で終わる(CoWの恩恵)。- VM(仮想マシン) を作成した際、内部で自動的に
nocow設定が適用されるため、断片化のリスクが抑えられる。 - ZFSよりもメモリ消費が少なく、個人用PCの500GB SSDで運用するにはリソース管理が楽。
3. LXD内でのDocker(Nextcloud/Immich)運用
LXDコンテナの中でDockerを動かす場合、以下の設定で「ホストを汚さない&高速」を実現します。
- コンテナの種別: 特権なし(Unprivileged)コンテナ(セキュリティのため)
- Dockerストレージドライバ:
overlay2- LXDがBtrfsプールなら、コンテナ内のDockerはデフォルトで
vfs(激重)やbtrfsになろうとしますが、これをoverlay2に強制設定します。
- LXDがBtrfsプールなら、コンテナ内のDockerはデフォルトで
- データの逃がし先(NOCOWの適用):
- ホスト側に
sudo mkdir /var/lib/lxd-data && sudo chattr +C /var/lib/lxd-dataで NOCOW領域 を作成。 - NextcloudやImmichのDB用ディレクトリだけ、ホストのこの領域を
lxc config device addでコンテナにパススルー(ディスクマウント)する。 - 写真データは、NOCOWではない「通常のBtrfs領域」をマウントして、チェックサムによる保護を受ける。
- ホスト側に
最適な構成まとめ表
| 階層 | 種類 / 設定 | 役割 |
|---|---|---|
| ホストOS | Ubuntu (Btrfs) | Timeshiftで土台を保護 |
| LXDプール | Btrfs (loopバックではない実体) | 高速なクローンとスナップショット |
| LXDコンテナ | Ubuntuコンテナ | Dockerを動かす「器」 |
| Dockerドライバ | overlay2 | コンテナ内の動作を標準化・高速化 |
| DBデータ | HostのNOCOW領域をマウント | PostgreSQL等の断片化防止 |
| 写真データ | Hostの通常Btrfs領域をマウント | データの腐敗防止(チェックサム) |
各ボリュームの構成
Ubuntuをインストールする際、パーティション設定で Btrfsを選び、インストール直後にTimeshift を設定するという流れになります。ホストのUbuntuをBtrfsでインストールしTimeshiftで保護、サブボリュームとしてLXDプールを作成、そのサブボリュームとしてホストとLXD内の共通してアクセス出来るlxd-dataボリュームを作成。Ubuntu(ホスト)を壊しても、LXD内のコンテナや大事なデータ(Nextcloudの写真など)は無傷で残せます。
- サブボリュームの階層設計は、Btrfsのサブボリュームを「入れ子」にせず、並列(フラット)に配置するのが安全です。これにより、Timeshiftが @(ルート)を書き換えても他が巻き込まれません。
@ (ルート): / にマウント。Timeshiftの保護対象。
@home: /home にマウント。
@lxd_pool: /var/lib/lxd/storage-pools/default 等にマウント。(Timeshift対象外)
@lxd_data: /mnt/lxd-data 等にマウント。ホスト・LXD共通。(Timeshift対象外) - インストールと設定の手順
① Ubuntuのインストール インストーラーで「Btrfs」を選択してインストール。
② 追加サブボリュームの作成(インストール後) LXDを入れる前に、手動で独立したサブボリュームを作成します。
Ubuntuセットアップ
セットアップ時に手動パーティションにして、Btrfsを選択。マウントポイントは「/」にします。EFIパーティションなどは自動で作成されますsが、それだと順序が逆になるので、先に1GB程度のEFIパーティションを作成し、そのあとBtrfsパーティションを作成します。

なお、KubuntuやFedora、openSUSEなどが採用しているインストーラー(Calamaresなど)は、Btrfsの高度な機能を活用する設計になっており、インストール時にBtrfsを指定すれば自動で@や@homeが作成されますが、Ubuntuの現在のバージョンのインストーラでは@や@homeが自動作成されないので、セットアップ後に修正が必要になります。
Ubuntu起動後
Ubuntuが起動したら、ひとまずマウント状況を確認します。
mount | grep btrfs
Ubuntu24.04以降では自動で@、@homeを作成しないため、下記のように、/や/homeのままとなっているはずです。
/dev/vda2 on / type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=5,subvol=/) # subvol=/@のようになっていない。
ホームフォルダ内に下記スクリプトを保存しておきます。
nano btrfs_migrate.sh
#!/bin/bash
# ============================================================
# Btrfs サブボリューム移行スクリプト(環境自動取得版)
# 実行環境: ライブUSB環境のターミナル
# 構成想定: 1ドライブ / ブートパーティション + Btrfsパーティション の2構成
# EFI(UEFI)/ BIOS(Legacy)両対応
# 使い方 : sudo bash btrfs_migrate.sh
# ============================================================
set -euo pipefail
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
step() { echo -e "\n${CYAN}=== $* ===${NC}"; }
# root確認
[[ $EUID -ne 0 ]] && error "sudo で実行してください"
# ============================================================
# 起動モード判定(EFI / BIOS)
# ============================================================
step "環境を自動検出"
if [[ -d /sys/firmware/efi ]]; then
BOOT_MODE="EFI"
else
BOOT_MODE="BIOS"
fi
info "起動モード: $BOOT_MODE"
# --- Btrfsパーティション検出 ---
BTRFS_PART=$(lsblk -lnpo NAME,FSTYPE | awk '$2=="btrfs"{print $1}' | head -1)
[[ -n "$BTRFS_PART" ]] || error "Btrfsパーティションが見つかりませんでした"
# --- ディスク本体(パーティション番号を除いたもの)---
DISK=$(lsblk -lnpo PKNAME "$BTRFS_PART" | head -1)
[[ -n "$DISK" ]] || DISK=$(echo "$BTRFS_PART" | sed 's/p\?[0-9]*$//')
DISK="/dev/${DISK##*/}"
# --- EFI / BIOS それぞれのブートパーティション検出 ---
EFI_PART=""
BIOS_BOOT_PART=""
if [[ "$BOOT_MODE" == "EFI" ]]; then
# EFI System Partition: PARTTYPE UUID で検出、なければ vfat で代替
EFI_PART=$(lsblk -lnpo NAME,PARTTYPE | \
awk 'tolower($2) == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" {print $1}' | head -1)
if [[ -z "$EFI_PART" ]]; then
EFI_PART=$(lsblk -lnpo NAME,FSTYPE | awk '$2=="vfat"{print $1}' | head -1)
fi
[[ -n "$EFI_PART" ]] || error "EFIパーティションが見つかりませんでした(vfat / EFI System)"
else
# BIOS Boot Partition: PARTTYPE UUID 21686148... で検出
BIOS_BOOT_PART=$(lsblk -lnpo NAME,PARTTYPE | \
awk 'tolower($2) == "21686148-6449-6e6f-744e-656564454649" {print $1}' | head -1)
# 見つからなければ警告のみ(MBRディスクでは不要な場合もある)
if [[ -z "$BIOS_BOOT_PART" ]]; then
warn "BIOS Bootパーティション(21686148...)が見つかりません。MBRディスクの場合は問題ありません。"
else
info "BIOS Bootパーティション: $BIOS_BOOT_PART"
fi
fi
# --- 結果表示 ---
echo ""
echo " 検出結果:"
echo " ┌──────────────────────────────────────────┐"
printf " │ 起動モード : %-21s│\n" "$BOOT_MODE"
printf " │ Btrfsパーティション : %-21s│\n" "$BTRFS_PART"
if [[ "$BOOT_MODE" == "EFI" ]]; then
printf " │ EFIパーティション : %-21s│\n" "$EFI_PART"
else
printf " │ BIOS Bootパーティション: %-18s│\n" "${BIOS_BOOT_PART:-(なし / MBR)}"
fi
printf " │ ディスク : %-21s│\n" "$DISK"
echo " └──────────────────────────────────────────┘"
echo ""
info "ディスク構成(参考):"
lsblk -o NAME,SIZE,FSTYPE,PARTTYPE,MOUNTPOINT "$DISK"
echo ""
warn "上記の検出結果で続行しますか? [yes/N]"
read -r answer
[[ "$answer" == "yes" ]] || { info "中止しました。"; exit 0; }
# ============================================================
# ステップ1: マウント
# ============================================================
step "Step 1: Btrfsパーティションをマウント"
# 実機ではライブUSB起動時に対象パーティションが自動マウントされている場合がある
# /mnt と /boot/efi 以外のマウントポイントは除外し、安全なものだけ解除
info "対象パーティションの既存マウントを解除"
for mp in $(findmnt -rno TARGET --source "$BTRFS_PART" 2>/dev/null | sort -r); do
# / は絶対に触らない
[[ "$mp" == "/" ]] && continue
info " 既存マウント解除: $mp"
umount "$mp" 2>/dev/null || umount -l "$mp" 2>/dev/null || true
done
if [[ -n "${EFI_PART:-}" ]]; then
for mp in $(findmnt -rno TARGET --source "$EFI_PART" 2>/dev/null | sort -r); do
[[ "$mp" == "/" ]] && continue
info " 既存マウント解除 (EFI): $mp"
umount "$mp" 2>/dev/null || umount -l "$mp" 2>/dev/null || true
done
fi
mkdir -p /mnt
mount "$BTRFS_PART" /mnt
# サブボリューム二重実行チェック
if btrfs subvolume list /mnt 2>/dev/null | grep -qE '\s@$|\s@home$'; then
error "サブボリューム @ または @home がすでに存在します。移行済みの可能性があります。"
fi
# ============================================================
# ステップ2: サブボリューム作成・データ移動
# ============================================================
step "Step 2: サブボリューム作成・データ移動"
cd /mnt
info "スナップショット @ を作成"
btrfs subvolume snapshot . @
info "サブボリューム @home を作成"
btrfs subvolume create @home
if [[ -d /mnt/home ]] && compgen -G "/mnt/home/*" > /dev/null 2>&1; then
info "home/* を @home/ へコピー"
cp -a /mnt/home/. /mnt/@home/
else
warn "home/ が空またはありません。スキップします。"
fi
info "フラットなルートデータを削除(@ へコピー済み)"
# 実機でマウント済みのパーティション(/boot/efi など)を先にアンマウント
for entry in /mnt/*; do
name=$(basename "$entry")
[[ "$name" == "@" || "$name" == "@home" ]] && continue
# マウント中のディレクトリは先にアンマウント(ビジーエラー対策)
if mountpoint -q "$entry" 2>/dev/null; then
info " アンマウント中: $entry"
umount -R "$entry" 2>/dev/null || umount -l "$entry" 2>/dev/null || true
fi
rm -rf "$entry"
info " 削除: $entry"
done
# ============================================================
# ステップ3: fstab 書き換え
# ============================================================
step "Step 3: fstab を更新"
FSTAB="/mnt/@/etc/fstab"
[[ -f "$FSTAB" ]] || error "fstab が見つかりません: $FSTAB"
UUID=$(blkid -s UUID -o value "$BTRFS_PART")
[[ -n "$UUID" ]] || error "UUIDの取得に失敗しました"
info "UUID: $UUID"
cp "$FSTAB" "${FSTAB}.bak"
info "バックアップ: ${FSTAB}.bak"
python3 - "$FSTAB" "$UUID" << 'PYEOF'
import sys, re
fstab_path = sys.argv[1]
uuid = sys.argv[2]
with open(fstab_path) as f:
lines = f.readlines()
new_lines = []
has_home = False
for line in lines:
stripped = line.strip()
if stripped.startswith("#") or stripped == "":
new_lines.append(line)
continue
cols = stripped.split()
if len(cols) < 4:
new_lines.append(line)
continue
mountpoint = cols[1]
fstype = cols[2]
if fstype == "btrfs" and mountpoint == "/":
opts = re.sub(r'subvol=[^,\s]+,?', '', cols[3]).strip(',')
opts = (opts + ',subvol=@') if opts else 'defaults,subvol=@'
# スペース区切りで1行に収める(タブによる折り返し防止)
new_lines.append(f"UUID={uuid} / btrfs {opts} 0 0\n")
elif fstype == "btrfs" and mountpoint == "/home":
opts = re.sub(r'subvol=[^,\s]+,?', '', cols[3]).strip(',')
opts = (opts + ',subvol=@home') if opts else 'defaults,subvol=@home'
new_lines.append(f"UUID={uuid} /home btrfs {opts} 0 0\n")
has_home = True
else:
new_lines.append(line)
if not has_home:
new_lines.append(f"UUID={uuid} /home btrfs defaults,subvol=@home 0 0\n")
with open(fstab_path, 'w') as f:
f.writelines(new_lines)
PYEOF
info "更新後の fstab:"
cat "$FSTAB"
# ============================================================
# ステップ4: chroot して GRUB 更新
# ============================================================
step "Step 4: chroot して GRUB を更新"
# --- Step 4a: subvol=@ を明示した専用マウントポイントを用意 ---
# /mnt はフラットマウントのまま残し、別に /mnt2 へ subvol=@ で再マウント
# → /proc/mounts に「デバイス → / (subvol=@)」が正しく登録される
mkdir -p /mnt2
mount -t btrfs -o subvol=@ "$BTRFS_PART" /mnt2
info "subvol=@ で /mnt2 に再マウント完了"
# /boot/grub ディレクトリが存在することを確認・作成
mkdir -p /mnt2/boot/grub
# --- Step 4b: 仮想FSを rbind+rslave でバインド ---
mount --rbind /dev /mnt2/dev && mount --make-rslave /mnt2/dev
mount --rbind /proc /mnt2/proc && mount --make-rslave /mnt2/proc
mount --rbind /sys /mnt2/sys && mount --make-rslave /mnt2/sys
mount --rbind /run /mnt2/run && mount --make-rslave /mnt2/run
if [[ "$BOOT_MODE" == "EFI" ]]; then
mount --bind /sys/firmware/efi/efivars /mnt2/sys/firmware/efi/efivars 2>/dev/null || \
warn "efivars のバインドに失敗しました"
mkdir -p /mnt2/boot/efi
mount "$EFI_PART" /mnt2/boot/efi
chroot /mnt2 /bin/bash -e << CHROOT
echo '[chroot] grub-install (EFI) ...'
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu --recheck
echo '[chroot] grub-mkconfig ...'
grub-mkconfig -o /boot/grub/grub.cfg
echo '[chroot] 完了'
CHROOT
else
chroot /mnt2 /bin/bash -e << CHROOT
echo '[chroot] grub-install (BIOS) ...'
grub-install --target=i386-pc --recheck $DISK
echo '[chroot] grub-mkconfig ...'
grub-mkconfig -o /boot/grub/grub.cfg
echo '[chroot] 完了'
CHROOT
fi
# ============================================================
# ステップ5: アンマウント
# ============================================================
step "Step 5: アンマウント"
umount -R /mnt2 2>/dev/null || true
umount -R /mnt 2>/dev/null || true
echo ""
echo -e "${GREEN}======================================================"
echo " 移行が完了しました!"
echo " 再起動後に以下で確認してください:"
echo " sudo btrfs subvolume list /"
echo -e "======================================================${NC}"
echo ""
warn "再起動しますか? [yes/N]"
read -r reboot_answer
[[ "$reboot_answer" == "yes" ]] && reboot || info "手動で再起動してください: sudo reboot"
ライブUSBで起動するために再起動します。
sudo reboot
ライブUSBで起動してスクリプトを実行
次に、インストール時に使用したUbuntuのライブUSBで起動します。今度は「Ubuntuを試す」を選択し、マウントしたドライブでhomeフォルダまで辿り、ターミナルを開き、保存しておいたスクリプトを実行します。
(失敗すれば起動しなくなります。それでも構わないような、インストール直後に実施を。あくまでも自己責任で!)
sudo bash btrfs_migrate.sh

無事終了したら指示に従い再起動します。
変換後に再度状況を確認
再起動で無事起動したら、先ほどのコマンドを再度実行します。
mount | grep btrfs
下記のような内容なら成功です。
/dev/vda1 on / type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=256,subvol=/@)
/dev/vda1 on /home type btrfs (rw,relatime,discard=async,space_cache=v2,subvolid=257,subvol=/@home)
管理者権限でBtrfsのサブボリュームの一覧を表示すれば確実です。
sudo btrfs subvolume list /
成功していればこのような表示になります。
ID 256 gen 123 top level 5 path @
ID 257 gen 111 top level 5 path @home


