From fac0d3202b6f8cbb3992d4731f5329707ae5ad3e Mon Sep 17 00:00:00 2001 From: Charlie Silverman Date: Mon, 26 Oct 2026 15:14:13 -0700 Subject: [PATCH] Updated to version 2.1.4 # 2.1.2 - Added EMAIL_TO and EMAIL_FROM variables for reports. # 2.1.3 - Added DOCKERUSER variable for account to use for launching contaners # 2.1.4 - Restic snapshot details (ID, parent, file/dir counts, data added, # total size, duration) are now written to the logs. # restic backup now runs with --json; snapshot size is read from the # backup summary instead of parsing 'restic snapshots' table output, # so it always refers to this run's snapshot. # Removed dependency on bc (awk used for arithmetic). # Added total new data uploaded to end of log. --- dockerbackup.sh | 167 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 118 insertions(+), 49 deletions(-) diff --git a/dockerbackup.sh b/dockerbackup.sh index 9c443f5..d8831a7 100644 --- a/dockerbackup.sh +++ b/dockerbackup.sh @@ -3,8 +3,8 @@ ### Docker Stack Backup Script ### Created by Charlie Silverman - ackitsme3@gmail.com ############################################################################### -### Version: 2.1.1 -### Updated: 2026-10-18 +### Version: 2.1.4 +### Updated: 2026-10-26 ### Purpose: Backs up all docker stacks in a given directory to Restic bucket ############################################################################### ### Requirements: @@ -38,6 +38,15 @@ # Added automatic RUN_LOG cleanup during failure and interruption. # Improved housekeeping to prevent orphaned staging data after # unexpected termination. +# 2.1.2 - Added EMAIL_TO and EMAIL_FROM variables for reports. +# 2.1.3 - Added DOCKERUSER variable for account to use for launching contaners +# 2.1.4 - Restic snapshot details (ID, parent, file/dir counts, data added, +# total size, duration) are now written to the logs. +# restic backup now runs with --json; snapshot size is read from the +# backup summary instead of parsing 'restic snapshots' table output, +# so it always refers to this run's snapshot. +# Removed dependency on bc (awk used for arithmetic). +# Added total new data uploaded to end of log. set -Eeuo pipefail @@ -45,10 +54,13 @@ set -Eeuo pipefail # Configuration ############################################################################### -COMPOSE_ROOT="/home/ackitsme/docker" -LOG_DIR="/home/ackitsme/docker/backuplogs" +COMPOSE_ROOT="/home/docker" +LOG_DIR="/home/docker/backuplogs" mkdir -p "${LOG_DIR}" LOG_FILE="${LOG_DIR}/docker-backup_$(date +%Y%m%d-%H%M%S).log" +EMAIL_TO="charlie@kiscc.com" +EMAIL_FROM="kisprodweb.dockerbackup@kiscc.com" +DOCKERUSER="csilverman" # Restic repository configuration export RESTIC_ENV_FILE="/root/.restic-env" @@ -61,6 +73,7 @@ TIMESTAMP="$(date +%Y%m%d-%H%M%S)" RUN_LOG="${LOG_DIR}/docker-backup_run_${TIMESTAMP}.log" CURRENT_STAGING_DIR="" +RESTIC_OUTPUT="" touch "${RUN_LOG}" @@ -68,7 +81,7 @@ touch "${RUN_LOG}" # Verify Requirements ############################################################################### -for cmd in docker restic tar gzip jq; do +for cmd in docker restic tar gzip jq awk; do command -v "$cmd" >/dev/null 2>&1 || { echo "Missing required command: $cmd" exit 1 @@ -91,8 +104,8 @@ fi ## Log to screen and log file log() { - local message - message="[$(date '+%F %T')] $*" + local message + message="[$(date '+%F %T')] $*" echo "$message" @@ -100,6 +113,60 @@ log() { echo "$message" >> "$RUN_LOG" } +## Convert a byte count to a human-readable size (e.g. 1.23GiB) +human_bytes() { + local bytes="${1:-0}" + + if command -v numfmt >/dev/null 2>&1; then + numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes" 2>/dev/null \ + || echo "${bytes} B" + else + echo "${bytes} B" + fi +} + +## Write Restic snapshot details from a backup summary (JSON) to the log +log_snapshot_info() { + local summary="$1" + + local snapshot_id parent_id + local files_new files_changed files_unmodified + local dirs_new dirs_changed dirs_unmodified + local data_added data_added_packed + local total_files total_bytes total_duration + + snapshot_id=$(jq -r '.snapshot_id // "none"' <<< "$summary") + parent_id=$(restic snapshots "$snapshot_id" --json 2>/dev/null \ + | jq -r '.[0].parent // "none"' 2>/dev/null || echo "unknown") + + files_new=$(jq -r '.files_new // 0' <<< "$summary") + files_changed=$(jq -r '.files_changed // 0' <<< "$summary") + files_unmodified=$(jq -r '.files_unmodified // 0' <<< "$summary") + dirs_new=$(jq -r '.dirs_new // 0' <<< "$summary") + dirs_changed=$(jq -r '.dirs_changed // 0' <<< "$summary") + dirs_unmodified=$(jq -r '.dirs_unmodified // 0' <<< "$summary") + data_added=$(jq -r '.data_added // 0' <<< "$summary") + data_added_packed=$(jq -r '.data_added_packed // empty' <<< "$summary") + total_files=$(jq -r '.total_files_processed // 0' <<< "$summary") + total_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary") + total_duration=$(jq -r '.total_duration // 0' <<< "$summary") + + log "---------------- Restic Snapshot -----------------" + log "Snapshot ID: ${snapshot_id:0:8} (${snapshot_id})" + log "Parent snapshot: ${parent_id:0:8}" + log "Tags: docker, compose, ${project_name}, ${HOSTNAME_SHORT}" + log "Files: ${files_new} new, ${files_changed} changed, ${files_unmodified} unmodified" + log "Directories: ${dirs_new} new, ${dirs_changed} changed, ${dirs_unmodified} unmodified" + if [[ -n "$data_added_packed" ]]; then + log "Data added: $(human_bytes "$data_added") ($(human_bytes "$data_added_packed") stored)" + else + log "Data added: $(human_bytes "$data_added")" + fi + log "Snapshot size: $(human_bytes "$total_bytes") in ${total_files} files" + log "Duration: $(awk -v d="$total_duration" 'BEGIN{printf "%.1f", d}') seconds" + log "--------------------------------------------------" +} + ## Check if specified project is in the exception list should_skip() { local project="$1" @@ -123,7 +190,7 @@ restart_stack() { log "Restarting stack: ${compose_dir}" - su - ackitsme -c \ + su - $DOCKERUSER -c \ "cd '${compose_dir}' && docker compose up -d" \ || log "WARNING: Failed to restart stack ${compose_dir}" } @@ -132,21 +199,7 @@ restart_stack() { send_email_log() { local subject="$1" - if command -v mailx >/dev/null 2>&1; then - - mailx -s "$subject" \ - ackitsme3@gmail.com < "$RUN_LOG" - - elif command -v mail >/dev/null 2>&1; then - - mail -s "$subject" \ - ackitsme3@gmail.com < "$RUN_LOG" - - else - - log "WARNING: mail/mailx command not found. Email not sent." - - fi + mailx -s "$subject" -r "$EMAIL_FROM" "$EMAIL_TO" < "$RUN_LOG" } ## Cleanup Function @@ -161,6 +214,10 @@ cleanup_staging() { log "WARNING: Failed to remove ${CURRENT_STAGING_DIR}" fi + + if [[ -n "${RESTIC_OUTPUT:-}" ]]; then + rm -f "${RESTIC_OUTPUT}" + fi } ## Error Handler @@ -185,7 +242,7 @@ error_handler() { ## Interrupt Handler interrupt_handler() { - trap - SIGINT SIGTERM + trap - SIGINT SIGTERM log "ERROR: Backup interrupted" @@ -194,7 +251,7 @@ interrupt_handler() { send_email_log \ "INTERRUPTED: Docker Backup (${HOSTNAME_SHORT})" - rm -f "${RUN_LOG}" + rm -f "${RUN_LOG}" exit 1 } @@ -240,6 +297,7 @@ fi ############################################################################### total_size=0.00 +total_added=0 for compose_file in "${COMPOSE_FILES[@]}"; do @@ -257,7 +315,7 @@ for compose_file in "${COMPOSE_FILES[@]}"; do log "==================================================" STAGING_DIR="/home/Backups/docker-backup-${project_name}" - CURRENT_STAGING_DIR="${STAGING_DIR}" + CURRENT_STAGING_DIR="${STAGING_DIR}" mkdir -p \ "${STAGING_DIR}/images" \ @@ -327,8 +385,8 @@ for compose_file in "${COMPOSE_FILES[@]}"; do log "Saving image: $image" - docker save "$image" \ - > "${STAGING_DIR}/images/${safe_name}.tar" + docker save "$image" \ + > "${STAGING_DIR}/images/${safe_name}.tar" fi done < "${STAGING_DIR}/metadata/images.list" @@ -380,43 +438,53 @@ for compose_file in "${COMPOSE_FILES[@]}"; do log "Uploading project backup to Restic..." + # Capture restic's JSON output (outside the staging dir so it is + # not part of the backup). Errors/warnings still go to stderr. + RESTIC_OUTPUT="$(mktemp /tmp/restic-backup-XXXXXX.json)" + restic backup \ "${STAGING_DIR}" \ + --json \ --tag docker \ --tag compose \ --tag "${project_name}" \ - --tag "${HOSTNAME_SHORT}" + --tag "${HOSTNAME_SHORT}" \ + > "${RESTIC_OUTPUT}" ################################################### - # Get backup size + # Log snapshot details and get backup size ################################################### -# lastsnap=$(restic ls latest) -# lastsnapid=$(echo "$lastsnap" | awk '{print $2}') - lastsnapid=$(restic snapshots --json | jq -r '.[-1].short_id') -# lastsnapid=$(restic snapshots latest --compact | awk 'NR==3 {print $1}') - snapdata=$(restic snapshots "$lastsnapid") - snapnum=$(echo $snapdata | awk '{print $14}') - snapsu=$(echo $snapdata | awk '{print $15}') - case "$snapsu" in - "TiB") - snapnum=$(echo "scale=2;$snapnum * 1024" | bc) - ;; - "MiB") - snapnum=$(echo "scale=2;$snapnum / 1024" | bc) - ;; - esac - snapnum=$(printf "%.2f\n" "$snapnum") + + summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \ + "${RESTIC_OUTPUT}" | tail -n 1) + + rm -f "${RESTIC_OUTPUT}" + RESTIC_OUTPUT="" + + if [[ -n "$summary" ]]; then + log_snapshot_info "$summary" + + snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary") + added_bytes=$(jq -r '.data_added // 0' <<< "$summary") + else + log "WARNING: Restic returned no snapshot summary for ${project_name}" + snap_bytes=0 + added_bytes=0 + fi + + snapnum=$(awk -v b="$snap_bytes" 'BEGIN{printf "%.2f", b / 1073741824}') ################################################### # Cleanup Local Backup ################################################### rm -rf "${STAGING_DIR}" - CURRENT_STAGING_DIR="" + CURRENT_STAGING_DIR="" log "Project complete: ${project_name} ${snapnum} GiB" log "" - total_size=$(bc <<< "$total_size + $snapnum") + total_size=$(awk -v a="$total_size" -v b="$snapnum" 'BEGIN{printf "%.2f", a + b}') + total_added=$(awk -v a="$total_added" -v b="$added_bytes" 'BEGIN{printf "%.0f", a + b}') done ############################################################################### @@ -433,9 +501,10 @@ restic forget \ log "All compose projects backed up successfully." log "Total Backup Size: $total_size GiB" +log "Total New Data Uploaded: $(human_bytes "$total_added")" send_email_log \ "SUCCESS: Docker Backup (${HOSTNAME_SHORT})" || \ log "WARNING: Unable to send success email." - + rm -f "${RUN_LOG}"