LXDコンテナに各種サービスをセットアップ

LXDコンテナ内で完結させたほうがよいものと、データ部分は分離しておくほうがバックアップがしやすくなるものがあります。このあたりもあらかじめ決めておいたほうが良いですね。

LXD内で完結させるべきもの(セキュリティ重視・データ小)

サービス理由
Vaultwardenパスワード管理、データ小、セキュリティ最優先
Outlineドキュメント、外部共有不要
Linkwardenブックマーク、データ小

ホスト共有が有効なもの(データ大・バックアップ重視)

サービス理由
Immich写真データが数十〜数百GB
Nextcloudファイルサーバー、大容量になりがち
FreshRSSフィード数が多いと肥大化

理想は1サービス1コンテナですが、最近はDocker環境で利用するサービスも多く、Dockerの基盤部分が重複して余分にリソースを消費してしまいます。現実的な落としどころとしてはセキュリティ・独立性でグループ分けでしょうか。

LXDコンテナ内容理由
lxd-securityVaultwarden単独パスワード管理は完全分離
lxd-mainOutline, Linkwarden, FreshRSS, 社内ツール系、まとめてOK
lxd-mediaImmich, Nextcloudデータ大、ホスト共有

これで現在の1コンテナ構成から3つに分けるだけでリスク分離とリソース効率を両立できます。可能ならば、ImmichとNextcloudも分けたほうが良いでしょうか。さらに、無くなっても構わないようなものやテスト環境など雑多なコンテナを用意しておくと便利そうです。

ディレクトリ規則の統一

LXD内では基本的に/opt以下に統一します。LXD内完結でDocker環境は/opt/docker以下に、LXD内でホストと共有するものは/opt/lxd-data以下とします。

/opt/
├── docker/          # LXD内完結のDockerサービス
│   ├── vaultwarden/
│   ├── outline/
│   └── linkwarden/
└── lxd-data/        # ホストと共有するもの
    ├── docker/      # ホスト共有が必要なDockerサービス
    │   ├── immich/
    │   └── nextcloud/
    └── taildrop/

コンテナテンプレート構成

nano+Tailscaleだけのコピーと、nano+Tailscale+Dockerまで加えたコンテナのコピーを取っておくと、コンテナを追加するときの作業が楽そうです。
整理すると、次のような形でしょうか。

lxd-base-minimal    nano + Tailscale
        ↓ コピー
lxd-base-docker     nano + Tailscale + Docker
        ↓ コピー
lxd-security        Vaultwarden(/opt/docker以下で完結)
lxd-main            Outline等(/opt/docker以下で完結)
lxd-media           Immich等(/opt/lxd-data でホスト共有)

テンプレート作成手順(ホスト側)

# minimal作成後にスナップショット
lxc snapshot lxd-base-minimal template

# docker版はminimalをコピーしてDockerを追加
lxc copy lxd-base-minimal lxd-base-docker
# Docker追加後にスナップショット
lxc snapshot lxd-base-docker template

# 新サービス追加時はコピーするだけ
lxc copy lxd-base-docker lxd-security

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

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

TARGET_DIR="/mnt/lxd-storage/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

ベースコンテナにパーミッションとIDマッピング設定

ベースコンテナを起動したら、パーミッションとIDマッピングを設定しておきます。

# ベースコンテナに設定
lxc config set lxd-base-minimal raw.idmap "both 1000 1000"
lxc restart lxd-base-minimal

これをベースコンテナに設定しておけば、コピー先にも引き継がれます。ただしコピー後にコンテナ名が変わると別途lxc restart が必要です。設定はコピーされても、再起動しないと反映されないためです。起動、終了、起動でもOKです。次回起動時に反映される、というわけです。

# コピー後
lxc copy lxd-base-minimal lxd-base-docker
lxc start lxd-base-docker
lxc restart lxd-base-docker  # ← これが必要

コンテナを起動したら、コンテナ内に入って作業します。

lxc exec lxd-base-minimal -- bash

ベースコンテナ(アップデート、nano、Tailscale)

# アップデート
sudo apt update
sudo apt upgrade -y

# nano
sudo apt install nano

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

このあたりがminimal環境そうです。あとは各コンテナでtailscaleを実行するなり、といったところです。

sudo tailscale up

Docker

# 必要パッケージのインストール
sudo apt install -y ca-certificates curl

# GPG キー
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# apt リポジトリの追加
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update

# Docker Engine・関連ツールのインストール
sudo apt install -y \
  docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# 作業用ディレクトリを作成
sudo mkdir -p /opt/docker
sudo mkdir -p /opt/lxd-data/docker
sudo chown -R $USER:docker /opt/docker
sudo chown -R $USER:docker /opt/lxd-data/docker
sudo chmod -R 775 /opt/docker
sudo chmod -R 775 /opt/lxd-data/docker

