Ubuntu 26.04ベースのコンテナで各サービスを動作

Dockerでインストールするサービスが増えてきたので、効率を考え、この機会に1つのLXDコンテナ内で複数のサービスを動かすようにしてみます。各サービスはTailscale ServeでHTTPS化します。
このスクリプトを使って作成した、Dockerが使えるコンテナで実行してください。

構成のイメージ

LANからはアクセスできない 各サービスは 127.0.0.1 にバインドされているので、同じLAN内のPCからポート番号を直打ちしてもつながりません。Tailscaleに参加していない端末はどこからでも弾かれます。

インターネットからも届かない Tailscaleネットワーク外には一切公開されません。 tailscale serve status とすれば、末尾に (tailnet only) と表示されます。

Tailscale経由はWireGuard VPNトンネル内のHTTPS 通信はWireGuardで暗号化された上にHTTPS(TLS)もかかっているので二重に安全です。証明書はTailscaleが自動管理してくれます。

下記からの続きのイメージです。

Ubuntu 26.04 LTS環境での初期セットアップ
Ubuntu 26.04の正式版が公開されたことで、LXDコンテナでもUbuntu 26.04が使えるようになりました。既存のスクリプトだとエラーが出たりするので、ここで一旦整理します。インストール直後を想定しています。アップデート・Tai…

Dockhand

#!/bin/bash
set -euo pipefail

INSTALL_DIR="/opt/docker/dockhand"
PORT=3333          # ホスト内部ポート(127.0.0.1バインド)
TAILSCALE_PORT=3302  # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Dockhand セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/4] Tailscaleドメインを取得..."

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Dockhand : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/4] ディレクトリを準備..."
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
echo -e "  ${GREEN}✓ ${INSTALL_DIR}${NC}"

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [3/4] 設定ファイルを生成..."

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

volumes:
  dockhand_data:
EOF

echo -e "  ${GREEN}✓ ${INSTALL_DIR}/docker-compose.yml${NC}"

# ── Docker起動 & tailscale serve設定 ─────────────
echo ""
echo "==> [4/4] コンテナを起動..."

docker compose pull
docker compose up -d

echo "  ⏳ Dockhandの起動を待機中(最大60秒)..."
for i in $(seq 1 12); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Dockhand 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 12 ]; then
        echo -e "\n${RED}ERROR: Dockhandがタイムアウトしました${NC}"
        docker logs dockhand --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

# 既存のserve設定を残しつつDockhandのポートのみ追加
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  Dockhand 起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL      : https://${TAILSCALE_DOMAIN}:${TAILSCALE_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 && \\"
echo "             docker compose -f ${INSTALL_DIR}/docker-compose.yml up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

起動したらlocalhostに接続します。

Linkwarden

#!/bin/bash
set -euo pipefail
# =============================================================
#  Linkwarden セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Linkwarden : https://<hostname>.<tailnet>.ts.net:3301 (tailscale serve 3301)
#
#  ディレクトリ構成:
#   /opt/docker/linkwarden/
#     docker-compose.yml
#     .secrets          ← パスワード等の永続化
#     data/             ← Linkwardenデータ
#     postgres/         ← PostgreSQLデータ (Dockerボリューム)
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

LINKWARDEN_DIR="/opt/docker/linkwarden"
SECRETS_FILE="${LINKWARDEN_DIR}/.secrets"
PORT=3300          # Dockerコンテナ→ホスト間の内部ポート
TAILSCALE_PORT=3301  # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Linkwarden セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/5] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Linkwarden : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/5] ディレクトリを準備..."
mkdir -p "${LINKWARDEN_DIR}"
echo -e "  ${GREEN}✓ /opt/docker/linkwarden/${NC}"

# ── シークレット生成 or 既存ファイルから読み込み ──
echo ""
echo "==> [3/5] シークレットを確認..."

if [ -f "${SECRETS_FILE}" ]; then
    source "${SECRETS_FILE}"
    echo -e "  ${GREEN}✓ 既存のシークレットを使用${NC}"
else
    NEXTAUTH_SECRET=$(openssl rand -hex 32)
    POSTGRES_PASSWORD=$(openssl rand -hex 16)

    cat > "${SECRETS_FILE}" <<SECRETS
NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
SECRETS
    chmod 600 "${SECRETS_FILE}"
    echo -e "  ${GREEN}✓ 新しいシークレットを生成: ${SECRETS_FILE}${NC}"
fi

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [4/5] 設定ファイルを生成..."

cat > "${LINKWARDEN_DIR}/docker-compose.yml" <<EOF
services:
  postgres:
    image: postgres:16-alpine
    container_name: linkwarden-postgres
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      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
    environment:
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
      NEXTAUTH_URL: https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}
      DATABASE_URL: postgresql://linkwarden:${POSTGRES_PASSWORD}@postgres:5432/linkwarden
    ports:
      - "127.0.0.1:${PORT}:3000"
    volumes:
      - linkwarden_data:/data/data
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
  linkwarden_data:
