#!/bin/sh set -eu # Cleanup of snapshots, added as trap so it's always executed even in case of a # failure or ctrl+c. cleanup () { btrfs subvolume delete "$snapshot" } usage () { echo "Usage: $(basename "$0") DESTINATION" >&2 exit 1 } get_mountpoint () { df --output=target "$1" | tail +2 } get_snapshot () { name="$1" mountpoint="$(docker volume inspect --format '{{ .Mountpoint }}' "$name" )" snapshot="$(dirname "$mountpoint")/snapshot)" echo "$snapshot" } if [ "$#" -ne 1 ] then usage elif [ ! -d "$1" ] then echo "Destination $1 not found or is not a directory." >&2 exit 1 elif [ "$(id -u)" -ne 0 ] then echo "This program must run as root." >&2 exit 1 elif ! command -v btrfs >/dev/null then echo 'btrfs-progs must be installed.' >&2 exit 1 elif ! docker info --format '{{ .ServerVersion }}' >/dev/null then echo 'Docker must be installed and running.' >&2 exit 1 elif ! df --type=btrfs /var/lib/docker/volumes >/dev/null then echo "Docker volumes are not on a btrfs filesystem." >&2 exit 1 elif ! df --type=btrfs "$1" >/dev/null then echo "Destination $1 is not on a btrfs filesystem." >&2 exit 1 elif [ "$(df --output=target /var/lib/docker/volumes)" != "$(df --output=target "$1")" ] then echo "The destination $1 is not on the same btrfs volume as the Docker volumes." >&2 exit 1 elif [ -z "$(docker volume ls --filter label=snapshot=true --format '{{ .Name }}')" ] then echo "No Docker volumes marked for backup, exiting." >&2 exit 1 fi dest="$1" root_volume="$(get_mountpoint /var/lib/docker/volumes)" snapshot="$(mktemp --dry-run "--tmpdir=$root_volume")" trap 'cleanup' INT QUIT EXIT TERM btrfs subvolume snapshot "$root_volume" "$snapshot" for name in $(docker volume ls --filter label=snapshot=true --format '{{ .Name }}') do echo "Backing up $name." mountpoint="$(docker volume inspect --format '{{ .Mountpoint }}' "$name")" src="$snapshot/${mountpoint#"$root_volume"/}" # shellcheck disable=SC2115 [ ! -e "$dest/$name" ] || rm -rf "$dest/$name" cp --archive --force --reflink=always "$src" "$dest/$name" done