セルフホストできるナレッジベースのソフトの中ではかなり使いやすいOutline。過去にここで紹介した時は個人利用を重視して@local.invalidでログイン出来るようにしていましたが、今後は複数人で利用する場合にも使いやすいようにするために、メールアドレスで登録する形に変更しました。
目次
LXDコンテナ内でセットアップ
セットアップは、LXDコンテナ内でコピペすればOKです。
#!/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@example.com): " U_EMAIL
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)
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 ""
定期的にエクスポートを実行
Outlineのバックアップや復元は別環境があると認証部分によって正常に動作しない危険性があるので、標準機能を利用してエクスポートすることにしました。これなら復元時は特別なスクリプトを使用しなくても、標準機能でインポート出来ます。
事前にAPIキーを設定しておきます。

Outlineエクスポートスクリプト
mkdir -p ~/outline-tmp
cd ~/outline-tmp
nano outline-export-backup.sh
nano outline-export-install.sh
# 下記スクリプトを貼り付け
# 実行権限を付与
chmod +x outline-export-backup.sh
chmod +x Outline-export-install.sh
bash outline-export-install.sh
rm -rf ~/outline-tmp
outline-export-backup.sh
#!/bin/bash
set -euo pipefail
# =============================================================
# Outline JSON エクスポート バックアップスクリプト
#
# Outline 公式 API を使って全コレクションを JSON 形式で
# エクスポートし、ローカルに保存します。
#
# 使い方:
# sudo bash outline-export-backup.sh # バックアップを作成
# sudo bash outline-export-backup.sh list # バックアップ一覧を表示
# sudo bash outline-export-backup.sh clean # 古いバックアップを削除
#
# 初回セットアップ:
# 1. Outline の Settings → API Keys で APIキーを作成
# 2. このスクリプトの設定欄に OUTLINE_URL と API_KEY を記入
# または環境変数 OUTLINE_URL / OUTLINE_API_KEY で渡す
#
# バックアップ保存先: /opt/lxd-data/outline-export/
# バックアップ内容:
# - 全コレクションのドキュメント (JSON形式)
# - 添付ファイル (includeAttachments: true)
# - プライベートコレクション (includePrivate: true)
#
# 注意:
# - ユーザー情報・コレクション権限設定は含まれません
# - 復元は Outline の Settings → Import から ZIP をアップロード
# =============================================================
# ════════════════════════════════════════════════════
# ★ 設定欄 (環境変数で渡す場合は空欄のままでOK)
# ════════════════════════════════════════════════════
OUTLINE_URL="${OUTLINE_URL:-}" # 例: https://hostname:3303
API_KEY="${OUTLINE_API_KEY:-}" # Outline API キー
BACKUP_DIR="${OUTLINE_BACKUP_DIR:-/opt/lxd-data/outline-export}"
KEEP_DAYS="${OUTLINE_KEEP_DAYS:-30}" # 何日分保持するか (デフォルト30日)
# エクスポートオプション
INCLUDE_ATTACHMENTS="true" # 添付ファイルを含める
INCLUDE_PRIVATE="true" # プライベートコレクションを含める
# API ポーリング設定
POLL_INTERVAL=5 # 秒: エクスポート完了確認の間隔
POLL_MAX=120 # 秒: 最大待機時間
# ════════════════════════════════════════════════════
# カラー出力
# ════════════════════════════════════════════════════
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
section() { echo -e "\n${CYAN}==> $*${NC}"; }
print_banner() {
echo ""
echo "════════════════════════════════════════════════"
echo " Outline JSON エクスポート バックアップ"
echo "════════════════════════════════════════════════"
echo ""
}
usage() {
print_banner
echo "使い方:"
echo " $0 # バックアップを作成"
echo " $0 list # バックアップ一覧を表示"
echo " $0 clean # 古いバックアップを削除 (${KEEP_DAYS}日以上)"
echo ""
echo "設定 (環境変数または設定欄に記入):"
echo " OUTLINE_URL Outline の公開URL 例: https://hostname:3303"
echo " OUTLINE_API_KEY Outline の API キー"
echo " OUTLINE_BACKUP_DIR バックアップ保存先 (デフォルト: ${BACKUP_DIR})"
echo " OUTLINE_KEEP_DAYS 保持日数 (デフォルト: ${KEEP_DAYS}日)"
echo ""
echo "保存先: ${BACKUP_DIR}/"
echo ""
}
# ════════════════════════════════════════════════════
# 依存コマンド確認
# ════════════════════════════════════════════════════
check_deps() {
local MISSING=()
for CMD in curl jq; do
if ! command -v "${CMD}" &>/dev/null; then
MISSING+=("${CMD}")
fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
error "以下のコマンドが見つかりません: ${MISSING[*]}"
error "インストール例: apt-get install -y curl jq"
exit 1
fi
}
# ════════════════════════════════════════════════════
# 設定値の確認・対話入力
# ════════════════════════════════════════════════════
check_config() {
# OUTLINE_URL
if [ -z "${OUTLINE_URL}" ]; then
# docker の .env から自動取得を試みる
local ENV_FILE="/opt/docker/outline/.env"
if [ -f "${ENV_FILE}" ]; then
local AUTO_URL
AUTO_URL=$(grep '^URL=' "${ENV_FILE}" 2>/dev/null \
| cut -d= -f2- | tr -d '"' || echo "")
if [ -n "${AUTO_URL}" ]; then
OUTLINE_URL="${AUTO_URL}"
info ".env から Outline URL を取得: ${OUTLINE_URL}"
fi
fi
fi
if [ -z "${OUTLINE_URL}" ]; then
echo ""
warn "OUTLINE_URL が設定されていません"
read -rp " Outline の URL を入力してください (例: https://hostname:3303): " OUTLINE_URL
if [ -z "${OUTLINE_URL}" ]; then
error "URL が入力されませんでした"
exit 1
fi
fi
# 末尾スラッシュを除去
OUTLINE_URL="${OUTLINE_URL%/}"
# API_KEY
if [ -z "${API_KEY}" ]; then
echo ""
warn "OUTLINE_API_KEY が設定されていません"
echo " Outline の Settings → API Keys でキーを作成してください"
echo " スクリプトの設定欄 (API_KEY=...) に記入すると次回から不要になります"
read -rsp " API キーを入力してください: " API_KEY
echo ""
if [ -z "${API_KEY}" ]; then
error "API キーが入力されませんでした"
exit 1
fi
fi
}
# ════════════════════════════════════════════════════
# API 接続確認
# ════════════════════════════════════════════════════
check_api_connection() {
section "Outline API に接続確認..."
local HTTP_CODE
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{}' \
"${OUTLINE_URL}/api/auth.info" 2>/dev/null || echo "000")
if [ "${HTTP_CODE}" = "200" ]; then
info "接続 OK (HTTP ${HTTP_CODE})"
elif [ "${HTTP_CODE}" = "401" ]; then
error "認証エラー (HTTP 401): API キーを確認してください"
exit 1
elif [ "${HTTP_CODE}" = "000" ]; then
error "接続できませんでした: ${OUTLINE_URL}"
error "Outline が起動しているか、URL が正しいか確認してください"
exit 1
else
error "予期しないレスポンス (HTTP ${HTTP_CODE})"
exit 1
fi
}
# ════════════════════════════════════════════════════
# エクスポートジョブを開始
# ════════════════════════════════════════════════════
start_export() {
section "エクスポートジョブを開始..."
local RESPONSE
RESPONSE=$(curl -sS -X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{
\"format\": \"json\",
\"includeAttachments\": ${INCLUDE_ATTACHMENTS},
\"includePrivate\": ${INCLUDE_PRIVATE}
}" \
"${OUTLINE_URL}/api/collections.export_all" 2>/dev/null)
if [ -z "${RESPONSE}" ]; then
error "API からレスポンスが返りませんでした"
exit 1
fi
# エラーチェック
local OK
OK=$(echo "${RESPONSE}" | jq -r '.ok // false' 2>/dev/null || echo "false")
if [ "${OK}" != "true" ]; then
local MSG
MSG=$(echo "${RESPONSE}" | jq -r '.message // "不明なエラー"' 2>/dev/null || echo "不明なエラー")
error "エクスポート開始に失敗しました: ${MSG}"
error "レスポンス: ${RESPONSE}"
exit 1
fi
EXPORT_ID=$(echo "${RESPONSE}" | jq -r '.data.fileOperation.id' 2>/dev/null || echo "")
if [ -z "${EXPORT_ID}" ] || [ "${EXPORT_ID}" = "null" ]; then
error "fileOperation.id を取得できませんでした"
error "レスポンス: ${RESPONSE}"
exit 1
fi
info "エクスポートジョブ ID: ${EXPORT_ID}"
info "オプション: attachments=${INCLUDE_ATTACHMENTS}, private=${INCLUDE_PRIVATE}"
}
# ════════════════════════════════════════════════════
# エクスポート完了待機
# ════════════════════════════════════════════════════
wait_for_export() {
section "エクスポート完了を待機中..."
local ELAPSED=0
local STATE=""
while true; do
local RESPONSE
RESPONSE=$(curl -sS -X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"id\": \"${EXPORT_ID}\"}" \
"${OUTLINE_URL}/api/fileOperations.info" 2>/dev/null)
STATE=$(echo "${RESPONSE}" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")
case "${STATE}" in
complete)
echo ""
info "エクスポート完了 (${ELAPSED}秒)"
return 0
;;
error)
echo ""
local ERR_MSG
ERR_MSG=$(echo "${RESPONSE}" | jq -r '.data.error // "不明なエラー"' 2>/dev/null || echo "不明なエラー")
error "エクスポートに失敗しました: ${ERR_MSG}"
exit 1
;;
creating|pending|*)
echo -n "."
;;
esac
ELAPSED=$(( ELAPSED + POLL_INTERVAL ))
if [ "${ELAPSED}" -ge "${POLL_MAX}" ]; then
echo ""
error "エクスポートがタイムアウトしました (${POLL_MAX}秒)"
error "最後の状態: ${STATE}"
exit 1
fi
sleep "${POLL_INTERVAL}"
done
}
# ════════════════════════════════════════════════════
# ZIP をダウンロード
# ════════════════════════════════════════════════════
download_export() {
local OUTPUT_FILE="${1}"
section "ZIP をダウンロード..."
# fileOperations.redirect は直接ダウンロードURLにリダイレクトする
local HTTP_CODE
HTTP_CODE=$(curl -sS -L \
--retry 5 \
--retry-delay 3 \
--retry-all-errors \
-w "%{http_code}" \
-o "${OUTPUT_FILE}" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
"${OUTLINE_URL}/api/fileOperations.redirect?id=${EXPORT_ID}" 2>/dev/null || echo "000")
if [ "${HTTP_CODE}" != "200" ]; then
error "ダウンロードに失敗しました (HTTP ${HTTP_CODE})"
rm -f "${OUTPUT_FILE}"
exit 1
fi
# ZIP ファイルの検証
if ! file "${OUTPUT_FILE}" 2>/dev/null | grep -qiE 'zip|archive'; then
# file コマンドがない環境向けにマジックバイト確認
local MAGIC
MAGIC=$(xxd -l 4 "${OUTPUT_FILE}" 2>/dev/null | head -1 || echo "")
if ! echo "${MAGIC}" | grep -q "504b 0304"; then
warn "ダウンロードファイルが ZIP 形式でない可能性があります"
warn "ファイルサイズ: $(du -sh "${OUTPUT_FILE}" | cut -f1)"
fi
fi
local FILE_SIZE
FILE_SIZE=$(du -sh "${OUTPUT_FILE}" | cut -f1)
info "ダウンロード完了: ${OUTPUT_FILE} (${FILE_SIZE})"
}
# ════════════════════════════════════════════════════
# サーバー側の一時ファイルを削除
# ════════════════════════════════════════════════════
delete_server_export() {
section "サーバー側の一時ファイルを削除..."
local RESPONSE
RESPONSE=$(curl -sS -X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"id\": \"${EXPORT_ID}\"}" \
"${OUTLINE_URL}/api/fileOperations.delete" 2>/dev/null)
local OK
OK=$(echo "${RESPONSE}" | jq -r '.ok // false' 2>/dev/null || echo "false")
if [ "${OK}" = "true" ]; then
info "サーバー側の一時ファイルを削除しました"
else
warn "サーバー側の一時ファイル削除に失敗しました (無視して続行)"
fi
}
# ════════════════════════════════════════════════════
# 古いバックアップを自動削除
# ════════════════════════════════════════════════════
auto_clean_old_backups() {
local OLD_FILES
OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
-mtime "+${KEEP_DAYS}" 2>/dev/null || true)
if [ -n "${OLD_FILES}" ]; then
local OLD_COUNT
OLD_COUNT=$(echo "${OLD_FILES}" | wc -l)
echo "${OLD_FILES}" | while IFS= read -r F; do
rm -f "${F}" "${F}.sha256"
info "古いバックアップを削除: $(basename "${F}")"
done
info "${OLD_COUNT} 件の古いバックアップを削除しました (${KEEP_DAYS}日以上前)"
fi
}
# ════════════════════════════════════════════════════
# バックアップ実行
# ════════════════════════════════════════════════════
do_backup() {
print_banner
check_deps
check_config
check_api_connection
mkdir -p "${BACKUP_DIR}"
local TIMESTAMP
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
local OUTPUT_FILE="${BACKUP_DIR}/outline_export_${TIMESTAMP}.zip"
# エクスポートジョブ開始 → 完了待機 → ダウンロード → 後片付け
start_export
wait_for_export
download_export "${OUTPUT_FILE}"
delete_server_export
# チェックサム生成
sha256sum "${OUTPUT_FILE}" > "${OUTPUT_FILE}.sha256"
info "チェックサム: ${OUTPUT_FILE}.sha256"
# 古いバックアップを自動削除
auto_clean_old_backups
local FILE_SIZE
FILE_SIZE=$(du -sh "${OUTPUT_FILE}" | cut -f1)
echo ""
echo "════════════════════════════════════════════════"
echo -e " ${GREEN}✅ バックアップ完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo " 📦 ファイル : ${OUTPUT_FILE}"
echo " 📏 サイズ : ${FILE_SIZE}"
echo " 📅 日時 : $(date '+%Y-%m-%d %H:%M:%S')"
echo " 🔗 Outline URL: ${OUTLINE_URL}"
echo ""
echo " 復元方法:"
echo " Outline の Settings → Import → 上記 ZIP をアップロード"
echo ""
}
# ════════════════════════════════════════════════════
# バックアップ一覧
# ════════════════════════════════════════════════════
do_list() {
print_banner
if [ ! -d "${BACKUP_DIR}" ]; then
warn "バックアップディレクトリが存在しません: ${BACKUP_DIR}"
exit 0
fi
local FILES
FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
-printf '%T@ %p\n' 2>/dev/null | sort -rn | cut -d' ' -f2-)
if [ -z "${FILES}" ]; then
warn "バックアップファイルが見つかりません"
exit 0
fi
echo "バックアップ一覧: ${BACKUP_DIR}"
echo ""
printf " %-55s %8s %s\n" "ファイル名" "サイズ" "作成日時"
echo " $(printf '─%.0s' {1..80})"
while IFS= read -r FILE; do
local BASENAME SIZE MTIME
BASENAME=$(basename "${FILE}")
SIZE=$(du -sh "${FILE}" | cut -f1)
MTIME=$(stat -c '%y' "${FILE}" | cut -d'.' -f1)
printf " %-55s %8s %s\n" "${BASENAME}" "${SIZE}" "${MTIME}"
done <<< "${FILES}"
echo ""
local TOTAL_COUNT TOTAL_SIZE
TOTAL_COUNT=$(echo "${FILES}" | wc -l)
TOTAL_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
echo " 合計: ${TOTAL_COUNT} 件 / ${TOTAL_SIZE} (保持日数: ${KEEP_DAYS}日)"
echo ""
}
# ════════════════════════════════════════════════════
# 古いバックアップ手動削除
# ════════════════════════════════════════════════════
do_clean() {
print_banner
local OLD_FILES
OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
-mtime "+${KEEP_DAYS}" 2>/dev/null || true)
if [ -z "${OLD_FILES}" ]; then
info "${KEEP_DAYS}日以上前のバックアップはありません"
exit 0
fi
warn "以下のファイルを削除します (${KEEP_DAYS}日以上前):"
echo "${OLD_FILES}" | while IFS= read -r F; do
echo " - $(basename "${F}") ($(du -sh "${F}" | cut -f1))"
done
echo ""
read -rp " 削除しますか? [y/N]: " CONFIRM
if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then
warn "キャンセルしました"
exit 0
fi
echo "${OLD_FILES}" | while IFS= read -r F; do
rm -f "${F}" "${F}.sha256"
info "削除: $(basename "${F}")"
done
info "クリーンアップ完了"
echo ""
}
# ════════════════════════════════════════════════════
# エントリーポイント
# ════════════════════════════════════════════════════
COMMAND="${1:-backup}"
case "${COMMAND}" in
backup|"") do_backup ;;
list) do_list ;;
clean) do_clean ;;
-h|--help|help) usage ;;
*)
error "不明なコマンド: ${COMMAND}"
usage
exit 1
;;
esac
outline-export-install.sh
#!/bin/bash
set -euo pipefail
# =============================================================
# Outline エクスポートバックアップ インストーラー
#
# 実行するだけで以下を一括セットアップします:
# - /opt/lxd-data/script/outline/outline-export-backup.sh
# - API キーの設定
# - 実行権限の付与
# - (任意) cron への自動バックアップ登録
#
# 使い方:
# sudo bash outline-export-install.sh
# =============================================================
SCRIPT_DIR="/opt/lxd-data/script/outline"
SCRIPT_PATH="${SCRIPT_DIR}/outline-export-backup.sh"
BACKUP_DIR="/opt/lxd-data/outline-export"
LOG_FILE="/var/log/outline-export-backup.log"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
section() { echo -e "\n${CYAN}==> $*${NC}"; }
if [ "$(id -u)" -ne 0 ]; then
error "このスクリプトは root または sudo で実行してください"
exit 1
fi
echo ""
echo "════════════════════════════════════════════════"
echo " Outline エクスポートバックアップ インストーラー"
echo "════════════════════════════════════════════════"
echo ""
echo " スクリプト配置先: ${SCRIPT_PATH}"
echo " バックアップ保存先: ${BACKUP_DIR}/"
echo ""
# 依存コマンド確認
section "[Step 1] 依存パッケージを確認..."
MISSING=()
for CMD in curl jq; do
if ! command -v "${CMD}" &>/dev/null; then
MISSING+=("${CMD}")
fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
warn "以下のパッケージをインストールします: ${MISSING[*]}"
apt-get install -y "${MISSING[@]}"
info "インストール完了"
else
info "curl / jq: OK"
fi
# ── Outline URL の取得 ─────────────────────────────
section "[Step 2] Outline URL を確認..."
AUTO_URL=""
ENV_FILE="/opt/docker/outline/.env"
if [ -f "${ENV_FILE}" ]; then
AUTO_URL=$(grep '^URL=' "${ENV_FILE}" 2>/dev/null \
| cut -d= -f2- | tr -d '"' || echo "")
if [ -n "${AUTO_URL}" ]; then
info ".env から自動取得: ${AUTO_URL}"
fi
fi
if [ -n "${AUTO_URL}" ]; then
read -rp " この URL を使いますか? [Y/n]: " USE_AUTO
if [[ "${USE_AUTO}" =~ ^[Nn]$ ]]; then
AUTO_URL=""
fi
fi
if [ -z "${AUTO_URL}" ]; then
read -rp " Outline の URL を入力してください (例: https://hostname:3303): " OUTLINE_URL
else
OUTLINE_URL="${AUTO_URL}"
fi
OUTLINE_URL="${OUTLINE_URL%/}"
info "Outline URL: ${OUTLINE_URL}"
# ── API キーの入力 ─────────────────────────────────
section "[Step 3] API キーを設定..."
echo ""
echo " Outline の Settings → API Keys でキーを作成してください。"
echo " ※ Workspace Admin 権限のユーザーで作成してください"
echo ""
read -rsp " API キーを入力してください: " API_KEY
echo ""
if [ -z "${API_KEY}" ]; then
error "API キーが入力されませんでした"
exit 1
fi
# API 接続テスト
echo ""
info "API 接続を確認中..."
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
-X POST \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{}' \
"${OUTLINE_URL}/api/auth.info" 2>/dev/null || echo "000")
if [ "${HTTP_CODE}" = "200" ]; then
info "API 接続 OK"
elif [ "${HTTP_CODE}" = "401" ]; then
error "認証エラー (HTTP 401): API キーを確認してください"
exit 1
elif [ "${HTTP_CODE}" = "000" ]; then
warn "Outline に接続できませんでした (Outline が停止中の可能性があります)"
warn "スクリプトの設置は続行しますが、実行前に Outline が起動していることを確認してください"
else
warn "予期しないレスポンス (HTTP ${HTTP_CODE}) — 設置は続行します"
fi
# ── 保持日数の確認 ─────────────────────────────────
section "[Step 4] バックアップ保持日数を設定..."
echo ""
echo " 古いバックアップを自動削除する日数を設定します。"
echo " 例: 30 → 30日より古いバックアップを削除"
echo ""
read -rp " 保持日数 [デフォルト: 30]: " KEEP_DAYS
KEEP_DAYS="${KEEP_DAYS:-30}"
if ! [[ "${KEEP_DAYS}" =~ ^[0-9]+$ ]] || [ "${KEEP_DAYS}" -lt 1 ]; then
warn "無効な値です。デフォルト (30日) を使用します"
KEEP_DAYS=30
fi
info "保持日数: ${KEEP_DAYS}日"
# ── cron スケジュールの確認 ───────────────────────
section "[Step 5] 自動バックアップ (cron) の設定..."
echo ""
echo " スケジュール例:"
echo " [1] 毎日 深夜 2:00 (推奨)"
echo " [2] 毎週日曜 深夜 2:00"
echo " [3] 毎月1日 深夜 2:00"
echo " [4] cron に登録しない"
echo ""
CRON_CHOICE=""
while true; do
read -rp " 選択してください [1-4]: " CRON_CHOICE
case "${CRON_CHOICE}" in 1|2|3|4) break ;; esac
warn "1 〜 4 を入力してください"
done
case "${CRON_CHOICE}" in
1) CRON_SCHEDULE="0 2 * * *" ; CRON_LABEL="毎日 深夜 2:00" ;;
2) CRON_SCHEDULE="0 2 * * 0" ; CRON_LABEL="毎週日曜 深夜 2:00" ;;
3) CRON_SCHEDULE="0 2 1 * *" ; CRON_LABEL="毎月1日 深夜 2:00" ;;
4) CRON_SCHEDULE="" ; CRON_LABEL="登録しない" ;;
esac
info "cron 設定: ${CRON_LABEL}"
# ── ディレクトリ作成 ──────────────────────────────
section "[Step 6] ディレクトリを作成..."
mkdir -p "${SCRIPT_DIR}"
mkdir -p "${BACKUP_DIR}"
info "作成: ${SCRIPT_DIR}"
info "作成: ${BACKUP_DIR}"
# ── スクリプトを配置 ──────────────────────────────
section "[Step 7] スクリプトを配置..."
INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${INSTALLER_DIR}/outline-export-backup.sh" ]; then
cp "${INSTALLER_DIR}/outline-export-backup.sh" "${SCRIPT_PATH}"
info "outline-export-backup.sh をコピーしました"
else
error "outline-export-backup.sh が見つかりません: ${INSTALLER_DIR}/"
error "インストーラーと同じディレクトリに置いてください"
exit 1
fi
# API キー・URL・保持日数をスクリプトに書き込む
sed -i "s|^OUTLINE_URL=\"\${OUTLINE_URL:-}\"|OUTLINE_URL=\"\${OUTLINE_URL:-${OUTLINE_URL}}\"|" "${SCRIPT_PATH}"
sed -i "s|^API_KEY=\"\${OUTLINE_API_KEY:-}\"|API_KEY=\"\${OUTLINE_API_KEY:-${API_KEY}}\"|" "${SCRIPT_PATH}"
sed -i "s|^KEEP_DAYS=\"\${OUTLINE_KEEP_DAYS:-30}\"|KEEP_DAYS=\"\${OUTLINE_KEEP_DAYS:-${KEEP_DAYS}}\"|" "${SCRIPT_PATH}"
chmod 700 "${SCRIPT_PATH}"
info "配置完了: ${SCRIPT_PATH}"
info "パーミッション: 700 (root のみ)"
# ── cron 登録 ─────────────────────────────────────
section "[Step 8] cron の設定..."
if [ -n "${CRON_SCHEDULE}" ]; then
# --- [修正] cron パッケージの確認・インストール ---
CRON_SVC=""
for SVC in cron crond; do
if command -v "${SVC}" &>/dev/null; then
CRON_SVC="${SVC}"
break
fi
done
if command -v systemctl &>/dev/null; then
for SVC in cron crond; do
if systemctl list-unit-files "${SVC}.service" 2>/dev/null | grep -q "${SVC}"; then
CRON_SVC="${SVC}"
break
fi
done
fi
if [ -z "${CRON_SVC}" ]; then
warn "cron が見つかりません。インストールします..."
apt-get update -qq
apt-get install -y cron
CRON_SVC="cron"
info "cron をインストールしました"
else
info "cron: OK (${CRON_SVC})"
fi
# --- [修正] cron.d ファイルを正しい書式で作成 ---
# 書式: SHELL/PATH 変数宣言 + "スケジュール ユーザー コマンド"
CRON_FILE="/etc/cron.d/outline-export-backup"
cat > "${CRON_FILE}" << CRONEOF
# Outline 自動エクスポートバックアップ (${CRON_LABEL})
# インストール日: $(date '+%Y-%m-%d %H:%M:%S')
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
${CRON_SCHEDULE} root bash ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1
CRONEOF
chmod 644 "${CRON_FILE}" # cron.d は 644 必須 (755 だと cron が無視する)
info "cron ファイル作成: ${CRON_FILE} (パーミッション: 644)"
# --- [修正] cron サービスの起動・有効化 ---
CRON_STARTED=false
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
if ! systemctl is-active --quiet "${CRON_SVC}" 2>/dev/null; then
warn "${CRON_SVC} が停止中です。起動します..."
systemctl start "${CRON_SVC}" && CRON_STARTED=true
systemctl enable "${CRON_SVC}" 2>/dev/null || true
else
CRON_STARTED=true
fi
elif command -v service &>/dev/null; then
if ! service "${CRON_SVC}" status &>/dev/null; then
warn "${CRON_SVC} が停止中です。起動します..."
service "${CRON_SVC}" start && CRON_STARTED=true
command -v update-rc.d &>/dev/null \
&& update-rc.d "${CRON_SVC}" enable 2>/dev/null || true
else
CRON_STARTED=true
fi
else
pgrep -x "${CRON_SVC}" &>/dev/null && CRON_STARTED=true
fi
if $CRON_STARTED; then
info "${CRON_SVC}: 動作中"
else
warn "${CRON_SVC} の起動確認ができませんでした"
warn "手動で起動してください: service cron start"
fi
# --- [修正] cron をリロードして新しいファイルを読み込ませる ---
RELOADED=false
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
systemctl reload "${CRON_SVC}" 2>/dev/null \
|| systemctl restart "${CRON_SVC}" 2>/dev/null \
&& RELOADED=true || true
elif command -v service &>/dev/null; then
service "${CRON_SVC}" reload 2>/dev/null \
|| service "${CRON_SVC}" restart 2>/dev/null \
&& RELOADED=true || true
fi
$RELOADED && info "cron をリロードしました" \
|| warn "cron のリロードに失敗しました。再起動後に自動で読み込まれます"
info "cron 登録完了: ${CRON_FILE}"
info "スケジュール : ${CRON_LABEL}"
info "ログ出力先 : ${LOG_FILE}"
else
info "cron への登録をスキップしました"
echo ""
echo " 後から登録する場合は付属の outline-cron-register.sh を使用してください:"
echo " sudo bash outline-cron-register.sh"
echo ""
echo " または手動で:"
echo " sudo crontab -e"
echo " # 例 (毎日 深夜 2:00):"
echo " 0 2 * * * bash ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1"
fi
# ── 完了 ─────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════"
echo -e " ${GREEN}✅ インストール完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo " 📂 スクリプト: ${SCRIPT_PATH}"
echo " 📦 保存先 : ${BACKUP_DIR}/"
echo " 📅 保持日数 : ${KEEP_DAYS}日"
echo ""
echo " 📋 使い方:"
echo " # 今すぐバックアップ"
echo " sudo bash ${SCRIPT_PATH}"
echo ""
echo " # バックアップ一覧"
echo " sudo bash ${SCRIPT_PATH} list"
echo ""
echo " # 古いバックアップを削除"
echo " sudo bash ${SCRIPT_PATH} clean"
echo ""
if [ -n "${CRON_SCHEDULE}" ]; then
echo " ⏰ 自動バックアップ: ${CRON_LABEL}"
echo " ログ: tail -f ${LOG_FILE}"
echo ""
echo " cron の登録確認:"
echo " sudo bash outline-cron-register.sh --show"
echo ""
fi
echo " 復元方法:"
echo " Outline の Settings → Import → ZIP をアップロード"
echo ""
cronの登録を後から実行
既にインストール済みの環境で、cronによる定期実行を追加したい場合は下記を実行してください。
#!/bin/bash
set -euo pipefail
# =============================================================
# Outline バックアップ cron 登録スクリプト
#
# outline-export-backup.sh を cron に登録します。
# LXDコンテナ環境に対応: cron の未インストール/未起動を検出・修正します。
#
# 使い方:
# sudo bash outline-cron-register.sh
# sudo bash outline-cron-register.sh --show # 現在の登録内容を確認
# sudo bash outline-cron-register.sh --remove # cron 登録を削除
# =============================================================
SCRIPT_PATH="/opt/lxd-data/script/outline/outline-export-backup.sh"
CRON_FILE="/etc/cron.d/outline-export-backup"
LOG_FILE="/var/log/outline-export-backup.log"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
section() { echo -e "\n${CYAN}==> $*${NC}"; }
# ── root チェック ─────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
error "このスクリプトは root または sudo で実行してください"
exit 1
fi
# ── サブコマンド処理 ──────────────────────────────
show_cron() {
echo ""
echo "════════════════════════════════════════════════"
echo " 現在の cron 登録状況"
echo "════════════════════════════════════════════════"
echo ""
# cron.d ファイル
if [ -f "${CRON_FILE}" ]; then
echo " 📄 ${CRON_FILE}:"
cat "${CRON_FILE}" | sed 's/^/ /'
echo ""
else
warn "${CRON_FILE} は存在しません"
echo ""
fi
# crontab -l (root)
echo " 📋 root の crontab -l:"
crontab -l 2>/dev/null | grep -i "outline" | sed 's/^/ /' || echo " (outline エントリなし)"
echo ""
# cron サービス状態
echo " ⚙️ cron サービス状態:"
if command -v systemctl &>/dev/null && systemctl list-units --type=service 2>/dev/null | grep -qE "cron|crond"; then
local SVC
SVC=$(systemctl list-units --type=service 2>/dev/null | grep -oE "(cron|crond)\.service" | head -1)
systemctl status "${SVC}" --no-pager 2>/dev/null | head -5 | sed 's/^/ /' || true
else
ps aux 2>/dev/null | grep -v grep | grep -qE "cron" \
&& echo " cron プロセス: 動作中" \
|| echo " cron プロセス: 停止中"
fi
echo ""
}
remove_cron() {
echo ""
echo "════════════════════════════════════════════════"
echo " cron 登録を削除"
echo "════════════════════════════════════════════════"
echo ""
if [ -f "${CRON_FILE}" ]; then
rm -f "${CRON_FILE}"
info "削除しました: ${CRON_FILE}"
else
warn "登録ファイルが見つかりません: ${CRON_FILE}"
fi
# crontab からも削除 (念のため)
if crontab -l 2>/dev/null | grep -q "outline-export-backup"; then
crontab -l 2>/dev/null | grep -v "outline-export-backup" | crontab -
info "crontab からも outline-export-backup のエントリを削除しました"
fi
echo ""
info "削除完了"
echo ""
}
# ── 引数処理 ─────────────────────────────────────
case "${1:-}" in
--show|-s) show_cron; exit 0 ;;
--remove|-r) remove_cron; exit 0 ;;
--help|-h)
echo "使い方:"
echo " sudo bash $0 # cron に登録"
echo " sudo bash $0 --show # 現在の登録内容を確認"
echo " sudo bash $0 --remove # cron 登録を削除"
exit 0
;;
"") ;; # 通常の登録処理へ
*)
error "不明なオプション: ${1}"
echo " sudo bash $0 --help で使い方を確認してください"
exit 1
;;
esac
# ════════════════════════════════════════════════
# メイン: cron 登録
# ════════════════════════════════════════════════
echo ""
echo "════════════════════════════════════════════════"
echo " Outline バックアップ cron 登録"
echo "════════════════════════════════════════════════"
echo ""
# ── バックアップスクリプトの確認 ─────────────────
section "[Step 1] バックアップスクリプトを確認..."
if [ ! -f "${SCRIPT_PATH}" ]; then
error "バックアップスクリプトが見つかりません: ${SCRIPT_PATH}"
error "先に outline-export-install.sh を実行してください"
exit 1
fi
if [ ! -x "${SCRIPT_PATH}" ] && ! [ "$(stat -c '%a' "${SCRIPT_PATH}")" = "700" ]; then
warn "スクリプトに実行権限がありません。付与します..."
chmod 700 "${SCRIPT_PATH}"
fi
info "スクリプト: OK (${SCRIPT_PATH})"
# ── cron パッケージの確認・インストール ──────────
section "[Step 2] cron パッケージを確認..."
CRON_SVC=""
# systemd 環境で cron サービス名を特定
if command -v systemctl &>/dev/null; then
for SVC in cron crond anacron; do
if systemctl list-unit-files "${SVC}.service" 2>/dev/null | grep -q "${SVC}"; then
CRON_SVC="${SVC}"
break
fi
done
fi
# コマンドでも確認
if [ -z "${CRON_SVC}" ]; then
for CMD in cron crond; do
if command -v "${CMD}" &>/dev/null; then
CRON_SVC="${CMD}"
break
fi
done
fi
if [ -z "${CRON_SVC}" ]; then
warn "cron が見つかりません。インストールします..."
apt-get update -qq
apt-get install -y cron
CRON_SVC="cron"
info "cron をインストールしました"
else
info "cron: OK (${CRON_SVC})"
fi
# ── cron サービスの起動確認 ───────────────────────
section "[Step 3] cron サービスを確認・起動..."
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
# systemd 環境
if ! systemctl is-active --quiet "${CRON_SVC}" 2>/dev/null; then
warn "${CRON_SVC} が停止しています。起動します..."
systemctl start "${CRON_SVC}"
systemctl enable "${CRON_SVC}"
info "${CRON_SVC} を起動・自動起動に設定しました"
else
info "${CRON_SVC}: 動作中"
fi
elif command -v service &>/dev/null; then
# sysvinit / service コマンド環境 (LXD コンテナでよくある)
if ! service "${CRON_SVC}" status &>/dev/null; then
warn "${CRON_SVC} が停止しています。起動します..."
service "${CRON_SVC}" start || true
# 自動起動 (update-rc.d)
if command -v update-rc.d &>/dev/null; then
update-rc.d "${CRON_SVC}" enable 2>/dev/null || true
fi
info "${CRON_SVC} を起動しました"
else
info "${CRON_SVC}: 動作中"
fi
else
# プロセスで確認
if ! pgrep -x "${CRON_SVC}" &>/dev/null; then
warn "${CRON_SVC} が動作していません。起動を試みます..."
"${CRON_SVC}" &
sleep 1
if pgrep -x "${CRON_SVC}" &>/dev/null; then
info "${CRON_SVC}: 起動しました"
else
warn "${CRON_SVC} の自動起動に失敗しました。手動で起動してください: service cron start"
fi
else
info "${CRON_SVC}: 動作中"
fi
fi
# ── スケジュール選択 ──────────────────────────────
section "[Step 4] スケジュールを選択..."
echo ""
echo " スケジュール例:"
echo " [1] 毎日 深夜 2:00 (推奨)"
echo " [2] 毎週日曜 深夜 2:00"
echo " [3] 毎月1日 深夜 2:00"
echo " [4] カスタム (cron 書式で入力)"
echo ""
CRON_CHOICE=""
while true; do
read -rp " 選択してください [1-4]: " CRON_CHOICE
case "${CRON_CHOICE}" in 1|2|3|4) break ;; esac
warn "1 〜 4 を入力してください"
done
case "${CRON_CHOICE}" in
1) CRON_SCHEDULE="0 2 * * *" ; CRON_LABEL="毎日 深夜 2:00" ;;
2) CRON_SCHEDULE="0 2 * * 0" ; CRON_LABEL="毎週日曜 深夜 2:00" ;;
3) CRON_SCHEDULE="0 2 1 * *" ; CRON_LABEL="毎月1日 深夜 2:00" ;;
4)
echo ""
echo " cron 書式: 分 時 日 月 曜日"
echo " 例: 30 3 * * 1 → 毎週月曜 3:30"
read -rp " スケジュールを入力: " CRON_SCHEDULE
CRON_LABEL="${CRON_SCHEDULE}"
;;
esac
info "スケジュール: ${CRON_LABEL}"
# ── cron.d にファイルを書き込む ───────────────────
section "[Step 5] cron.d に登録..."
# /etc/cron.d/ のファイルは 644 (実行可能にしてはいけない)
# 書式: "スケジュール ユーザー コマンド"
cat > "${CRON_FILE}" << CRONEOF
# Outline 自動エクスポートバックアップ
# 登録日: $(date '+%Y-%m-%d %H:%M:%S')
# スケジュール: ${CRON_LABEL}
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
${CRON_SCHEDULE} root bash ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1
CRONEOF
chmod 644 "${CRON_FILE}"
info "登録完了: ${CRON_FILE}"
# ── cron サービスをリロード ───────────────────────
section "[Step 6] cron をリロード..."
RELOAD_OK=false
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
if systemctl reload "${CRON_SVC}" 2>/dev/null || systemctl restart "${CRON_SVC}" 2>/dev/null; then
RELOAD_OK=true
fi
elif command -v service &>/dev/null; then
if service "${CRON_SVC}" reload 2>/dev/null || service "${CRON_SVC}" restart 2>/dev/null; then
RELOAD_OK=true
fi
fi
if $RELOAD_OK; then
info "cron をリロードしました"
else
warn "cron のリロードに失敗しました"
warn "手動でリロードしてください: service cron reload または systemctl reload cron"
fi
# ── 動作確認 ─────────────────────────────────────
section "[Step 7] 登録内容を確認..."
echo ""
echo " 登録ファイルの内容:"
cat "${CRON_FILE}" | sed 's/^/ /'
echo ""
# ── 完了 ─────────────────────────────────────────
echo "════════════════════════════════════════════════"
echo -e " ${GREEN}✅ cron 登録完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo " ⏰ スケジュール : ${CRON_LABEL}"
echo " 📄 cron ファイル: ${CRON_FILE}"
echo " 📋 ログ出力先 : ${LOG_FILE}"
echo ""
echo " 確認コマンド:"
echo " sudo bash $0 --show # 登録内容の確認"
echo " sudo tail -f ${LOG_FILE} # ログ監視"
echo " sudo bash ${SCRIPT_PATH} # 今すぐ手動実行"
echo ""
echo " 削除コマンド:"
echo " sudo bash $0 --remove # cron 登録の削除"
echo ""
設定内容を確認するには下記コマンド
cat /etc/cron.d/outline-export-backup

