Ubuntu Desktop 26.04をインストール(2026年5月版)

Ubuntuをデスクトップ環境で使う方法をまとめました。

後半にISOを置きISOブートでインストール

デスクトップ環境で使うなら不具合時の復元しやすさを重視して後半パーティションにISOイメージを保存し、そこからインストールするのが楽だと思います。まだ作っていないならインストール時に作成してもよいですし、あらかじめ作成してそこからブートしてクリーンインストールしても良いかと。

パーティションのみ作成してこれからコピーする場合。下記は/dev/nvme0n1p3/iso にマウントする例。もっともクリーンインストールするなら、マウントしたフォルダで右クリックしてターミナルを開き、スクリプトを保存して実行で構いませんが。

# マウントポイントの作成
sudo mkdir /iso

# パーティションをマウント
sudo mount /dev/nvme0n1p3 /iso

# 所有者変更と書き込み権限を付与
sudo chown $USER:$USER /iso
chmod u+rwx /iso

続いてスクリプトを保存して実行します。スクリプトの保存先を/isoにしておけば、このISOブートを利用してクリーンインストールした時など、再度GRUBメニューに項目を追加したい場合に便利です。

cd /iso
nano isoboot-grub.sh
sudo bash isoboot-grub.sh
#!/usr/bin/env bash
# =============================================================================
# isoboot-grub.sh
# ISOブート用パーティション確認 → GRUB エントリ追加スクリプト
# 前提:ISOファイル入りのext4パーティションが既に存在すること
# =============================================================================

set -euo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
success() { echo -e "${GREEN}[OK]${RESET}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die()     { error "$*"; exit 1; }
hr()      { echo -e "${CYAN}$(printf '=%.0s' {1..70})${RESET}"; }

confirm() {
    local ans
    while true; do
        echo -en "${YELLOW}[確認]${RESET} $1 [y/N]: "
        read -r ans
        case "$ans" in
            [yY]*) return 0 ;;
            [nN]*|"") return 1 ;;
            *) echo "  y または n を入力してください。" ;;
        esac
    done
}

# -----------------------------------------------------------------------------
check_root() {
    [[ $EUID -eq 0 ]] || die "sudo で実行してください。\n  例: sudo bash $0"
}

