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が自動管理してくれます。
下記からの続きのイメージです。

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ホスト) |

