Outlineのバックアップ&復元スクリプト

Outlineにはインポート/エクスポート機能があるので基本的に移行時には標準機能を利用し、念の為のバックアップとします。ただデータの取り扱いには十分注意を。

バックアップスクリプト

コンテナ内で実行

mkdir -p /opt/lxd-data/script/outline
cd /opt/lxd-data/script/outline
nano outline-backup.sh
# 下記スクリプトを貼り付け
# 実行権限を付与
chmod +x outline-backup.sh
#!/bin/bash
set -euo pipefail
# =============================================================
#  Outline バックアップ / 復元スクリプト
#
#  使い方:
#    バックアップ : sudo bash outline-backup.sh backup
#    復元        : sudo bash outline-backup.sh restore [バックアップファイルパス]
#    一覧表示    : sudo bash outline-backup.sh list
#    古いファイル削除: sudo bash outline-backup.sh clean
#
#  バックアップ保存先: /opt/lxd-data/outline/
#  バックアップ対象:
#    - PostgreSQL DB ダンプ (pg_dump)
#    - data/storage/  (アップロードファイル)
#    - dex/config/    (Dex設定・SQLite DB)
#    - docker-compose.yml / .env
# =============================================================

INSTALL_DIR="/opt/docker/outline"
BACKUP_DIR="/opt/lxd-data/outline"
POSTGRES_CONTAINER="outline-postgres"
OUTLINE_CONTAINER="outline"
DEX_CONTAINER="outline-dex"
POSTGRES_USER="outline"
POSTGRES_DB="outline"
KEEP_DAYS=7   # バックアップの保持日数

# ── カラー出力 ────────────────────────────────────
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 バックアップ管理スクリプト"
    echo "════════════════════════════════════════"
    echo ""
}

usage() {
    print_banner
    echo "使い方:"
    echo "  $0 backup              # バックアップを作成"
    echo "  $0 restore [ファイル]  # バックアップから復元"
    echo "                           ファイル省略時は最新を使用"
    echo "  $0 list                # バックアップ一覧を表示"
    echo "  $0 clean               # 古いバックアップを削除 (${KEEP_DAYS}日以上)"
    echo ""
    echo "保存先: ${BACKUP_DIR}/"
    echo ""
}

check_root() {
    if [ "$(id -u)" -ne 0 ]; then
        error "このスクリプトはrootまたはsudoで実行してください"
        exit 1
    fi
}

check_install_dir() {
    if [ ! -f "${INSTALL_DIR}/docker-compose.yml" ]; then
        error "Outlineディレクトリが見つかりません: ${INSTALL_DIR}"
        exit 1
    fi
}

check_postgres_running() {
    if ! docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
        error "PostgreSQLコンテナが起動していません: ${POSTGRES_CONTAINER}"
        exit 1
    fi
}

wait_postgres_healthy() {
    info "PostgreSQLの起動を待機中..."
    for i in $(seq 1 24); do
        local STATUS
        STATUS=$(docker inspect "${POSTGRES_CONTAINER}" \
            --format='{{.State.Health.Status}}' 2>/dev/null || echo "not_found")
        if [ "${STATUS}" = "healthy" ]; then
            info "PostgreSQL 起動完了"
            return 0
        fi
        if [ "${i}" -eq 24 ]; then
            error "PostgreSQLがタイムアウトしました"
            docker logs "${POSTGRES_CONTAINER}" --tail=20
            return 1
        fi
        sleep 5
        echo -n "."
    done
    echo ""
}

