TDProxyやCaddyを使う方法を紹介しましたが、個人用途でTailscale環境限定なら、一番トラブルが少ないのは、単独コンテナにして、以前にも紹介したTailscale ServeでHTTPS化することです。
小さなツール類はCaddyで飛ばす、くらいがよさそうです。
前提:
- コンテナで
tailscale up済み - Tailscale管理コンソールでHTTPS Certificates有効化済み
- コンテナにDockerインストール済み(Dockerを使わないものは不要)
- Linkwarden(Tailscale Serve版)専用コンテナ
- Outline(Tailscale Serve版)専用コンテナ
- Nextcloud&OnlyOffice (Tailscale Serve版)専用コンテナ
- Immich (Tailscale Serve版)専用コンテナ
- Vaultwarden (Tailscale Serve版)専用コンテナ
- FreshRSS (Tailscale Serve版)専用コンテナ
- Syncthings (Tailscale Serve版)専用コンテナ
- Syncthings(Immichコンテナ内で動作バージョン)
- Syncthings(Nextcloudコンテナ内で動作バージョン)
Linkwarden(Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# Linkwarden セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Linkwarden : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
#
# ディレクトリ構成:
# /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
# ── カラー出力 ────────────────────────────────────
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}${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}
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
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 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}"
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(Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# Outline セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Outline : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
# - Dex : https://<hostname>.<tailnet>.ts.net:8443 (tailscale serve 8443)
#
# 前提条件:
# - 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経由)
# ── カラー出力 ────────────────────────────────────
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
# OutlineとDexは同じホスト名でポートで分ける
BASE_URL="https://${HOSTNAME}"
DEX_URL="https://${HOSTNAME}:8443"
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 reset 2>/dev/null || true
# Outline(443でserve)
tailscale serve --bg --https=443 http://localhost:${OUTLINE_PORT}
echo -e " ${GREEN}✓ Outline : ${BASE_URL}${NC}"
# Dex(8443でserve)
tailscale serve --bg --https=8443 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 (Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# Nextcloud + OnlyOffice セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Nextcloud : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
# - OnlyOffice : https://<hostname>.<tailnet>.ts.net:8443 (tailscale serve 8443)
#
# ディレクトリ構成:
# /opt/docker/nextcloud/
# docker-compose.yml
# .secrets ← パスワード等の永続化
# db/ ← MariaDBデータ
# userdata/ ← Nextcloudユーザーデータ
# appdata/ ← Nextcloudアプリデータ
#
# /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=8080
ONLYOFFICE_PORT=9000
# ── カラー出力 ────────────────────────────────────
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}${NC}"
echo -e " ${GREEN}OnlyOffice : https://${TAILSCALE_DOMAIN}:8443${NC}"
# ── ディレクトリ作成(DBチェックより先に実行)────
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
# DBにデータが残っていないか確認(空ディレクトリはOK)
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}"
# ── docker-compose.yml 生成 ───────────────────────
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}"
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 設定 ──────────────────────────
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 http://localhost:${NEXTCLOUD_PORT}
tailscale serve --bg --https=8443 http://localhost:${ONLYOFFICE_PORT}
echo ""
tailscale serve status
# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e " ${GREEN}✅ 起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo " 🌐 Nextcloud : https://${TAILSCALE_DOMAIN}"
echo " 🌐 OnlyOffice : https://${TAILSCALE_DOMAIN}:8443"
echo ""
echo "════════════════════════════════════════"
echo " 📋 初回セットアップ(Nextcloud)"
echo "════════════════════════════════════════"
echo ""
echo " ブラウザで https://${TAILSCALE_DOMAIN} にアクセスして"
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}:8443"
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 (Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# Immich セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Immich : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
#
# ディレクトリ構成:
# /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"
# ── カラー出力 ────────────────────────────────────
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}${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
# DBにデータが残っていないか確認(空ディレクトリはOK)
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:2283/" 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
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 http://localhost:2283
echo -e " ${GREEN}✓ tailscale serve 設定完了${NC}"
echo ""
tailscale serve status
# ── 完了メッセージ ────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo -e " ${GREEN}✅ 起動完了!${NC}"
echo "════════════════════════════════════════"
echo ""
echo " 🌐 URL : https://${TAILSCALE_DOMAIN}"
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 (Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# Vaultwarden セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Vaultwarden : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
#
# ディレクトリ構成:
# /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=8080
# ── カラー出力 ────────────────────────────────────
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}${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}"
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
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 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}"
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 (Tailscale Serve版)専用コンテナ
#!/bin/bash
set -euo pipefail
# =============================================================
# FreshRSS セットアップスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - FreshRSS : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
#
# ディレクトリ構成:
# /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
# ── カラー出力 ────────────────────────────────────
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}${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 設定 ──────────────────────────
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 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}"
echo ""
echo " 📁 ディレクトリ構成:"
echo " 設定 : ${FRESHRSS_DIR}/docker-compose.yml"
echo ""
echo "════════════════════════════════════════"
echo " ⚠️ 初回セットアップ時の注意"
echo "════════════════════════════════════════"
echo ""
echo " ブラウザでアクセスして初期設定を行う際、"
echo " 「FreshRSSのURL」には以下を入力してください:"
echo " https://${TAILSCALE_DOMAIN}"
echo ""
echo "════════════════════════════════════════"
echo " 🔧 アップデート手順"
echo "════════════════════════════════════════"
echo ""
echo " cd ${FRESHRSS_DIR} && docker compose pull && docker compose up -d"
echo ""
echo "════════════════════════════════════════"
echo ""
Syncthings (Tailscale Serve版)専用コンテナ
#!/bin/bash
# =============================================================
# Syncthing 直インストールスクリプト (tailscale serve版)
#
# 構成 (LXDコンテナ内で実行):
# - Syncthing Web UI : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
#
# 前提条件:
# - 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; }
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}${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 をローカルホストのみに変更 ────────────────
# tailscale serve経由でアクセスするためlocalhostバインドで十分
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 を設定..."
tailscale serve reset 2>/dev/null || true
tailscale serve --bg --https=443 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}"
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 ""
Syncthings(Immichコンテナ内で動作バージョン)