# 一般ユーザーへ権限追加
if [ "$(id -u)" -ne 0 ]; then
  sudo usermod -aG docker $USER
  echo "一度ログアウト・ログイン、または「sudo reboot」で再起動してください"
else
  echo "rootで実行中のため、usermodはスキップしました"
fi

ここまでで、Docker環境も構築したベース構成です。

code-server(直インストール)

小さなコンテナには必要ありませんが、多数のサービスを稼働させる場合や、開発環境に入れておくと便利です。

mkdir -p /opt/lxd-data/scripts
cd /opt/lxd-data/scripts
nano install-codeserver.sh
# 下記スクリプトを貼り付け
bash install-codeserver.sh
#!/bin/bash
# ============================================================
#  Code-Server ネイティブインストール
#  対象OS: Ubuntu 25.10 (Docker不使用)
#  実行方法: bash install-codeserver.sh  (rootで実行)
#
#  起動後にブラウザで 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権限で実行してください: bash $0"

# --- 対象ユーザーの確定 ---
if [[ -n "${SUDO_USER:-}" ]]; then
  REAL_USER="$SUDO_USER"
else
  # logname が使えない環境(LXDなど)では直接入力
  read -rp "code-serverを実行するユーザー名を入力してください (Enterでrootを使用): " REAL_USER
  REAL_USER="${REAL_USER:-root}"
  id "$REAL_USER" &>/dev/null || error "ユーザー '$REAL_USER' が存在しません"
fi
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:3150
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 でポート 3150 を開放中..."
  ufw allow 3150/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}:3150${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}systemctl start code-server${NC}"
echo -e "  停止    : ${CYAN}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
sudo systemctl restart code-server

/opt/lxd-dataを追加しコンソールを開いて作業すると楽になります。

Vaultwarden

Tailscale upが有効な環境で実行

#!/bin/bash
set -e

# ── 設定 ──────────────────────────────────────────
INSTALL_DIR="/opt/docker/vaultwarden"
PORT="8388" 
# ──────────────────────────────────────────────────

echo "==> Tailscale MagicDNS名を取得中..."
TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(d['Self']['DNSName'].strip('.'))
")
if [ -z "$TAILSCALE_DOMAIN" ]; then
  echo "ERROR: Tailscale MagicDNS名を取得できませんでした。"
  echo "  tailscale status で接続状態を確認してください。"
  exit 1
fi
echo "    取得したドメイン: https://${TAILSCALE_DOMAIN}"

echo "==> ディレクトリ作成: ${INSTALL_DIR}"
mkdir -p "${INSTALL_DIR}/data"

echo "==> docker-compose.yml を生成中..."
cat > "${INSTALL_DIR}/docker-compose.yml" <<EOF
services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - SIGNUPS_ALLOWED=true
      - DOMAIN=https://${TAILSCALE_DOMAIN}
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:${PORT}:80"
EOF

echo "==> Vaultwarden コンテナを起動中..."
cd "${INSTALL_DIR}"
docker compose up -d

echo "==> Tailscale Serve を設定中..."
tailscale serve reset 2>/dev/null || true
tailscale serve --bg http://127.0.0.1:${PORT}

echo "==> 設定確認..."
tailscale serve status

echo ""
echo "✅ セットアップ完了!"
echo "   ブラウザで開く: https://${TAILSCALE_DOMAIN}"
echo ""
echo "⚠️  アカウント作成後は SIGNUPS_ALLOWED を false に変更してください:"
echo "   sed -i 's/SIGNUPS_ALLOWED=true/SIGNUPS_ALLOWED=false/' ${INSTALL_DIR}/docker-compose.yml"
echo "   cd ${INSTALL_DIR} && docker compose up -d"

# Tailscale Serve機能を無効にする方法
tailscale serve reset

# 443ポートだけ消す場合
tailscale serve https 443 off

Linkwarden

#!/bin/bash
set -e
# ─────────────────────────────────────────
#  Linkwarden セルフホスト セットアップスクリプト
# ─────────────────────────────────────────
INSTALL_DIR="/opt/docker/linkwarden"

echo "📁 インストールディレクトリを作成: $INSTALL_DIR"
sudo mkdir -p "$INSTALL_DIR"
# オーナーを実行ユーザーに変更(tee後のファイルも$USERが扱えるように)
sudo chown "$USER":"$USER" "$INSTALL_DIR"
cd "$INSTALL_DIR" || { echo "❌ ディレクトリへの移動に失敗しました"; exit 1; }

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

# ── .env 生成 ────────────────────────────────
# tee ではなく直接書き込み(オーナーを$USERに保つため)
cat > .env <<EOF
# ── PostgreSQL ──────────────────────────────
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# ── Linkwarden ──────────────────────────────
NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
NEXTAUTH_URL=http://localhost:3300
# ── (オプション) メール認証を使う場合は以下を設定 ──
# EMAIL_FROM=no-reply@example.com
# EMAIL_SERVER=smtp://user:pass@smtp.example.com:587
EOF
chmod 600 .env
echo "✅ .env を生成しました"