EOF

echo -e "  ${GREEN}✓ ${LINKWARDEN_DIR}/docker-compose.yml${NC}"

# ── Docker起動 & tailscale serve設定 ─────────────
echo ""
echo "==> [5/5] コンテナを起動..."

cd "${LINKWARDEN_DIR}"
docker compose pull
docker compose up -d

echo "  ⏳ PostgreSQLの初期化を待機中(最大60秒)..."
for i in $(seq 1 12); do
    STATUS=$(docker inspect linkwarden-postgres --format='{{.State.Health.Status}}' 2>/dev/null || echo "not_found")
    if [ "$STATUS" = "healthy" ]; then
        echo -e "\n  ${GREEN}✓ PostgreSQL 初期化完了${NC}"
        break
    fi
    if [ "$i" -eq 12 ]; then
        echo -e "\n${RED}ERROR: PostgreSQLがタイムアウトしました${NC}"
        docker logs linkwarden-postgres --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

echo "  ⏳ Linkwardenの起動を待機中(最大120秒)..."
for i in $(seq 1 24); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Linkwarden 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 24 ]; then
        echo -e "\n${RED}ERROR: Linkwardenがタイムアウトしました${NC}"
        docker logs linkwarden --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

# 既存のserve設定を残しつつLinkwardenのポートのみ追加
# 同ポートが既に登録済みの場合は一度削除してから再登録
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo ""
echo "  📁 ディレクトリ構成:"
echo "    設定  : ${LINKWARDEN_DIR}/docker-compose.yml"
echo "    秘密鍵: ${LINKWARDEN_DIR}/.secrets"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  cd ${LINKWARDEN_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

Outline

#!/bin/bash
set -euo pipefail
# =============================================================
#  Outline セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Outline : https://<hostname>.<tailnet>.ts.net:3303 (tailscale serve 3303)
#   - Dex     : https://<hostname>.<tailnet>.ts.net:3304 (tailscale serve 3304)
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

INSTALL_DIR="/opt/docker/outline"
OUTLINE_PORT=3900      # ループバックのみ(tailscale serve経由)
DEX_PORT=15556         # ループバックのみ(tailscale serve経由)
OUTLINE_TS_PORT=3303   # tailscale serveで公開するポート
DEX_TS_PORT=3304       # tailscale serveで公開するポート(Dex)

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Outline セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメインとホスト名を取得 ───────────────
echo "==> [1/6] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILNET=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('MagicDNSSuffix', ''))
" 2>/dev/null)

HOSTNAME=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
dns = d.get('Self', {}).get('DNSName', '').rstrip('.')
print(dns)
" 2>/dev/null)

if [ -z "$TAILNET" ] || [ -z "$HOSTNAME" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

BASE_URL="https://${HOSTNAME}:${OUTLINE_TS_PORT}"
DEX_URL="https://${HOSTNAME}:${DEX_TS_PORT}"
DEX_INTERNAL="http://dex:5556"

echo -e "  ${GREEN}Outline : ${BASE_URL}${NC}"
echo -e "  ${GREEN}Dex     : ${DEX_URL}${NC}"

# ── ユーザー登録 ──────────────────────────────────
echo ""
echo "==> [2/6] ユーザーを登録..."
if ! command -v htpasswd &>/dev/null; then
    echo "  htpasswdをインストール中..."
    apt-get install -y apache2-utils &>/dev/null
fi

USERS_YAML=""
USER_EMAILS=""
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}\""
    USER_EMAILS="${USER_EMAILS}    - ${U_EMAIL}\n"
    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)

# ── インストールディレクトリ ──────────────────────
echo ""
echo "==> [3/6] ファイルを生成..."
mkdir -p "$INSTALL_DIR/dex/config"
mkdir -p "$INSTALL_DIR/data/storage"
mkdir -p "$INSTALL_DIR/data/postgres"
mkdir -p "$INSTALL_DIR/data/redis"
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: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

# ── .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_REDIRECT_URI=${BASE_URL}/auth/oidc.callback
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:
      - "127.0.0.1:${OUTLINE_PORT}:3000"
    volumes:
      - ./data/storage:/var/lib/outline/data
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      dex:
        condition: service_healthy
    networks:
      - outline-net

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

  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
    networks:
      - outline-net

  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
    networks:
      - outline-net

networks:
  outline-net:
    driver: bridge
EOF

# ── Docker起動 ────────────────────────────────────
echo ""
echo "==> [4/6] Dockerコンテナを起動..."
docker compose pull
docker compose up -d