# ── バックアップ関数 ──────────────────────────────
do_backup() {
    print_banner
    check_root
    check_install_dir
    check_postgres_running

    local TIMESTAMP
    TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
    local WORK_DIR
    WORK_DIR=$(mktemp -d)
    local BACKUP_FILE="${BACKUP_DIR}/outline_${TIMESTAMP}.tar.gz"

    trap "rm -rf '${WORK_DIR}'" EXIT

    section "[1/5] バックアップディレクトリを準備..."
    mkdir -p "${BACKUP_DIR}"
    info "保存先: ${BACKUP_DIR}"

    section "[2/5] OutlineとDexコンテナを一時停止..."
    local OUTLINE_WAS_RUNNING=false
    local DEX_WAS_RUNNING=false

    if docker ps --format '{{.Names}}' | grep -q "^${OUTLINE_CONTAINER}$"; then
        docker stop "${OUTLINE_CONTAINER}"
        info "Outlineコンテナを停止しました"
        OUTLINE_WAS_RUNNING=true
    else
        warn "Outlineコンテナは既に停止しています"
    fi

    if docker ps --format '{{.Names}}' | grep -q "^${DEX_CONTAINER}$"; then
        docker stop "${DEX_CONTAINER}"
        info "Dexコンテナを停止しました"
        DEX_WAS_RUNNING=true
    else
        warn "Dexコンテナは既に停止しています"
    fi

    section "[3/5] PostgreSQL DBをダンプ..."
    docker exec "${POSTGRES_CONTAINER}" \
        pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -F c \
        > "${WORK_DIR}/postgres.dump"
    info "DBダンプ完了: $(du -sh "${WORK_DIR}/postgres.dump" | cut -f1)"

    section "[4/5] ファイル・設定をコピー..."

    # ストレージ (アップロードファイル)
    if [ -d "${INSTALL_DIR}/data/storage" ]; then
        tar -czf "${WORK_DIR}/storage.tar.gz" \
            -C "${INSTALL_DIR}/data" storage
        info "ストレージバックアップ完了: $(du -sh "${WORK_DIR}/storage.tar.gz" | cut -f1)"
    else
        warn "data/storage が見つかりません。スキップします"
        touch "${WORK_DIR}/storage.tar.gz"
    fi

    # Dex設定ディレクトリ (config.yaml + dex.db)
    if [ -d "${INSTALL_DIR}/dex/config" ]; then
        tar -czf "${WORK_DIR}/dex-config.tar.gz" \
            -C "${INSTALL_DIR}/dex" config
        info "Dex設定バックアップ完了"
    else
        warn "dex/config が見つかりません。スキップします"
        touch "${WORK_DIR}/dex-config.tar.gz"
    fi

    # 設定ファイル
    cp "${INSTALL_DIR}/docker-compose.yml" "${WORK_DIR}/docker-compose.yml"
    if [ -f "${INSTALL_DIR}/.env" ]; then
        cp "${INSTALL_DIR}/.env" "${WORK_DIR}/.env"
        info ".env をコピーしました"
    else
        warn ".env が見つかりません"
    fi

    # メタ情報
    cat > "${WORK_DIR}/backup_info.txt" <<EOF
backup_date=$(date '+%Y-%m-%d %H:%M:%S')
install_dir=${INSTALL_DIR}
postgres_container=${POSTGRES_CONTAINER}
postgres_db=${POSTGRES_DB}
postgres_user=${POSTGRES_USER}
EOF

    section "[5/5] アーカイブを作成..."
    tar -czf "${BACKUP_FILE}" -C "${WORK_DIR}" .
    local BACKUP_SIZE
    BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1)
    info "アーカイブ作成完了: ${BACKUP_FILE} (${BACKUP_SIZE})"

    # チェックサム生成
    sha256sum "${BACKUP_FILE}" > "${BACKUP_FILE}.sha256"
    info "チェックサム: ${BACKUP_FILE}.sha256"

    # コンテナを再起動 (docker compose はサービス名で指定)
    if [ "${DEX_WAS_RUNNING}" = true ]; then
        cd "${INSTALL_DIR}"
        docker compose up -d dex
        info "Dexコンテナを再起動しました"
    fi
    if [ "${OUTLINE_WAS_RUNNING}" = true ]; then
        cd "${INSTALL_DIR}"
        docker compose up -d outline
        info "Outlineコンテナを再起動しました"
    fi

    # 古いバックアップを自動削除 (cronでの自動実行対応)
    local OLD_FILES
    OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_*.tar.gz" \
        -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 read -r F; do
            rm -f "${F}" "${F}.sha256"
            info "古いバックアップを削除: $(basename "${F}")"
        done
        info "${OLD_COUNT} 件の古いバックアップを削除しました (${KEEP_DAYS}日以上前)"
    fi

    echo ""
    echo "════════════════════════════════════════"
    echo -e "  ${GREEN}✅  バックアップ完了!${NC}"
    echo "════════════════════════════════════════"
    echo ""
    echo "  📦 ファイル : ${BACKUP_FILE}"
    echo "  📏 サイズ   : ${BACKUP_SIZE}"
    echo "  📅 日時     : $(date '+%Y-%m-%d %H:%M:%S')"
    echo ""
}