# ── docker-compose.yml 生成 ──────────────────
# '区切り文字' でヒアドキュメント内の変数展開を抑制(.envから読み込むため意図的)
cat > docker-compose.yml <<'COMPOSE'
services:
  postgres:
    image: postgres:16-alpine
    container_name: linkwarden-db
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: linkwarden
      POSTGRES_USER: linkwarden
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    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-app
    restart: unless-stopped
    env_file: .env
    environment:
      DATABASE_URL: postgresql://linkwarden:${POSTGRES_PASSWORD}@postgres:5432/linkwarden
      NEXTAUTH_URL: ${NEXTAUTH_URL}
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
    ports:
      - "3300:3000"
    volumes:
      - linkwarden_data:/data/data
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
  linkwarden_data:
COMPOSE
echo "✅ docker-compose.yml を生成しました"

# ── 起動 ─────────────────────────────────────
echo ""
echo "🚀 Linkwarden を起動します..."
sudo docker compose pull
sudo docker compose up -d

echo ""
echo "════════════════════════════════════════"
echo "  ✅  Linkwarden の起動が完了しました!"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 アクセスURL : http://localhost:3300"
echo "  📂 インストール先: $INSTALL_DIR"
echo ""
echo "  🔑 生成された認証情報 (.env に保存済み)"
echo "  POSTGRES_PASSWORD : ${POSTGRES_PASSWORD}"
echo "  NEXTAUTH_SECRET   : ${NEXTAUTH_SECRET}"
echo ""
echo "  📋 よく使うコマンド"
echo "  ログ確認 : sudo docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止     : sudo docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "  更新     : sudo docker compose -f $INSTALL_DIR/docker-compose.yml pull && sudo docker compose -f $INSTALL_DIR/docker-compose.yml up -d"
echo ""

Outline

ログイン時は「ユーザー名@local.invalid」でログインします。

sudo mkdir -p /opt/docker/outline
cd /opt/docker/outline
sudo nano setup-outline.sh
# 下記スクリプトを貼り付け
sudo bash setup-outline.sh
#!/bin/bash
# =============================================================
#  Outline セルフホスト 完全セットアップスクリプト
#  対象OS: Ubuntu 25.10 / インストール先: /opt/docker/outline
#
#  認証方式: Dex (OIDC) ユーザー名+パスワード
#  ログインID: <ユーザー名>@local.invalid
#             (Dex・Outline両方で統一)
#  HTTP対応 : ✓(HTTPS不要)
#
#  実施内容:
#   1. Outline + Dex + Postgres + Redis 起動
#   2. ユーザーをインタラクティブに登録
#   3. バックアップ / リストア / ユーザー追加スクリプト作成
#   4. cron による毎日 2:00 AM 自動バックアップ設定
# =============================================================

if [ -n "$SUDO_USER" ]; then
  REAL_USER="$SUDO_USER"
  REAL_HOME=$(eval echo "~$SUDO_USER")
else
  REAL_USER="$USER"
  REAL_HOME="$HOME"
fi

INSTALL_DIR="/opt/docker/outline"
BACKUP_DIR="/opt/lxd-data/outline-bk"

echo "================================================"
echo "  Outline 完全セットアップ(Dex OIDC認証)"
echo "  インストール先: $INSTALL_DIR"
echo "  バックアップ先: $BACKUP_DIR"
echo "  実行ユーザー  : $REAL_USER"
echo "================================================"
echo ""

# ── ホスト名 / IP を入力 ──────────────────────────────────────
DETECTED=$(hostname -I | awk '{print $1}')
echo "アクセスURLを設定します。"
echo "例: 192.168.1.100  /  hostname  /  outline.example.com"
echo "(そのままEnterで検出されたIP: ${DETECTED} を使用)"
echo ""
read -rp "ホスト名またはIPアドレス: " INPUT_HOST
HOST="${INPUT_HOST:-$DETECTED}"
BASE_URL="http://${HOST}:3000"
DEX_EXTERNAL="http://${HOST}:5556"
DEX_INTERNAL="http://dex:5556"
echo ""
echo "▼ Outline URL : ${BASE_URL}"
echo "▼ Dex URL     : ${DEX_EXTERNAL}"
echo ""

# ── ユーザー登録(複数可)──────────────────────────────────────
echo "================================================"
echo "  ログインユーザーを登録します"
echo ""
echo "  ユーザー名を入力してください(例: yamada)"
echo "  ログインIDは自動で「ユーザー名@local.invalid」"
echo "  に設定されます"
echo "================================================"

if ! command -v htpasswd &>/dev/null; then
  echo "▼ apache2-utils をインストールしています..."
  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