echo "  ⏳ コンテナの起動を待機中(最大90秒)..."
for i in $(seq 1 18); do
    STATUS=$(docker compose ps --format json 2>/dev/null | python3 -c "
import json,sys
lines=[l for l in sys.stdin if l.strip()]
healthy=sum(1 for l in lines if 'healthy' in l or '\"running\"' in l.lower())
print(healthy)
" 2>/dev/null || echo "0")
    if [ "$STATUS" -ge 4 ]; then
        break
    fi
    sleep 5
    echo -n "."
done
echo ""

# ── tailscale serve 設定 ──────────────────────────
echo ""
echo "==> [5/6] tailscale serve を設定..."

# 既存のserve設定を残しつつ、このポートのみ追加(冪等対応)
tailscale serve --https=${OUTLINE_TS_PORT} off 2>/dev/null || true
tailscale serve --https=${DEX_TS_PORT} off 2>/dev/null || true

tailscale serve --bg --https=${OUTLINE_TS_PORT} http://localhost:${OUTLINE_PORT}
echo -e "  ${GREEN}✓ Outline : ${BASE_URL}${NC}"

tailscale serve --bg --https=${DEX_TS_PORT} http://localhost:${DEX_PORT}
echo -e "  ${GREEN}✓ Dex     : ${DEX_URL}${NC}"

echo ""
echo "==> [6/6] serve状態を確認..."
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  Outline 起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Outline : ${BASE_URL}"
echo "  🌐 Dex     : ${DEX_URL}"
echo "  📂 インストール先: ${INSTALL_DIR}"
echo ""
echo "  ▼ 登録済みユーザー:"
echo -e "${USER_EMAILS}"
echo "  ⏳ 初回アクセスまで1〜2分かかる場合があります"
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 && \\"
echo "             docker compose -f ${INSTALL_DIR}/docker-compose.yml up -d"
echo "════════════════════════════════════════"
echo ""

Nextcloud&OnlyOffice

#!/bin/bash
set -euo pipefail
# =============================================================
#  Nextcloud + OnlyOffice セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Nextcloud  : https://<hostname>.<tailnet>.ts.net:3305 (tailscale serve 3305)
#   - OnlyOffice : https://<hostname>.<tailnet>.ts.net:3306 (tailscale serve 3306)
#
#  ディレクトリ構成:
#   /opt/docker/nextcloud/
#     docker-compose.yml
#     .secrets
#     db/ userdata/ appdata/
#   /opt/docker/onlyoffice/
#     docker-compose.yml .env
#     logs/ data/ lib/ db/
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

NEXTCLOUD_DIR="/opt/docker/nextcloud"
ONLYOFFICE_DIR="/opt/docker/onlyoffice"
SECRETS_FILE="${NEXTCLOUD_DIR}/.secrets"

NEXTCLOUD_PORT=8181      # ホスト内部ポート(127.0.0.1バインド)※8080は既存サービスが使用中
ONLYOFFICE_PORT=9090     # ホスト内部ポート(127.0.0.1バインド)※9000から変更
NEXTCLOUD_TS_PORT=3305   # tailscale serveで公開するポート
ONLYOFFICE_TS_PORT=3306  # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Nextcloud + OnlyOffice セットアップ"
echo "  (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/6] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Nextcloud  : https://${TAILSCALE_DOMAIN}:${NEXTCLOUD_TS_PORT}${NC}"
echo -e "  ${GREEN}OnlyOffice : https://${TAILSCALE_DOMAIN}:${ONLYOFFICE_TS_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/6] ディレクトリを準備..."
mkdir -p "${NEXTCLOUD_DIR}"/{db,userdata,appdata}
mkdir -p "${ONLYOFFICE_DIR}"/{logs,data,lib,db}
echo -e "  ${GREEN}✓ /opt/docker/nextcloud/${NC}"
echo -e "  ${GREEN}✓ /opt/docker/onlyoffice/${NC}"

# ── シークレット生成 or 既存ファイルから読み込み ──
echo ""
echo "==> [3/6] シークレットを確認..."

if [ -f "${SECRETS_FILE}" ]; then
    source "${SECRETS_FILE}"
    echo -e "  ${GREEN}✓ 既存のシークレットを使用${NC}"
else
    if [ "$(find "${NEXTCLOUD_DIR}/db" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 ]; then
        echo -e "${RED}ERROR: DBデータが存在しますがシークレットファイルが見つかりません${NC}"
        echo -e "${RED}       ${SECRETS_FILE} が必要です${NC}"
        echo ""
        echo "  対処法: DBデータを削除して再セットアップ:"
        echo "    docker compose -f ${NEXTCLOUD_DIR}/docker-compose.yml down -v"
        echo "    rm -rf ${NEXTCLOUD_DIR}/db/*"
        exit 1
    fi

    MYSQL_ROOT_PASSWORD=$(openssl rand -hex 16)
    MYSQL_PASSWORD=$(openssl rand -hex 16)
    JWT_SECRET=$(openssl rand -hex 32)

    cat > "${SECRETS_FILE}" <<SECRETS
MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
MYSQL_PASSWORD=${MYSQL_PASSWORD}
JWT_SECRET=${JWT_SECRET}
SECRETS
    chmod 600 "${SECRETS_FILE}"
    echo -e "  ${GREEN}✓ 新しいシークレットを生成: ${SECRETS_FILE}${NC}"
fi

# ── 共有ネットワーク作成 ──────────────────────────
echo ""
echo "==> [4/6] Dockerネットワークを確認..."
docker network inspect onlyoffice_net >/dev/null 2>&1 \
    || docker network create onlyoffice_net
echo -e "  ${GREEN}✓ onlyoffice_net${NC}"

# ── 設定ファイル生成 ──────────────────────────────
echo ""
echo "==> [5/6] 設定ファイルを生成..."

cat > "${NEXTCLOUD_DIR}/docker-compose.yml" <<EOF
services:
  db:
    image: mariadb:10.6
    container_name: nextcloud-db
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
    volumes:
      - ${NEXTCLOUD_DIR}/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    networks:
      - default

  app:
    image: nextcloud:latest
    container_name: nextcloud-app
    restart: unless-stopped
    ports:
      - "127.0.0.1:${NEXTCLOUD_PORT}:80"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - ${NEXTCLOUD_DIR}/appdata:/var/www/html
      - ${NEXTCLOUD_DIR}/userdata:/var/www/html/data
    environment:
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: nextcloud
      MYSQL_USER: nextcloud
      MYSQL_HOST: nextcloud-db
      NEXTCLOUD_TRUSTED_DOMAINS: "${TAILSCALE_DOMAIN}"
      OVERWRITEHOST: "${TAILSCALE_DOMAIN}:${NEXTCLOUD_TS_PORT}"
      OVERWRITEPROTOCOL: https
    networks:
      - default
      - onlyoffice_net

networks:
  default:
  onlyoffice_net:
    external: true
EOF

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

cat > "${ONLYOFFICE_DIR}/docker-compose.yml" <<EOF
services:
  onlyoffice-docs:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice-docs
    restart: unless-stopped
    stdin_open: true
    tty: true
    ports:
      - "127.0.0.1:${ONLYOFFICE_PORT}:80"
    environment:
      JWT_ENABLED: "true"
      JWT_SECRET: "${JWT_SECRET}"
    volumes:
      - ${ONLYOFFICE_DIR}/logs:/var/log/onlyoffice
      - ${ONLYOFFICE_DIR}/data:/var/www/onlyoffice/Data
      - ${ONLYOFFICE_DIR}/lib:/var/lib/onlyoffice
      - ${ONLYOFFICE_DIR}/db:/var/lib/postgresql
    networks:
      - onlyoffice_net

networks:
  onlyoffice_net:
    external: true
EOF

echo -e "  ${GREEN}✓ ${NEXTCLOUD_DIR}/docker-compose.yml${NC}"
echo -e "  ${GREEN}✓ ${ONLYOFFICE_DIR}/docker-compose.yml${NC}"

# ── Docker起動 ────────────────────────────────────
echo ""
echo "==> [6/6] Dockerコンテナを起動..."

echo "  ⏳ Nextcloudを起動中..."
cd "${NEXTCLOUD_DIR}"
docker compose pull
docker compose up -d

echo "  ⏳ MariaDBの初期化を待機中(最大120秒)..."
for i in $(seq 1 24); do
    STATUS=$(docker inspect nextcloud-db --format='{{.State.Health.Status}}' 2>/dev/null || echo "not_found")
    if [ "$STATUS" = "healthy" ]; then
        echo -e "\n  ${GREEN}✓ MariaDB 初期化完了${NC}"
        break
    fi
    if [ "$i" -eq 24 ]; then
        echo -e "\n${RED}ERROR: MariaDBがタイムアウトしました${NC}"
        docker logs nextcloud-db --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

echo "  ⏳ Nextcloud appの起動を待機中(最大120秒)..."
for i in $(seq 1 24); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${NEXTCLOUD_PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Nextcloud app 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 24 ]; then
        echo -e "\n${RED}ERROR: Nextcloud appがタイムアウトしました${NC}"
        docker logs nextcloud-app --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

echo "  ⏳ OnlyOfficeを起動中..."
cd "${ONLYOFFICE_DIR}"
docker compose pull
docker compose up -d
echo -e "  ${GREEN}✓ OnlyOffice 起動${NC}"

# ── tailscale serve 設定 ──────────────────────────
# 既存のserve設定を残しつつ、このポートのみ追加(冪等対応)
tailscale serve --https=${NEXTCLOUD_TS_PORT} off 2>/dev/null || true
tailscale serve --https=${ONLYOFFICE_TS_PORT} off 2>/dev/null || true

tailscale serve --bg --https=${NEXTCLOUD_TS_PORT} http://localhost:${NEXTCLOUD_PORT}
echo -e "  ${GREEN}✓ Nextcloud  : https://${TAILSCALE_DOMAIN}:${NEXTCLOUD_TS_PORT}${NC}"

tailscale serve --bg --https=${ONLYOFFICE_TS_PORT} http://localhost:${ONLYOFFICE_PORT}
echo -e "  ${GREEN}✓ OnlyOffice : https://${TAILSCALE_DOMAIN}:${ONLYOFFICE_TS_PORT}${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 Nextcloud  : https://${TAILSCALE_DOMAIN}:${NEXTCLOUD_TS_PORT}"
echo "  🌐 OnlyOffice : https://${TAILSCALE_DOMAIN}:${ONLYOFFICE_TS_PORT}"
echo ""
echo "════════════════════════════════════════"
echo "  📋 初回セットアップ(Nextcloud)"
echo "════════════════════════════════════════"
echo ""
echo "  ブラウザで https://${TAILSCALE_DOMAIN}:${NEXTCLOUD_TS_PORT} にアクセスして"
echo "  以下を入力してください:"
echo ""
echo "  管理者ユーザー名 : 任意"
echo "  管理者パスワード : 任意"
echo "  データベース     : MySQL/MariaDB を選択"
echo "  DBユーザー       : nextcloud"
echo "  DBパスワード     : ${MYSQL_PASSWORD}"
echo "  DB名             : nextcloud"
echo "  DBホスト         : nextcloud-db"
echo ""
echo "════════════════════════════════════════"
echo "  📋 Nextcloud → ONLYOFFICE 連携設定"
echo "════════════════════════════════════════"
echo ""
echo "  管理画面 → アプリ → ONLYOFFICE をインストール後、"
echo "  管理画面 → ONLYOFFICE で以下を設定:"
echo ""
echo "  ONLYOFFICE Docs アドレス:"
echo "    https://${TAILSCALE_DOMAIN}:${ONLYOFFICE_TS_PORT}"
echo ""
echo "  JWT シークレット:"
echo "    ${JWT_SECRET}"
echo ""
echo "  認証ヘッダー:(空白のまま)"
echo ""
echo "  サーバーから内部リクエストに利用されるアドレス:"
echo "    http://onlyoffice-docs"
echo ""
echo "  ONLYOFFICE Docsから内部リクエストに利用されるアドレス:"
echo "    http://nextcloud-app"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  Nextcloud:"
echo "    cd ${NEXTCLOUD_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "  OnlyOffice:"
echo "    cd ${ONLYOFFICE_DIR} && docker compose pull && docker compose up -d"
echo "════════════════════════════════════════"
echo ""

Immich

#!/bin/bash
set -euo pipefail
# =============================================================
#  Immich セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Immich : https://<hostname>.<tailnet>.ts.net:3307 (tailscale serve 3307)
#
#  ディレクトリ構成:
#   /opt/docker/immich/
#     docker-compose.yml
#     .env              ← DBパスワード等の永続化
#     library/          ← アップロードファイル
#     postgres/         ← PostgreSQLデータ
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

IMMICH_DIR="/opt/docker/immich"
IMMICH_PORT=2283         # ホスト内部ポート(127.0.0.1バインド・元のまま)
TAILSCALE_PORT=3307      # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Immich セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/5] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Immich : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/5] ディレクトリを準備..."
mkdir -p "${IMMICH_DIR}"/{library,postgres}
echo -e "  ${GREEN}✓ /opt/docker/immich/${NC}"

# ── .env 生成 or 既存を使用 ───────────────────────
echo ""
echo "==> [3/5] .env を確認..."

if [ -f "${IMMICH_DIR}/.env" ]; then
    source "${IMMICH_DIR}/.env"
    echo -e "  ${GREEN}✓ 既存の .env を使用${NC}"
else
    if [ "$(find "${IMMICH_DIR}/postgres" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 ]; then
        echo -e "${RED}ERROR: DBデータが存在しますが .env が見つかりません${NC}"
        echo -e "${RED}       ${IMMICH_DIR}/.env が必要です${NC}"
        echo ""
        echo "  対処法: DBデータを削除して再セットアップ:"
        echo "    docker compose -f ${IMMICH_DIR}/docker-compose.yml down -v"
        echo "    rm -rf ${IMMICH_DIR}/postgres/*"
        exit 1
    fi

    DB_PASSWORD=$(openssl rand -hex 16)

    cat > "${IMMICH_DIR}/.env" <<EOF
UPLOAD_LOCATION=${IMMICH_DIR}/library
DB_DATA_LOCATION=${IMMICH_DIR}/postgres
IMMICH_VERSION=release
DB_PASSWORD=${DB_PASSWORD}
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
EOF
    chmod 600 "${IMMICH_DIR}/.env"
    echo -e "  ${GREEN}✓ 新しい .env を生成${NC}"
fi

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [4/5] 設定ファイルを生成..."

cat > "${IMMICH_DIR}/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

echo -e "  ${GREEN}✓ ${IMMICH_DIR}/docker-compose.yml${NC}"

# ── Docker起動 & tailscale serve設定 ─────────────
echo ""
echo "==> [5/5] コンテナを起動..."

cd "${IMMICH_DIR}"
docker compose pull
docker compose up -d

echo "  ⏳ Immich serverの起動を待機中(最大180秒)..."
for i in $(seq 1 36); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${IMMICH_PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ Immich server 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 36 ]; then
        echo -e "\n${RED}ERROR: Immich serverがタイムアウトしました${NC}"
        docker logs immich_server --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

# 既存のserve設定を残しつつImmichのポートのみ追加(冪等対応)
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${IMMICH_PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL         : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo "  🗄️  DBパスワード : ${DB_PASSWORD}"
echo ""
echo "  📁 ディレクトリ構成:"
echo "    設定  : ${IMMICH_DIR}/.env"
echo "    写真  : ${IMMICH_DIR}/library/"
echo "    DB    : ${IMMICH_DIR}/postgres/"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  cd ${IMMICH_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

テンプレート

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

Vaultwarden

#!/bin/bash
set -euo pipefail
# =============================================================
#  Vaultwarden セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Vaultwarden : https://<hostname>.<tailnet>.ts.net:3308 (tailscale serve 3308)
#
#  ディレクトリ構成:
#   /opt/docker/vaultwarden/
#     docker-compose.yml
#     data/             ← Vaultwardenデータ
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

VAULTWARDEN_DIR="/opt/docker/vaultwarden"
PORT=8282            # ホスト内部ポート(127.0.0.1バインド)※8080は既存サービスが使用中
TAILSCALE_PORT=3308  # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  Vaultwarden セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/4] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Vaultwarden : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/4] ディレクトリを準備..."
mkdir -p "${VAULTWARDEN_DIR}/data"
echo -e "  ${GREEN}✓ /opt/docker/vaultwarden/${NC}"

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [3/4] 設定ファイルを生成..."

cat > "${VAULTWARDEN_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}:${TAILSCALE_PORT}"
    volumes:
      - ./data:/data
    ports:
      - "127.0.0.1:${PORT}:80"
EOF

echo -e "  ${GREEN}✓ ${VAULTWARDEN_DIR}/docker-compose.yml${NC}"

# ── Docker起動 & tailscale serve設定 ─────────────
echo ""
echo "==> [4/4] コンテナを起動..."

cd "${VAULTWARDEN_DIR}"
docker compose pull
docker compose up -d

# 既存のserve設定を残しつつVaultwardenのポートのみ追加(冪等対応)
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo ""
echo "  📁 ディレクトリ構成:"
echo "    設定  : ${VAULTWARDEN_DIR}/docker-compose.yml"
echo "    データ: ${VAULTWARDEN_DIR}/data/"
echo ""
echo "════════════════════════════════════════"
echo "  ⚠️  初回アカウント作成後の手順"
echo "════════════════════════════════════════"
echo ""
echo "  新規登録を無効化してください:"
echo "    sed -i 's/SIGNUPS_ALLOWED: \"true\"/SIGNUPS_ALLOWED: \"false\"/' \\"
echo "      ${VAULTWARDEN_DIR}/docker-compose.yml"
echo "    cd ${VAULTWARDEN_DIR} && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  cd ${VAULTWARDEN_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

FreshRSS

#!/bin/bash
set -euo pipefail
# =============================================================
#  FreshRSS セットアップスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - FreshRSS : https://<hostname>.<tailnet>.ts.net:3309 (tailscale serve 3309)
#
#  ディレクトリ構成:
#   /opt/docker/freshrss/
#     docker-compose.yml
#     data/             ← FreshRSSデータ (Dockerボリューム)
#     extensions/       ← 拡張機能 (Dockerボリューム)
#
#  前提条件:
#   - LXDコンテナ内でrootまたはsudoで実行
#   - Docker がインストール済みであること
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================

FRESHRSS_DIR="/opt/docker/freshrss"
PORT=6060            # ホスト内部ポート(127.0.0.1バインド・元のまま)
TAILSCALE_PORT=3309  # tailscale serveで公開するポート

# ── カラー出力 ────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

echo ""
echo "════════════════════════════════════════"
echo "  FreshRSS セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    echo -e "${RED}ERROR: tailscaleがインストールされていません${NC}"
    exit 1
fi

if ! tailscale status &>/dev/null 2>&1; then
    echo -e "${RED}ERROR: tailscaleが接続されていません。tailscale up を実行してください${NC}"
    exit 1
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/4] Tailscaleドメインを取得..."
sudo tailscale set --operator=$USER 2>/dev/null || true

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    echo -e "${RED}ERROR: Tailscaleドメインを取得できませんでした${NC}"
    echo "Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
    exit 1
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}FreshRSS : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── ディレクトリ作成 ──────────────────────────────
echo ""
echo "==> [2/4] ディレクトリを準備..."
mkdir -p "${FRESHRSS_DIR}"
echo -e "  ${GREEN}✓ ${FRESHRSS_DIR}/${NC}"

# ── docker-compose.yml 生成 ───────────────────────
echo ""
echo "==> [3/4] 設定ファイルを生成..."

cat > "${FRESHRSS_DIR}/docker-compose.yml" <<EOF
services:
  freshrss:
    image: freshrss/freshrss:latest
    container_name: freshrss
    restart: unless-stopped
    ports:
      - "127.0.0.1:${PORT}: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

echo -e "  ${GREEN}✓ ${FRESHRSS_DIR}/docker-compose.yml${NC}"

# ── Docker起動 ────────────────────────────────────
echo ""
echo "==> [4/4] コンテナを起動..."

cd "${FRESHRSS_DIR}"
docker compose pull
docker compose up -d

echo "  ⏳ FreshRSSの起動を待機中(最大60秒)..."
for i in $(seq 1 12); do
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null || echo "000")
    if [ "$HTTP_CODE" != "000" ]; then
        echo -e "\n  ${GREEN}✓ FreshRSS 起動完了 (HTTP ${HTTP_CODE})${NC}"
        break
    fi
    if [ "$i" -eq 12 ]; then
        echo -e "\n${RED}ERROR: FreshRSSがタイムアウトしました${NC}"
        docker logs freshrss --tail=20
        exit 1
    fi
    sleep 5
    echo -n "."
done

# ── 拡張機能インストール ──────────────────────────
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
docker exec freshrss chown -R www-data:www-data \
    /var/www/FreshRSS/extensions/xExtension-ThreePanesView
rm -rf /tmp/tpv.zip /tmp/xextension-threepanesview-master
echo -e "  ${GREEN}✓ Three Panes View${NC}"

# 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
docker exec freshrss chown -R www-data:www-data \
    /var/www/FreshRSS/extensions/xExtension-af_readability
rm -rf /tmp/af.zip /tmp/freshrss-af-readability-master
echo -e "  ${GREEN}✓ AF Readability${NC}"

# 権限を一括修正
docker exec freshrss chown -R www-data:www-data /var/www/FreshRSS/data/
docker exec freshrss chown -R www-data:www-data /var/www/FreshRSS/extensions/
echo -e "  ${GREEN}✓ 権限修正完了${NC}"

# ── tailscale serve 設定 ──────────────────────────
# 既存のserve設定を残しつつFreshRSSのポートのみ追加(冪等対応)
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:${PORT}
echo -e "  ${GREEN}✓ tailscale serve 設定完了${NC}"

echo ""
tailscale serve status

# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo ""
echo "  📁 ディレクトリ構成:"
echo "    設定  : ${FRESHRSS_DIR}/docker-compose.yml"
echo ""
echo "════════════════════════════════════════"
echo "  ⚠️  初回セットアップ時の注意"
echo "════════════════════════════════════════"
echo ""
echo "  ブラウザでアクセスして初期設定を行う際、"
echo "  「FreshRSSのURL」には以下を入力してください:"
echo "    https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo ""
echo "════════════════════════════════════════"
echo "  🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo "  cd ${FRESHRSS_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""

Syncthings

#!/bin/bash
# =============================================================
#  Syncthing 直インストールスクリプト (tailscale serve版)
#
#  構成 (LXDコンテナ内で実行):
#   - Syncthing Web UI : https://<hostname>.<tailnet>.ts.net:3310 (tailscale serve 3310)
#
#  前提条件:
#   - LXDコンテナ内でrootで実行
#   - tailscale up 済みであること
#   - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
#     https://login.tailscale.com/admin/dns
# =============================================================
set -euo pipefail

GREEN='\033[0;32m'
RED='\033[0;31m'
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; }

TAILSCALE_PORT=3310  # tailscale serveで公開するポート

echo ""
echo "════════════════════════════════════════"
echo "  Syncthing セットアップ (tailscale serve版)"
echo "════════════════════════════════════════"
echo ""

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

# ── Tailscale確認 ─────────────────────────────────
if ! command -v tailscale &>/dev/null; then
    error "tailscaleがインストールされていません"
fi

if ! tailscale status &>/dev/null 2>&1; then
    error "tailscaleが接続されていません。tailscale up を実行してください"
fi

# ── tailnetドメイン取得 ───────────────────────────
echo "==> [1/5] Tailscaleドメインを取得..."

TAILSCALE_DOMAIN=$(tailscale status --json | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('Self', {}).get('DNSName', '').rstrip('.'))
" 2>/dev/null)

if [ -z "$TAILSCALE_DOMAIN" ]; then
    error "Tailscaleドメインを取得できませんでした。Tailscale管理コンソールでMagicDNSが有効になっているか確認してください"
fi

echo -e "  ${GREEN}ドメイン: ${TAILSCALE_DOMAIN}${NC}"
echo -e "  ${GREEN}Syncthing : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}${NC}"

# ── Syncthingユーザーの作成 ───────────────────────
echo ""
echo "==> [2/5] Syncthingユーザーを準備..."

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

# ── 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 リポジトリ追加 & インストール ─────────────
echo ""
echo "==> [3/5] 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 サービス登録 ───────────────────────────
systemctl enable syncthing@$SYNCTHING_USER
systemctl start syncthing@$SYNCTHING_USER
success "Syncthing サービスを起動しました。"

# ── config.xml 生成待機(最大60秒)───────────────
echo ""
echo "==> [4/5] 設定ファイルを構成..."
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をlocalhost:8384 に設定します(tailscale serve経由でアクセス)..."
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', '127.0.0.1:8384')
set_or_create(gui, 'insecureSkipHostcheck', 'true')
tree.write(sys.argv[1], encoding='unicode', xml_declaration=True)
print("GUIアドレスを 127.0.0.1:8384 に変更しました。")
print("insecureSkipHostcheck を有効にしました。")
PYEOF

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

# ── tailscale serve 設定 ──────────────────────────
echo ""
echo "==> [5/5] tailscale serve を設定..."

# 既存のserve設定を残しつつSyncthingのポートのみ追加(冪等対応)
tailscale serve --https=${TAILSCALE_PORT} off 2>/dev/null || true
tailscale serve --bg --https=${TAILSCALE_PORT} http://localhost:8384
success "tailscale serve 設定完了"

echo ""
tailscale serve status

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

# ── 完了メッセージ ─────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e "  ${GREEN}✅  起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo "  🌐 URL          : https://${TAILSCALE_DOMAIN}:${TAILSCALE_PORT}"
echo "  📄 設定ファイル : ${CONFIG_FILE}"
echo "  👤 実行ユーザー : ${SYNCTHING_USER}"
echo ""
echo "  Device ID(バックアップPCへの接続時に使用):"
echo -e "  ${YELLOW}${DEVICE_ID}${NC}"
echo ""
echo "════════════════════════════════════════"
echo "  次のステップ"
echo "════════════════════════════════════════"
echo ""
echo "  1. Web UI にアクセスしてパスワードを設定"
echo "     (右上メニュー → Settings → GUI → GUI Authentication)"
echo "  2. 同期したいフォルダを追加"
echo "  3. 各フォルダのタイプを Send Only に設定"
echo "  4. バックアップPCと Device ID を交換して接続"
echo "  5. バックアップPC側を Receive Only + 階段状バージョニング に設定"
echo ""

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

echo "════════════════════════════════════════"
echo ""

使用ポート番号

ここまでの通りで設定した場合の使用ポートです。Tailscale内で安全ですが、それぞれ導入する際は各自の環境に応じて変更すると良いでしょう。

外部ポート 内部ポート カテゴリ サービス名 コンテナ / プロセス
4096 4096 AI opencode web systemd (LXCホスト)
8089 8089 Dev code-server (VS Code) systemd (LXCホスト)
8080 8080 Files File Browser systemd (LXCホスト)
3301 3300 Bookmark Linkwarden linkwarden (3300:3000)
3302 3333 Mgmt Dockhand dockhand (3333:3000)
3303 3900 Wiki Outline outline (3900:3000)
3304 15556 Auth Outline Dex (OIDC) outline-dex (15556:5556)
3305 8181 Cloud Nextcloud nextcloud-app (8181:80)
3306 9090 Docs OnlyOffice onlyoffice-docs (9090:80)
3307 2283 Photos Immich immich_server (2283:2283)
3308 8282 PW Vaultwarden vaultwarden (8282:80)
3309 6060 RSS FreshRSS freshrss (6060:80)
3310 8384 Sync Syncthing systemd (LXCホスト)
タイトルとURLをコピーしました