Ubuntu26.04+LXDでセットアップ2026年4月版

色々な環境で構築しているので、パスが分散していて少し分かりにくくなってきました。LXDコンテナ内にマウントする場所も、/mnt/lxd-dataになっているものや、/opt/lxd-dataになってたりと。Linuxの標準的なディレクトリ構成のルール(FHS: Filesystem Hierarchy Standard)に照らし合わせると/mnt/lxd-dataが正しいような気もしますが、大容量外部ディスクをマウントしているわけでもなく、永続的データを保存するという観点では/opt以下に全てまとめるのも理にかなっている気もします。
環境によって変わるのも混乱なので/opt以下に統一しようかと思います。
このブログ内もごちゃごちゃしていますが、いつかまとめられれば。

ここではUbuntu 26.04 betaにセットアップしています。基本的にLXDコンテナに閉じ込める設定にしています。

Tailscale

# アップデート
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

以降はSSHで。

Taildrop(ホストにインストール)

これはホスト側に入れておくと便利でしょう。受け取ったデータを/opt/lxd-data/taildropに保存することで、どのコンテナからも参照出来ます。サービス化で再起動しても自動実行されます。

TARGET_DIR="/opt/lxd-data/taildrop"

# ディレクトリが存在しない場合は作成
sudo mkdir -p "$TARGET_DIR"

# サービスファイル作成(sudo tee を使う)
sudo tee /etc/systemd/system/taildrop.service > /dev/null <<EOF
[Unit]
Description=Tailscale File Receiver (Taildrop)
After=network.target tailscaled.service
Requires=tailscaled.service

[Service]
Type=simple
ExecStart=tailscale file get --loop --conflict=rename $TARGET_DIR
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# 有効化・起動
sudo systemctl daemon-reload
sudo systemctl enable taildrop
sudo systemctl start taildrop

# 確認
sudo systemctl status taildrop

よく使うコマンド

# ログ確認
journalctl -u taildrop -f

# 停止・再起動
sudo systemctl stop taildrop
sudo systemctl restart taildrop

LXD-UI

# 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

一度再起動

sudo reboot

LXD-UI https://ホスト名:8443

ベースコンテナを作成(最小、Docker入り)

lxd-base-minimal

lxd-base-minimalという名前でコンテナを作成。ディスクの追加でlxd-dataをマウント。

/opt/lxd-dataを作成してコンテナに追加

コンテナを作成したら、ホストとの連携用にDiskでマウント先を追加

/opt/lxd-data
/opt/lxd-data

パーミッションを設定

lxc config set lxd-base-minimal raw.idmap "both 1000 1000"
lxc restart lxd-base-minimal
sudo chown -R 1000:1000 /opt/lxd-data

コンテナに入って最小限のインストール

次のコマンドでコンテナに入る。

lxc exec lxd-base-minimal -- bash

コンテナ内で作業。

apt update && apt upgrade -y
apt install -y nano curl

# Tailscaleインストール
curl -fsSL https://tailscale.com/install.sh | sh
shutdown now

lxd-base-docker

lxd-base-minimalコンテナをコピーして作業。
こちらはNestingAllowにしてから起動します。

# 権限設定
lxc config set lxd-base-docker raw.idmap "both 1000 1000"
lxc restart lxd-base-docker
# コンテナに入る
lxc exec lxd-base-docker -- bash
# Dockerインストール
curl -fsSL https://get.docker.com | sh

#!/bin/bash
# Docker 関連用ディレクトリを作成
mkdir -p /opt/docker

# 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

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

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

作成してLXDコンテナ

これでベースコンテナの作成は終了。必要に応じて、どちらかをコピーして利用。
起動したら一度再起動を忘れずに。
共有フォルダが不要なら、ディスクから外す。
コンテナに入ったら、Tailscaleを有効にする。

tailscale up --authkey=tskey-auth-xxxx

code-server

mkdir /opt/script -p
cd /opt/script
nano install-codeserver.sh
bash install-codeserver.sh
#!/bin/bash
# ============================================================
#  Code-Server ネイティブインストール
#  対象OS: Ubuntu 24.04 (LXD,Docker不使用)
#  実行方法: sudo bash install-codeserver.sh
#
#  起動後にブラウザで Japanese Language Pack をインストール後、
#  Ctrl+Shift+R でリロードすれば日本語UIになります。
# ============================================================

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}   $*"; }
error()   { echo -e "${RED}[ERR]${NC}  $*"; exit 1; }

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

REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || echo $USER)}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
info "インストール対象ユーザー: $REAL_USER ($REAL_HOME)"

# --- パスワード自動生成 ---
info "パスワードを自動生成中..."
PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)

# --- 依存パッケージ ---
info "依存パッケージをインストール中..."
apt-get update -qq
apt-get install -y -qq curl wget locales

# --- 日本語ロケール ---
info "日本語ロケールを設定中..."
locale-gen ja_JP.UTF-8
update-locale LANG=ja_JP.UTF-8 LC_ALL=ja_JP.UTF-8
success "日本語ロケール設定完了"

# --- 最新バージョン取得 ---
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} インストール完了"

# --- config.yaml 生成 ---
CONFIG_DIR="$REAL_HOME/.config/code-server"
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_DIR/config.yaml" <<EOF
bind-addr: 0.0.0.0:8080
auth: password
password: ${PASSWORD}
cert: false
EOF
chown -R "$REAL_USER:$REAL_USER" "$CONFIG_DIR"
chmod 600 "$CONFIG_DIR/config.yaml"
success "config.yaml 生成完了"

# --- argv.json: 日本語化のキモ ---
info "argv.json を配置中..."
VSCODE_DIR="$REAL_HOME/.local/share/code-server/User"
mkdir -p "$VSCODE_DIR"
cat > "$VSCODE_DIR/argv.json" <<'EOF'
{
  "locale": "ja"
}
EOF
chown -R "$REAL_USER:$REAL_USER" "$REAL_HOME/.local/share/code-server"
success "argv.json 配置完了"