echo ""
echo "▼ ${USER_COUNT} 人のユーザーを登録します"

# ================================================================
#  STEP 1: 既存環境をクリーンアップ
# ================================================================
echo "------------------------------------------------"
echo "  STEP 1: 既存環境をクリーンアップ"
echo "------------------------------------------------"
if [ -f "$INSTALL_DIR/docker-compose.yml" ]; then
  docker compose -f "$INSTALL_DIR/docker-compose.yml" down -v 2>/dev/null || true
fi
rm -rf "$INSTALL_DIR"
echo " クリーンアップ完了"

# ================================================================
#  STEP 2: ディレクトリ作成・権限設定
# ================================================================
echo "------------------------------------------------"
echo "  STEP 2: ディレクトリ作成・権限設定"
echo "------------------------------------------------"

mkdir -p "$INSTALL_DIR"/{data/postgres,data/redis,data/storage,dex/config}
mkdir -p "$BACKUP_DIR"

chown -R 1001:1001 "$INSTALL_DIR/data/storage"
chown -R "$REAL_USER":"$REAL_USER" "$INSTALL_DIR/data/postgres"
chown -R "$REAL_USER":"$REAL_USER" "$INSTALL_DIR/data/redis"
chown -R "$REAL_USER":"$REAL_USER" "$BACKUP_DIR"
# Dex は UID 1001 で動作 → config ディレクトリを 1001 所有に設定
# (SQLite DB ファイルを /config/dex.db に書き込むため)
chown -R 1001:1001 "$INSTALL_DIR/dex"

if ! command -v setfacl &>/dev/null; then
  apt-get install -y acl &>/dev/null
fi
setfacl -R -m u:"$REAL_USER":rwx "$INSTALL_DIR"
setfacl -d -m u:"$REAL_USER":rwx "$INSTALL_DIR"

echo " ディレクトリ・権限設定完了"

cd "$INSTALL_DIR"

# ================================================================
#  STEP 3: シークレットキー生成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 3: シークレットキー生成"
echo "------------------------------------------------"

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)

echo " キー生成完了"

# ================================================================
#  STEP 4: Dex 設定ファイル作成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 4: Dex 設定ファイル作成"
echo "------------------------------------------------"

cat > "$INSTALL_DIR/dex/config/config.yaml" <<EOF
# =============================================================
#  Dex 設定
#  issuer  : ブラウザからアクセスできる外部URL
#  storage : SQLite(/config/dex.db に保存、UID 1001 書き込み可)
# =============================================================

issuer: ${DEX_EXTERNAL}

storage:
  type: sqlite3
  config:
    file: /config/dex.db

web:
  http: 0.0.0.0:5556

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

chown -R 1001:1001 "$INSTALL_DIR/dex"

echo " dex/config/config.yaml 作成完了"
echo ""
echo " ▼ 登録ユーザー一覧:"
echo "$USERS_YAML" | grep 'email:' | sed 's/.*email: "\(.*\)"/    ログインID: \1/'
echo ""

# ================================================================
#  STEP 5: .env 作成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 5: .env 作成"
echo "------------------------------------------------"

cat > "$INSTALL_DIR/.env" <<EOF
# ============================================================
#  Outline 環境変数
#  生成日時: $(date '+%Y-%m-%d %H:%M:%S')
#  認証方式: Dex OIDC
# ============================================================

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認証 (Dex)
# OIDC_AUTH_URI     : ブラウザ用(外部ホスト名)
# OIDC_TOKEN_URI    : コンテナ間通信用(コンテナ名)
# OIDC_USERINFO_URI : コンテナ間通信用(コンテナ名)
OIDC_CLIENT_ID=outline
OIDC_CLIENT_SECRET=${DEX_CLIENT_SECRET}
OIDC_AUTH_URI=${DEX_EXTERNAL}/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 "$INSTALL_DIR/.env"
echo " .env 作成完了"

# ================================================================
#  STEP 6: docker-compose.yml 作成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 6: docker-compose.yml 作成"
echo "------------------------------------------------"

cat > "$INSTALL_DIR/docker-compose.yml" <<EOF
services:

  outline:
    image: outlinewiki/outline:latest
    container_name: outline
    restart: unless-stopped
    env_file: .env
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      dex:
        condition: service_healthy
    volumes:
      - ./data/storage:/var/lib/outline/data
    networks:
      - outline-net

  dex:
    image: ghcr.io/dexidp/dex:latest
    container_name: outline-dex
    restart: unless-stopped
    command: dex serve /config/config.yaml
    ports:
      - "5556:5556"
    volumes:
      - ./dex/config:/config
    networks:
      - outline-net
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:5556/healthz"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 10s

  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
    networks:
      - outline-net
    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
    networks:
      - outline-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  outline-net:
    driver: bridge
EOF

docker compose config --quiet && echo " docker-compose.yml 作成完了"

# ================================================================
#  STEP 7: バックアップスクリプト作成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 7: バックアップスクリプト作成"
echo "------------------------------------------------"

cat > "$INSTALL_DIR/outline-backup.sh" <<'BACKUP_SCRIPT'
#!/bin/bash
set -e

INSTALL_DIR="/opt/docker/outline"
BACKUP_DIR="/opt/lxd-data/outline-bk"
DATE=$(date '+%Y%m%d_%H%M%S')
BACKUP_NAME="outline_backup_${DATE}"
KEEP_DAYS=7

echo "================================================"
echo "  Outline バックアップ開始: $DATE"
echo "================================================"

mkdir -p "$BACKUP_DIR/$BACKUP_NAME"

echo "▼ データベースをバックアップしています..."
docker exec outline-postgres pg_dump -U outline outline \
  > "$BACKUP_DIR/$BACKUP_NAME/database.sql"
echo " データベースダンプ完了"

echo "▼ ストレージをバックアップしています..."
tar -czf "$BACKUP_DIR/$BACKUP_NAME/storage.tar.gz" \
  -C "$INSTALL_DIR/data" storage
echo " ストレージバックアップ完了"

echo "▼ Dex設定をバックアップしています..."
tar -czf "$BACKUP_DIR/$BACKUP_NAME/dex-config.tar.gz" \
  -C "$INSTALL_DIR/dex" config
echo " Dex設定バックアップ完了"

cp "$INSTALL_DIR/.env" "$BACKUP_DIR/$BACKUP_NAME/.env"
echo " .env バックアップ完了"

tar -czf "$BACKUP_DIR/${BACKUP_NAME}.tar.gz" \
  -C "$BACKUP_DIR" "$BACKUP_NAME"
rm -rf "$BACKUP_DIR/$BACKUP_NAME"

find "$BACKUP_DIR" -name "outline_backup_*.tar.gz" \
  -mtime +${KEEP_DAYS} -delete

BACKUP_SIZE=$(du -sh "$BACKUP_DIR/${BACKUP_NAME}.tar.gz" | cut -f1)
echo ""
echo "================================================"
echo "  バックアップ完了!"
echo "  ファイル: $BACKUP_DIR/${BACKUP_NAME}.tar.gz"
echo "  サイズ  : $BACKUP_SIZE"
echo "  保持数  : $(ls $BACKUP_DIR/outline_backup_*.tar.gz 2>/dev/null | wc -l) 件"
echo "================================================"
BACKUP_SCRIPT

chmod +x "$INSTALL_DIR/outline-backup.sh"
echo " outline-backup.sh 作成完了"

# ================================================================
#  STEP 8: リストアスクリプト作成
# ================================================================

cat > "$INSTALL_DIR/outline-restore.sh" <<'RESTORE_SCRIPT'
#!/bin/bash
# 使い方:
#   sudo bash outline-restore.sh                        # 最新を自動選択
#   sudo bash outline-restore.sh /path/to/backup.tar.gz # ファイル直接指定
set -e

INSTALL_DIR="/opt/docker/outline"
BACKUP_DIR="/opt/lxd-data/outline-bk"

if [ -n "$1" ]; then
  BACKUP_FILE="$1"
else
  BACKUP_FILE=$(ls -t "$BACKUP_DIR"/outline_backup_*.tar.gz 2>/dev/null | head -1)
  if [ -z "$BACKUP_FILE" ]; then
    echo "エラー: バックアップファイルが見つかりません"
    exit 1
  fi
  echo "================================================"
  echo "  最新バックアップ: $(basename $BACKUP_FILE)"
  echo "  サイズ: $(du -sh "$BACKUP_FILE" | cut -f1)"
  echo "================================================"
fi

read -rp "リストアしますか?現在のデータは上書きされます。(yes/no): " CONFIRM
[ "$CONFIRM" = "yes" ] || { echo "キャンセルしました。"; exit 0; }

# カレントディレクトリを固定(rm後にgetcwdエラーが出ないよう /tmp に移動)
cd /tmp

WORK_DIR=$(mktemp -d)
tar -xzf "$BACKUP_FILE" -C "$WORK_DIR"
BACKUP_PATH="$WORK_DIR/$(ls "$WORK_DIR")"

docker compose -f "$INSTALL_DIR/docker-compose.yml" stop outline dex

docker exec -i outline-postgres psql -U outline -d postgres -c "DROP DATABASE IF EXISTS outline;"
docker exec -i outline-postgres psql -U outline -d postgres -c "CREATE DATABASE outline;"
docker exec -i outline-postgres psql -U outline -d outline < "$BACKUP_PATH/database.sql"
echo " DBリストア完了"

rm -rf "$INSTALL_DIR/data/storage"
tar -xzf "$BACKUP_PATH/storage.tar.gz" -C "$INSTALL_DIR/data"
chown -R 1001:1001 "$INSTALL_DIR/data/storage"
echo " ストレージリストア完了"

