#!/usr/bin/env bash
# backup/stages/docker-volumes.sh
# tar up the small "state" volumes that cannot be reconstructed from
# config alone (e.g. vaultwarden bitwarden_rs data, radarr config).
# Excludes live media / large caches - those are backed up at the fs level.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
. "${HERE}/../../scripts/lib/log.sh"
DEST_ROOT="${DEST_ROOT:-/srv/homelab/backup/snapshots/vols}"
LATEST="${LATEST:-/srv/homelab/backup/latest/vols}"
TS="$(date +%Y%m%dT%H%M%S)"
DEST="${DEST_ROOT}/${TS}"
VOLS=(
homelab_vaultwarden_data
homelab_paperless_data
homelab_paperless_media
homelab_gitea_data
homelab_traefik_letsencrypt
homelab_immich_upload
)
EXCLUDES=(
--exclude=./caches
--exclude=./thumbnails
--exclude=./lost+found
)
dump_one() {
local vol="$1"
if ! docker volume inspect "${vol}" >/dev/null 2>&1; then
log_info "skip ${vol} (not present)"
return 0
fi
local out="${DEST}/${vol}.tar.zst"
log_info "tar ${vol}"
docker run --rm \
-v "${vol}:/src:ro" \
-v "${DEST}:/out" \
--entrypoint /bin/sh \
alpine:3.19 -c '
set -eu
cd /src
apk add --no-cache zstd tar >/dev/null
tar -cf - . | zstd -q -T0 -3 -o "/out/'"${vol}"'.tar.zst"
' >/dev/null || { log_err "dump ${vol} failed"; return 1; }
if [[ ! -s "${out}" ]]; then
log_err "dump ${vol} produced empty file"
return 1
fi
}
main() {
install -d "${DEST}"
local failed=0
for v in "${VOLS[@]}"; do
dump_one "${v}" || failed=$((failed + 1))
done
if (( failed > 0 )); then
exit 1
fi
install -d "$(dirname "${LATEST}")"
rm -f "${LATEST}"
ln -s "${DEST}" "${LATEST}"
}
main "$@"