Linkwardenはインポート/エクスポート機能があるので基本的に移行時には標準機能を利用し、念の為のバックアップとします。
バックアップスクリプト
mkdir -p /opt/lxd-data/script/linkwarden
cd /opt/lxd-data/script/linkwarden
nano linkwarden-backup.sh
# 下記スクリプトを貼り付け
# 実行権限を付与
chmod +x linkwarden-backup.sh
#!/bin/bash
set -euo pipefail
# =============================================================
# Linkwarden バックアップ / 復元スクリプト
#
# 使い方:
# バックアップ : sudo bash linkwarden-backup.sh backup
# 復元 : sudo bash linkwarden-backup.sh restore [バックアップファイルパス]
# 一覧表示 : sudo bash linkwarden-backup.sh list
# 古いファイル削除: sudo bash linkwarden-backup.sh clean
#
# バックアップ保存先: /opt/lxd-data/linkwarden/
# バックアップ対象:
# - PostgreSQL DB ダンプ (pg_dump)
# - Dockerボリューム: linkwarden_data (アップロードファイル等)
# - docker-compose.yml / .secrets
# =============================================================
LINKWARDEN_DIR="/opt/docker/linkwarden"
BACKUP_DIR="/opt/lxd-data/linkwarden"
POSTGRES_CONTAINER="linkwarden-postgres"
LINKWARDEN_CONTAINER="linkwarden"
POSTGRES_USER="linkwarden"
POSTGRES_DB="linkwarden"
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 " Linkwarden バックアップ管理スクリプト"
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_linkwarden_dir() {
if [ ! -f "${LINKWARDEN_DIR}/docker-compose.yml" ]; then
error "Linkwardenディレクトリが見つかりません: ${LINKWARDEN_DIR}"
exit 1
fi
}
check_postgres_running() {
if ! docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
error "PostgreSQLコンテナが起動していません: ${POSTGRES_CONTAINER}"
exit 1
fi
}
# ── バックアップ関数 ──────────────────────────────
do_backup() {
print_banner
check_root
check_linkwarden_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}/linkwarden_${TIMESTAMP}.tar.gz"
# 終了時に作業ディレクトリを削除
trap "rm -rf '${WORK_DIR}'" EXIT
section "[1/5] バックアップディレクトリを準備..."
mkdir -p "${BACKUP_DIR}"
info "保存先: ${BACKUP_DIR}"
section "[2/5] Linkwardenコンテナを一時停止..."
local LINKWARDEN_WAS_RUNNING=false
if docker ps --format '{{.Names}}' | grep -q "^${LINKWARDEN_CONTAINER}$"; then
docker stop "${LINKWARDEN_CONTAINER}"
info "Linkwardenコンテナを停止しました"
LINKWARDEN_WAS_RUNNING=true
else
warn "Linkwardenコンテナは既に停止しています"
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] Dockerボリューム・設定ファイルをコピー..."
# Linkwardenデータボリューム (アップロードファイル等)
# コンテナ経由でボリュームの中身をtarで取り出す
docker run --rm \
--volumes-from "${LINKWARDEN_CONTAINER}" \
-v "${WORK_DIR}:/backup" \
alpine:latest \
tar -czf /backup/linkwarden_data.tar.gz -C /data/data . 2>/dev/null || {
warn "linkwarden_dataボリュームのバックアップをスキップ (空またはマウント失敗)"
touch "${WORK_DIR}/linkwarden_data.tar.gz"
}
info "ボリュームバックアップ完了"
# 設定ファイル
cp "${LINKWARDEN_DIR}/docker-compose.yml" "${WORK_DIR}/docker-compose.yml"
if [ -f "${LINKWARDEN_DIR}/.secrets" ]; then
cp "${LINKWARDEN_DIR}/.secrets" "${WORK_DIR}/.secrets"
info "設定ファイル・シークレットをコピーしました"
else
warn ".secretsファイルが見つかりません"
fi
# メタ情報を記録
cat > "${WORK_DIR}/backup_info.txt" <<EOF
backup_date=$(date '+%Y-%m-%d %H:%M:%S')
linkwarden_dir=${LINKWARDEN_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"
# Linkwardenを再起動
if [ "${LINKWARDEN_WAS_RUNNING}" = true ]; then
cd "${LINKWARDEN_DIR}"
docker compose up -d "${LINKWARDEN_CONTAINER}"
info "Linkwardenコンテナを再起動しました"
fi
# 古いバックアップを自動削除 (cronでの自動実行対応)
local OLD_FILES
OLD_FILES=$(find "${BACKUP_DIR}" -name "linkwarden_*.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 "linkwarden_*.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 " 復元先 : ${LINKWARDEN_DIR}/"
echo " 対象 : PostgreSQL DB・Dockerボリューム・設定ファイル"
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/5] バックアップを展開..."
tar -xzf "${BACKUP_FILE}" -C "${WORK_DIR}"
info "展開完了"
section "[2/5] コンテナをすべて停止..."
cd "${LINKWARDEN_DIR}"
docker compose down
info "全コンテナを停止しました"
section "[3/5] 設定ファイルを復元..."
local ESCAPE_TIMESTAMP
ESCAPE_TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# docker-compose.yml を退避して復元
if [ -f "${LINKWARDEN_DIR}/docker-compose.yml" ]; then
cp "${LINKWARDEN_DIR}/docker-compose.yml" \
"${LINKWARDEN_DIR}/docker-compose.yml.bak_${ESCAPE_TIMESTAMP}"
fi
cp "${WORK_DIR}/docker-compose.yml" "${LINKWARDEN_DIR}/docker-compose.yml"
# .secrets を退避して復元
if [ -f "${WORK_DIR}/.secrets" ]; then
if [ -f "${LINKWARDEN_DIR}/.secrets" ]; then
cp "${LINKWARDEN_DIR}/.secrets" \
"${LINKWARDEN_DIR}/.secrets.bak_${ESCAPE_TIMESTAMP}"
fi
cp "${WORK_DIR}/.secrets" "${LINKWARDEN_DIR}/.secrets"
chmod 600 "${LINKWARDEN_DIR}/.secrets"
info "設定ファイル・シークレットを復元しました"
fi
section "[4/5] PostgreSQLを起動してDBを復元..."
docker compose up -d postgres
# PostgreSQL起動待ち
info "PostgreSQLの起動を待機中..."
for i in $(seq 1 24); do
STATUS=$(docker inspect "${POSTGRES_CONTAINER}" \
--format='{{.State.Health.Status}}' 2>/dev/null || echo "not_found")
if [ "${STATUS}" = "healthy" ]; then
info "PostgreSQL 起動完了"
break
fi
if [ "${i}" -eq 24 ]; then
error "PostgreSQLがタイムアウトしました"
docker logs "${POSTGRES_CONTAINER}" --tail=20
exit 1
fi
sleep 5
echo -n "."
done
echo ""
# 既存DBを削除して復元 (DROP/CREATEはトランザクション外で個別実行)
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/5] Linkwardenボリュームを復元して起動..."
# linkwarden_dataボリュームを復元
if [ -s "${WORK_DIR}/linkwarden_data.tar.gz" ]; then
# 一時コンテナでボリュームにデータを書き戻す
docker run --rm \
-v linkwarden_data:/data/data \
-v "${WORK_DIR}:/backup" \
alpine:latest \
sh -c "rm -rf /data/data/* && tar -xzf /backup/linkwarden_data.tar.gz -C /data/data"
info "Linkwardenデータボリュームを復元しました"
else
warn "linkwarden_dataのバックアップが空のためスキップします"
fi
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 " ${LINKWARDEN_DIR}/docker-compose.yml.bak_${ESCAPE_TIMESTAMP}"
[ -f "${LINKWARDEN_DIR}/.secrets.bak_${ESCAPE_TIMESTAMP}" ] && \
echo " ${LINKWARDEN_DIR}/.secrets.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 "linkwarden_*.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 " %-52s %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 " %-52s %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 "linkwarden_*.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 linkwarden-backup.sh backup
バックアップ一覧表示
sudo bash linkwarden-backup.sh list
最新バックアップから復元
sudo bash linkwarden-backup.sh restore
特定のバックアップから復元
sudo bash linkwarden-backup.sh restore /opt/lxd-data/vaultwarden/vaultwarden_20250101_120000.tar.gz
古いバックアップを削除(自動で7日以上前を削除しています)
sudo bash linkwarden-backup.sh clean
cronで自動バックアップ
crontab -e
crontab -e で追加(毎日午前2時に実行)
# Linkwarden
0 2 * * * bash /opt/lxd-data/script/linkwarden/linkwarden-backup.sh backup >> /var/log/linkwarden-backup.log 2>&1