# --- 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=LANG=ja_JP.UTF-8
Environment=LC_ALL=ja_JP.UTF-8
Environment=HOME=${REAL_HOME}

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable code-server
systemctl restart code-server
success "systemd サービス登録・起動完了"

# --- ファイアウォール ---
if ufw status 2>/dev/null | grep -q "Status: active"; then
  info "ufw でポート 8080 を開放中..."
  ufw allow 8080/tcp
  success "ポート開放済み"
fi

# --- 起動確認 ---
info "起動確認中..."
sleep 4
systemctl is-active --quiet code-server && success "code-server 正常起動" \
  || { echo ""; journalctl -u code-server -n 20 --no-pager; }

# --- 完了メッセージ ---
HOST_IP=$(hostname -I | awk '{print $1}')
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN}  Code-Server インストール完了!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e "  🌐 アクセスURL : ${CYAN}http://${HOST_IP}:8080${NC}"
echo -e "  🔑 パスワード  : ${YELLOW}${PASSWORD}${NC}"
echo ""
echo -e "  日本語化手順:"
echo -e "    1. Extensions で 'Japanese Language Pack' をインストール"
echo -e "    2. Ctrl+Shift+R でリロード → 日本語UIになります"
echo ""
echo -e "  起動    : ${CYAN}sudo systemctl start code-server${NC}"
echo -e "  停止    : ${CYAN}sudo systemctl stop code-server${NC}"
echo -e "  ログ確認: ${CYAN}journalctl -u code-server -f${NC}"
echo ""
echo -e "${YELLOW}  ⚠ 本番公開時はリバースプロキシ + HTTPS を推奨します${NC}"
echo ""
Japanese Language Pack

パスワードを変更したい場合は下記ファイルを編集してリスタート。

nano ~/.config/code-server/config.yaml
systemctl restart code-server

/optを追加。以降はcodeserverで。

TSDProxy

bash -c '
INSTALL_DIR="/opt/docker/tsdproxy"

echo ""
echo "========================================="
echo " TSDProxy セットアップ"
echo "========================================="
echo " Tailscale Auth Key を入力してください"
echo " (https://login.tailscale.com/admin/settings/keys)"
echo "========================================="
read -rp " Auth Key: " TS_AUTHKEY
if [ -z "$TS_AUTHKEY" ]; then
  echo "エラー: Auth Key が入力されていません。処理を中止します。"
  exit 1
fi

echo ""
echo "[1/4] ディレクトリを作成中..."
mkdir -p "${INSTALL_DIR}/config"
mkdir -p "${INSTALL_DIR}/data"

echo "[2/4] Auth Key ファイルを書き込み中..."
echo "${TS_AUTHKEY}" > "${INSTALL_DIR}/config/authkey"
chmod 600 "${INSTALL_DIR}/config/authkey"

echo "[3/4] docker-compose.yml を生成中..."
cat > "${INSTALL_DIR}/docker-compose.yml" << EOF
services:
  tsdproxy:
    image: almeidapaulopt/tsdproxy:1
    container_name: tsdproxy
    restart: unless-stopped
    ports:
      - "8081:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${INSTALL_DIR}/data:/data
      - ${INSTALL_DIR}/config:/config
    environment:
      - TSDPROXY_AUTHKEYFILE=/config/authkey
EOF

echo "[4/4] TSDProxy を起動中..."
cd "${INSTALL_DIR}"
docker compose up -d

echo ""
sleep 3
if docker ps --filter "name=tsdproxy" --filter "status=running" | grep -q tsdproxy; then
  echo "========================================="
  echo " ✅ TSDProxy が正常に起動しました!"
  echo "========================================="
  echo " ダッシュボード: http://$(hostname -I | awk '"'"'{print $1}'"'"'):8081"
  echo " 設定ファイル:   ${INSTALL_DIR}/config/tsdproxy.yaml"
  echo " ログ確認:       docker logs -f tsdproxy"
  echo "========================================="
else
  echo "❌ 起動に失敗した可能性があります。ログを確認してください:"
  docker logs tsdproxy 2>&1 | tail -20
fi
'

基本的な書き方

services:
  hoarder:
    image: ghcr.io/hoarder-app/hoarder:latest
    container_name: hoarder
    restart: unless-stopped
    ports:
      - "3000:3000"
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "hoarder"          # Tailscale上のホスト名 → hoarder.<tailnet>.ts.net
      tsdproxy.containerport: "3000"    # コンテナ内部のポート番号

よく使うラベル一覧