#!/bin/bash
# =============================================================
# Syncthing セットアップスクリプト (Immich同居 / tailscale serve版)
#
# 構成 (ImmichがすでにセットアップされたLXDコンテナ内で実行):
# - Immich : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
# - Syncthing : https://<hostname>.<tailnet>.ts.net:8443 (tailscale serve 8443)
#
# ディレクトリ構成:
# /opt/docker/immich/
# library/ ← Immichのアップロード先(Syncthingで同期する場合はここを指定)
#
# Syncthing:
# - インストール方法 : apt (syncthing公式リポジトリ)
# - 実行ユーザー : syncthing (専用システムユーザー)
# - systemdサービス : syncthing@syncthing
# - Web UI : 127.0.0.1:8384 (tailscale serve経由で公開)
#
# 前提条件:
# - Immichセットアップスクリプト実行済みであること
# - LXDコンテナ内でrootまたはsudoで実行
# - 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; }
echo ""
echo "════════════════════════════════════════════════"
echo " Syncthing セットアップ (Immich同居 / 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
# ── Immich確認 ────────────────────────────────────
echo "==> [1/6] 既存のImmich構成を確認..."
IMMICH_DIR="/opt/docker/immich"
if [ ! -f "${IMMICH_DIR}/docker-compose.yml" ]; then
warn "${IMMICH_DIR}/docker-compose.yml が見つかりません"
warn "Immichセットアップスクリプトを先に実行することを推奨しますが、続行します"
else
success "Immich構成を確認: ${IMMICH_DIR}/docker-compose.yml"
fi
# tailscale serveで443が使用中か確認
if tailscale serve status 2>/dev/null | grep -q ":443"; then
success "tailscale serve :443 (Immich) は設定済みです"
else
warn "tailscale serve :443 が見つかりません(Immichが先にセットアップされているか確認してください)"
fi
# ── tailnetドメイン取得 ───────────────────────────
echo ""
echo "==> [2/6] 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
success "ドメイン : ${TAILSCALE_DOMAIN}"
success "Immich : https://${TAILSCALE_DOMAIN}"
success "Syncthing : https://${TAILSCALE_DOMAIN}:8443"
# ── Syncthingユーザー作成 ─────────────────────────
echo ""
echo "==> [3/6] 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
SYNCTHING_HOME=$(getent passwd "$SYNCTHING_USER" | cut -d: -f6)
# ── Immich libraryへのアクセス権付与(ACL使用)────
# chownではなくACLでSyncthingユーザーに権限を付与する。
# chownするとImmich(Docker)がlibrary/に書けなくなるためACLを使用。
#
# Syncthingは同期フォルダに .stfolder マーカーファイルが必要。
# Send Onlyフォルダは書き込み不要だが .stfolder だけは作成が必要なため、
# 対象サブディレクトリに事前に .stfolder を作成し、
# Syncthingユーザーには読み取り(r-x) + .stfolder作成用の書き込み(rwx)をACLで付与する。
# ただし書き込みACLはImmich library/ 直下には付与せず、サブディレクトリ単位で管理する。
IMMICH_LIBRARY="${IMMICH_DIR}/library"
# Syncthingの同期ルートは library/library/<userID>/ 単位。
# Immichはユーザーごとにサブディレクトリを作成するため、
# 各ユーザーディレクトリに .stfolder を作成する必要がある。
# ※ library/library/ 直下に .stfolder を作っても、
# admin/ 等のサブディレクトリをルートに指定した場合はエラーになる。
# apply_immich_acl <target_dir>
# - target_dir 以下に再帰的に読み取りACLを付与
# - target_dir/.stfolder を作成(なければ)
apply_immich_acl() {
local target="$1"
if [ ! -d "$target" ]; then
warn "ディレクトリが存在しません(スキップ): ${target}"
return
fi
# 読み取りACL(既存ファイル+デフォルト)
setfacl -R -m "u:${SYNCTHING_USER}:r-x" "$target"
setfacl -R -d -m "u:${SYNCTHING_USER}:r-x" "$target"
# .stfolder マーカー作成
local stfolder="${target}/.stfolder"
if [ ! -f "$stfolder" ]; then
touch "$stfolder"
info " .stfolder を作成: ${stfolder}"
else
info " .stfolder 既存: ${stfolder}"
fi
setfacl -m "u:${SYNCTHING_USER}:r--" "$stfolder"
success "ACL設定完了: ${target}"
}
if [ -d "$IMMICH_LIBRARY" ]; then
# acl パッケージが必要
if ! command -v setfacl &>/dev/null; then
info "aclパッケージをインストールします..."
apt-get install -y acl || error "aclのインストールに失敗しました"
fi
# library/ と library/library/ 自体にはディレクトリ一覧権限のみ付与
setfacl -m "u:${SYNCTHING_USER}:r-x" "$IMMICH_LIBRARY"
IMMICH_LIBRARY_DIR="${IMMICH_LIBRARY}/library"
if [ -d "$IMMICH_LIBRARY_DIR" ]; then
setfacl -m "u:${SYNCTHING_USER}:r-x" "$IMMICH_LIBRARY_DIR"
# ユーザーディレクトリ(admin 等)を自動検出して各々に .stfolder を作成
USER_DIR_COUNT=0
while IFS= read -r -d '' userdir; do
apply_immich_acl "$userdir"
USER_DIR_COUNT=$((USER_DIR_COUNT + 1))
done < <(find "$IMMICH_LIBRARY_DIR" -mindepth 1 -maxdepth 1 -type d -print0)
if [ "$USER_DIR_COUNT" -eq 0 ]; then
# ユーザーディレクトリがまだ存在しない場合は library/library/ 自体に設定
warn "library/library/ 配下にユーザーディレクトリが見つかりません"
warn "Immichに初回ログイン・アップロード後に以下を再実行してください:"
warn " $(basename "$0")"
apply_immich_acl "$IMMICH_LIBRARY_DIR"
else
info "${USER_DIR_COUNT} 個のユーザーディレクトリに設定しました"
fi
else
warn "library/library/ ディレクトリがまだ存在しません"
warn "Immichに初回ログイン後に以下を手動実行してください:"
warn " setfacl -R -m u:${SYNCTHING_USER}:r-x ${IMMICH_LIBRARY_DIR}"
warn " setfacl -R -d -m u:${SYNCTHING_USER}:r-x ${IMMICH_LIBRARY_DIR}"
warn " touch ${IMMICH_LIBRARY_DIR}/<userID>/.stfolder"
fi
success "Immich library へのACL設定が完了しました"
info "SyncthingのフォルダタイプはSend Onlyに設定してください"
info "(誤ってImmichのファイルを削除・上書きしないよう)"
else
warn "Immich libraryディレクトリが見つかりません: ${IMMICH_LIBRARY}"
warn "Immichセットアップ後にスクリプトを再実行してください"
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 "==> [4/6] Syncthingをインストール..."
if command -v syncthing &>/dev/null; then
CURRENT_VER=$(syncthing --version 2>/dev/null | awk '{print $2}' || echo "不明")
info "Syncthing はすでにインストールされています (${CURRENT_VER})。スキップします。"
else
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 をインストールしました。"
fi
# ── systemd サービス登録・初回起動 ────────────────
echo ""
echo "==> [5/6] 設定ファイルを構成..."
# 初回起動してconfig.xmlを生成させる
if ! systemctl is-active --quiet "syncthing@${SYNCTHING_USER}" 2>/dev/null; then
systemctl enable "syncthing@${SYNCTHING_USER}"
systemctl start "syncthing@${SYNCTHING_USER}"
success "Syncthing サービスを起動しました。"
else
info "Syncthing サービスはすでに起動中です。"
fi
# 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 が生成されませんでした。ログを確認してください: journalctl -u syncthing@${SYNCTHING_USER} -n 30"
success "config.xml を検出しました: ${CONFIG_FILE}"
# ── GUI をlocalhostのみに変更・insecureSkipHostcheck有効化 ──
# tailscale serve経由でアクセスするため、HostヘッダーチェックをスキップしないとUI表示エラーになる
info "GUIを 127.0.0.1:8384 に設定します..."
systemctl stop "syncthing@${SYNCTHING_USER}"
python3 - "$CONFIG_FILE" << 'PYEOF'
import xml.etree.ElementTree as ET, sys
ET.register_namespace('', '')
tree = ET.parse(sys.argv[1])
root = tree.getroot()
gui = root.find('gui')
if gui is None:
print("ERROR: <gui> 要素が見つかりません", file=sys.stderr)
sys.exit(1)
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}"
# ── Web UI 起動待機 ────────────────────────────────
info "Syncthing Web UI の起動を待機中(最大60秒)..."
for i in $(seq 1 30); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:8384/" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" != "000" ]; then
echo ""
success "Syncthing Web UI 起動完了 (HTTP ${HTTP_CODE})"
break
fi
if [ "$i" -eq 30 ]; then
echo ""
error "Syncthing Web UIがタイムアウトしました。ログ: journalctl -u syncthing@${SYNCTHING_USER} -n 30"
fi
sleep 2
echo -n "."
done
# ── tailscale serve 設定(:8443 を追加)─────────────
echo ""
echo "==> [6/6] tailscale serve :8443 を設定..."
# 既存の443設定を壊さないよう、8443のみ設定
# tailscale serve reset は全ポートをリセットしてしまうため使用しない
tailscale serve --bg --https=8443 http://localhost:8384
success "tailscale serve :8443 設定完了(既存の:443設定は維持されます)"
echo ""
tailscale serve status
# ── Device ID 取得 ─────────────────────────────────
DEVICE_ID=$(sudo -u "$SYNCTHING_USER" syncthing --device-id 2>/dev/null \
|| echo "(GUIの「Actions」→「Show ID」から確認してください)")
# ── 完了メッセージ ─────────────────────────────────
echo ""
echo "════════════════════════════════════════════════"
echo -e " ${GREEN}✅ 起動完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo " 🌐 Immich : https://${TAILSCALE_DOMAIN}"
echo " 🔄 Syncthing : https://${TAILSCALE_DOMAIN}:8443"
echo ""
echo " 📄 設定ファイル : ${CONFIG_FILE}"
echo " 👤 実行ユーザー : ${SYNCTHING_USER}"
echo " 🏠 ホームDir : ${SYNCTHING_HOME}"
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 ""
echo " 2. 同期フォルダを追加"
echo " → Add Folder から同期元のパスを指定"
echo " ✅ Immich library への読み取りACL付与済み:"
echo " フォルダパス: ${IMMICH_DIR}/library/library/admin/ (等、ユーザーIDごと)"
echo " (各ユーザーディレクトリに .stfolder マーカーも作成済み)"
echo " ⚠️ フォルダタイプは必ず Send Only に設定してください"
echo " (Receive Only/Send & Receive にするとImmichのファイルが"
echo " 削除・上書きされる危険があります)"
echo " ⚠️ 写真の追加はImmich Web UIのアップロード/External Library"
echo " 機能を使用してください(DBと整合が取れます)"
echo ""
echo " 3. フォルダタイプを用途に合わせて設定"
echo " - バックアップ元(このサーバー) → Send Only"
echo " - バックアップ先(外部PC) → Receive Only"
echo ""
echo " 4. バックアップPCでSyncthingを起動し、Device IDを交換して接続"
echo ""
echo " 5. バックアップPC側はフォルダを Receive Only +"
echo " Staggered File Versioning (階段状バージョニング) に設定"
echo ""
echo "════════════════════════════════════════════════"
echo " アップデート手順"
echo "════════════════════════════════════════════════"
echo ""
echo " apt-get update && apt-get install --only-upgrade syncthing"
echo ""
echo "════════════════════════════════════════════════"
echo ""
Syncthingフォルダ設定時の注意点:
- フォルダタイプは必ず Send Only に設定してください
- バックアップ先PCは Receive Only にすることで、誤ってImmichのファイルが削除・上書きされるのを防げます
Syncthings(Nextcloudコンテナ内で動作バージョン)
#!/bin/bash
# =============================================================
# Syncthing セットアップスクリプト (Nextcloud+OnlyOffice同居 / tailscale serve版)
#
# 構成 (Nextcloud+OnlyOfficeがセットアップされたLXDコンテナ内で実行):
# - Nextcloud : https://<hostname>.<tailnet>.ts.net (tailscale serve 443)
# - OnlyOffice : https://<hostname>.<tailnet>.ts.net:8443 (tailscale serve 8443)
# - Syncthing : https://<hostname>.<tailnet>.ts.net:9443 (tailscale serve 9443)
#
# ディレクトリ構成:
# /opt/docker/nextcloud/
# userdata/ ← Nextcloudユーザーデータ(Syncthingで同期する場合はここを指定)
# <userID>/files/ ← ユーザーごとのファイル実体
#
# Syncthing:
# - インストール方法 : apt (syncthing公式リポジトリ)
# - 実行ユーザー : syncthing (専用システムユーザー)
# - systemdサービス : syncthing@syncthing
# - Web UI : 127.0.0.1:8384 (tailscale serve経由で公開)
#
# 前提条件:
# - Nextcloud+OnlyOfficeセットアップスクリプト実行済みであること
# - LXDコンテナ内でrootまたはsudoで実行
# - tailscale up 済みであること
# - Tailscale管理コンソールでHTTPS Certificatesを有効化済み
# https://login.tailscale.com/admin/dns
#
# ⚠️ Nextcloudの注意事項:
# - Syncthing経由でファイルが変更された場合、Nextcloudのインデックスが
# ズレるため、同期後に以下を実行してください:
# docker exec -u www-data nextcloud-app php occ files:scan --all
# - フォルダタイプは Send Only を推奨(誤削除・上書きを防ぐため)
# =============================================================
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; }
echo ""
echo "════════════════════════════════════════════════════════"
echo " Syncthing セットアップ (Nextcloud+OnlyOffice同居 / tailscale serve版)"
echo "════════════════════════════════════════════════════════"
echo ""
# ── root確認 ──────────────────────────────────────
[[ $EUID -ne 0 ]] && error "このスクリプトはrootで実行してください(sudo bash install-syncthing-nextcloud.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
# ── Nextcloud確認 ─────────────────────────────────
echo "==> [1/6] 既存のNextcloud構成を確認..."
NEXTCLOUD_DIR="/opt/docker/nextcloud"
NEXTCLOUD_USERDATA="${NEXTCLOUD_DIR}/userdata"
if [ ! -f "${NEXTCLOUD_DIR}/docker-compose.yml" ]; then
warn "${NEXTCLOUD_DIR}/docker-compose.yml が見つかりません"
warn "Nextcloudセットアップスクリプトを先に実行することを推奨しますが、続行します"
else
success "Nextcloud構成を確認: ${NEXTCLOUD_DIR}/docker-compose.yml"
fi
# tailscale serveで443・8443が使用中か確認
if tailscale serve status 2>/dev/null | grep -q ":443"; then
success "tailscale serve :443 (Nextcloud) は設定済みです"
else
warn "tailscale serve :443 が見つかりません(Nextcloudが先にセットアップされているか確認してください)"
fi
if tailscale serve status 2>/dev/null | grep -q ":8443"; then
success "tailscale serve :8443 (OnlyOffice) は設定済みです"
else
warn "tailscale serve :8443 が見つかりません(OnlyOfficeが先にセットアップされているか確認してください)"
fi
# ── tailnetドメイン取得 ───────────────────────────
echo ""
echo "==> [2/6] 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
success "ドメイン : ${TAILSCALE_DOMAIN}"
success "Nextcloud : https://${TAILSCALE_DOMAIN}"
success "OnlyOffice : https://${TAILSCALE_DOMAIN}:8443"
success "Syncthing : https://${TAILSCALE_DOMAIN}:9443"
# ── Syncthingユーザー作成 ─────────────────────────
echo ""
echo "==> [3/6] 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
SYNCTHING_HOME=$(getent passwd "$SYNCTHING_USER" | cut -d: -f6)
# ── Nextcloud userdataへのアクセス権付与(ACL使用)────
# chownではなくACLでSyncthingユーザーに権限を付与する。
# chownするとNextcloud(Docker=www-data)がuserdata/に書けなくなるためACLを使用。
#
# Nextcloudのユーザーファイルは以下の構造になっている:
# userdata/<nextcloudユーザー名>/files/
#
# Syncthingは同期フォルダに .stfolder マーカーファイルが必要。
# Send Onlyフォルダは書き込み不要だが .stfolder だけは作成が必要なため、
# 対象サブディレクトリに事前に .stfolder を作成し、
# Syncthingユーザーには読み取り(r-x) + .stfolder作成用の権限をACLで付与する。
# apply_nextcloud_acl <target_dir>
# - target_dir 以下に再帰的に読み取りACLを付与
# - target_dir/.stfolder を作成(なければ)
apply_nextcloud_acl() {
local target="$1"
if [ ! -d "$target" ]; then
warn "ディレクトリが存在しません(スキップ): ${target}"
return
fi
# 読み取りACL(既存ファイル+デフォルト)
setfacl -R -m "u:${SYNCTHING_USER}:r-x" "$target"
setfacl -R -d -m "u:${SYNCTHING_USER}:r-x" "$target"
# .stfolder マーカー作成
local stfolder="${target}/.stfolder"
if [ ! -f "$stfolder" ]; then
touch "$stfolder"
info " .stfolder を作成: ${stfolder}"
else
info " .stfolder 既存: ${stfolder}"
fi
setfacl -m "u:${SYNCTHING_USER}:r--" "$stfolder"
success "ACL設定完了: ${target}"
}
if [ -d "$NEXTCLOUD_USERDATA" ]; then
# acl パッケージが必要
if ! command -v setfacl &>/dev/null; then
info "aclパッケージをインストールします..."
apt-get install -y acl || error "aclのインストールに失敗しました"
fi
# userdata/ 自体にはディレクトリ一覧権限のみ付与
setfacl -m "u:${SYNCTHING_USER}:r-x" "$NEXTCLOUD_USERDATA"
# Nextcloudのユーザーディレクトリ(admin等)を自動検出
# userdata/<username>/files/ が実体なので、files/ ディレクトリ単位でACLを設定する
USER_DIR_COUNT=0
while IFS= read -r -d '' userdir; do
local_files_dir="${userdir}/files"
# userdata/<username>/ にも一覧権限を付与
setfacl -m "u:${SYNCTHING_USER}:r-x" "$userdir"
if [ -d "$local_files_dir" ]; then
apply_nextcloud_acl "$local_files_dir"
USER_DIR_COUNT=$((USER_DIR_COUNT + 1))
else
warn "${local_files_dir} が存在しません(スキップ): Nextcloudへ初回ログイン後に再実行してください"
fi
done < <(find "$NEXTCLOUD_USERDATA" -mindepth 1 -maxdepth 1 -type d -print0)
if [ "$USER_DIR_COUNT" -eq 0 ]; then
warn "userdata/ 配下にNextcloudユーザーディレクトリが見つかりません"
warn "Nextcloudに初回ログイン・ファイルアップロード後に以下を再実行してください:"
warn " $(basename "$0")"
else
info "${USER_DIR_COUNT} 個のユーザーの files/ ディレクトリに設定しました"
fi
success "Nextcloud userdata へのACL設定が完了しました"
info "SyncthingのフォルダタイプはSend Onlyに設定してください"
info "(誤ってNextcloudのファイルを削除・上書きしないよう)"
else
warn "Nextcloud userdataディレクトリが見つかりません: ${NEXTCLOUD_USERDATA}"
warn "Nextcloudセットアップ後にスクリプトを再実行してください"
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 "==> [4/6] Syncthingをインストール..."
if command -v syncthing &>/dev/null; then
CURRENT_VER=$(syncthing --version 2>/dev/null | awk '{print $2}' || echo "不明")
info "Syncthing はすでにインストールされています (${CURRENT_VER})。スキップします。"
else
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 をインストールしました。"
fi
# ── systemd サービス登録・初回起動 ────────────────
echo ""
echo "==> [5/6] 設定ファイルを構成..."
# 初回起動してconfig.xmlを生成させる
if ! systemctl is-active --quiet "syncthing@${SYNCTHING_USER}" 2>/dev/null; then
systemctl enable "syncthing@${SYNCTHING_USER}"
systemctl start "syncthing@${SYNCTHING_USER}"
success "Syncthing サービスを起動しました。"
else
info "Syncthing サービスはすでに起動中です。"
fi
# 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 が生成されませんでした。ログを確認してください: journalctl -u syncthing@${SYNCTHING_USER} -n 30"
success "config.xml を検出しました: ${CONFIG_FILE}"
# ── GUI をlocalhostのみに変更・insecureSkipHostcheck有効化 ──
# tailscale serve経由でアクセスするため、HostヘッダーチェックをスキップしないとUI表示エラーになる
info "GUIを 127.0.0.1:8384 に設定します..."
systemctl stop "syncthing@${SYNCTHING_USER}"
python3 - "$CONFIG_FILE" << 'PYEOF'
import xml.etree.ElementTree as ET, sys
ET.register_namespace('', '')
tree = ET.parse(sys.argv[1])
root = tree.getroot()
gui = root.find('gui')
if gui is None:
print("ERROR: <gui> 要素が見つかりません", file=sys.stderr)
sys.exit(1)
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}"
# ── Web UI 起動待機 ────────────────────────────────
info "Syncthing Web UI の起動を待機中(最大60秒)..."
for i in $(seq 1 30); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:8384/" 2>/dev/null || echo "000")
if [ "$HTTP_CODE" != "000" ]; then
echo ""
success "Syncthing Web UI 起動完了 (HTTP ${HTTP_CODE})"
break
fi
if [ "$i" -eq 30 ]; then
echo ""
error "Syncthing Web UIがタイムアウトしました。ログ: journalctl -u syncthing@${SYNCTHING_USER} -n 30"
fi
sleep 2
echo -n "."
done
# ── tailscale serve 設定(:9443 を追加)─────────────
echo ""
echo "==> [6/6] tailscale serve :9443 を設定..."
# 既存の443・8443設定を壊さないよう、9443のみ設定
# tailscale serve reset は全ポートをリセットしてしまうため使用しない
tailscale serve --bg --https=9443 http://localhost:8384
success "tailscale serve :9443 設定完了(既存の:443・:8443設定は維持されます)"
echo ""
tailscale serve status
# ── Device ID 取得 ─────────────────────────────────
DEVICE_ID=$(sudo -u "$SYNCTHING_USER" syncthing --device-id 2>/dev/null \
|| echo "(GUIの「Actions」→「Show ID」から確認してください)")
# ── 完了メッセージ ─────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════"
echo -e " ${GREEN}✅ 起動完了!${NC}"
echo "════════════════════════════════════════════════════════"
echo ""
echo " 🌐 Nextcloud : https://${TAILSCALE_DOMAIN}"
echo " 🌐 OnlyOffice : https://${TAILSCALE_DOMAIN}:8443"
echo " 🔄 Syncthing : https://${TAILSCALE_DOMAIN}:9443"
echo ""
echo " 📄 設定ファイル : ${CONFIG_FILE}"
echo " 👤 実行ユーザー : ${SYNCTHING_USER}"
echo " 🏠 ホームDir : ${SYNCTHING_HOME}"
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 ""
echo " 2. 同期フォルダを追加"
echo " → Add Folder から同期元のパスを指定"
echo " ✅ Nextcloud userdata への読み取りACL付与済み:"
echo " フォルダパス: ${NEXTCLOUD_USERDATA}/<nextcloudユーザー名>/files/"
echo " (各ユーザーの files/ ディレクトリに .stfolder マーカーも作成済み)"
echo " ⚠️ フォルダタイプは必ず Send Only に設定してください"
echo " (Receive Only/Send & Receive にするとNextcloudのファイルが"
echo " 削除・上書きされる危険があります)"
echo " ⚠️ ファイルの追加・変更はNextcloud Web UIまたはクライアントから"
echo " 行ってください(DBインデックスと整合が取れます)"
echo ""
echo " 3. フォルダタイプを用途に合わせて設定"
echo " - バックアップ元(このサーバー) → Send Only"
echo " - バックアップ先(外部PC) → Receive Only"
echo ""
echo " 4. バックアップPCでSyncthingを起動し、Device IDを交換して接続"
echo ""
echo " 5. バックアップPC側はフォルダを Receive Only +"
echo " Staggered File Versioning (階段状バージョニング) に設定"
echo ""
echo "════════════════════════════════════════════════════════"
echo " ⚠️ Nextcloud固有の注意事項"
echo "════════════════════════════════════════════════════════"
echo ""
echo " Syncthing経由でファイルが変更・追加された場合、"
echo " Nextcloudのファイルインデックスが更新されません。"
echo " 以下のコマンドで再スキャンしてください:"
echo ""
echo " docker exec -u www-data nextcloud-app php occ files:scan --all"
echo ""
echo " 定期的に自動スキャンするcronを設定することも推奨します。"
echo ""
echo "════════════════════════════════════════════════════════"
echo " アップデート手順"
echo "════════════════════════════════════════════════════════"
echo ""
echo " apt-get update && apt-get install --only-upgrade syncthing"
echo ""
echo "════════════════════════════════════════════════════════"
echo ""