if [ -f "$BACKUP_PATH/dex-config.tar.gz" ]; then
  rm -rf "$INSTALL_DIR/dex/config"
  mkdir -p "$INSTALL_DIR/dex/config"
  tar -xzf "$BACKUP_PATH/dex-config.tar.gz" -C "$INSTALL_DIR/dex"
  chown -R 1001:1001 "$INSTALL_DIR/dex"
  echo " Dex設定リストア完了"
fi

rm -rf "$WORK_DIR"

docker compose -f "$INSTALL_DIR/docker-compose.yml" start outline dex
sleep 10
docker compose -f "$INSTALL_DIR/docker-compose.yml" ps

echo ""
echo "================================================"
echo "  リストア完了!"
echo "================================================"
RESTORE_SCRIPT

chmod +x "$INSTALL_DIR/outline-restore.sh"
echo " outline-restore.sh 作成完了"

# ================================================================
#  STEP 9: ユーザー追加スクリプト作成
# ================================================================
echo "------------------------------------------------"
echo "  STEP 9: ユーザー追加スクリプト作成"
echo "------------------------------------------------"

cat > "$INSTALL_DIR/outline-adduser.sh" <<'ADDUSER_SCRIPT'
#!/bin/bash
# 使い方: sudo bash /opt/docker/outline/outline-adduser.sh

INSTALL_DIR="/opt/docker/outline"
DEX_CONFIG="$INSTALL_DIR/dex/config/config.yaml"

if ! command -v htpasswd &>/dev/null; then
  apt-get install -y apache2-utils &>/dev/null
fi

echo "================================================"
echo "  Outline ユーザー追加"
echo "================================================"
read -rp "ユーザー名(例: yamada): " U_NAME
read -rsp "パスワード: " U_PASS
echo ""

U_EMAIL="${U_NAME}@local.invalid"

if grep -q "\"${U_EMAIL}\"" "$DEX_CONFIG"; then
  echo "エラー: ユーザー「${U_NAME}」はすでに登録されています。"
  exit 1
fi

U_HASH=$(htpasswd -bnBC 10 "" "${U_PASS}" | tr -d ':\n' | sed 's/\$2y/\$2a/')
U_UUID=$(cat /proc/sys/kernel/random/uuid)

python3 - <<PYEOF
content = open("${DEX_CONFIG}").read()
entry = '\n  - email: "${U_EMAIL}"\n    hash: "${U_HASH}"\n    username: "${U_NAME}"\n    userID: "${U_UUID}"\n'
content = content.replace('\nlogger:', entry + '\nlogger:')
open("${DEX_CONFIG}", 'w').write(content)
PYEOF

docker compose -f "$INSTALL_DIR/docker-compose.yml" restart dex
sleep 5

echo ""
echo "================================================"
echo "  追加完了!ログインID: ${U_NAME}@local.invalid"
echo "================================================"
ADDUSER_SCRIPT

chmod +x "$INSTALL_DIR/outline-adduser.sh"
echo " outline-adduser.sh 作成完了"

# ================================================================
#  STEP 10: cron 設定
# ================================================================
echo "------------------------------------------------"
echo "  STEP 10: cron 設定"
echo "------------------------------------------------"

if ! command -v crontab &>/dev/null; then
  apt-get install -y cron &>/dev/null
  systemctl enable cron
  systemctl start cron
fi

CRON_JOB="0 2 * * * bash $INSTALL_DIR/outline-backup.sh >> $BACKUP_DIR/backup.log 2>&1"
( sudo -u "$REAL_USER" crontab -l 2>/dev/null | grep -v "outline-backup.sh"; echo "$CRON_JOB" ) \
  | sudo -u "$REAL_USER" crontab -

echo " cron 登録完了(毎日 2:00 AM)"

# ================================================================
#  STEP 11: 起動
# ================================================================
echo "------------------------------------------------"
echo "  STEP 11: Outline 起動"
echo "------------------------------------------------"

docker compose up -d

echo ""
echo "▼ 起動を待機しています(30秒)..."
sleep 30
docker compose ps