ラベル必須説明
tsdproxy.enable: "true"TSDProxy管理下に置く
tsdproxy.name: "xxxxx"推奨Tailscaleホスト名(省略するとコンテナ名)
tsdproxy.containerport: "3000"推奨コンテナのポート(省略すると最初に公開されているポート)
tsdproxy.scheme: "http"任意http or https(デフォルトはhttps
tsdproxy.funnel: "true"任意Tailscale外のインターネットにも公開する場合

注意点

ports: の記載はなくてもTSDProxy経由ではアクセスできます。ホストから直接アクセスする必要がない場合は省略してセキュリティを高めることもできます。

Linkwarden

#!/bin/bash
set -euo pipefail

INSTALL_DIR="/opt/docker/linkwarden"
PORT=3300

# ── TailscaleのMagicDNS名を自動取得 ──────────────
TAILSCALE_URL=$(tailscale status --json | python3 -c "
import json, sys
data = json.load(sys.stdin)
dns = data.get('MagicDNSSuffix', '')
name = data.get('Self', {}).get('HostName', '')
print(f'https://linkwarden.{dns}' if dns else f'http://localhost:${PORT}')
")
echo "🌐 NEXTAUTH_URL: $TAILSCALE_URL"

# ── シークレットキー自動生成 ──────────────────────
NEXTAUTH_SECRET=$(openssl rand -base64 36)
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)

# ── インストールディレクトリ ──────────────────────
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"

# ── .env 生成 ────────────────────────────────────
cat > .env <<EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
NEXTAUTH_URL=${TAILSCALE_URL}
EOF
chmod 600 .env

# ── docker-compose.yml 生成 ──────────────────────
cat > docker-compose.yml <<EOF
services:
  postgres:
    image: postgres:16-alpine
    container_name: linkwarden-postgres
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: linkwarden
      POSTGRES_USER: linkwarden
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U linkwarden -d linkwarden"]
      interval: 10s
      timeout: 5s
      retries: 5

  linkwarden:
    image: ghcr.io/linkwarden/linkwarden:latest
    container_name: linkwarden
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgresql://linkwarden:\${POSTGRES_PASSWORD}@postgres:5432/linkwarden
    ports:
      - "${PORT}:3000"
    volumes:
      - linkwarden_data:/data/data
    depends_on:
      postgres:
        condition: service_healthy
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "linkwarden"
      tsdproxy.containerport: "3000"

volumes:
  postgres_data:
  linkwarden_data:
EOF

# ── 起動 ─────────────────────────────────────────
docker compose pull
docker compose up -d

echo ""
echo "════════════════════════════════════════"
echo "  ✅  Linkwarden 起動完了"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Tailscale URL : $TAILSCALE_URL"
echo "  🌐 ローカル URL  : http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "  📂 インストール先: $INSTALL_DIR"
echo ""
echo "  ログ確認 : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : docker compose -f $INSTALL_DIR/docker-compose.yml pull && docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

Vaultwarden

#!/bin/bash
set -euo pipefail

INSTALL_DIR="/opt/docker/vaultwarden"
PORT=8222

# ── TailscaleのMagicDNS名を自動取得 ──────────────
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
dns = d.get('MagicDNSSuffix', '')
print(f'vaultwarden.{dns}' if dns else '')
")
if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo "ERROR: Tailscale MagicDNS名を取得できませんでした"
    exit 1
fi
echo "🌐 ドメイン: https://${TAILSCALE_DOMAIN}"

# ── インストールディレクトリ ──────────────────────
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"

# ── docker-compose.yml 生成 ──────────────────────
cat > docker-compose.yml <<EOF
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      DOMAIN: "https://${TAILSCALE_DOMAIN}"
      SIGNUPS_ALLOWED: "true"
    volumes:
      - ./data:/data
    ports:
      - "${PORT}:80"
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "vaultwarden"
      tsdproxy.containerport: "80"
EOF

# ── 起動 ─────────────────────────────────────────
docker compose pull
docker compose up -d

echo ""
echo "════════════════════════════════════════"
echo "  ✅  Vaultwarden 起動完了"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Tailscale URL : https://${TAILSCALE_DOMAIN}"
echo "  🌐 ローカル URL  : http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "  📂 インストール先: $INSTALL_DIR"
echo ""
echo "  ⚠️  アカウント作成後はSIGNUPS_ALLOWEDをfalseに変更してください"
echo "  sed -i 's/SIGNUPS_ALLOWED: \"true\"/SIGNUPS_ALLOWED: \"false\"/' $INSTALL_DIR/docker-compose.yml"
echo "  docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""
echo "  ログ確認 : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : docker compose -f $INSTALL_DIR/docker-compose.yml pull && docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

Outline

#!/bin/bash
set -euo pipefail
# =============================================================
#  Outline セットアップスクリプト (TSDProxy版)
#
#  構成:
#   - Outline     : https://outline.<tailnet>.ts.net      (TSDProxy)
#   - Dex (OIDC)  : https://outline-auth.<tailnet>.ts.net (TSDProxy)
#   - コンテナ間通信はhttp://dex:5556で直接通信
#
#  前提条件:
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

INSTALL_DIR="/opt/docker/outline"
OUTLINE_PORT=3900
DEX_PORT=5556

# ── TailscaleのMagicDNS名を自動取得 ──────────────
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
dns = d.get('MagicDNSSuffix', '')
print(dns if dns else '')
")
if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo "ERROR: Tailscale MagicDNS名を取得できませんでした"
    exit 1
fi

BASE_URL="https://outline.${TAILSCALE_DOMAIN}"
DEX_URL="https://outline-auth.${TAILSCALE_DOMAIN}"
DEX_INTERNAL="http://dex:${DEX_PORT}"

echo "🌐 Outline URL : $BASE_URL"
echo "🌐 Dex URL     : $DEX_URL"

# ── ユーザー登録 ──────────────────────────────────
if ! command -v htpasswd &>/dev/null; then
    apt-get install -y apache2-utils &>/dev/null
fi

USERS_YAML=""
USER_COUNT=0
while true; do
    USER_COUNT=$((USER_COUNT + 1))
    echo ""
    echo "── ユーザー ${USER_COUNT} ──────────────────────"
    read -rp "ユーザー名(例: yamada): " U_NAME
    read -rsp "パスワード: " U_PASS
    echo ""
    U_HASH=$(htpasswd -bnBC 10 "" "${U_PASS}" | tr -d ':\n' | sed 's/\$2y/\$2a/')
    U_UUID=$(cat /proc/sys/kernel/random/uuid)
    U_EMAIL="${U_NAME}@local.invalid"
    USERS_YAML="${USERS_YAML}
  - email: \"${U_EMAIL}\"
    hash: \"${U_HASH}\"
    username: \"${U_NAME}\"
    userID: \"${U_UUID}\""
    read -rp "もう1人追加しますか? (y/N): " ADD_MORE
    [[ "$ADD_MORE" =~ ^[Yy]$ ]] || break
done

# ── シークレットキー生成 ──────────────────────────
SECRET_KEY=$(openssl rand -hex 32)
UTILS_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 16)
DEX_CLIENT_SECRET=$(openssl rand -hex 16)

# ── インストールディレクトリ ──────────────────────
mkdir -p "$INSTALL_DIR/dex/config"
mkdir -p "$INSTALL_DIR/data/storage"
chown -R 1001:1001 "$INSTALL_DIR/data/storage"
chown -R 1001:1001 "$INSTALL_DIR/dex"
cd "$INSTALL_DIR"

# ── Dex設定ファイル生成 ───────────────────────────
cat > dex/config/config.yaml <<EOF
issuer: ${DEX_URL}
storage:
  type: sqlite3
  config:
    file: /config/dex.db
web:
  http: 0.0.0.0:${DEX_PORT}
oauth2:
  skipApprovalScreen: true
  responseTypes:
    - code
staticClients:
  - id: outline
    name: "Outline Wiki"
    secret: "${DEX_CLIENT_SECRET}"
    redirectURIs:
      - "${BASE_URL}/auth/oidc.callback"
enablePasswordDB: true
staticPasswords:
${USERS_YAML}
logger:
  level: info
  format: text
EOF

# ── .env 生成 ────────────────────────────────────
cat > .env <<EOF
SECRET_KEY=${SECRET_KEY}
UTILS_SECRET=${UTILS_SECRET}
URL=${BASE_URL}
PORT=3000
FORCE_HTTPS=false
DATABASE_URL=postgres://outline:${POSTGRES_PASSWORD}@postgres:5432/outline?sslmode=disable
REDIS_URL=redis://redis:6379
FILE_STORAGE=local
FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data
FILE_STORAGE_UPLOAD_MAX_SIZE=26214400
OIDC_CLIENT_ID=outline
OIDC_CLIENT_SECRET=${DEX_CLIENT_SECRET}
OIDC_AUTH_URI=${DEX_URL}/auth
OIDC_TOKEN_URI=${DEX_INTERNAL}/token
OIDC_USERINFO_URI=${DEX_INTERNAL}/userinfo
OIDC_DISPLAY_NAME=ログイン
OIDC_SCOPES=openid profile email offline_access
DEFAULT_LANGUAGE=ja_JP
LOG_LEVEL=info
EOF
chmod 600 .env

# ── docker-compose.yml 生成 ──────────────────────
cat > docker-compose.yml <<EOF
services:
  outline:
    image: outlinewiki/outline:latest
    container_name: outline
    restart: unless-stopped
    env_file: .env
    ports:
      - "${OUTLINE_PORT}:3000"
    volumes:
      - ./data/storage:/var/lib/outline/data
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      dex:
        condition: service_healthy
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "outline"
      tsdproxy.containerport: "3000"

  dex:
    image: ghcr.io/dexidp/dex:latest
    container_name: outline-dex
    restart: unless-stopped
    command: dex serve /config/config.yaml
    ports:
      - "${DEX_PORT}:${DEX_PORT}"
    volumes:
      - ./dex/config:/config
    healthcheck:
      test: ["CMD-SHELL", "echo OK"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 10s
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "outline-auth"
      tsdproxy.containerport: "${DEX_PORT}"

  postgres:
    image: postgres:16-alpine
    container_name: outline-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: outline
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: outline
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U outline"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: outline-redis
    restart: unless-stopped
    command: redis-server --save 60 1 --loglevel warning
    volumes:
      - ./data/redis:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
EOF

# ── 起動 ─────────────────────────────────────────
docker compose pull
docker compose up -d

# ── TSDProxyをoutlineネットワークに接続 ───────────
echo ""
echo "⏳ TSDProxyをoutlineネットワークに接続中..."
sleep 5
docker network connect outline_default tsdproxy 2>/dev/null || true
docker restart tsdproxy
sleep 8

# ── 起動確認 ─────────────────────────────────────
echo ""
if docker compose ps | grep -E "^outline\s" | grep -q "Up"; then
    echo "════════════════════════════════════════"
    echo "  ✅  Outline 起動完了"
    echo "════════════════════════════════════════"
    echo ""
    echo "  🌐 Outline URL : $BASE_URL"
    echo "  🌐 Dex URL     : $DEX_URL"
    echo "  📂 インストール先: $INSTALL_DIR"
    echo ""
    echo "  ▼ 登録済みユーザー:"
    echo "$USERS_YAML" | grep 'email:' | sed 's/.*email: "\(.*\)"/    ログインID: \1/'
    echo ""
    echo "  ⏳ outline / outline-auth がTailscaleに登録されるまで"
    echo "     初回は1〜2分ほどかかります"
    echo "════════════════════════════════════════"
else
    echo "❌ 起動確認に失敗しました。ログを確認してください:"
    docker compose logs --tail=20
fi

# ── tsdproxy compose.ymlにネットワーク永続化 ──────
echo ""
echo "⏳ TSDProxy設定にoutlineネットワークを永続化中..."
TSDPROXY_COMPOSE="/opt/docker/tsdproxy/docker-compose.yml"

# networksブロックをservicesのtsdproxyに追加(未追加の場合のみ)
if ! grep -q "outline_default" "$TSDPROXY_COMPOSE"; then
    python3 - <<PYEOF
import re

with open("$TSDPROXY_COMPOSE", "r") as f:
    content = f.read()

# servicesブロック内のtsdproxyにnetworksを追加
content = re.sub(
    r'(  tsdproxy:(?:.*\n)*?)(  \w)',
    lambda m: m.group(1) + '    networks:\n      - default\n      - outline_default\n' + m.group(2)
    if 'networks:' not in m.group(1) else m.group(0),
    content
)

# ファイル末尾にnetworksブロックを追加
if 'outline_default:' not in content:
    content = content.rstrip() + '''

networks:
  default:
  outline_default:
    external: true
'''

with open("$TSDPROXY_COMPOSE", "w") as f:
    f.write(content)
print("✅ tsdproxy docker-compose.yml を更新しました")
PYEOF
else
    echo "✅ outline_defaultネットワークは既に設定済みです"
fi

echo ""
echo "════════════════════════════════════════"
echo "  ✅  セットアップ完了"
echo "════════════════════════════════════════"
echo ""
echo "  ログ確認 : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : docker compose -f $INSTALL_DIR/docker-compose.yml pull && docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

ユーザー名@local.invalid

FleshRSS

#!/bin/bash
set -euo pipefail

INSTALL_DIR="/opt/docker/freshrss"
PORT=6060

# ── インストールディレクトリ & compose生成 ──────
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"

cat > docker-compose.yml <<EOF
services:
  freshrss:
    image: freshrss/freshrss:latest
    container_name: freshrss
    restart: unless-stopped
    ports:
      - "${PORT}:80"
    volumes:
      - freshrss_data:/var/www/FreshRSS/data
      - freshrss_extensions:/var/www/FreshRSS/extensions
    environment:
      TZ: Asia/Tokyo
      CRON_MIN: '*/15'
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "freshrss"
      tsdproxy.containerport: "80"

volumes:
  freshrss_data:
  freshrss_extensions:
EOF

# ── 起動 ────────────────────────────────────────
docker compose pull
docker compose up -d
echo "⏳ コンテナ起動待機中..."
sleep 10

# ── 拡張機能インストール ─────────────────────────
echo "📦 拡張機能をインストール中..."
apt-get install -y unzip &>/dev/null

# Three Panes View
curl -sL "https://framagit.org/nicofrand/xextension-threepanesview/-/archive/master/xextension-threepanesview-master.zip" -o /tmp/tpv.zip
unzip -q /tmp/tpv.zip -d /tmp/
docker cp /tmp/xextension-threepanesview-master freshrss:/var/www/FreshRSS/extensions/xExtension-ThreePanesView
rm -rf /tmp/tpv.zip /tmp/xextension-threepanesview-master

# AF Readability
curl -sL "https://github.com/Niehztog/freshrss-af-readability/archive/refs/heads/master.zip" -o /tmp/af.zip
unzip -q /tmp/af.zip -d /tmp/
docker cp /tmp/freshrss-af-readability-master freshrss:/var/www/FreshRSS/extensions/xExtension-af_readability
rm -rf /tmp/af.zip /tmp/freshrss-af-readability-master

echo ""
echo "════════════════════════════════════════"
echo "  ✅  FreshRSS 起動完了"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Tailscale URL : https://freshrss.$(tailscale status --json | python3 -c "import json,sys; print(json.load(sys.stdin).get('MagicDNSSuffix',''))")"
echo "  🌐 ローカル URL  : http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "  📂 インストール先: $INSTALL_DIR"
echo ""
echo "  ⚠️  初回セットアップ時のURL設定は Tailscale URL を入力してください"
echo ""
echo "  ログ確認 : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : docker compose -f $INSTALL_DIR/docker-compose.yml pull && docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

CSS

#stream {
  width: 280px !important;
  min-width: 280px !important;
}

Dockhand

#!/bin/bash
set -euo pipefail

INSTALL_DIR="/opt/docker/dockhand"
PORT=3333

mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"

cat > docker-compose.yml <<EOF
services:
  dockhand:
    image: fnsys/dockhand:latest
    container_name: dockhand
    restart: unless-stopped
    ports:
      - "${PORT}:3000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - dockhand_data:/app/data
    labels:
      tsdproxy.enable: "true"
      tsdproxy.name: "dockhand"
      tsdproxy.containerport: "3000"

volumes:
  dockhand_data:
EOF

docker compose pull
docker compose up -d

echo ""
echo "════════════════════════════════════════"
echo "  ✅  Dockhand 起動完了"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Tailscale URL : https://dockhand.$(tailscale status --json | python3 -c "import json,sys; print(json.load(sys.stdin).get('MagicDNSSuffix',''))")"
echo "  🌐 ローカル URL  : http://$(hostname -I | awk '{print $1}'):${PORT}"
echo "  📂 インストール先: $INSTALL_DIR"
echo ""
echo "  ログ確認 : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : docker compose -f $INSTALL_DIR/docker-compose.yml pull && docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

Immich(/opt/lxd-data使用)

重いので単独のコンテナにします。

# 権限設定
lxc config set immich raw.idmap "both 1000 1000"
lxc restart immich
# コンテナに入る
lxc exec immich -- bash
tailscale up --authkey=tskey-auth-xxxx
nano setup-immich.sh
# 下記スクリプトを貼り付け
bash setup-immich.sh
#!/bin/bash
set -e
WORK_DIR="/opt/docker/immich"
DATA_DIR="/opt/lxd-data/docker/immich"
mkdir -p "${WORK_DIR}"
mkdir -p "${DATA_DIR}"
cd "${WORK_DIR}"
# --- .env 生成(既存があればスキップ)---
if [ ! -f .env ]; then
  DB_PASSWORD=$(openssl rand -hex 16)
  cat > .env <<EOF
UPLOAD_LOCATION=${DATA_DIR}/library
DB_DATA_LOCATION=${DATA_DIR}/postgres
IMMICH_VERSION=release
DB_PASSWORD=${DB_PASSWORD}
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOF
  chmod 600 .env
  echo "[OK] .env を新規作成"
else
  echo "[SKIP] .env は既存のものを使用"
  DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d= -f2)
fi
# --- docker-compose.yml 生成 ---
cat > docker-compose.yml <<'EOF'
name: immich
services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '127.0.0.1:2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false
  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false
  redis:
    container_name: immich_redis
    image: docker.io/redis:6.2-alpine
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always
  database:
    container_name: immich_postgres
    image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    healthcheck:
      test: >
        pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1;
        Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}'
        --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0)
        FROM pg_stat_database')";
        echo "checksum failure count is $$Chksum";
        [ "$$Chksum" = '0' ] || exit 1
      interval: 5m
      start_interval: 30s
      start_period: 5m
    restart: always
volumes:
  model-cache:
EOF
# --- Tailscale Serve 設定(冪等) ---
tailscale serve --bg 2283
# --- フルドメイン名取得 ---
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d['Self']['DNSName'].rstrip('.'))
")
# --- 起動 ---
docker compose up -d
echo ""
echo "======================================"
echo "  Immich 起動完了"
echo "======================================"
echo "  URL        : https://${TAILSCALE_DOMAIN}"
echo "  DB_PASSWORD: ${DB_PASSWORD}"
echo "  設定ファイル: ${WORK_DIR}/.env"
echo "  データ保存先: ${DATA_DIR}"
echo "======================================"
パス内容
/opt/docker/immich/docker-compose.ymlCompose定義
/opt/docker/immich/.env設定・パスワード
/opt/lxd-data/docker/immich/library/写真・動画データ
/opt/lxd-data/docker/immich/postgres/DBデータ

Nextcloud & Onlyoffice(/opt/lxd-data使用)

こちらも単独のコンテナ

# 権限設定
lxc config set nextcloud raw.idmap "both 1000 1000"
lxc restart nextcloud
# コンテナに入る
lxc exec nextcloud -- bash
tailscale up --authkey=tskey-auth-xxxx
#!/bin/bash
set -e

# === Tailscale フルドメイン自動取得 ===
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d['Self']['DNSName'].rstrip('.'))
")
echo "[OK] Tailscale ドメイン: ${TAILSCALE_DOMAIN}"

# === JWT シークレット自動生成 ===
JWT_SECRET=$(openssl rand -hex 32)

# === 共有ネットワーク作成(既存なら再利用) ===
docker network inspect onlyoffice_net >/dev/null 2>&1 \
  || docker network create onlyoffice_net
echo "[OK] ネットワーク onlyoffice_net を確認しました"

# === Tailscale Serve 設定 ===
tailscale serve --bg --https=443 8080 || true
tailscale serve --bg --https=444 9000 || true
echo "[OK] Tailscale Serve 設定完了 (443 → 8080)"

# ============================================================
# Nextcloud
# ============================================================
NEXTCLOUD_WORK_DIR="/opt/docker/nextcloud"
NEXTCLOUD_DATA_DIR="/opt/lxd-data/docker/nextcloud"
mkdir -p "${NEXTCLOUD_WORK_DIR}"
mkdir -p "${NEXTCLOUD_DATA_DIR}"/{userdata,db}

cat > "${NEXTCLOUD_WORK_DIR}/docker-compose.yml" <<EOF
services:
  db:
    image: mariadb:10.6
    container_name: nextcloud-db
    restart: always
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    volumes:
      - ${NEXTCLOUD_DATA_DIR}/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_PASSWORD: nextcloudpass
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
    networks:
      - default
  app:
    image: nextcloud:latest
    container_name: nextcloud-app
    restart: always
    ports:
      - "127.0.0.1:8080:80"
    depends_on:
      - db
    volumes:
      - nextcloud_app:/var/www/html
      - ${NEXTCLOUD_DATA_DIR}/userdata:/var/www/html/data
    environment:
      MYSQL_PASSWORD: nextcloudpass
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_HOST: nextcloud-db
      NEXTCLOUD_TRUSTED_DOMAINS: "${TAILSCALE_DOMAIN}"
      OVERWRITEHOST: "${TAILSCALE_DOMAIN}"
      OVERWRITEPROTOCOL: https
    networks:
      - default
      - onlyoffice_net
volumes:
  nextcloud_app:
networks:
  default:
  onlyoffice_net:
    external: true
EOF

cd "${NEXTCLOUD_WORK_DIR}"
docker compose up -d
echo "[1/2] Nextcloud 起動完了"

# ============================================================
# OnlyOffice
# ============================================================
ONLYOFFICE_WORK_DIR="/opt/docker/onlyoffice"
mkdir -p "${ONLYOFFICE_WORK_DIR}"

cat > "${ONLYOFFICE_WORK_DIR}/.env" <<EOF
JWT_SECRET=${JWT_SECRET}
EOF
chmod 600 "${ONLYOFFICE_WORK_DIR}/.env"

cat > "${ONLYOFFICE_WORK_DIR}/docker-compose.yml" <<EOF
services:
  onlyoffice-docs:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice-docs
    restart: always
    stdin_open: true
    tty: true
    ports:
      - "127.0.0.1:9000:80"
    environment:
      JWT_ENABLED: "true"
      JWT_SECRET: "${JWT_SECRET}"
    volumes:
      - onlyoffice_logs:/var/log/onlyoffice
      - onlyoffice_data:/var/www/onlyoffice/Data
      - onlyoffice_lib:/var/lib/onlyoffice
      - onlyoffice_db:/var/lib/postgresql
    networks:
      - onlyoffice_net
volumes:
  onlyoffice_logs:
  onlyoffice_data:
  onlyoffice_lib:
  onlyoffice_db:
networks:
  onlyoffice_net:
    external: true
EOF

cd "${ONLYOFFICE_WORK_DIR}"
docker compose up -d
echo "[2/2] OnlyOffice 起動完了"

