Ubuntuをデスクトップ環境で使う方法をまとめました。
- 後半にISOを置きISOブートでインストール
- Ubuntuインストール直後。アップデート・Tailscale・SSH
- ノートPCなどでDesktop版をサーバにしている場合
- モニターを接続していない環境でデスクトップ共有
- ISOブートしていた場合
- Timeshift
- Google Chrome
- code-server
- Cockpit
- virt-manager
- VM Manager
- LXD-UI
- Filebrowser
- Copyparty
- nextExplorer & OnlyOffice
- Linkwarden
- Karakeep
- selfmark
- Dockhand
- Outline
- AFFiNE
- Memos
- EasyNote
- Immich
- Vaultwarden
- SPW
- Nextcloud&OnlyOffice
- FreshRSS
- Syncthing
- opencode
- KonomiTV
後半にISOを置きISOブートでインストール
デスクトップ環境で使うなら不具合時の復元しやすさを重視して後半パーティションにISOイメージを保存し、そこからインストールするのが楽だと思います。まだ作っていないならインストール時に作成してもよいですし、あらかじめ作成してそこからブートしてクリーンインストールしても良いかと。

ISO用のパーティションを/isoにマウントするスクリプト
パーティションのみ作成してこれからコピーする場合。下記スクリプトはそのパーティションを /iso にマウントするスクリプト。一時的にマウントすることも、永続的にマウントすることも可能です。もっともクリーンインストールするならあえて/isoにマウントする必要もないかもしれませんが。ただ、このスクリプトをISOイメージのあるパーティションに保存しておけば、クリーンインストールしたあとも役立つので。
後半パーティションをマウントし、右クリックしてターミナルを表示して下記を実行。
nano mount-setup.sh
sudo bash mount-setup.sh
#!/bin/bash
# ============================================================
# 対話式 fstab マウント登録スクリプト
# - ディスク一覧を表示して番号で選択
# - 永続マウント(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 "続けますか?(現在のマウントは変更しません)" || exit 0
fi
# ============================================================
# 3. マウント方式の選択
# ============================================================
echo ""
echo "======================================================"
echo " マウント方式を選択してください"
echo "======================================================"
echo ""
echo " 1) 永続マウント - fstab に登録し、再起動後も自動マウント"
echo " 2) 一時マウント - 今回のみマウント(再起動で解除)"
echo ""
MOUNT_MODE=""
while true; do
read -rp "$(echo -e "\033[1;36m[入力]\033[0m 番号を入力してください [1/2]: ")" MODE_SEL
case "$MODE_SEL" in
1) MOUNT_MODE="persistent"; break ;;
2) MOUNT_MODE="temporary"; break ;;
*) warn "1 または 2 を入力してください。" ;;
esac
done
echo ""
if [[ "$MOUNT_MODE" == "persistent" ]]; then
info "モード: 永続マウント(fstab 登録あり)"
else
info "モード: 一時マウント(fstab 登録なし)"
fi
# ============================================================
# 4. 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"
# FS タイプの決定(空なら auto)
FS_TYPE="${SELECTED_FS:-auto}"
# ntfs の場合は ntfs-3g に補正
[[ "$FS_TYPE" == "ntfs" ]] && FS_TYPE="ntfs-3g"
# ============================================================
# 5. /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
# ============================================================
# 6. fstab の重複チェックと追記(永続マウント時のみ)
# ============================================================
if [[ "$MOUNT_MODE" == "persistent" ]]; then
# 同じ 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
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 への追記が完了しました。"
fi
# ============================================================
# 7. マウント実行
# ============================================================
echo ""
if [[ "$MOUNT_MODE" == "persistent" ]]; then
info "mount -a でマウントを実行します..."
if mount -a 2>&1; then
info "マウント成功!"
else
warn "mount -a でエラーが発生しました。fstab の内容を確認してください:"
tail -5 "$FSTAB"
exit 1
fi
else
info "mount -t ${FS_TYPE} ${SELECTED_DEV} ${MOUNT_POINT} でマウントを実行します..."
if mount -t "${FS_TYPE}" "${SELECTED_DEV}" "${MOUNT_POINT}" 2>&1; then
info "マウント成功!"
else
warn "マウントに失敗しました。デバイスと FS タイプを確認してください。"
exit 1
fi
fi
# ============================================================
# 8. 確認
# ============================================================
echo ""
echo "======================================================"
if [[ "$MOUNT_MODE" == "persistent" ]]; then
echo " 完了!(永続マウント)マウント状態:"
else
echo " 完了!(一時マウント・再起動で解除)マウント状態:"
fi
echo "======================================================"
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT | grep -E "NAME|$(basename "$SELECTED_DEV")"
echo ""
df -h "$MOUNT_POINT"
echo "======================================================"
grubメニューに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 ""
# ループデバイス除外・パーティションのみを配列に収集
mapfile -t part_list < <(
lsblk -lno NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS \
| grep -v '^loop' \
| awk 'NF && $1 ~ /[0-9]$/ { print $0 }'
)
if [[ ${#part_list[@]} -eq 0 ]]; then
die "パーティションが見つかりませんでした。"
fi
# 番号付きで表示
echo -e " ${BOLD}No. NAME SIZE FSTYPE LABEL MOUNTPOINTS${RESET}"
printf ' %s\n' "$(printf -- '-%.0s' {1..60})"
for i in "${!part_list[@]}"; do
printf " ${CYAN}[%2d]${RESET} %s\n" "$((i+1))" "${part_list[$i]}"
done
echo ""
while true; do
echo -en "${BOLD}番号を入力してください${RESET} [1-${#part_list[@]}]: "
read -r sel
# 数値チェック
if ! [[ "$sel" =~ ^[0-9]+$ ]] || \
(( sel < 1 || sel > ${#part_list[@]} )); then
error "1 〜 ${#part_list[@]} の番号を入力してください。"
continue
fi
local chosen_line="${part_list[$((sel-1))]}"
local part_name
part_name=$(echo "$chosen_line" | awk '{print $1}')
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 "$@"
Ubuntuインストール直後。アップデート・Tailscale・SSH
Ubuntuのインストールが完了したらまずはアップデート。
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個くらいなら一時的なマウントで良いと思うので不要)。
最初に行ったスクリプトを保存していたなら、それを実行。
sudo bash mount-setup.sh
Timeshift
スナップショットを保存するために。rsyncバージョンだとしてもこのあたりで一度取っておくと楽なのでは。
sudo apt install -y timeshift
一通りセットアップした後、ここで紹介している方法でVMなどを構築したなら、/opt以下も対象外にするように除外フォルダを追加しておくと良いかも。またLXDのストレージプール/var/snap/lxd/common/lxd/も外しておいて良いでしょう。
Google Chrome
# 必要なパッケージを準備
sudo apt update
sudo apt install curl gnupg
# GoogleのGPGキーを追加
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/google-chrome.gpg
# リポジトリを追加
echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list
# インストール
sudo apt update
sudo apt install google-chrome-stable
# 起動
google-chrome-stable
Yahoo!ニュースの動画やもしかしたらTVerなどの動画を見たいなら。Chromeウェブストアで下記拡張機能を検索してインストールします。
User-Agent Switcher and Manager
デスクトップにショートカットを置いたり、プロファイルごとのショートカットを作るならこちら。
起動時にマウスカーソルが長時間クルクルするのを止めるには下記。
cp /usr/share/applications/google-chrome.desktop ~/.local/share/applications/ && sed -i 's/StartupNotify=true/StartupNotify=false/g' ~/.local/share/applications/google-chrome.desktop
code-server
これはセルフホスト版。そのパソコンで直接操作するなら普通にVSCodeをインストールしたほうが良いです。
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 内のみ公開)
# - Japanese Language Pack 自動インストール済み
# 前提:
# - tailscale up 済み
# - 実行: sudo bash install-codeserver2.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=""
# ============================================================
# ① パスワードを最初に聞く(デフォルト: M=手動入力)
# ============================================================
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 -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
A)
PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
success "パスワードを自動生成しました。(後で表示します)"
;;
*)
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
;;
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} インストール完了"
}
# ============================================================
# ⑤ Japanese Language Pack のインストール
# ============================================================
install_japanese_language_pack() {
info "Japanese Language Pack をインストール中..."
# code-server の拡張機能ディレクトリを確保
EXT_DIR="$REAL_HOME/.local/share/code-server/extensions"
mkdir -p "$EXT_DIR"
chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.local/share/code-server"
# 実ユーザーとして拡張機能をインストール
sudo -u "$REAL_USER" code-server --install-extension MS-CEINTL.vscode-language-pack-ja \
|| error "Japanese Language Pack のインストールに失敗しました"
success "Japanese Language Pack インストール完了"
}
# ============================================================
# ⑥ 設定ファイルの生成(日本語化設定込み)
# ============================================================
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
# locale.json: 表示言語の明示設定
cat > "$VSCODE_USER_DIR/locale.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 / locale.json 配置完了"
}
# ============================================================
# ⑦ systemd サービスの登録・起動
# ============================================================
setup_systemd() {
info "systemd サービスを登録中..."
NLS_CONFIG='{"locale":"ja","osLocale":"ja","availableLanguages":{"*":"ja"}}'
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}
Environment=VSCODE_NLS_CONFIG=${NLS_CONFIG}
[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}) を追加中..."
tailscale serve --bg --https=${CODE_SERVER_PORT} "http://127.0.0.1:${CODE_SERVER_PORT}"
success "Tailscale serve 設定完了(tailnet 内 HTTPS のみ)"
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 ""
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 # ① パスワード設定(デフォルト: M)
check_prerequisites # ② Tailscale確認
install_deps # ③ 依存パッケージ
install_code_server # ④ code-server インストール
install_japanese_language_pack # ⑤ Japanese Language Pack インストール
setup_config # ⑥ 設定ファイル生成(日本語化込み)
setup_systemd # ⑦ systemd 登録・起動
setup_tailscale_serve # ⑧ Tailscale serve 設定
}
main "$@"
日本語化パックはインストール済みにしてありますが、もしなければExtensionsで「Japanese Language Pack for VS Code」をインストール。
Japanese Language Pack
日本語表示にする
こちらの設定が必要。上のバーで下記を入力して「日本語」を選択。
>Configure Display Language
階層をまとめない設定
「設定」画面を開き、「機能」-「エクスプローラー」で、「CompactFolders」のチェックをOffにします。
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
virt-manager
Ubuntuデスクトップで使うならvirt-managerが高機能です。
sudo apt install -y virt-manager
VM Manager
Cockpit仮想マシンプラグインの置き換えを目指したツールで、仮想マシンプラグインでは行えない機能の追加などがやりやすくなっています。またこのあと説明するディスク拡張機能も組み込まれています。インストールはこちら。
qcow2ディスクの拡張
sudo qemu-img resize /opt/vm/Win11.qcow2 +60G
qcow2ディスクの拡張をスクリプトで簡単に
cd /opt/vm
nano qcow2_resize.sh
chmod +x qcow2_resize.sh
bash qcow2_resize.sh
#!/bin/bash
# ============================================================
# qcow2_resize.sh
# qcow2 ファイル+スナップショットファイルを一覧・選択して拡張
# ============================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
RESET='\033[0m'
# ---- sudo プレフィックス(root でなければ sudo を使う)----
if [[ $EUID -ne 0 ]]; then
SUDO="sudo"
else
SUDO=""
fi
# ---- 依存確認 ---------------------------------------------
check_deps() {
local missing=()
for cmd in qemu-img find numfmt python3; do
command -v "$cmd" &>/dev/null || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo -e "${RED}エラー: 以下のコマンドが見つかりません: ${missing[*]}${RESET}"
echo -e " sudo apt install qemu-utils coreutils python3"
exit 1
fi
}
usage() {
echo -e "${BOLD}使い方:${RESET} $0 [検索ディレクトリ]"
exit 0
}
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
SEARCH_DIR="${1:-.}"
[[ ! -d "$SEARCH_DIR" ]] && { echo -e "${RED}エラー: ディレクトリが見つかりません: $SEARCH_DIR${RESET}"; exit 1; }
SEARCH_DIR="$(realpath "$SEARCH_DIR")"
echo -e ""
echo -e "${CYAN}${BOLD}╔══════════════════════════════════════════╗${RESET}"
echo -e "${CYAN}${BOLD}║ qcow2 ディスク拡張スクリプト ║${RESET}"
echo -e "${CYAN}${BOLD}╚══════════════════════════════════════════╝${RESET}"
echo -e " 検索対象: ${BOLD}${SEARCH_DIR}${RESET}"
if [[ -n "$SUDO" ]]; then
echo -e " ${YELLOW}root 以外で実行中のため qemu-img / resize に sudo を使用します${RESET}"
fi
echo -e ""
check_deps
# ---- ヘルパー: 仮想サイズ・実サイズ取得 -------------------
get_size_info() {
local f="$1"
local json vsize dsize vsize_h dsize_h
# Permission denied 対策: sudo で実行、失敗しても処理継続
json=$($SUDO qemu-img info --output=json "$f" 2>/dev/null || echo '{}')
vsize=$(echo "$json" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d.get('virtual-size',0))" 2>/dev/null || echo 0)
dsize=$(echo "$json" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d.get('actual-size',0))" 2>/dev/null || echo 0)
vsize_h=$(numfmt --to=iec-i --suffix=B "$vsize" 2>/dev/null || echo "${vsize}B")
dsize_h=$(numfmt --to=iec-i --suffix=B "$dsize" 2>/dev/null || echo "${dsize}B")
echo "${vsize_h} ${dsize_h}"
}
# ---- ヘルパー: backing file パス取得 ----------------------
get_backing() {
local f="$1"
$SUDO qemu-img info --output=json "$f" 2>/dev/null \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
for key in ('full-backing-filename', 'backing-filename'):
v = d.get(key, '')
if v:
print(v)
break
else:
print('')
" 2>/dev/null || echo ''
}
# ---- 除外する拡張子パターン --------------------------------
is_excluded() {
local name
name="$(basename "$1")"
[[ "$name" =~ \.(iso|sh|img|raw|vmdk|vdi|vhd|vhdx|ova|ovf|log|conf|xml|json|txt|py|yaml|yml)$ ]] && return 0
return 1
}
# ================================================================
# ステップ1: .qcow2 ファイルをベースとして収集
# ================================================================
mapfile -t BASE_FILES < <(
find "$SEARCH_DIR" -maxdepth 3 -type f -name "*.qcow2" | sort
)
if [[ ${#BASE_FILES[@]} -eq 0 ]]; then
echo -e "${YELLOW}.qcow2 ファイルが見つかりませんでした。${RESET}"
exit 0
fi
# ================================================================
# ステップ2: 各ベースと同ディレクトリにある
# "ベース名." プレフィックスのファイルをスナップとして収集
# ================================================================
declare -a ENTRIES=()
declare -a ENTRY_LABELS=()
declare -a ENTRY_TYPES=()
for base in "${BASE_FILES[@]}"; do
base_dir="$(dirname "$base")"
base_stem="$(basename "$base" .qcow2)"
sizes=$(get_size_info "$base")
vsize_h=$(echo "$sizes" | awk '{print $1}')
dsize_h=$(echo "$sizes" | awk '{print $2}')
ENTRIES+=("$base")
ENTRY_TYPES+=("base")
ENTRY_LABELS+=("$(printf '%-48s 仮想:%-9s 実:%-9s' \
"$(basename "$base")" "$vsize_h" "$dsize_h")")
# "ベース名." にマッチするファイルを列挙(.qcow2 本体を除く)
while IFS= read -r candidate; do
cname="$(basename "$candidate")"
[[ "$cname" == "${base_stem}.qcow2" ]] && continue
is_excluded "$candidate" && continue
[[ ! -s "$candidate" ]] && continue
snap_sizes=$(get_size_info "$candidate")
sv_h=$(echo "$snap_sizes" | awk '{print $1}')
sd_h=$(echo "$snap_sizes" | awk '{print $2}')
ENTRIES+=("$candidate")
ENTRY_TYPES+=("snap")
ENTRY_LABELS+=("$(printf ' └─ %-44s 仮想:%-9s 実:%-9s' \
"[snap] ${cname}" "$sv_h" "$sd_h")")
done < <(find "$base_dir" -maxdepth 1 -type f -name "${base_stem}.*" | sort)
done
# ================================================================
# ステップ3: 一覧表示
# ================================================================
echo -e "${BOLD} 番号 ファイル名 仮想サイズ 実サイズ${RESET}"
echo -e " ───────────────────────────────────────────────────────────────────────────────────"
for i in "${!ENTRY_LABELS[@]}"; do
if [[ "${ENTRY_TYPES[$i]}" == "snap" ]]; then
color="$MAGENTA"
else
color="$RESET"
fi
printf " ${CYAN}%3d${RESET} ${color}%s${RESET}\n" "$((i+1))" "${ENTRY_LABELS[$i]}"
done
echo -e " ───────────────────────────────────────────────────────────────────────────────────"
echo -e " ${MAGENTA}紫: スナップショットファイル${RESET} 白: ベース (.qcow2)"
echo -e ""
# ================================================================
# ステップ4: 番号選択
# ================================================================
while true; do
read -rp "$(echo -e " ${BOLD}拡張するファイルの番号 [1-${#ENTRIES[@]}]:${RESET} ")" sel
if [[ "$sel" =~ ^[0-9]+$ ]] && (( sel >= 1 && sel <= ${#ENTRIES[@]} )); then
break
fi
echo -e " ${RED}無効な入力です。1〜${#ENTRIES[@]} の数値を入力してください。${RESET}"
done
IDX=$((sel-1))
TARGET_FILE="${ENTRIES[$IDX]}"
TARGET_TYPE="${ENTRY_TYPES[$IDX]}"
echo -e ""
echo -e " 選択: ${BOLD}$(basename "$TARGET_FILE")${RESET}"
echo -e " パス: ${TARGET_FILE}"
if [[ "$TARGET_TYPE" == "snap" ]]; then
backing=$(get_backing "$TARGET_FILE")
echo -e " ${MAGENTA}スナップショットファイル (backing: $(basename "${backing:-不明}"))${RESET}"
fi
# ================================================================
# ステップ5: 拡大量の入力
# ================================================================
echo -e ""
echo -e " ${BOLD}拡大するサイズを入力してください。${RESET}"
echo -e " 例: ${CYAN}10G${RESET} (+10 GiB) ${CYAN}500M${RESET} (+500 MiB) ${CYAN}2T${RESET} (+2 TiB)"
while true; do
read -rp "$(echo -e " ${BOLD}拡大量:${RESET} +")" size_input
size_input="${size_input#+}"
if [[ "$size_input" =~ ^[0-9]+(\.[0-9]+)?[MGTKmgtk]?$ ]]; then
break
fi
echo -e " ${RED}無効な形式です。例: 10G / 500M${RESET}"
done
ADD_SIZE="+${size_input}"
# ================================================================
# ステップ6: 確認・実行
# ================================================================
echo -e ""
echo -e " ${YELLOW}${BOLD}── 実行内容の確認 ──────────────────────────────────────${RESET}"
echo -e " 対象ファイル: ${BOLD}${TARGET_FILE}${RESET}"
echo -e " 拡大量 : ${BOLD}${ADD_SIZE}${RESET}"
echo -e " ${YELLOW}${BOLD}────────────────────────────────────────────────────────${RESET}"
echo -e ""
read -rp "$(echo -e " ${BOLD}実行しますか? [y/N]:${RESET} ")" confirm
[[ ! "$confirm" =~ ^[Yy]$ ]] && { echo -e " ${YELLOW}キャンセルしました。${RESET}"; exit 0; }
before_vsize=$($SUDO qemu-img info --output=json "$TARGET_FILE" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('virtual-size',0))" 2>/dev/null || echo 0)
echo -e ""
echo -e " ${CYAN}実行中...${RESET}"
$SUDO qemu-img resize "$TARGET_FILE" "$ADD_SIZE"
after_vsize=$($SUDO qemu-img info --output=json "$TARGET_FILE" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('virtual-size',0))" 2>/dev/null || echo 0)
before_h=$(numfmt --to=iec-i --suffix=B "$before_vsize" 2>/dev/null || echo "${before_vsize}B")
after_h=$(numfmt --to=iec-i --suffix=B "$after_vsize" 2>/dev/null || echo "${after_vsize}B")
echo -e ""
echo -e " ${GREEN}${BOLD}✔ 完了しました!${RESET}"
echo -e " 仮想サイズ: ${before_h} → ${BOLD}${GREEN}${after_h}${RESET}"
echo -e ""
echo -e " ${YELLOW}ヒント: ゲスト OS 内でのパーティション拡張も忘れずに。${RESET}"
echo -e " 例(Linux): sudo growpart /dev/vda 1 && sudo resize2fs /dev/vda1"
echo -e ""
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個のスクリプトを一度に作成して保存するスクリプト
LXDコンテナの操作に役立つ6つのスクリプトを作りました。より新しいバージョンを作ったので、スクリプトの作成はこちらで。
スクリプトを利用してベースコンテナを作成するなら次のコマンドで。
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
LXD操作やコンテナ管理を手軽に行える「EasyLXD」
上記のような、ホストにディレクトリをマウントしてコンテナを作成したい場合などに便利な簡易Web UIも用意しました。こちらがあれば上記スクリプトがなくても手軽にコンテナ作成などが行えると思います。導入はこちら。
Filebrowser
LXDコンテナに入れておくと便利。導入方法はこちら。
Copyparty
Filebrowserと同じようなサービスですが、自分用で行うならこちらのほうが便利かも。導入方法はこちら。
nextExplorer & OnlyOffice
こちらもFilebrowserと同じようなサービスですが、見た目が良いので他人にも使わせるならこちらが良いでしょう。OnlyOfficeと連携もすれば、使い方によってはNextcloudから置き換えられると思います。導入方法はこちら。
Linkwarden
インストールはこちらの記事を参考に。
日々のバックアップはエクスポート機能を使用した方法で。
別環境に移行する時などはフルバックアップで。
Karakeep
上のLinkwardenのようなブックマーク管理サービス。こちらはサクサク検索が行えます。
インストールやバックアップ・復元はこちら。
selfmark
ブックマーク管理に特化したシンプルなバージョンが必要ならこちら。
Dockhand
こちらの記事を参考にインストール。
Outline
こちらを参考にインストールと定期的エクスポートを設定。
AFFiNE
Outlineのようなナレッジツール。多機能でクライアントソフトも用意されている点が魅力。エクスポート機能がない点だけ残念で、これがあればOutlineから乗り換えても良いかなという状況なんですけれど。
インストールやバックアップ・復元はこちら。
Memos
OutlineやAFFiNEほど多機能なものは不要で手軽にメモするならこちら。手軽さ重視で、Tailscale serveせず、マジックDNSでアクセス出来るようにしています。
EasyNote
.mdファイルで管理するシンプルなMarkdown対応ノートアプリ。ファイル管理が楽です。インストールはこちら。
Immich
インストールはこちら。
普段の写真バックアップはSyncthingを使用して同期。
環境移行時などのフルバックアップはこちら。
Vaultwarden
こちらを参考にインストール。
バックアップ・復元スクリプトはこちら。
SPW
アプリが無くなってもファイル自体を取り出しやすいシンプルなパスワードマネージャが良いならこちら。
Nextcloud&OnlyOffice
こちらを参考にインストール。バックアップ・復元スクリプトもあります。
FreshRSS
こちらを参考にインストール。
バックアップや復元を行うならこちら。
Syncthing
こちらを参考にインストール。
opencode
こちらを参考にインストール。
KonomiTV
Mirakurun+EDCB+KonomiTVはこちら。
mirakc(Docker)+EDCB+KonomiTVはこちら。
mirakc(Docker不使用)+EDCB+KonomiTVならこちら。