# ── 復元関数 ──────────────────────────────────────
do_restore() {
    print_banner
    check_root

    local BACKUP_FILE="${1:-}"

    # ファイル指定がなければ最新を使用
    if [ -z "${BACKUP_FILE}" ]; then
        section "最新バックアップを検索..."
        BACKUP_FILE=$(find "${BACKUP_DIR}" -name "outline_*.tar.gz" \
            -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1 | cut -d' ' -f2-)
        if [ -z "${BACKUP_FILE}" ]; then
            error "バックアップファイルが見つかりません: ${BACKUP_DIR}"
            exit 1
        fi
        info "最新バックアップ: ${BACKUP_FILE}"
    fi

    if [ ! -f "${BACKUP_FILE}" ]; then
        error "バックアップファイルが見つかりません: ${BACKUP_FILE}"
        exit 1
    fi

    # チェックサム検証
    local SHA256_FILE="${BACKUP_FILE}.sha256"
    if [ -f "${SHA256_FILE}" ]; then
        section "チェックサムを検証..."
        if sha256sum -c "${SHA256_FILE}" --quiet 2>/dev/null; then
            info "チェックサム OK"
        else
            error "チェックサム検証に失敗しました!ファイルが破損している可能性があります"
            exit 1
        fi
    else
        warn "チェックサムファイルが見つかりません。スキップします"
    fi

    # 確認プロンプト
    local BACKUP_SIZE
    BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1)
    warn "以下の内容で復元します:"
    echo "  バックアップファイル : ${BACKUP_FILE} (${BACKUP_SIZE})"
    echo "  復元先               : ${INSTALL_DIR}/"
    echo "  対象                 : PostgreSQL DB・ストレージ・Dex設定・.env"
    echo ""
    read -rp "  現在のデータが上書きされます。続行しますか? [y/N]: " CONFIRM
    if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then
        warn "復元をキャンセルしました"
        exit 0
    fi

    local WORK_DIR
    WORK_DIR=$(mktemp -d)
    trap "rm -rf '${WORK_DIR}'" EXIT

    section "[1/6] バックアップを展開..."
    tar -xzf "${BACKUP_FILE}" -C "${WORK_DIR}"
    info "展開完了"

    section "[2/6] 全コンテナを停止..."
    cd "${INSTALL_DIR}"
    docker compose stop outline dex 2>/dev/null || true
    info "OutlineとDexを停止しました"

    local ESCAPE_TIMESTAMP
    ESCAPE_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")

    section "[3/6] 設定ファイルを復元..."
    # docker-compose.yml
    [ -f "${INSTALL_DIR}/docker-compose.yml" ] && \
        cp "${INSTALL_DIR}/docker-compose.yml" \
           "${INSTALL_DIR}/docker-compose.yml.bak_${ESCAPE_TIMESTAMP}"
    cp "${WORK_DIR}/docker-compose.yml" "${INSTALL_DIR}/docker-compose.yml"

    # .env
    if [ -f "${WORK_DIR}/.env" ]; then
        [ -f "${INSTALL_DIR}/.env" ] && \
            cp "${INSTALL_DIR}/.env" \
               "${INSTALL_DIR}/.env.bak_${ESCAPE_TIMESTAMP}"
        cp "${WORK_DIR}/.env" "${INSTALL_DIR}/.env"
        chmod 600 "${INSTALL_DIR}/.env"
        info ".env を復元しました"
    fi

    section "[4/6] PostgreSQL DBを復元..."
    wait_postgres_healthy

    docker exec "${POSTGRES_CONTAINER}" \
        psql -U "${POSTGRES_USER}" -d postgres \
        -c "DROP DATABASE IF EXISTS ${POSTGRES_DB};"
    docker exec "${POSTGRES_CONTAINER}" \
        psql -U "${POSTGRES_USER}" -d postgres \
        -c "CREATE DATABASE ${POSTGRES_DB};"
    docker exec -i "${POSTGRES_CONTAINER}" \
        pg_restore -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
        --no-owner --role="${POSTGRES_USER}" \
        < "${WORK_DIR}/postgres.dump"
    info "DBの復元完了"

    section "[5/6] ストレージとDex設定を復元..."

    # ストレージ
    if [ -s "${WORK_DIR}/storage.tar.gz" ]; then
        rm -rf "${INSTALL_DIR}/data/storage"
        tar -xzf "${WORK_DIR}/storage.tar.gz" -C "${INSTALL_DIR}/data"
        chown -R 1001:1001 "${INSTALL_DIR}/data/storage"
        info "ストレージを復元しました"
    else
        warn "storage.tar.gz が空のためスキップします"
    fi

    # Dex設定
    if [ -s "${WORK_DIR}/dex-config.tar.gz" ]; then
        rm -rf "${INSTALL_DIR}/dex/config"
        mkdir -p "${INSTALL_DIR}/dex"
        tar -xzf "${WORK_DIR}/dex-config.tar.gz" -C "${INSTALL_DIR}/dex"
        chown -R 1001:1001 "${INSTALL_DIR}/dex"
        info "Dex設定を復元しました"
    else
        warn "dex-config.tar.gz が空のためスキップします"
    fi

    section "[6/6] 全コンテナを起動..."
    cd "${INSTALL_DIR}"
    docker compose up -d
    info "全コンテナを起動しました"

    echo ""
    echo "════════════════════════════════════════"
    echo -e "  ${GREEN}✅  復元完了!${NC}"
    echo "════════════════════════════════════════"
    echo ""
    echo "  📦 復元元  : ${BACKUP_FILE}"
    echo "  📅 日時    : $(date '+%Y-%m-%d %H:%M:%S')"
    echo ""
    warn "退避した設定ファイルが残っています (確認後に削除してください):"
    echo "    ${INSTALL_DIR}/docker-compose.yml.bak_${ESCAPE_TIMESTAMP}"
    [ -f "${INSTALL_DIR}/.env.bak_${ESCAPE_TIMESTAMP}" ] && \
        echo "    ${INSTALL_DIR}/.env.bak_${ESCAPE_TIMESTAMP}"
    echo ""
}