# ================================================================
#  完了
# ================================================================
echo ""
echo "================================================"
echo "  セットアップ完了!"
echo ""
echo "  Outline : ${BASE_URL}"
echo "  Dex     : ${DEX_EXTERNAL}"
echo ""
echo "  ログイン手順:"
echo "  1. ${BASE_URL} を開く"
echo "  2.「ログイン」ボタンをクリック"
echo "  3. Dexのログイン画面でIDとパスワードを入力"
echo ""
echo "  ▼ 登録済みユーザー:"
echo "$USERS_YAML" | grep 'email:' | sed 's/.*email: "\(.*\)"/    ログインID: \1/'
echo ""
echo "  ユーザー追加    : sudo bash $INSTALL_DIR/outline-adduser.sh"
echo "  手動バックアップ: bash $INSTALL_DIR/outline-backup.sh"
echo "  リストア        : sudo bash $INSTALL_DIR/outline-restore.sh"
echo "  cron確認        : crontab -l"
echo "  ログ確認        : docker compose -f $INSTALL_DIR/docker-compose.yml logs -f"
echo "  停止            : docker compose -f $INSTALL_DIR/docker-compose.yml down"
echo "================================================"
ユーザー追加    : sudo bash /opt/docker/outline/outline-adduser.sh
手動バックアップ: bash /opt/docker/outline/outline-backup.sh
リストア : sudo bash /opt/docker/outline/outline-restore.sh
cron確認 : crontab -l
ログ確認 : docker compose -f /opt/docker/outline/docker-compose.yml logs -f
停止 : docker compose -f /opt/docker/outline/docker-compose.yml down

バックアップ先はホスト共有ディレクトリ/opt/lxd-data/outline-bkに保存されます。

FleshRSS

mkdir -p /opt/docker/freshrss

cat > /opt/docker/freshrss/docker-compose.yml <<'EOF'
services:
  freshrss:
    image: freshrss/freshrss:latest
    container_name: freshrss
    restart: unless-stopped
    ports:
      - "6060:80"
    volumes:
      - freshrss_data:/var/www/FreshRSS/data
      - freshrss_extensions:/var/www/FreshRSS/extensions
    environment:
      TZ: Asia/Tokyo
      CRON_MIN: '*/15'

volumes:
  freshrss_data:
  freshrss_extensions:
EOF

cd /opt/docker/freshrss
docker compose up -d
sleep 10

# unzip の確認・インストール
if ! command -v unzip &>/dev/null; then
  apt install -y unzip
fi

# 拡張機能: Three Panes View
curl -L "https://framagit.org/nicofrand/xextension-threepanesview/-/archive/master/xextension-threepanesview-master.zip" -o tpv.zip
unzip -q tpv.zip
docker cp xextension-threepanesview-master freshrss:/var/www/FreshRSS/extensions/xExtension-ThreePanesView
docker exec freshrss chown -R www-data:www-data /var/www/FreshRSS/extensions/xExtension-ThreePanesView
rm -rf tpv.zip xextension-threepanesview-master

# 拡張機能: AF Readability
curl -L "https://github.com/Niehztog/freshrss-af-readability/archive/refs/heads/master.zip" -o af.zip
unzip -q af.zip
docker cp freshrss-af-readability-master freshrss:/var/www/FreshRSS/extensions/xExtension-af_readability
docker exec freshrss chown -R www-data:www-data /var/www/FreshRSS/extensions/xExtension-af_readability
rm -rf af.zip freshrss-af-readability-master

echo ""
echo "======================================"
echo "  FreshRSS 起動完了"
echo "======================================"
echo "  URL: http://localhost:6060"
echo "  ブラウザでアクセスして初期設定を行ってください"
echo ""
echo "  導入済み拡張機能:"
echo "    - xExtension-ThreePanesView"
echo "    - xExtension-af_readability"
echo "======================================"

CSS例

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

Nextcloud & Onlyoffice

#!/bin/bash
set -e

# === ホスト名を入力 ===
read -rp "ホスト名またはIPアドレスを入力してください: " HOST_NAME

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

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

# ============================================================
# Nextcloud
# ============================================================
mkdir -p /opt/lxd-data/docker/nextcloud/{data,db}
cd /opt/lxd-data/docker/nextcloud

cat > 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:
      - ./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:
      - "8080:80"
    depends_on:
      - db
    volumes:
      - ./data:/var/www/html
    environment:
      MYSQL_PASSWORD: nextcloudpass
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_HOST: nextcloud-db
      NEXTCLOUD_TRUSTED_DOMAINS: "${HOST_NAME}:8080,nextcloud-app,localhost"
      OVERWRITEHOST: "${HOST_NAME}:8080"
      OVERWRITEPROTOCOL: http
    networks:
      - default
      - onlyoffice_net

networks:
  default:
  onlyoffice_net:
    external: true
EOF

docker compose up -d
echo "[1/2] Nextcloud 起動完了"

# ============================================================
# OnlyOffice
# ============================================================
mkdir -p /opt/lxd-data/docker/onlyoffice/{logs,data,lib,db}
cd /opt/lxd-data/docker/onlyoffice

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

cat > docker-compose.yml <<'EOF'
services:

  onlyoffice-docs:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice-docs
    restart: always
    stdin_open: true
    tty: true
    ports:
      - "9000:80"
    environment:
      JWT_ENABLED: "true"
      JWT_SECRET: "${JWT_SECRET}"   # .env から自動読み込み
    volumes:
      - /opt/lxd-data/docker/onlyoffice/logs:/var/log/onlyoffice
      - /opt/lxd-data/docker/onlyoffice/data:/var/www/onlyoffice/Data
      - /opt/lxd-data/docker/onlyoffice/lib:/var/lib/onlyoffice
      - /opt/lxd-data/docker/onlyoffice/db:/var/lib/postgresql
    networks:
      - onlyoffice_net

