Skip to content

Instantly share code, notes, and snippets.

@mafredri
Created September 26, 2021 21:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mafredri/b412d63f86e7518dc8c3303c2e8f287a to your computer and use it in GitHub Desktop.
Save mafredri/b412d63f86e7518dc8c3303c2e8f287a to your computer and use it in GitHub Desktop.
Restic backup from ZFS dataset snapshots mounted via Docker to mirror real directory structure
FROM alpine:3.14.2
LABEL maintainer="Mathias Fredriksson <mafredri@gmail.com>"
RUN apk add --no-cache \
bzip2 \
ca-certificates
ARG RESTIC_VERSION='0.12.1'
RUN wget -O - https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2 \
| bzip2 -d >/usr/local/bin/restic \
&& chmod +x /usr/local/bin/restic
ARG RCLONE_VERSION='1.56.1'
RUN cd /usr/local/bin \
&& wget -O - https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip \
| busybox unzip -j - '*/rclone' \
&& chmod +x /usr/local/bin/rclone
VOLUME ["/root/.config/rclone", "/root/.cache/restic"]
#!/usr/bin/env zsh
emulate -R zsh
# List of root datasets, recursive snapshots will be applied to these.
typeset -a root_dataset=(
rpool/data
)
# List of datasets to be backed up, must be within a root dataset.
typeset -a datasets=(
${(f)"$(zfs list -r -o name -H rpool/data/backup)"}
rpool/data/private
rpool/data/share
rpool/data/share/home/user
rpool/data/share/public
)
cleanup() {
# Clean up the recursive snapshots.
for rd in $root_dataset; do
zfs destroy -r -v $rd@restic
done
}
# Create recursive snapshots of the entire root dataset for accurate
# point-in-time state.
for rd in $root_dataset; do
zfs snapshot -r $rd@restic
done
trap 'exit $?' INT HUP
trap 'cleanup' EXIT
# List of mountpoints for the datasets.
dirs=(${(f)"$(zfs get -H mountpoint -o value ${(o)datasets})"})
typeset -a vol_args=() included=()
for d in $dirs; do
# Simple logic to try to determine hierarchy, may not work
# properly with some fringe combination of zfs mountpoints.
d2=$(zfs get mountpoint -o value -H ${d:h})
if (( ${included[(I)$d2]} )); then
typeset -a parents=()
# Parent not included, check if it must be (to avoid the
# need to create a directory on a read-only filesystem).
while [[ $d2 != / ]]; do
if (( ${included[(I)$d2]} )); then
parents+=($d2)
else
for parent in ${(o)parents}; do
print Including parent $parent
vol_args+=(-v ${parent}:${parent}:ro)
included=+($parent)
done
break
fi
d2=$(zfs get mountpoint -o value -H ${d2:h})
done
fi
vol_args+=(-v ${d}/.zfs/snapshot/restic:${d}:ro)
included=+($d)
done
if ! docker volume ls --format '{{.Name}}' | grep -q '^restic-cache$'; then
# Create the restic cache volume.
docker volume create restic-cache
fi
# NOTE: restic-rclone.env sets the RESTIC_PASSWORD environment variable.
docker run -it --rm \
--env-file /root/.config/restic/restic-rclone.env \
-v /root/.config/rclone:/root/.config/rclone \
-v /root/.config/restic/restic-rclone.include:/root/.config/restic/restic-rclone.include:ro \
-v /root/.config/restic/restic-rclone.exclude:/root/.config/restic/restic-rclone.exclude:ro \
-v restic-cache:/root/.cache/restic \
$vol_args \
mafredri/restic-rclone restic -r rclone:remote:restic-repo backup \
--one-file-system \
--files-from /root/.config/restic/restic-rclone.include \
--exclude-file /root/.config/restic/restic-rclone.exclude \
$dirs
# Give time to settle. Hopefully avoid:
# cannot destroy snapshot rpool/...@restic: dataset is busy.
sleep 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment