#!/usr/bin/env bash # exit if a command fails set -o errexit # exit if required variables are not set set -o nounset # subshells and functions inherit ERR traps set -E # check for dependencies script_commands="docker jq rclone" missing_counter=0 for needed_command in ${script_commands}; do if ! hash "${needed_command}" >/dev/null 2>&1; then printf "Command not found in PATH: %s\\n" "${needed_command}" >&2 ((missing_counter++)) fi done if ((missing_counter > 0)); then printf "Minimum %d commands are missing in PATH, aborting.\\n" "${missing_counter}" >&2 exit 1 fi # store hostname host_name="${host_name:-"$(hostname)"}" # store date date="$(date +%Y-%m-%d)" # determine which rsync.net endpoint to use if [ "$(hostname)" == "gate" ]; then city="zurich" else city="denver" fi # set docker compose variables project_name="${project_name:-traefik}" compose_dir="${compose_dir:-"${HOME}/${project_name}"}" # set docker image to use for backups backup_image="${backup_image:-"docker.seedno.de/seednode/backup:latest"}" # set b2 bucket name prefix bucket_name_prefix="$(<"${HOME}"/bin/.backup_prefix.file)" # set remote backup directories b2_remote="${b2_remote:-"b2"}" b2_path="${b2_path:-"${bucket_name_prefix}-backups-docker-${host_name}"}" dropbox_remote="${dropbox_remote:-"dropbox"}" dropbox_path="${dropbox_path:-"Backups/Docker/${host_name}/${date}"}" rsync_remote="${rsync_remote:-"${city}.rsync.net"}" rsync_path="${rsync_path:-"backups/docker/${host_name}"}" # set password file for gpg symmetric encryption gpg_password_file="${gpg_password_file:-"${HOME}/bin/.backup_password.file"}" # declare volumes to skip backing up declare -a nobackup="(prometheus)" # set colour codes for interactive use if [ -t 1 ]; then red=$(echo -e "setaf 1\\nbold" | tput -S) green=$(echo -e "setaf 2\\nbold" | tput -S) reset=$(tput sgr0) backspace=$(tput cuu 1) clear=$(tput el) else red="" green="" reset="" backspace="" clear="" fi # create temporary backup directory local_path="$(mktemp -d)" # run on exit function cleanup { # remove temp directory rm -rf "${local_path}" # start all containers cd "${compose_dir}" && /usr/local/bin/docker-compose -f "${compose_dir}"/docker-compose.yml -p "${project_name}" start } trap cleanup EXIT # update docker backup image docker pull "${backup_image}" # initialize counters backup_count=0 skip_count=0 # set log file log_file="${HOME}/logs/rclone-docker.log" # if file does not exist, create it and any parent directories if [ ! -f "${log_file}" ]; then mkdir -p "${log_file%/*}" touch "${log_file}" fi # display log location echo -e "\\nLogging script output to ${log_file}" # log sript output { # prepend timestamp to logfile echo -e "### Backup began at $(date +%Y/%m/%d-%H:%M) ###" >> "${log_file}" 2>&1 # echo a blank line to begin echo # iterate through all containers, backing up any volumes for container in $(/usr/local/bin/docker-compose -f "${compose_dir}"/docker-compose.yml ps -q); do # store container name container_name="$(docker ps --filter "id=${container}" --format "{{.Names}}")" # find all volume mounts volumes=() while IFS='' read -r line; do volumes+=("${line}") done < <(docker inspect -f '{{ json .Mounts }}' "${container}" \ | jq '.[] | .Name' \ | grep -v "null" \ | grep "${project_name}_" \ | sed 's/"//g') # count number of volume mounts volume_count=${#volumes[@]} # if no volume mounts are found, do nothing if [ "${volume_count}" -eq 0 ]; then echo -e "No volumes found for ${container_name}.\\n" # otherwise, check how many volumes should be backed up else # create empty arrays for volumes to back up and volumes to skip declare -a backup_volumes=() declare -a skipped_volumes=() # iterate through volumes for volume in ${volumes[*]}; do # check if volume should be skipped skip_volume="false" for nobackup in "${nobackup[@]}"; do if [ "${project_name}"_"${nobackup}" == "${volume}" ]; then skipped_volumes+=("${volume}") skip_count=$((skip_count+1)) skip_volume="true" break fi done # if volume should not be skipped, add to array to back up if [ "${skip_volume}" == "false" ]; then backup_volumes+=("${volume}") backup_count=$((backup_count+1)) fi done # if array of volumes to back up is empty, do nothing if [ "${#backup_volumes[@]}" -eq 0 ]; then echo -e "Skipped all volumes for container ${container_name}.\\n" # otherwise, stop container, back up volumes, and start it back up else # stop container echo "Stopping container ${container_name}." docker stop "${container}" 1> /dev/null echo -e "${backspace}${clear}Stopped container ${container_name}." # back up all specified volumes for volume_to_back_up in ${backup_volumes[*]}; do echo " [ ] Backing up volume ${volume_to_back_up}." docker run --tty --rm \ --cpu-shares 512 \ --mount type=bind,source="${gpg_password_file}",destination=/password.file,readonly \ --mount type=volume,source="${volume_to_back_up}",destination=/"${volume_to_back_up}",readonly \ --mount type=bind,source="${local_path}",destination=/target \ "${backup_image}" \ /bin/ash -c "cd /${volume_to_back_up} \ && tar cf - . \ | pigz -c \ | gpg --batch --yes --passphrase-file /password.file --symmetric --cipher-algo AES256 -z 0 2>/dev/null \ | pv -t -r -b \ > /target/\"${date}\"_\"${volume_to_back_up}\".tar.gz.gpg" echo -e "${backspace}${backspace}${clear} [${green}🗸${reset}] Backed up volume ${volume_to_back_up}. (""$(du --apparent-size --human-readable --summarize "${local_path}"/"${date}"_"${volume_to_back_up}".tar.gz.gpg | awk '{print $1}')"")" done # list all skipped volumes, if any for volume_to_skip in ${skipped_volumes[*]}; do echo -e " [${red}X${reset}] Skipped backing up volume ${volume_to_skip}." done # start container echo "Starting container ${container_name}." docker start "${container}" 1> /dev/null echo -e "${backspace}${clear}Started container ${container_name}.\\n" fi fi done # list number of backed up volumes echo -e "Backed up volumes: ""${green}${backup_count}${reset}""." # list number of skipped volumes echo -e "Skipped volumes: ""${red}${skip_count}${reset}""." # list total backup size echo -e "Total backup size: ${green}""$(du -sh --apparent-size "${local_path}" | awk '{print $1}')""${reset}.\\n" # create backup directory on dropbox rclone mkdir "${dropbox_remote}":"${dropbox_path}" # copy files over to dropbox echo "Copying to dropbox..." rclone copy --verbose "${local_path}" "${dropbox_remote}":"${dropbox_path}" # create b2 bucket rclone mkdir "${b2_remote}":"${b2_path}" # sync files to b2 bucket echo "Copying to backblaze..." rclone copy --verbose --fast-list "${local_path}" "${b2_remote}":"${b2_path}" # create rsync.net directory ssh "${rsync_remote}" mkdir -p "${rsync_path}" # sync files to rsync.net echo "Copying to rsync.net..." rsync -avH --progress "${local_path}"/ "${rsync_remote}":"${rsync_path}" --delete # append timestamp to logfile echo -e "\\nFinished backing up Docker volumes.\\n\\n### Backup finished at $(date +%Y/%m/%d-%H:%M) ###\\n" >> "${log_file}" 2>&1 # finish logging } 2>&1 | tee -a "${log_file}"