UbuntuをBtrfsパーティションにインストール

個人用途で、ホストを極力汚さず、メンテナンス性と安全性を両立させるには、LXDを基盤としてVMやDocker(LXDコンテナ内)を運用する「ネスト構造」が便利です。そして、ホストOSの環境は「Timeshift」を導入しておけば、アップデートの失敗などによってOSが起動しなくなった時の復旧にも対応しやすくなります。

構成のイメージ

1. ホストOSの構成

  • ファイルシステム: Btrfs
  • 理由:
    • Timeshift との相性が抜群で、ホストの設定(LXDのインストール状態など)をいつでもロールバックできる。
    • ホスト側で透過圧縮(zstd)を効かせれば、LXDのイメージや仮想ディスクの容量を節約できる。

2. LXDのストレージプール構成

LXDのデフォルトプールには 「Btrfsバックエンド」 を推奨。

  • バックエンド: Btrfs(ホストのBtrfs上にサブボリュームとして作成)
  • 理由:
    • lxc copylxc snapshot が一瞬で終わる(CoWの恩恵)。
    • VM(仮想マシン) を作成した際、内部で自動的に nocow 設定が適用されるため、断片化のリスクが抑えられる。
    • ZFSよりもメモリ消費が少なく、個人用PCの500GB SSDで運用するにはリソース管理が楽。

3. LXD内でのDocker(Nextcloud/Immich)運用

LXDコンテナの中でDockerを動かす場合、以下の設定で「ホストを汚さない&高速」を実現します。

  • コンテナの種別: 特権なし(Unprivileged)コンテナ(セキュリティのため)
  • Dockerストレージドライバ: overlay2
    • LXDがBtrfsプールなら、コンテナ内のDockerはデフォルトで vfs(激重)や btrfs になろうとしますが、これを overlay2 に強制設定します。
  • データの逃がし先(NOCOWの適用):
    1. ホスト側に sudo mkdir /var/lib/lxd-data && sudo chattr +C /var/lib/lxd-dataNOCOW領域 を作成。
    2. NextcloudやImmichのDB用ディレクトリだけ、ホストのこの領域を lxc config device add でコンテナにパススルー(ディスクマウント)する。
    3. 写真データは、NOCOWではない「通常のBtrfs領域」をマウントして、チェックサムによる保護を受ける。

最適な構成まとめ表

階層種類 / 設定役割
ホストOSUbuntu (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の写真など)は無傷で残せます。

  1. サブボリュームの階層設計は、Btrfsのサブボリュームを「入れ子」にせず、並列(フラット)に配置するのが安全です。これにより、Timeshiftが @(ルート)を書き換えても他が巻き込まれません。
    @ (ルート): / にマウント。Timeshiftの保護対象。
    @home: /home にマウント。
    @lxd_pool: /var/lib/lxd/storage-pools/default 等にマウント。(Timeshift対象外)
    @lxd_data: /mnt/lxd-data 等にマウント。ホスト・LXD共通。(Timeshift対象外)
  2. インストールと設定の手順
    ① 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
タイトルとURLをコピーしました