networks:
  onlyoffice_net:
    external: true
EOF

docker compose up -d
echo "[2/2] OnlyOffice 起動完了"

# ============================================================
# 完了メッセージ
# ============================================================
echo ""
echo "======================================"
echo "  起動完了"
echo "======================================"
echo ""
echo "  Nextcloud  : http://${HOST_NAME}:8080"
echo "  OnlyOffice : http://${HOST_NAME}:9000"
echo ""
echo "======================================"
echo "  Nextcloud 管理画面 → ONLYOFFICE 設定"
echo "======================================"
echo ""
echo "  ONLYOFFICE Docs アドレス:"
echo "    http://${HOST_NAME}:9000"
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 /opt/lxd-data/docker/nextcloud && docker compose pull && docker compose up -d"
echo ""
echo "  OnlyOffice のみ更新:"
echo "    cd /opt/lxd-data/docker/onlyoffice && 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/lxd-data/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

Nextcloudアクセス時に権限エラーが出るようなら修正

docker exec nextcloud-app chown -R www-data:www-data /var/www/html/config

Immich

# ディレクトリ作成
mkdir -p /opt/lxd-data/docker/immich
cd /opt/lxd-data/docker/immich

# .env 作成(パスワード自動生成のためクォートなしEOF)
DB_PASSWORD=$(openssl rand -hex 16)

cat > .env <<EOF
UPLOAD_LOCATION=./library
DB_DATA_LOCATION=./postgres
IMMICH_VERSION=release
DB_PASSWORD=${DB_PASSWORD}
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOF
chmod 600 .env

# 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}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '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

# コンテナ起動
docker compose up -d

echo ""
echo "======================================"
echo "  Immich 起動完了"
echo "======================================"
echo "  URL        : http://localhost:2283"
echo "  DB_PASSWORD: ${DB_PASSWORD}  (.env に保存済み)"
echo "======================================"
echo "  ブラウザでアクセスして初期設定を行ってください"
echo "======================================"

テンプレート例

{{y}}/{{y}}{{MM}}/{{y}}{{MM}}{{dd}}_{{album}}/{{filename}}

Dockhand

mkdir -p /opt/docker/dockhand

cat > /opt/docker/dockhand/docker-compose.yml <<'EOF'
services:
  dockhand:
    image: fnsys/dockhand:latest
    container_name: dockhand
    restart: unless-stopped
    ports:
      - "3333:3000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - dockhand_data:/app/data

volumes:
  dockhand_data:
EOF

cd /opt/docker/dockhand
docker compose up -d

echo ""
echo "======================================"
echo "  Dockhand 起動完了"
echo "======================================"
echo "  URL: http://localhost:3333"
echo "  ブラウザでアクセスして初期設定を行ってください"
echo "======================================"

Syncthing(直インストール)

#!/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 ディレクトリのパーミッションを設定します..."
chmod o+rx /opt/lxd-data/docker/nextcloud
chmod o+rx /opt/lxd-data/docker/nextcloud/data
chmod o+rx /opt/lxd-data/docker/nextcloud/data/data
chmod o+rx /opt/lxd-data/docker/nextcloud/data/data/user
chmod -R o+rX /opt/lxd-data/docker/nextcloud/data/data/user/files
chmod o+w /opt/lxd-data/docker/nextcloud/data/data/user/files
success "パーミッション設定が完了しました。"

# ── 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"
echo -e "         /opt/lxd-data/docker/nextcloud/data/data/user/files"
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

復元後に権限エラーが出る場合

LXDコンテナをエクスポートして復元した場合、ファイルの「内容」は保持しますが「所有者(UID/GID)」の解釈がずれることがあり、エラーが発生するケースがあります。その場合は下記のように権限の修正が必要です。

#!/bin/bash
# restore-permissions.sh
echo "==> Nextcloud"
chown -R 33:33 /opt/lxd-data/docker/nextcloud/data/config

echo "==> Outline / Dex"
chown -R 1001:1001 /opt/lxd-data/docker/outline/dex
chown -R 1001:1001 /opt/lxd-data/docker/outline/data/storage

echo "==> Immich"
chown -R 999:999 /opt/lxd-data/docker/immich/postgres

echo "==> 完了。コンテナを再起動します..."
docker compose -f /opt/lxd-data/docker/nextcloud/docker-compose.yml restart
docker compose -f /opt/lxd-data/docker/outline/docker-compose.yml restart
docker compose -f /opt/lxd-data/docker/immich/docker-compose.yml restart

echo "==> 完了!"
タイトルとURLをコピーしました