#!/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 paths to skip backing up declare -a nobackup=( ) # 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 bind mount source directories 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 directories directories=() while IFS='' read -r line; do if [ -d "${line}" ]; then directories+=("${line}") fi done < <(docker inspect -f '{{ json .Mounts }}' "${container}" \ | jq '.[] | .Source' \ | grep -v "null" \ | sed 's/"//g' \ | grep -E "^/docker") # count number of directories directory_count=${#directories[@]} # if no directories are found, do nothing if [ "${directory_count}" -eq 0 ]; then echo -e "No bind mounts found for ${container_name}\\n" # otherwise, check how many directories should be backed up else # create empty arrays for directories to back up and directories to skip declare -a backup_directories=() declare -a skipped_directories=() # iterate through directories for directory in ${directories[*]}; do # check if directory should be skipped skip_directory="false" for nobackup in "${nobackup[@]}"; do if [ "${nobackup}" == "${directory}" ]; then skipped_directories+=("${directory}") skip_count=$((skip_count+1)) skip_directory="true" break fi done # if directory should not be skipped, add to array to back up if [ "${skip_directory}" == "false" ]; then backup_directories+=("${directory}") backup_count=$((backup_count+1)) fi done # if array of directories to back up is empty, do nothing if [ "${#backup_directories[@]}" -eq 0 ]; then echo -e "Skipped all directories for container ${container_name}\\n" # otherwise, stop container, back up directories, 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 directories for directory_to_back_up in ${backup_directories[*]}; do # store new directory filename base_name="${directory_to_back_up//\/docker\//}" file_name="${base_name//\//_}" echo " [ ] Backing up ${directory_to_back_up}" docker run --tty --rm \ --cpu-shares 512 \ --mount type=bind,source="${gpg_password_file}",destination=/password.file,readonly \ --mount type=bind,source="${directory_to_back_up}",destination=/"${directory_to_back_up}",readonly \ --mount type=bind,source="${local_path}",destination=/target \ "${backup_image}" \ /bin/ash -c "cd /${directory_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}\"_\"${file_name}\".tar.gz.gpg" echo -e "${backspace}${backspace}${clear} [${green}🗸${reset}] Backed up ${directory_to_back_up} (""$(du --apparent-size --human-readable --summarize "${local_path}"/"${date}"_"${file_name}".tar.gz.gpg | awk '{print $1}')"")" done # list all skipped directories, if any for directory_to_skip in ${skipped_directories[*]}; do echo -e " [${red}X${reset}] Skipped backing up ${directory_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 directories echo -e "Backed up directories: ""${green}${backup_count}${reset}""" # list number of skipped directories echo -e "Skipped directories: ""${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 bind mounts\\n\\n### Backup finished at $(date +%Y/%m/%d-%H:%M) ###\\n" >> "${log_file}" 2>&1 # finish logging } 2>&1 | tee -a "${log_file}"