# ── 一覧表示関数 ──────────────────────────────────
do_list() {
    print_banner

    if [ ! -d "${BACKUP_DIR}" ]; then
        warn "バックアップディレクトリが存在しません: ${BACKUP_DIR}"
        exit 0
    fi

    local FILES
    FILES=$(find "${BACKUP_DIR}" -name "outline_*.tar.gz" \
        -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 "  %-50s  %8s  %s\n" "ファイル名" "サイズ" "作成日時"
    echo "  $(printf '─%.0s' {1..75})"

    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 "  %-50s  %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}"
    echo ""
}

# ── 古いバックアップ削除関数 ──────────────────────
do_clean() {
    print_banner
    check_root

    local OLD_FILES
    OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_*.tar.gz" \
        -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 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 read -r F; do
        rm -f "${F}" "${F}.sha256"
        info "削除: $(basename "${F}")"
    done

    info "クリーンアップ完了"
    echo ""
}

# ── メイン ────────────────────────────────────────
COMMAND="${1:-}"

case "${COMMAND}" in
    backup)
        do_backup
        ;;
    restore)
        do_restore "${2:-}"
        ;;
    list)
        do_list
        ;;
    clean)
        do_clean
        ;;
    *)
        usage
        exit 1
        ;;
esac

バックアップ作成

sudo bash outline-backup.sh backup

バックアップ一覧表示

sudo bash outline-backup.sh list

最新バックアップから復元

sudo bash outline-backup.sh restore

特定のバックアップから復元

sudo bash outline-backup.sh restore /opt/lxd-data/vaultwarden/vaultwarden_20250101_120000.tar.gz

古いバックアップを削除(自動で7日以上前を削除しています)

sudo bash outline-backup.sh clean

cronで自動バックアップ

crontab -e

crontab -e で追加(毎日午前2時に実行)

# Outline
0 2 * * * bash /opt/lxd-data/script/outline/outline-backup.sh backup >> /var/log/outline-backup.log 2>&1

タイトルとURLをコピーしました