check_deps() {
    local missing=()
    for cmd in lsblk blkid mount umount file grub-mkconfig update-grub; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    [[ ${#missing[@]} -eq 0 ]] || die "コマンドが見つかりません: ${missing[*]}"
}

# -----------------------------------------------------------------------------
# ステップ1: ISOが入ったパーティションを選択
# -----------------------------------------------------------------------------
select_partition() {
    hr
    echo -e "${BOLD}【ステップ 1/3】ISO パーティションの選択${RESET}"
    hr
    echo ""
    info "現在のパーティション一覧:"
    echo ""
    lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS | grep -v loop
    echo ""

    while true; do
        echo -en "${BOLD}ISOが入ったパーティションを入力${RESET} (例: vda3, sda4, nvme0n1p3): /dev/"
        read -r part_name
        PART_DEV="/dev/${part_name}"

        if [[ ! -b "$PART_DEV" ]]; then
            error "${PART_DEV} はブロックデバイスではありません。再入力してください。"
            continue
        fi

        local fstype
        fstype=$(lsblk -no FSTYPE "$PART_DEV" 2>/dev/null || true)
        if [[ "$fstype" != "ext4" ]]; then
            warn "${PART_DEV} のファイルシステムは ${fstype:-不明} です(ext4 を想定)。"
            confirm "このまま続けますか?" || continue
        fi

        echo ""
        info "${PART_DEV} の情報:"
        lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINTS "$PART_DEV" 2>/dev/null || true
        echo ""
        confirm "${PART_DEV} を使用しますか?" && break
    done

    success "対象パーティション: ${PART_DEV}"

    # UUID取得(デバイス名非依存・VM/実機共通)
    PART_UUID=$(blkid -s UUID -o value "$PART_DEV")
    if [[ -z "$PART_UUID" ]]; then
        die "UUIDを取得できませんでした: ${PART_DEV}"
    fi
    success "UUID: ${PART_UUID}"
}

# -----------------------------------------------------------------------------
# ステップ2: ISOファイルの確認と起動パス検出
# -----------------------------------------------------------------------------
inspect_isos() {
    hr
    echo -e "${BOLD}【ステップ 2/3】ISO ファイルの確認${RESET}"
    hr
    echo ""

    MOUNT_POINT="/mnt/isoboot_grub_$$"
    mkdir -p "$MOUNT_POINT"
    mount "$PART_DEV" "$MOUNT_POINT"
    info "${PART_DEV} を ${MOUNT_POINT} にマウントしました"
    echo ""

    # ISOファイルを列挙
    mapfile -t iso_candidates < <(find "$MOUNT_POINT" -maxdepth 2 -name "*.iso" 2>/dev/null)

    if [[ ${#iso_candidates[@]} -eq 0 ]]; then
        umount "$MOUNT_POINT"; rmdir "$MOUNT_POINT"
        die "ISOファイルが見つかりませんでした。パーティションにISOを配置してから再実行してください。"
    fi

    info "見つかったISOファイル:"
    for i in "${!iso_candidates[@]}"; do
        local size
        size=$(du -sh "${iso_candidates[$i]}" 2>/dev/null | cut -f1)
        echo "  [$((i+1))] ${iso_candidates[$i]##"$MOUNT_POINT"}  (${size})"
    done
    echo ""

    ISO_ENTRIES=()  # "iso_path:vmlinuz:initrd:boot_type" の配列

    for iso_full in "${iso_candidates[@]}"; do
        local iso_rel="${iso_full##"$MOUNT_POINT"}"
        local iso_name
        iso_name=$(basename "$iso_full")
        echo ""
        info "--- ${iso_name} を検査中 ---"

        if confirm "${iso_name} をGRUBメニューに追加しますか?"; then
            local paths
            paths=$(detect_boot_paths "$iso_full")
            local vmlinuz="${paths%%:*}"
            local rest="${paths#*:}"
            local initrd="${rest%%:*}"
            local boot_type="${rest##*:}"

            if [[ "$vmlinuz" == "UNKNOWN" ]]; then
                warn "カーネルパスを自動検出できませんでした。手動入力してください。"
                echo -en "  vmlinuz のパス (例: /casper/vmlinuz): "
                read -r vmlinuz
                echo -en "  initrd のパス  (例: /casper/initrd): "
                read -r initrd
                boot_type="custom"
            else
                success "カーネル : ${vmlinuz}"
                success "initrd   : ${initrd}"
                success "起動方式 : ${boot_type}"
            fi

            ISO_ENTRIES+=("${iso_rel}:${vmlinuz}:${initrd}:${boot_type}")
        else
            info "スキップ: ${iso_name}"
        fi
    done

    umount "$MOUNT_POINT"
    rmdir "$MOUNT_POINT"
    success "アンマウント完了"

    if [[ ${#ISO_ENTRIES[@]} -eq 0 ]]; then
        die "追加するエントリがありません。終了します。"
    fi
}

# ISOをループマウントして起動パスを自動検出
detect_boot_paths() {
    local iso_path="$1"
    local tmp="/mnt/iso_inspect_$$"
    mkdir -p "$tmp"

    if ! mount -o loop,ro "$iso_path" "$tmp" 2>/dev/null; then
        echo "UNKNOWN:UNKNOWN:custom"
        return
    fi

    local vmlinuz="" initrd="" boot_type=""

    if   [[ -f "$tmp/casper/vmlinuz" ]];     then vmlinuz="/casper/vmlinuz";   initrd="/casper/initrd";     boot_type="casper"
    elif [[ -f "$tmp/casper/vmlinuz.efi" ]]; then vmlinuz="/casper/vmlinuz.efi"; initrd="/casper/initrd.lz"; boot_type="casper"
    elif [[ -f "$tmp/live/vmlinuz" ]];        then vmlinuz="/live/vmlinuz";     initrd="/live/initrd.img";   boot_type="live"
    elif [[ -f "$tmp/live/vmlinuz.efi" ]];    then vmlinuz="/live/vmlinuz.efi"; initrd="/live/initrd.img";   boot_type="live"
    else
        # フォールバック:vmlinuz を再帰検索
        local found
        found=$(find "$tmp" -name "vmlinuz*" | head -1 || true)
        if [[ -n "$found" ]]; then
            vmlinuz="${found##"$tmp"}"
            local dir
            dir=$(dirname "$found")
            initrd=$(find "$dir" -name "initrd*" | head -1 || true)
            initrd="${initrd##"$tmp"}"
            boot_type="custom"
        else
            vmlinuz="UNKNOWN"; initrd="UNKNOWN"; boot_type="custom"
        fi
    fi

    umount "$tmp"; rmdir "$tmp"
    echo "${vmlinuz}:${initrd}:${boot_type}"
}

# -----------------------------------------------------------------------------
# ステップ3: GRUBエントリ追加
# -----------------------------------------------------------------------------
add_grub_entries() {
    hr
    echo -e "${BOLD}【ステップ 3/3】GRUB エントリの追加${RESET}"
    hr
    echo ""

    local custom_file="/etc/grub.d/40_custom"
    cp "$custom_file" "${custom_file}.bak.$(date +%Y%m%d_%H%M%S)"
    info "40_custom をバックアップしました"
    echo ""

    for entry in "${ISO_ENTRIES[@]}"; do
        local iso_rel vmlinuz initrd boot_type menu_label
        IFS=':' read -r iso_rel vmlinuz initrd boot_type <<< "$entry"
        menu_label=$(basename "$iso_rel" .iso)

        local params=""
        case "$boot_type" in
            casper) params="boot=casper iso-scan/filename=\$isofile quiet splash ---" ;;
            live)   params="boot=live iso-scan/filename=\$isofile quiet splash" ;;
            *)      params="iso-scan/filename=\$isofile quiet splash" ;;
        esac

        local grub_entry
        grub_entry=$(cat <<EOF

menuentry "${menu_label} (ISO Loop Boot)" {
    insmod part_gpt
    insmod ext2
    insmod loopback
    insmod iso9660
    search --no-floppy --fs-uuid --set=isodev ${PART_UUID}
    set isofile="${iso_rel}"
    loopback loop (\$isodev)\$isofile
    linux  (loop)${vmlinuz} ${params}
    initrd (loop)${initrd}
}
EOF
)
        echo -e "${CYAN}追加予定エントリ:${RESET}"
        echo "$grub_entry"
        echo ""

        if confirm "このエントリを追加しますか?"; then
            echo "$grub_entry" >> "$custom_file"
            success "追加しました: ${menu_label}"
        else
            warn "スキップ: ${menu_label}"
        fi
    done

    echo ""
    info "GRUB を更新中..."
    if update-grub 2>&1; then
        success "update-grub 完了"
    else
        warn "update-grub でエラーが発生しました。手動で確認してください:"
        warn "  sudo update-grub"
    fi
}

# -----------------------------------------------------------------------------
# 完了サマリー
# -----------------------------------------------------------------------------
print_summary() {
    hr
    echo -e "${BOLD}${GREEN}【完了】セットアップサマリー${RESET}"
    hr
    echo ""
    echo -e "  対象パーティション : ${BOLD}${PART_DEV}${RESET}"
    echo -e "  パーティションUUID : ${BOLD}${PART_UUID}${RESET}"
    echo -e "  追加エントリ数     : ${BOLD}${#ISO_ENTRIES[@]}${RESET}"
    echo ""
    echo -e "${YELLOW}【次のステップ】${RESET}"
    echo "  1. 再起動して GRUB メニューに新しいエントリが表示されることを確認"
    echo "  2. 起動に失敗した場合は /etc/grub.d/40_custom を確認し"
    echo "     カーネルパスを修正後 sudo update-grub を再実行"
    echo ""
    warn "バックアップ: /etc/grub.d/40_custom.bak.* に保存済み"
    hr
}

# -----------------------------------------------------------------------------
main() {
    clear
    hr
    echo -e "${BOLD}  isoboot-grub.sh — ISO ブート GRUB エントリ 追加スクリプト${RESET}"
    echo -e "  前提: ISOファイル入りのext4パーティションが既に存在すること"
    hr
    echo ""
    check_root
    check_deps
    select_partition
    inspect_isos
    add_grub_entries
    print_summary
}

main "$@"

現在のGRUBエントリーを確認

cd /iso
nano list-grub-entries.sh
chmod +x list-grub-entries.sh
bash list-grub-entries.sh
#!/usr/bin/env bash
# ============================================================
#  list-grub-entries.sh
#  GRUBエントリー一覧表示スクリプト (Ubuntu 26.04 対応)
# ============================================================

set -euo pipefail

# ---------- 色定義 ----------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'

# ---------- ヘルパー関数 ----------
info()    { echo -e "${CYAN}[INFO]${RESET}  $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET}  $*"; }
error()   { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
header()  { echo -e "\n${BOLD}${GREEN}$*${RESET}"; echo -e "${GREEN}$(printf '=%.0s' {1..60})${RESET}"; }

# ---------- grub-mkconfig / grub2-mkconfig の確認 ----------
find_grub_cfg() {
    local candidates=(
        /boot/grub/grub.cfg
        /boot/grub2/grub.cfg
        /boot/efi/EFI/ubuntu/grub.cfg
    )
    for f in "${candidates[@]}"; do
        [[ -f "$f" ]] && echo "$f" && return
    done
    echo ""
}

# ---------- メイン ----------
main() {
    header "GRUB エントリー一覧表示ツール"
    echo -e "実行日時: $(date '+%Y-%m-%d %H:%M:%S')\n"

    # ---- grub.cfg を探す ----
    GRUB_CFG=$(find_grub_cfg)

    if [[ -z "$GRUB_CFG" ]]; then
        error "grub.cfg が見つかりませんでした。"
        error "次のいずれかに存在するか確認してください:"
        error "  /boot/grub/grub.cfg"
        error "  /boot/grub2/grub.cfg"
        error "  /boot/efi/EFI/ubuntu/grub.cfg"
        exit 1
    fi

    info "設定ファイル: ${BOLD}${GRUB_CFG}${RESET}"

    # ---- 読み取り権限チェック ----
    if [[ ! -r "$GRUB_CFG" ]]; then
        warn "読み取り権限がありません。sudo で再実行します..."
        if ! sudo -n true 2>/dev/null; then
            warn "sudoパスワードが必要です。"
        fi
        GRUB_CFG_CONTENT=$(sudo cat "$GRUB_CFG")
    else
        GRUB_CFG_CONTENT=$(cat "$GRUB_CFG")
    fi

    # ---- エントリーを抽出 ----
    header "■ menuentry 一覧"

    # menuentry 行を抽出 (インデント・サブメニューも含む)
    ENTRIES=$(echo "$GRUB_CFG_CONTENT" | grep -n '^\(menuentry\|submenu\)' || true)

    if [[ -z "$ENTRIES" ]]; then
        warn "menuentryが見つかりませんでした。"
    else
        INDEX=0
        while IFS= read -r line; do
            LINENO_=$(echo "$line" | cut -d: -f1)
            ENTRY=$(echo "$line" | cut -d: -f2-)

            # menuentry か submenu かを判定
            if echo "$ENTRY" | grep -q '^submenu'; then
                TYPE="${YELLOW}[サブメニュー]${RESET}"
            else
                TYPE="${GREEN}[エントリー ]${RESET}"
            fi

            # エントリー名を抽出(クォート内の文字列)
            NAME=$(echo "$ENTRY" | sed "s/.*menuentry[[:space:]]*['\"]\\([^'\"]*\\)['\"].*/\\1/")

            printf "  ${BOLD}%3d${RESET}  %b  %s  ${CYAN}(行 %s)${RESET}\n" \
                "$INDEX" "$TYPE" "$NAME" "$LINENO_"
            ((INDEX++)) || true
        done <<< "$ENTRIES"
    fi

    # ---- デフォルトエントリー ----
    header "■ デフォルトエントリー"

    DEFAULT_GRUB="/etc/default/grub"
    if [[ -f "$DEFAULT_GRUB" ]]; then
        DEFAULT=$(grep '^GRUB_DEFAULT=' "$DEFAULT_GRUB" | cut -d= -f2 | tr -d '"' || echo "未設定")
        echo -e "  GRUB_DEFAULT = ${BOLD}${DEFAULT}${RESET}"

        TIMEOUT=$(grep '^GRUB_TIMEOUT=' "$DEFAULT_GRUB" | cut -d= -f2 | tr -d '"' || echo "未設定")
        echo -e "  GRUB_TIMEOUT = ${BOLD}${TIMEOUT}${RESET} 秒"
    else
        warn "${DEFAULT_GRUB} が見つかりません。"
    fi

    # ---- grubenv の確認 ----
    header "■ grubenv (保存されたデフォルト)"

    GRUBENV_CANDIDATES=(
        /boot/grub/grubenv
        /boot/grub2/grubenv
    )
    FOUND_ENV=0
    for env_file in "${GRUBENV_CANDIDATES[@]}"; do
        if [[ -f "$env_file" ]]; then
            info "grubenv: $env_file"
            if [[ -r "$env_file" ]]; then
                grep -v '^#' "$env_file" | grep -v '^$' | while IFS= read -r envline; do
                    echo -e "    ${envline}"
                done
            else
                sudo grep -v '^#' "$env_file" | grep -v '^$' | while IFS= read -r envline; do
                    echo -e "    ${envline}"
                done
            fi
            FOUND_ENV=1
            break
        fi
    done
    [[ $FOUND_ENV -eq 0 ]] && warn "grubenv が見つかりませんでした。"

    # ---- EFIブートエントリー (efibootmgr) ----
    header "■ EFI ブートエントリー (efibootmgr)"

    if command -v efibootmgr &>/dev/null; then
        if efibootmgr &>/dev/null 2>&1; then
            efibootmgr | while IFS= read -r efil; do
                if echo "$efil" | grep -q '^\*'; then
                    echo -e "  ${GREEN}${efil}${RESET}  ${CYAN}← 現在のブートエントリー${RESET}"
                else
                    echo -e "  ${efil}"
                fi
            done
        else
            # sudo が必要な場合
            sudo efibootmgr 2>/dev/null | while IFS= read -r efil; do
                if echo "$efil" | grep -q '^\*'; then
                    echo -e "  ${GREEN}${efil}${RESET}  ${CYAN}← 現在のブートエントリー${RESET}"
                else
                    echo -e "  ${efil}"
                fi
            done
        fi
    else
        warn "efibootmgr がインストールされていません。"
        info "インストール: sudo apt install efibootmgr"
    fi

    echo ""
    info "完了しました。"
    echo ""
}

main "$@"

インストール完了直後。アップデート・Tailscale・SSH

インストールが完了したらまずはアップデート。

sudo apt update
sudo apt upgrade -y

Tailscale

sudo apt install curl 
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

SSH

sudo apt install -y openssh-server

別PCからの接続時エラーの場合。何度も繰り返して接続していると発生。接続元で。

ssh-keygen -R ホスト名やIPアドレス

ノートPCなどでDesktop版をサーバにしている場合

デスクトップ共有を有効化。キーリングへのパスワードは空のままで保存。

自動画面ロックを無効化

プライバシーとセキュリティ」-「画面ロック」で画面が暗くなるまでの時間や自動画面ロックの設定を行います。ちなみに「電源管理」-「自動画面ブランク」の設定も、ここの設定が反映されます。

そのほか「電源管理」-「省電力」の項目もチェック。

ロック時でもデスクトップ共有

デスクトップ共有の場合、ロックされてもアクセス出来るように。
まずGNOMEのシェル拡張機能をインストール。

sudo apt install gnome-shell-extension-manager

「探す」で「Allow locked Remote Desktop」を検索してインストールすれば有効に。

Allow locked Remote Desktop

ノートPCで画面が閉じてもスリープしない設定

sudo sed -i 's/#HandleLidSwitch=suspend/HandleLidSwitch=ignore/' /etc/systemd/logind.conf

一度再起動。以降はSSHやリモートデスクトップで。

sudo reboot

ISOブートしていた場合

ブートローダーもクリーンインストールした場合はISOブートがメニューから消えているので再設定。一番後ろのパーティションをマウントして右クリックしてターミナルを開き実行。

sudo bash isoboot-grub.sh

永続的に/isoへマウントするなら

もし、ISOパーティションに複数のISOを保存しており、今後VMでもそのISOを利用するなら永続的に/isoへマウントする設定を実施します(ISO1個くらいなら一時的なマウントで良いと思うので不要)。

nano mount-setup.sh
sudo bash mount-setup.sh
#!/bin/bash
# ============================================================
# 対話式 fstab マウント登録スクリプト
#   - ディスク一覧を表示して番号で選択
#   - /iso ディレクトリがなければ作成・権限設定
#   - UUID を /etc/fstab に追記してマウント
# ============================================================
set -euo pipefail

MOUNT_POINT="/iso"
FSTAB="/etc/fstab"

info()    { echo -e "\033[1;32m[INFO]\033[0m  $*"; }
warn()    { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
error()   { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
confirm() {
  local msg="$1"
  local ans
  read -rp "$(echo -e "\033[1;33m[確認]\033[0m $msg [y/N]: ")" ans
  [[ "${ans,,}" == "y" ]]
}

# root 確認
[[ "$EUID" -ne 0 ]] && error "このスクリプトは sudo で実行してください。"

# ============================================================
# 1. パーティション一覧を表示
# ============================================================
echo ""
echo "======================================================"
echo " 現在のディスク・パーティション一覧"
echo "======================================================"
echo ""
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT,UUID | grep -v "^loop"
echo ""

# パーティションのみ抽出(ディスク本体除く)
mapfile -t PARTS < <(lsblk -lnpo NAME,SIZE,FSTYPE,MOUNTPOINT \
  | awk '$3!="" && $1!~/^(loop|sr)/' \
  | grep -v "^$")

if [[ ${#PARTS[@]} -eq 0 ]]; then
  error "マウント可能なパーティションが見つかりませんでした。"
fi

echo "------------------------------------------------------"
printf " %-3s  %-15s %-8s %-10s %s\n" "No." "デバイス" "サイズ" "FS種別" "現在のマウント先"
echo "------------------------------------------------------"
for i in "${!PARTS[@]}"; do
  read -r dev size fstype mount <<< "${PARTS[$i]}"
  mount="${mount:-(未マウント)}"
  printf " %-3s  %-15s %-8s %-10s %s\n" "$((i+1))" "$dev" "$size" "$fstype" "$mount"
done
echo "------------------------------------------------------"
echo ""

# ============================================================
# 2. パーティション選択
# ============================================================
while true; do
  read -rp "$(echo -e "\033[1;36m[入力]\033[0m マウントするパーティションの番号を入力してください: ")" SEL
  if [[ "$SEL" =~ ^[0-9]+$ ]] && (( SEL >= 1 && SEL <= ${#PARTS[@]} )); then
    break
  fi
  warn "1〜${#PARTS[@]} の番号を入力してください。"
done

read -r SELECTED_DEV _ SELECTED_FS CURRENT_MOUNT <<< "${PARTS[$((SEL-1))]}"
CURRENT_MOUNT="${CURRENT_MOUNT:-(未マウント)}"

echo ""
info "選択: $SELECTED_DEV  FS: $SELECTED_FS  現在: $CURRENT_MOUNT"

# すでに /iso にマウント済みなら確認
if [[ "$CURRENT_MOUNT" == "$MOUNT_POINT" ]]; then
  warn "$SELECTED_DEV はすでに $MOUNT_POINT にマウントされています。"
  confirm "再登録を続けますか?" || exit 0
fi

# 別の場所にマウント済みなら警告
if [[ "$CURRENT_MOUNT" != "(未マウント)" && "$CURRENT_MOUNT" != "$MOUNT_POINT" ]]; then
  warn "$SELECTED_DEV は現在 $CURRENT_MOUNT にマウントされています。"
  confirm "続けますか?(fstab への追記のみ行い、現在のマウントは変更しません)" || exit 0
fi

# ============================================================
# 3. UUID 取得
# ============================================================
UUID=$(blkid -s UUID -o value "$SELECTED_DEV" 2>/dev/null || true)
if [[ -z "$UUID" ]]; then
  error "$SELECTED_DEV の UUID を取得できませんでした。FS が認識されているか確認してください。"
fi
info "UUID: $UUID"

# ============================================================
# 4. /iso ディレクトリの準備
# ============================================================
if [[ ! -d "$MOUNT_POINT" ]]; then
  info "$MOUNT_POINT が存在しないため作成します..."
  mkdir -p "$MOUNT_POINT"
  # 所有者を実行前のユーザーに設定
  REAL_USER="${SUDO_USER:-root}"
  chown "${REAL_USER}:${REAL_USER}" "$MOUNT_POINT"
  chmod 775 "$MOUNT_POINT"
  info "作成完了: $MOUNT_POINT(所有者: ${REAL_USER}, パーミッション: 775)"
else
  info "$MOUNT_POINT はすでに存在します。"
fi

# ============================================================
# 5. fstab の重複チェックと追記
# ============================================================

# 同じ UUID がすでに fstab にないか確認
if grep -q "$UUID" "$FSTAB"; then
  warn "UUID=$UUID はすでに $FSTAB に登録されています。"
  grep "$UUID" "$FSTAB"
  confirm "上書き(既存行をコメントアウトして追記)しますか?" || exit 0
  # 既存行をコメントアウト
  sed -i "s|.*${UUID}.*|# &  # mount-setup.sh により無効化 $(date '+%Y-%m-%d')|" "$FSTAB"
fi

# fstab に /iso のマウント先がすでにあるか確認
if grep -qP "^\s*[^#].*\s${MOUNT_POINT}\s" "$FSTAB"; then
  warn "$MOUNT_POINT のエントリがすでに $FSTAB にあります。"
  grep -P "^\s*[^#].*\s${MOUNT_POINT}\s" "$FSTAB"
  confirm "既存行をコメントアウトして上書きしますか?" || exit 0
  sed -i "s|^\([^#].*\s${MOUNT_POINT}\s.*\)|# \1  # mount-setup.sh により無効化 $(date '+%Y-%m-%d')|" "$FSTAB"
fi

# FS タイプの決定(空なら auto)
FS_TYPE="${SELECTED_FS:-auto}"
# ntfs の場合は ntfs-3g に補正
[[ "$FS_TYPE" == "ntfs" ]] && FS_TYPE="ntfs-3g"

FSTAB_LINE="UUID=${UUID}  ${MOUNT_POINT}  ${FS_TYPE}  defaults  0  2"

echo ""
info "以下の行を $FSTAB に追記します:"
echo ""
echo "  $FSTAB_LINE"
echo ""
confirm "追記しますか?" || exit 0

# バックアップ
cp "$FSTAB" "${FSTAB}.bak.$(date '+%Y%m%d%H%M%S')"
info "バックアップ: ${FSTAB}.bak.* を作成しました。"

echo "" >> "$FSTAB"
echo "# /iso - mount-setup.sh により追加 $(date '+%Y-%m-%d %H:%M:%S')" >> "$FSTAB"
echo "$FSTAB_LINE" >> "$FSTAB"
info "fstab への追記が完了しました。"

# ============================================================
# 6. マウント実行
# ============================================================
echo ""
info "mount -a でマウントを実行します..."
if mount -a 2>&1; then
  info "マウント成功!"
else
  warn "mount -a でエラーが発生しました。fstab の内容を確認してください:"
  tail -5 "$FSTAB"
  exit 1
fi

# ============================================================
# 7. 確認
# ============================================================
echo ""
echo "======================================================"
echo " 完了!マウント状態:"
echo "======================================================"
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT | grep -E "NAME|$(basename "$SELECTED_DEV")"
echo ""
df -h "$MOUNT_POINT"
echo "======================================================"

Timeshift

スナップショットを保存するために。rsyncバージョンだとしてもこのあたりで一度取っておくと楽なのでは。

sudo apt install -y timeshift

Google Chrome

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo apt install ./google-chrome-stable_current_amd64.deb

Chromeウェブストアで下記拡張機能を検索してインストールします。

User-Agent Switcher and Manager

デスクトップにショートカットを置いたり、プロファイルごとのショートカットを作るならこちら

code-server

sudo mkdir -p /opt/script
sudo chown $USER:$USER /opt/script
chmod u+rwx /opt/script
cd /opt/script
nano install-codeserver.sh
chmod +x install-codeserver.sh
sudo bash install-codeserver.sh
#!/bin/bash
# ============================================================
#  Code-Server インストール(Ubuntu 26.04 直接インストール版)
#  構成:
#    - code-server を localhost のみで待受
#    - tailscale serve で HTTPS化(tailnet 内のみ公開)
#  前提:
#    - tailscale up 済み
#    - 実行: sudo bash install-codeserver.sh
# ============================================================

set -euo pipefail

GREEN='\033[0;32m'; CYAN='\033[0;36m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
info()    { echo -e "${CYAN}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC}   $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC} $*"; }
error()   { echo -e "${RED}[ERR]${NC}  $*"; exit 1; }

[[ $EUID -ne 0 ]] && error "root権限で実行してください: sudo bash $0"

REAL_USER="${SUDO_USER:-${USER:-$(id -un)}}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
CODE_SERVER_PORT=8089
PASSWORD=""

# ============================================================
# ① パスワードを最初に聞く
# ============================================================
prompt_password() {
  echo ""
  echo -e "${CYAN}╔══════════════════════════════════════════════════════╗${NC}"
  echo -e "${CYAN}║   Code-Server + Tailscale Serve セットアップ         ║${NC}"
  echo -e "${CYAN}║   Ubuntu 26.04 / tailnet 内 HTTPS 公開               ║${NC}"
  echo -e "${CYAN}╚══════════════════════════════════════════════════════╝${NC}"
  echo ""
  echo -e "インストール対象ユーザー: ${YELLOW}${REAL_USER}${NC} (${REAL_HOME})"
  echo ""
  echo "code-server のパスワードを設定してください。"
  echo -e "  ${CYAN}[A]${NC} 自動生成する(ランダム32文字)"
  echo -e "  ${CYAN}[M]${NC} 手動で入力する"
  echo ""
  read -r -p "選択 [A/M] (Enterで自動生成): " choice
  case "${choice^^}" in
    M)
      while true; do
        read -r -s -p "🔑 パスワードを入力してください: " PASSWORD
        echo ""
        read -r -s -p "🔑 もう一度入力してください:     " PASSWORD2
        echo ""
        if [[ "$PASSWORD" == "$PASSWORD2" && -n "$PASSWORD" ]]; then
          success "パスワードを設定しました。"
          break
        else
          warn "パスワードが一致しないか空です。もう一度入力してください。"
        fi
      done
      ;;
    *)
      PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
      success "パスワードを自動生成しました。(後で表示します)"
      ;;
  esac
  echo ""
}

# ============================================================
# ② Tailscale の動作確認
# ============================================================
check_prerequisites() {
  info "前提条件を確認中..."
  command -v tailscale &>/dev/null \
    || error "tailscale がインストールされていません。先に tailscale をセットアップしてください。"
  tailscale status &>/dev/null \
    || error "tailscale が起動していません。'tailscale up' を先に実行してください。"
  success "Tailscale 動作確認OK"
}

# ============================================================
# ③ 依存パッケージのインストール
# ============================================================
install_deps() {
  info "依存パッケージをインストール中..."
  apt-get update -qq
  apt-get install -y -qq curl wget openssl
  success "依存パッケージ インストール完了"
}

# ============================================================
# ④ code-server のインストール
# ============================================================
install_code_server() {
  info "最新バージョンを確認中..."
  LATEST=$(curl -fsSL https://api.github.com/repos/coder/code-server/releases/latest \
    | grep '"tag_name"' | sed 's/.*"v\([^"]*\)".*/\1/')
  info "最新バージョン: v${LATEST}"

  DEB_URL="https://github.com/coder/code-server/releases/download/v${LATEST}/code-server_${LATEST}_amd64.deb"
  info "ダウンロード中..."
  wget -q --show-progress -O /tmp/code-server.deb "$DEB_URL"
  dpkg -i /tmp/code-server.deb
  rm /tmp/code-server.deb
  success "code-server v${LATEST} インストール完了"
}

# ============================================================
# ⑤ 設定ファイルの生成
# ============================================================
setup_config() {
  info "config.yaml を生成中..."
  CONFIG_DIR="$REAL_HOME/.config/code-server"
  mkdir -p "$CONFIG_DIR"
  cat > "$CONFIG_DIR/config.yaml" <<EOF
bind-addr: 127.0.0.1:${CODE_SERVER_PORT}
auth: password
password: ${PASSWORD}
cert: false
EOF
  chown -R "$REAL_USER:$REAL_USER" "$CONFIG_DIR"
  chmod 600 "$CONFIG_DIR/config.yaml"
  success "config.yaml 生成完了(127.0.0.1:${CODE_SERVER_PORT} でローカルのみ待受)"

  info "settings.json(日本語化 + 推奨設定)を配置中..."
  VSCODE_USER_DIR="$REAL_HOME/.local/share/code-server/User"
  mkdir -p "$VSCODE_USER_DIR"

  # argv.json: ロケール設定
  cat > "$VSCODE_USER_DIR/../argv.json" <<'EOF'
{
  "locale": "ja"
}
EOF

  # settings.json: 基本的な快適設定
  cat > "$VSCODE_USER_DIR/settings.json" <<'EOF'
{
  "editor.fontSize": 14,
  "editor.tabSize": 2,
  "editor.formatOnSave": true,
  "editor.minimap.enabled": false,
  "workbench.colorTheme": "Default Dark Modern",
  "terminal.integrated.fontSize": 13,
  "files.autoSave": "afterDelay",
  "files.autoSaveDelay": 1000
}
EOF

  chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.local/share/code-server"
  success "settings.json / argv.json 配置完了"
}

# ============================================================
# ⑥ systemd サービスの登録・起動
# ============================================================
setup_systemd() {
  info "systemd サービスを登録中..."
  cat > /etc/systemd/system/code-server.service <<EOF
[Unit]
Description=code-server (VS Code in Browser)
After=network.target

[Service]
Type=exec
User=${REAL_USER}
WorkingDirectory=${REAL_HOME}
ExecStart=/usr/bin/code-server
Restart=always
RestartSec=5
Environment=HOME=${REAL_HOME}

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable code-server
  systemctl restart code-server

  info "起動確認中..."
  sleep 4
  systemctl is-active --quiet code-server \
    && success "code-server 正常起動" \
    || { journalctl -u code-server -n 20 --no-pager; error "code-server の起動に失敗しました"; }
}

# ============================================================
# ⑦ Tailscale serve 設定(tailnet 内のみ HTTPS 公開)
# ============================================================
setup_tailscale_serve() {
  info "Tailscale serve に code-server (port ${CODE_SERVER_PORT}) を追加中..."

  # --https で tailnet 内に HTTPS 公開(インターネットには公開されない)
  tailscale serve --bg --https=${CODE_SERVER_PORT} "http://127.0.0.1:${CODE_SERVER_PORT}"
  success "Tailscale serve 設定完了(tailnet 内 HTTPS のみ)"

  # Tailscale ホスト名取得
  TS_HOSTNAME=$(tailscale status --json 2>/dev/null | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    s = d.get('Self', {})
    dns = s.get('DNSName', '').rstrip('.')
    print(dns if dns else s.get('HostName', 'unknown'))
except:
    print('unknown')
" 2>/dev/null || echo "unknown")

  echo ""
  echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo -e "${GREEN}  ✅ セットアップ完了!${NC}"
  echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
  echo ""
  echo -e "  🌐 アクセスURL(tailnet 内のデバイスからのみアクセス可):"
  echo -e "     ${CYAN}https://${TS_HOSTNAME}:${CODE_SERVER_PORT}${NC}"
  echo ""
  echo -e "  🔑 code-server パスワード: ${YELLOW}${PASSWORD}${NC}"
  echo ""
  echo -e "  🧩 日本語化手順(初回のみ):"
  echo -e "     1. Extensions で 'Japanese Language Pack for VS Code' をインストール"
  echo -e "     2. Ctrl+Shift+P → 'Configure Display Language' → 日本語を選択 → 再起動"
  echo ""
  echo -e "  📋 管理コマンド:"
  echo -e "     ログ確認:       journalctl -u code-server -f"
  echo -e "     再起動:         sudo systemctl restart code-server"
  echo -e "     停止:           sudo systemctl stop code-server"
  echo -e "     serve 確認:     tailscale serve status"
  echo -e "     serve 削除:     tailscale serve --https=${CODE_SERVER_PORT} off"
  echo ""
  echo -e "  ⚠️  このパスワードを安全な場所に保管してください。"
  echo -e "     設定ファイル: ${REAL_HOME}/.config/code-server/config.yaml"
  echo ""
}

# ============================================================
# メイン
# ============================================================
main() {
  prompt_password        # ① パスワード設定
  check_prerequisites    # ② Tailscale確認
  install_deps           # ③ 依存パッケージ
  install_code_server    # ④ code-server インストール
  setup_config           # ⑤ 設定ファイル生成
  setup_systemd          # ⑥ systemd 登録・起動
  setup_tailscale_serve  # ⑦ Tailscale serve 設定
}

main "$@"

Extensionsで「Japanese Language Pack for VS Code」をインストール。

Japanese Language Pack


Ctrl+Shift+Pを押して上の画面で下記を検索して「日本語」を選択。

Configure Display Language

Cockpit

#!/bin/bash
# ============================================================
# Cockpit を Tailscale serve 経由で HTTPS 公開するスクリプト
#   - BindTo + systemd socket で 127.0.0.1:19090 のみ待ち受け
#   - LAN からの直接アクセスはバインドアドレスで物理的に遮断
#   - TLS は tailscale serve が担当
# ============================================================
set -euo pipefail

# ============================================================
# ログ設定: /tmp/cockpit-setup.log に全出力を記録
# ============================================================
LOGFILE="/tmp/cockpit-setup.log"
exec > >(tee -a "$LOGFILE") 2>&1
echo "===== 開始: $(date '+%Y-%m-%d %H:%M:%S') =====" >> "$LOGFILE"

info()  { echo -e "\033[1;32m[INFO]\033[0m  $*"; }
warn()  { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; }
step()  { echo -e "\033[1;36m[STEP]\033[0m  >>> $* <<<"; }

# エラー発生時に行番号とコマンドを表示して終了
trap 'echo -e "\n\033[1;31m[ABORT]\033[0m スクリプトが異常終了しました(行: ${LINENO}, コマンド: ${BASH_COMMAND})" | tee -a "$LOGFILE"; echo "ログ: $LOGFILE"' ERR

# ============================================================
# 前提チェック
# ============================================================
step "前提チェック"
command -v tailscale >/dev/null 2>&1 || error "tailscale が見つかりません。先にインストールしてください。"

info "tailscale の状態を確認しています..."

TS_INFO=$(tailscale status --json 2>/dev/null | python3 -c "
import sys, json
d = json.load(sys.stdin)
state = d.get('BackendState', '')
name  = d.get('Self', {}).get('DNSName', '').rstrip('.')
ip    = (d.get('Self', {}).get('TailscaleIPs') or [''])[0]
print(state)
print(name or ip)
" 2>/dev/null) || error "tailscale status の取得に失敗しました。"

TS_STATE=$(echo "$TS_INFO" | sed -n '1p')
TAILSCALE_HOST=$(echo "$TS_INFO" | sed -n '2p')

info "BackendState: ${TS_STATE}"
info "Tailscale ホスト: ${TAILSCALE_HOST}"

if [[ "$TS_STATE" != "Running" ]]; then
  error "tailscale が Running 状態ではありません(現在: ${TS_STATE})。'sudo tailscale up' を実行してください。"
fi
if [[ -z "$TAILSCALE_HOST" ]]; then
  error "Tailscale のホスト名/IPを取得できませんでした。"
fi

# ============================================================
# 1. Cockpit インストール
# ============================================================
step "1. Cockpit インストール"
info "Cockpit をインストールします..."
sudo apt install -y cockpit cockpit-machines

# ============================================================
# 2. cockpit.conf を配置(systemd 起動前に済ませる)
# ============================================================
step "2. cockpit.conf 配置"
info "Cockpit の設定を書き込みます..."

sudo mkdir -p /etc/cockpit
sudo tee /etc/cockpit/cockpit.conf > /dev/null << EOF
[WebService]
Origins = https://${TAILSCALE_HOST}:9090 https://localhost:9090
AllowUnencrypted = true
EOF

# ============================================================
# 3. systemd socket を 127.0.0.1:19090 に上書き
#
#    cockpit.conf の Port/BindTo より systemd socket の
#    ListenStream が優先されるため、socket 側で確実に制御する
# ============================================================
step "3. cockpit.socket オーバーライド設定"
info "cockpit.socket を 127.0.0.1:19090 に設定します..."

sudo mkdir -p /etc/systemd/system/cockpit.socket.d
sudo tee /etc/systemd/system/cockpit.socket.d/override.conf > /dev/null << EOF
[Socket]
ListenStream=
ListenStream=127.0.0.1:19090
EOF

# daemon-reload → socket を完全停止 → 再起動(設定確実反映のため stop が重要)
sudo systemctl daemon-reload
sudo systemctl stop cockpit.socket cockpit.service 2>/dev/null || true
sudo systemctl enable --now cockpit.socket

info "Cockpit を 127.0.0.1:19090 で起動しました。"

# 本当に 127.0.0.1:19090 だけで LISTEN しているか確認
LISTEN_CHECK=$(sudo ss -tlnp | grep "19090" || true)
if [[ -z "$LISTEN_CHECK" ]]; then
  warn "19090 で LISTEN していません。'sudo systemctl status cockpit.socket' を確認してください。"
elif echo "$LISTEN_CHECK" | grep -qv "127.0.0.1:19090"; then
  warn "19090 が 127.0.0.1 以外でも LISTEN しています!設定を確認してください。"
  echo "$LISTEN_CHECK"
else
  info "確認OK: 127.0.0.1:19090 のみで LISTEN しています。"
fi

# ============================================================
# 4. tailscale serve に :9090 → localhost:19090 を追加
#    既存の設定は保持する
# ============================================================
step "4. tailscale serve 設定"
info "tailscale serve の設定を確認・追加します..."

echo ""
echo "----- 既存の tailscale serve 設定 -----"
tailscale serve status 2>/dev/null || echo "(設定なし)"
echo "----------------------------------------"
echo ""

if tailscale serve status 2>/dev/null | grep -q "proxy http://localhost:19090"; then
  info ":9090 → localhost:19090 はすでに設定されています。スキップします。"
elif tailscale serve status 2>/dev/null | grep -q ":9090"; then
  warn ":9090 に別の設定があります。上書きします..."
  sudo tailscale serve --bg --https=9090 http://localhost:19090
else
  sudo tailscale serve --bg --https=9090 http://localhost:19090
  info "tailscale serve に :9090 を追加しました。"
fi

# funnel(インターネット公開)が有効なら警告
if tailscale funnel status 2>/dev/null | grep -q ":9090"; then
  warn "!!! :9090 に tailscale funnel が設定されています(インターネット公開状態)!!!"
  warn "無効化するには: sudo tailscale funnel --https=9090 off"
fi

# ============================================================
# 5. 動作確認
# ============================================================
step "5. 動作確認"
echo ""
info "最終確認..."
echo ""

echo "----- LISTEN ポート確認 -----"
sudo ss -tlnp | grep -E "19090|9090" || true

echo ""
echo "----- tailscale serve 設定 -----"
tailscale serve status

# ============================================================
# 6. libvirt デフォルトプール・ISOプールの設定
# ============================================================
step "6. libvirt プール設定"
echo ""
info "libvirt プールを設定します..."

# libvirtd の有効化
step "6a. libvirtd 起動"
sudo systemctl enable libvirtd
sudo systemctl start libvirtd
# libvirtd が完全に起動するまで少し待つ
sleep 2

# VM ストレージ用ディレクトリの準備
step "6b. /opt/vm ディレクトリ準備"
sudo mkdir -p /opt/vm
sudo chown root:libvirt /opt/vm
sudo chmod 2775 /opt/vm

# 実行ユーザーを libvirt グループへ追加
CURRENT_USER="${SUDO_USER:-$USER}"
sudo usermod -aG libvirt "$CURRENT_USER"
info "ユーザー '${CURRENT_USER}' を libvirt グループに追加しました(次回ログイン時に有効)。"

# --- デフォルトプールの再定義 ---
step "6c. default プール定義"
# virsh の既存 default プールを一旦削除して /opt/vm で再作成する
if sudo virsh pool-info default &>/dev/null; then
  warn "既存の 'default' プールを削除して再定義します..."
  sudo virsh pool-destroy default 2>/dev/null || true
  sudo virsh pool-undefine default
fi

sudo virsh pool-define-as default dir --target /opt/vm
sudo virsh pool-build default || warn "pool-build default は既存ディレクトリのためスキップ扱い(問題なし)"
sudo virsh pool-start default
sudo virsh pool-autostart default
info "デフォルトプール: /opt/vm で設定完了。"

# --- ISO プールの設定(/iso が存在する場合のみ) ---
step "6d. iso プール定義(/iso チェック)"
if [[ -d /iso ]]; then
  info "/iso ディレクトリを検出しました。ISO イメージ用プールを登録します..."

  if sudo virsh pool-info iso &>/dev/null; then
    warn "既存の 'iso' プールを削除して再定義します..."
    sudo virsh pool-destroy iso 2>/dev/null || true
    sudo virsh pool-undefine iso
  fi

  sudo virsh pool-define-as iso dir --target /iso
  sudo virsh pool-build iso || warn "pool-build iso は既存ディレクトリのためスキップ扱い(問題なし)"
  sudo virsh pool-start iso
  sudo virsh pool-autostart iso
  info "ISO プール: /iso で設定完了。"
else
  info "/iso ディレクトリが存在しないため、ISO プールの設定をスキップしました。"
  info "後から追加する場合: sudo mkdir -p /iso && sudo virsh pool-define-as iso dir --target /iso && sudo virsh pool-build iso && sudo virsh pool-start iso && sudo virsh pool-autostart iso"
fi

echo ""
echo "----- virsh プール一覧 -----"
sudo virsh pool-list --all

echo ""
echo "======================================================"
echo " セットアップ完了!"
echo "======================================================"
echo ""
echo " アクセス先(Tailnet 内のみ):"
echo "   https://${TAILSCALE_HOST}:9090/"
echo ""
echo " セキュリティ:"
echo "   - Cockpit バックエンド : 127.0.0.1:19090(LAN 到達不可)"
echo "   - TLS 終端            : tailscale serve (:9090)"
echo "   - インターネット公開  : tailscale funnel 未設定"
echo "   - UFW                 : 変更なし(既存サービス影響なし)"
echo ""
echo " libvirt プール:"
echo "   - default : /opt/vm(VM ディスクイメージ用)"
if [[ -d /iso ]]; then
  echo "   - iso     : /iso(ISO イメージ用)"
else
  echo "   - iso     : 未設定(/iso が存在しないためスキップ)"
fi
echo "======================================================"
echo ""
echo " ログファイル: $LOGFILE"
echo "======================================================"

一度再起動。

sudo reboot

qcow2ディスクの拡張

sudo qemu-img resize /opt/vm/Win11.qcow2 +60G

virt-managerもインストールするなら

sudo apt install -y virt-manager

Windows 11の場合

マイクロソフトのサイトからWin11_25H2_Japanese_x64_v2.isoをダウンロードします。

mv Win11_25H2_Japanese_x64_v2.iso /opt/vm

次にvirtio-win ISOをダウンロードします。

cd /opt/vm
wget https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso

WindowsのISOとvirtio-win.isoをマウントして、セットアップを開始します。なお、Cockpit用のVNCと、virt-viewer用のSPICEの両方を有効にしています。

nano vm-windows11-setup.sh
#!/bin/bash
VM_NAME="Windows11"
ISO_PATH="/opt/vm/Win11_25H2_Japanese_x64_v2.iso"
ISO2_PATH="/opt/vm/virtio-win.iso"
DISK_PATH="/opt/vm/Windows11.qcow2"
DISK_SIZE="64"
RAM="4096"
VCPUS="4"

qemu-img create -f qcow2 "$DISK_PATH" "${DISK_SIZE}G"

virt-install \
  --name "$VM_NAME" \
  --ram "$RAM" \
  --vcpus "$VCPUS" \
  --cpu host-passthrough \
  --os-variant win11 \
  --disk path="$DISK_PATH",format=qcow2,bus=virtio \
  --disk path="$ISO_PATH",device=cdrom,bus=sata \
  --disk path="$ISO2_PATH",device=cdrom,bus=sata \
  --network network=default,model=e1000e \
  --graphics spice,listen=127.0.0.1,gl.enable=no \
  --graphics vnc,listen=127.0.0.1 \
  --video qxl \
  --boot uefi,bootmenu.enable=yes,cdrom,hd \
  --features smm=on,kvm_hidden=on \
  --clock hypervclock_present=yes \
  --tpm backend.type=emulator,backend.version=2.0,model=tpm-crb \
  --noautoconsole

echo "VM '$VM_NAME' を作成しました。"
bash vm-windows11-setup.sh

ゲストエージェントのインストール

virtio-win.isoが無いなら、サイトからゲストエージェントをダウンロード(現時点ではvirtio-win-guest-tools.exe)。SPICEを利用するならSPICE Guest Toolsをインストール。

ゲストエージェントをインストールしたらネットワークドライバをvirtioに変更。

パスワードを設定した場合の自動ログイン設定

コマンドプロンプトを管理者で実行してレジストリの設定を変更。

reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\PasswordLess\Device" /v DevicePasswordLessBuildVersion /t REG_DWORD /d 0 /f

パスワード設定画面を表示して設定。

control userpasswords2

デスクトップアイコンを表示するならこちら

Ubuntu 26.04 デスクトップ版の場合

下記で作成(–os-variantは26.04がまだ登録されていないため)。ちなみにISOブート用に保存したISOイメージを使用するならsudo mount /dev/nvme0n1p3 /isoで一時的にマウントしてISO_PATHを/iso/ubuntu-26.04-desktop-amd64.isoなどに。永続的にマウントするならこちらで設定。

cd /opt/vm
wget https://releases.ubuntu.com/26.04/ubuntu-26.04-desktop-amd64.iso
nano vm-ubuntu2604-setup.sh
#!/bin/bash
VM_NAME="Ubuntu2604"
ISO_PATH="/opt/vm/ubuntu-26.04-desktop-amd64.iso"
DISK_PATH="/opt/vm/Ubuntu2604.qcow2"
DISK_SIZE="60"
RAM="6144"
VCPUS="4"

qemu-img create -f qcow2 "$DISK_PATH" "${DISK_SIZE}G"

virt-install \
  --name "$VM_NAME" \
  --ram "$RAM" \
  --vcpus "$VCPUS" \
  --cpu host-passthrough \
  --os-variant ubuntu25.10 \
  --disk path="$DISK_PATH",format=qcow2,bus=virtio \
  --disk path="$ISO_PATH",device=cdrom,bus=sata \
  --network network=default,model=virtio \
  --graphics spice,listen=127.0.0.1,gl.enable=no \
  --graphics vnc,listen=127.0.0.1 \
  --video virtio \
  --boot uefi,bootmenu.enable=yes,cdrom,hd \
  --noautoconsole

echo "VM '$VM_NAME' を作成しました。"
bash vm-ubuntu2604-setup.sh

ゲストエージェントをインストール

spice-vdagentの確認(多分不要)

# インストール済みか確認
dpkg -l spice-vdagent
# サービスの状態確認
systemctl status spice-vdagent
# 実行中のプロセス確認
pgrep -a vdagent

インストールされていなかった場合

sudo apt install spice-vdagent

qemu-guest-agentの確認(こちらは多分必要)

# インストール済みか確認
dpkg -l qemu-guest-agent

# サービスの状態確認
systemctl status qemu-guest-agent

# 実行中のプロセス確認
pgrep -a qemu-ga

インストールされていなかった場合

sudo apt install -y qemu-guest-agent
sudo systemctl enable --now qemu-guest-agent

Ubuntu 26.04 サーバ版の場合

cd /opt/vm
wget https://releases.ubuntu.com/26.04/ubuntu-26.04-live-server-amd64.iso
nano vm-ubuntu2604sv-setup.sh
#!/bin/bash
VM_NAME="Ubuntu2604sv"
ISO_PATH="/opt/vm/ubuntu-26.04-live-server-amd64.iso"
DISK_PATH="/opt/vm/Ubuntu2604sv.qcow2"
DISK_SIZE="60"
RAM="6144"
VCPUS="4"

qemu-img create -f qcow2 "$DISK_PATH" "${DISK_SIZE}G"

virt-install \
  --name "$VM_NAME" \
  --ram "$RAM" \
  --vcpus "$VCPUS" \
  --cpu host-passthrough \
  --os-variant ubuntu25.10 \
  --disk path="$DISK_PATH",format=qcow2,bus=virtio \
  --disk path="$ISO_PATH",device=cdrom,bus=sata \
  --network network=default,model=virtio \
  --graphics spice,listen=127.0.0.1,gl.enable=no \
  --graphics vnc,listen=127.0.0.1 \
  --video vga \
  --boot uefi,bootmenu.enable=yes,cdrom,hd \
  --noautoconsole

echo "VM '$VM_NAME' を作成しました。"
bash vm-ubuntu2604sv-setup.sh

LXD-UI

#!/bin/bash
set -e
# LXD インストール
sudo snap install lxd --channel=latest/stable
# 初期化
sudo lxd init --minimal
# HTTPS API を有効化
sudo lxc config set core.https_address :8443
# UI 有効化
sudo snap set lxd ui.enable=true
sudo systemctl reload snap.lxd.daemon
# ユーザーを lxd グループに追加
sudo usermod -aG lxd $USER
echo ""
echo "=========================================="
echo " LXD-UI セットアップ完了"
echo "=========================================="
echo " アクセス先:"
echo "   https://$(hostname):8443"
echo "=========================================="
echo ""
echo "※ グループ変更を反映するため、一度ログアウト&再ログインしてください"
sudo reboot

6個のスクリプトを一度に作成して保存するスクリプト

コピーボタンでまとめてコピーして、ターミナルに貼り付けて実行するだけです。
実行後、以下の6ファイルが /opt/script/lxd/ に作成され、実行権限も付与されます。

生成される6つのスクリプトの内容:

ファイル名内容
minimal-lxd-base-create.shコンテナ作成・マウントディレクトリチェック付き
first-setup-minimal-lxd-base.shapt update/upgrade・curl・Tailscaleインストール後シャットダウン
docker-lxd-base-create.shlxd-base-minimalをコピーしてDocker環境構築・シャットダウン
copy-lxd-create.sh既存コンテナをコピーして新規作成
snapshot-lxd.shスナップショット作成
enter-lxd-container.shコンテナ選択して入る
#!/bin/bash
set -euo pipefail

sudo mkdir -p /opt/script/lxd

cat > /tmp/minimal-lxd-base-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

CONTAINER="lxd-base-minimal"
MOUNT_PATH="/opt/lxd-data"

echo "=== lxd-base-minimal コンテナを作成 ==="
lxc launch ubuntu:26.04 "$CONTAINER"

echo "=== $MOUNT_PATH が存在しない場合は作成 ==="
if [ ! -d "$MOUNT_PATH" ]; then
    sudo mkdir -p "$MOUNT_PATH"
    if [ ! -d "$MOUNT_PATH" ]; then
        echo "エラー: $MOUNT_PATH の作成に失敗しました"
        exit 1
    fi
    echo "    $MOUNT_PATH を作成しました"
else
    echo "    $MOUNT_PATH は既に存在します"
fi

echo "=== ホストの $MOUNT_PATH をコンテナに同じパスでマウント ==="
lxc config device add "$CONTAINER" opt-lxd-data disk source="$MOUNT_PATH" path="$MOUNT_PATH"

echo "=== ID マッピング設定を適用 ==="
lxc config set "$CONTAINER" raw.idmap "both 1000 1000"

echo "=== コンテナを再起動します ==="
lxc restart "$CONTAINER"

echo "=== 完了しました ==="
EOF

cat > /tmp/first-setup-minimal-lxd-base.sh << 'EOF'
#!/bin/bash
set -euo pipefail

CONTAINER="lxd-base-minimal"

echo "==> コンテナ '${CONTAINER}' に接続してセットアップを開始します..."

lxc exec "${CONTAINER}" -- bash -euo pipefail << 'INNER'
echo "==> apt update"
sudo apt update

echo "==> apt upgrade"
sudo apt upgrade -y

echo "==> curl インストール"
sudo apt install -y curl

echo "==> Tailscale インストール"
curl -fsSL https://tailscale.com/install.sh | sh

echo "==> セットアップ完了"
INNER

echo "==> コンテナをシャットダウンします..."
lxc stop "${CONTAINER}"

echo "==> 完了"
EOF

cat > /tmp/docker-lxd-base-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

SRC="lxd-base-minimal"
NEW="lxd-base-docker"
MOUNT_PATH="/opt/lxd-data"

echo "=== コンテナをコピー: $SRC → $NEW ==="
lxc copy "$SRC" "$NEW"

echo "=== コンテナを停止します(設定適用のため) ==="
lxc stop "$NEW" 2>/dev/null || true

echo "=== raw.idmap を設定 ==="
lxc config set "$NEW" raw.idmap "both 1000 1000"

echo "=== Nesting を Allow に設定 ==="
lxc config set "$NEW" security.nesting true

echo "=== コンテナを起動 ==="
lxc start "$NEW"

echo "=== マウントディレクトリの権限設定(ホスト側) ==="
sudo chown 1000:1000 "$MOUNT_PATH"
sudo chmod 775 "$MOUNT_PATH"

echo "=== 設定反映のため再起動 ==="
lxc restart "$NEW"

echo "=== ネットワーク疎通を待機中 ==="
for i in $(seq 1 30); do
    if lxc exec "$NEW" -- curl -fsSL --max-time 3 https://get.docker.com -o /dev/null 2>/dev/null; then
        echo "    ネットワーク疎通確認 (${i}秒)"
        break
    fi
    echo "    待機中... (${i}/30秒)"
    sleep 1
    if [ "$i" -eq 30 ]; then
        echo "エラー: ネットワークが30秒以内に疎通しませんでした" >&2
        exit 1
    fi
done

echo "=== コンテナ内でセットアップを実行 ==="
lxc exec "$NEW" -- bash -euo pipefail << 'INNER'

echo "--- Docker インストール ---"
curl -fsSL https://get.docker.com | sh

echo "--- /opt/docker ディレクトリのセットアップ ---"
mkdir -p /opt/docker

if getent group docker > /dev/null 2>&1; then
    chown -R "${USER:-root}:docker" /opt/docker
    chmod -R 775 /opt/docker
    chmod -R g+s /opt/docker
    if [ "${USER:-root}" != "root" ]; then
        usermod -aG docker "$USER"
        echo "グループ変更を反映するには、newgrp docker を実行してください(再起動不要)"
    else
        echo "rootユーザーのため usermod はスキップしました"
    fi
else
    chown -R "${USER:-root}:${USER:-root}" /opt/docker
    chmod -R 755 /opt/docker
    echo "docker グループが存在しないため、オーナー権限のみ設定しました"
fi

echo "/opt/docker のセットアップ完了"
INNER

echo "=== コンテナをシャットダウン ==="
lxc stop "$NEW"

echo "=== 完了: $NEW ==="
EOF

cat > /tmp/copy-lxd-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

containers=($(lxc list -c n --format csv))

echo "=== コピー元のコンテナを選択してください ==="
i=1
for c in "${containers[@]}"; do
    echo "$i) $c"
    ((i++))
done

read -p "番号を入力: " index

if ! [[ "$index" =~ ^[0-9]+$ ]]; then
    echo "エラー: 数字を入力してください" >&2
    exit 1
fi

index=$((index - 1))

if [ "$index" -lt 0 ] || [ "$index" -ge "${#containers[@]}" ]; then
    echo "エラー: 不正な番号です" >&2
    exit 1
fi

SRC="${containers[$index]}"
echo "選択されたコピー元: $SRC"

read -p "新しいコンテナ名を入力: " NEW

if [ -z "$NEW" ]; then
    echo "エラー: コンテナ名が空です" >&2
    exit 1
fi

echo "=== コピー開始: $SRC → $NEW ==="
lxc copy "$SRC" "$NEW"

echo "=== 一旦コンテナを停止します(ID マップ適用のため) ==="
lxc stop "$NEW" 2>/dev/null || true

echo "=== raw.idmap を設定します ==="
lxc config set "$NEW" raw.idmap "both 1000 1000"

echo "=== コンテナを起動します ==="
lxc start "$NEW"

echo "=== 設定反映のため再起動 ==="
lxc restart "$NEW"

echo "=== コンテナに入ります: $NEW ==="
lxc exec "$NEW" -- bash
EOF

cat > /tmp/snapshot-lxd.sh << 'EOF'
#!/bin/bash
set -euo pipefail

echo "=== LXD コンテナ一覧 ==="

# コンテナ名一覧を配列に格納
containers=($(lxc list -c n --format csv))

# 一覧表示
i=1
for c in "${containers[@]}"; do
    echo "$i) $c"
    ((i++))
done

# 番号選択
read -p "番号を入力: " index

# 数字チェック
if ! [[ "$index" =~ ^[0-9]+$ ]]; then
    echo "エラー: 数字を入力してください" >&2
    exit 1
fi

index=$((index - 1))

# 範囲チェック
if [ "$index" -lt 0 ] || [ "$index" -ge "${#containers[@]}" ]; then
    echo "エラー: 不正な番号です" >&2
    exit 1
fi

TARGET="${containers[$index]}"
echo "選択されたコンテナ: $TARGET"

# コメント入力
read -p "スナップショットのコメントを入力(任意・空可): " COMMENT

# スペースをハイフンに変換
COMMENT="${COMMENT// /-}"

# スナップショット名(コメントがあれば付加)
SNAP="snap-$(date +%Y%m%d-%H%M%S)"
if [ -n "$COMMENT" ]; then
    SNAP="${SNAP}-${COMMENT}"
fi

echo "=== コンテナ停止: $TARGET ==="
lxc stop "$TARGET" 2>/dev/null || true

echo "=== スナップショット作成: $TARGET/$SNAP ==="
lxc snapshot "$TARGET" "$SNAP"

echo "=== コンテナ起動: $TARGET ==="
lxc start "$TARGET"

echo "=== 完了しました ==="
echo "作成されたスナップショット: $TARGET/$SNAP"
EOF

cat > /tmp/enter-lxd-container.sh << 'EOF'
#!/bin/bash
set -euo pipefail

echo "=== LXD コンテナ一覧 ==="

containers=($(lxc list -c n --format csv))

i=1
for c in "${containers[@]}"; do
    echo "$i) $c"
    ((i++))
done

read -p "番号を入力: " index

if ! [[ "$index" =~ ^[0-9]+$ ]]; then
    echo "エラー: 数字を入力してください" >&2
    exit 1
fi

index=$((index - 1))

if [ "$index" -lt 0 ] || [ "$index" -ge "${#containers[@]}" ]; then
    echo "エラー: 不正な番号です" >&2
    exit 1
fi

TARGET="${containers[$index]}"
echo "選択されたコンテナ: $TARGET"

STATUS=$(lxc info "$TARGET" | awk '/Status:/ {print tolower($2)}')

if [ "$STATUS" != "running" ]; then
    echo "=== コンテナが停止中です。起動します ==="
    lxc start "$TARGET"
else
    echo "=== コンテナは既に起動中です ==="
fi

echo "=== コンテナに入ります: $TARGET ==="
lxc exec "$TARGET" -- bash
EOF

sudo mv /tmp/minimal-lxd-base-create.sh /opt/script/lxd/
sudo mv /tmp/first-setup-minimal-lxd-base.sh /opt/script/lxd/
sudo mv /tmp/docker-lxd-base-create.sh /opt/script/lxd/
sudo mv /tmp/copy-lxd-create.sh /opt/script/lxd/
sudo mv /tmp/snapshot-lxd.sh /opt/script/lxd/
sudo mv /tmp/enter-lxd-container.sh /opt/script/lxd/

sudo chmod +x /opt/script/lxd/*.sh
sudo chown $USER:$USER /opt/script/lxd/*.sh

echo "=== 完了 ==="
ls -la /opt/script/lxd/

スクリプトを利用してベースコンテナを作成するなら次のコマンドで。

cd /opt/script/lxd/

# ベースコンテナを作成
./minimal-lxd-base-create.sh

# 作成したコンテナ内をアップデートしてTailscaleをインストール
./first-setup-minimal-lxd-base.sh

# そのコンテナをコピーしDocker環境のコンテナを作成
./docker-lxd-base-create.sh

以後は、いずれかのコンテナをコピーして利用します。

./copy-lxd-create.sh

コピー後にコンテナに入りますが、一度出た環境でも、LXD-UIでターミナルに入って作業しても良いですし、下記でコンテナを選択して入れます。

./enter-lxd-container.sh

コピーしたコンテナに入ったら下記で認証しておきましょう。

tailscale up

exitで一度出て、スナップショットを取っておくと安心。

./snapshot-lxd.sh
タイトルとURLをコピーしました