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