# ============================================================
# 完了メッセージ
# ============================================================
echo ""
echo "======================================"
echo "  起動完了"
echo "======================================"
echo ""
echo "  Nextcloud  : https://${TAILSCALE_DOMAIN}"
echo "  OnlyOffice : 内部ネットワークのみ (http://onlyoffice-docs)"
echo ""
echo "======================================"
echo "  Nextcloud 管理画面 → ONLYOFFICE 設定"
echo "======================================"
echo ""
echo "  ONLYOFFICE Docs アドレス:"
echo "    https://${TAILSCALE_DOMAIN}:444"
echo ""
echo "  JWT シークレット: ${JWT_SECRET}"
echo ""
echo "  認証ヘッダー:(空白のまま)"
echo ""
echo "  サーバーから内部リクエストに利用されるONLYOFFICE Docs アドレス:"
echo "    http://onlyoffice-docs"
echo ""
echo "  ONLYOFFICE Docsから内部リクエストに利用されるサーバーアドレス:"
echo "    http://nextcloud-app"
echo ""
echo "======================================"
echo "  個別アップデート手順"
echo "======================================"
echo ""
echo "  Nextcloud のみ更新:"
echo "    cd ${NEXTCLOUD_WORK_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "  OnlyOffice のみ更新:"
echo "    cd ${ONLYOFFICE_WORK_DIR} && docker compose pull && docker compose up -d"
echo ""

フォント追加

/opt/lxd-data/taildrop/fonts.zipがあると仮定。

cd /opt/lxd-data/taildrop
apt install unzip
unzip Fonts.zip
mv Fonts /opt/lxd-data

コンテナで作業

# 設定ファイル編集。下記1行を追加
cd /opt/docker/onlyoffice/
nano docker-compose.yml

OnlyOffice がフォントを探す標準パス(/usr/share/fonts 以下)にマウントします。

volumes:
      - /opt/lxd-data/Fonts:/usr/share/fonts/custom

そして、コンテナを再作成すれば多数のフォントを使えるようになります。

# コンテナ再作成(ボリューム変更を反映)
docker compose up -d --force-recreate onlyoffice-docs

# フォント再生成
docker exec -it onlyoffice-docs documentserver-generate-allfonts.sh

# フォント一覧確認
docker exec -it onlyoffice-docs fc-list

Syncthings

