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.sh | apt update/upgrade・curl・Tailscaleインストール後シャットダウン |
docker-lxd-base-create.sh | lxd-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