#!/bin/bash
# =============================================================================
# Syncthing 直インストールスクリプト
# 使い方: bash install-syncthing.sh  (rootで実行)
# =============================================================================
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
info()    { echo -e "${BLUE}[INFO]${NC}  $*"; }
success() { echo -e "${GREEN}[OK]${NC}    $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }

# ── root確認 ───────────────────────────────────────────────
[[ $EUID -ne 0 ]] && error "このスクリプトはrootで実行してください(sudo bash install-syncthing.sh)"

# ── Syncthing実行ユーザーの作成 ───────────────────────────
SYNCTHING_USER="syncthing"
if id "$SYNCTHING_USER" &>/dev/null; then
    info "ユーザー '$SYNCTHING_USER' はすでに存在します。スキップします。"
else
    info "ユーザー '$SYNCTHING_USER' を作成します..."
    useradd -r -m -s /bin/bash "$SYNCTHING_USER"
    success "ユーザー '$SYNCTHING_USER' を作成しました。"
fi

# www-dataグループに追加(Nextcloudファイルへのアクセス用)
usermod -aG www-data "$SYNCTHING_USER"
success "ユーザー '$SYNCTHING_USER' を www-data グループに追加しました。"

# ── config.xml パスを解決する関数 ─────────────────────────
find_config() {
    local home
    home=$(getent passwd "$SYNCTHING_USER" | cut -d: -f6)
    local candidates=(
        "$home/.local/state/syncthing/config.xml"
        "$home/.local/share/syncthing/config.xml"
        "$home/.config/syncthing/config.xml"
    )
    for p in "${candidates[@]}"; do
        [[ -f "$p" ]] && echo "$p" && return
    done
    find "$home" -name "config.xml" -path "*/syncthing/*" 2>/dev/null | head -1
}

# ── apt リポジトリ追加 & インストール ─────────────────────
info "Syncthing 公式リポジトリを追加します..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://syncthing.net/release-key.gpg \
    | tee /etc/apt/keyrings/syncthing-archive-keyring.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" \
    | tee /etc/apt/sources.list.d/syncthing.list > /dev/null
apt-get update -q || true
apt-get install -y syncthing || error "Syncthing のインストールに失敗しました。"
success "Syncthing をインストールしました。"

# ── systemd サービス登録 ───────────────────────────────────
info "systemd サービスを登録します(ユーザー: $SYNCTHING_USER)..."
systemctl enable syncthing@$SYNCTHING_USER
systemctl start syncthing@$SYNCTHING_USER
success "Syncthing サービスを起動しました。"

# ── config.xml 生成待機(最大60秒)────────────────────────
info "config.xml の生成を待機中..."
CONFIG_FILE=""
for i in $(seq 1 30); do
    CONFIG_FILE=$(find_config)
    [[ -n "$CONFIG_FILE" ]] && break
    sleep 2
done
[[ -z "$CONFIG_FILE" ]] && error "config.xml が見つかりませんでした。"
success "config.xml を検出しました: $CONFIG_FILE"

# ── GUI をリモートアクセス可能に変更 ──────────────────────
info "GUI をリモートアクセス可能に設定します..."
systemctl stop syncthing@$SYNCTHING_USER
python3 - "$CONFIG_FILE" << 'PYEOF'
import xml.etree.ElementTree as ET, sys
tree = ET.parse(sys.argv[1])
root = tree.getroot()
gui = root.find('gui')
def set_or_create(parent, tag, text):
    el = parent.find(tag)
    if el is None:
        el = ET.SubElement(parent, tag)
    el.text = text
set_or_create(gui, 'address', '0.0.0.0:8384')
tree.write(sys.argv[1], encoding='unicode', xml_declaration=True)
print("GUIアドレスを 0.0.0.0:8384 に変更しました。")
PYEOF

# ── Nextcloud ディレクトリのパーミッション設定 ────────────
info "Nextcloud ディレクトリのパーミッションを設定します..."
NC_DATA="/opt/lxd-data/docker/nextcloud/data/data"

# Nextcloudのユーザーディレクトリを動的に検出
if [ -d "$NC_DATA" ]; then
    chmod o+rx /opt/lxd-data/docker/nextcloud
    chmod o+rx /opt/lxd-data/docker/nextcloud/data
    chmod o+rx "$NC_DATA"

    # NC_DATA配下のユーザーディレクトリを全て処理
    for USER_DIR in "$NC_DATA"/*/; do
        NC_USER=$(basename "$USER_DIR")
        # システムディレクトリはスキップ
        [[ "$NC_USER" == "appdata_"* ]] && continue
        [[ "$NC_USER" == "__groupfolders" ]] && continue
        [[ "$NC_USER" == "files_external" ]] && continue
        if [ -d "$USER_DIR/files" ]; then
            info "Nextcloudユーザー '$NC_USER' のパーミッションを設定中..."
            chmod o+rx "$USER_DIR"
            chmod -R o+rX "$USER_DIR/files"
            chmod o+w "$USER_DIR/files"
            success "  $USER_DIR/files のパーミッション設定完了"
        fi
    done
    success "Nextcloud パーミッション設定が完了しました。"
else
    warn "Nextcloudデータディレクトリが見つかりません: $NC_DATA"
    warn "Nextcloudセットアップ後に手動でパーミッションを設定してください。"
fi

# ── Syncthing 再起動 ───────────────────────────────────────
info "Syncthing を起動します..."
systemctl start syncthing@$SYNCTHING_USER
sleep 3

# ── Device ID 取得 ─────────────────────────────────────────
DEVICE_ID=$(sudo -u "$SYNCTHING_USER" syncthing --device-id 2>/dev/null \
    || echo "(GUIの「情報」から確認してください)")
HOST_IP=$(hostname -I | awk '{print $1}')

# ── 完了メッセージ ─────────────────────────────────────────
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}   Syncthing インストール完了!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "   Web UI        : ${BLUE}http://$HOST_IP:8384${NC}"
echo -e "   設定ファイル  : $CONFIG_FILE"
echo -e "   実行ユーザー  : $SYNCTHING_USER"
echo ""
echo -e "   Device ID(バックアップPCへの接続時に使用):"
echo -e "   ${YELLOW}$DEVICE_ID${NC}"
echo ""
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "   次のステップ:"
echo -e "  1. Web UI にアクセスしてパスワードを設定"
echo -e "     (右上メニュー → Settings → GUI → GUI Authentication)"
echo -e "  2. フォルダを追加(ホストの絶対パスをそのまま入力)"
echo -e "     例: /opt/lxd-data/docker/immich/library/library/admin"

# 検出されたNextcloudユーザーを表示
if [ -d "$NC_DATA" ]; then
    for USER_DIR in "$NC_DATA"/*/; do
        NC_USER=$(basename "$USER_DIR")
        [[ "$NC_USER" == "appdata_"* ]] && continue
        [[ "$NC_USER" == "__groupfolders" ]] && continue
        [[ "$NC_USER" == "files_external" ]] && continue
        [ -d "$USER_DIR/files" ] && \
            echo -e "         /opt/lxd-data/docker/nextcloud/data/data/${NC_USER}/files"
    done
fi

echo -e "  3. 各フォルダのタイプを ${YELLOW}Send Only${NC} に設定"
echo -e "  4. バックアップPCと Device ID を交換して接続"
echo -e "  5. バックアップPC側を ${YELLOW}Receive Only${NC} + ${YELLOW}階段状バージョニング${NC} に設定"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""

# ── UFW 案内 ───────────────────────────────────────────────
if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then
    echo -e "${YELLOW} UFW が有効です。以下でポートを開放してください:${NC}"
    echo ""
    echo "  ufw allow 8384/tcp   # Web UI"
    echo "  ufw allow 22000/tcp  # Sync (TCP)"
    echo "  ufw allow 22000/udp  # Sync (QUIC)"
    echo "  ufw allow 21027/udp  # ローカル探索"
    echo ""
fi

Cockpit

VMが必要ならCockpitが便利なのでホストにインストール

#!/bin/bash
set -e

# --- Cockpit + Tailscale セットアップ ---

sudo apt install -y nano cockpit

# Ubuntu 26.04で管理者権限変更できるように
sudo update-alternatives --set sudo /usr/bin/sudo.ws

# 仮想マシンプラグイン
sudo apt install -y cockpit-machines

# ファイラー Navigator プラグイン
sudo apt install -y git
git clone https://github.com/45Drives/cockpit-navigator.git
cd cockpit-navigator
git checkout v0.5.8
sudo apt install -y make
sudo make install
cd ..
rm -rf cockpit-navigator

# --- Tailscale Serve 設定 ---

sudo tailscale serve --bg http://localhost:9090

# Tailscaleのフルドメイン名を自動取得
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
data = json.load(sys.stdin)
self = data.get('Self', {})
dns = self.get('DNSName', '').rstrip('.')
print(dns)
")

# CockpitのCSPにTailscaleドメインを許可(HTTPSアクセス対応)
sudo mkdir -p /etc/cockpit
sudo tee /etc/cockpit/cockpit.conf << EOF
[WebService]
AllowUnencrypted=true
Origins = https://${TAILSCALE_DOMAIN}
EOF

sudo systemctl restart cockpit

echo ""
echo "======================================"
echo " セットアップ完了!"
echo "======================================"
echo " アクセス先: https://${TAILSCALE_DOMAIN}"
echo "======================================"

デフォルトの場所(/var/lib/libvirt/images)よりもアクセスしやすいように変更。

# ディレクトリ作成
sudo mkdir -p /opt/vm

# グループをlibvirtに統一
sudo chown root:libvirt /opt/vm

# setgidビット付与
sudo chmod 2775 /opt/vm

# 自分をlibvirtグループに追加
sudo usermod -aG libvirt $USER

# デフォルトプールとして登録
virsh pool-define-as default dir --target /opt/vm
virsh pool-build default
virsh pool-start default
virsh pool-autostart default

一度再起動。

sudo reboot

ISOをコピー

mv ~/iso/*.iso /opt/vm
タイトルとURLをコピーしました