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.
This commit is contained in:
ackitsme committed 2026-10-26 15:14:13 -07:00
1 parent fc26387aeb
commit fac0d3202b
1 file changed
+109 -40
+109 -40
View File
@@ -3,8 +3,8 @@
### Docker Stack Backup Script ### Docker Stack Backup Script
### Created by Charlie Silverman - ackitsme3@gmail.com ### Created by Charlie Silverman - ackitsme3@gmail.com
############################################################################### ###############################################################################
### Version: 2.1.1 ### Version: 2.1.4
### Updated: 2026-10-18 ### Updated: 2026-10-26
### Purpose: Backs up all docker stacks in a given directory to Restic bucket ### Purpose: Backs up all docker stacks in a given directory to Restic bucket
############################################################################### ###############################################################################
### Requirements: ### Requirements:
@@ -38,6 +38,15 @@
# Added automatic RUN_LOG cleanup during failure and interruption. # Added automatic RUN_LOG cleanup during failure and interruption.
# Improved housekeeping to prevent orphaned staging data after # Improved housekeeping to prevent orphaned staging data after
# unexpected termination. # 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 set -Eeuo pipefail
@@ -45,10 +54,13 @@ set -Eeuo pipefail
# Configuration # Configuration
############################################################################### ###############################################################################
COMPOSE_ROOT="/home/ackitsme/docker" COMPOSE_ROOT="/home/docker"
LOG_DIR="/home/ackitsme/docker/backuplogs" LOG_DIR="/home/docker/backuplogs"
mkdir -p "${LOG_DIR}" mkdir -p "${LOG_DIR}"
LOG_FILE="${LOG_DIR}/docker-backup_$(date +%Y%m%d-%H%M%S).log" 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 # Restic repository configuration
export RESTIC_ENV_FILE="/root/.restic-env" 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" RUN_LOG="${LOG_DIR}/docker-backup_run_${TIMESTAMP}.log"
CURRENT_STAGING_DIR="" CURRENT_STAGING_DIR=""
RESTIC_OUTPUT=""
touch "${RUN_LOG}" touch "${RUN_LOG}"
@@ -68,7 +81,7 @@ touch "${RUN_LOG}"
# Verify Requirements # 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 || { command -v "$cmd" >/dev/null 2>&1 || {
echo "Missing required command: $cmd" echo "Missing required command: $cmd"
exit 1 exit 1
@@ -100,6 +113,60 @@ log() {
echo "$message" >> "$RUN_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 ## Check if specified project is in the exception list
should_skip() { should_skip() {
local project="$1" local project="$1"
@@ -123,7 +190,7 @@ restart_stack() {
log "Restarting stack: ${compose_dir}" log "Restarting stack: ${compose_dir}"
su - ackitsme -c \ su - $DOCKERUSER -c \
"cd '${compose_dir}' && docker compose up -d" \ "cd '${compose_dir}' && docker compose up -d" \
|| log "WARNING: Failed to restart stack ${compose_dir}" || log "WARNING: Failed to restart stack ${compose_dir}"
} }
@@ -132,21 +199,7 @@ restart_stack() {
send_email_log() { send_email_log() {
local subject="$1" local subject="$1"
if command -v mailx >/dev/null 2>&1; then mailx -s "$subject" -r "$EMAIL_FROM" "$EMAIL_TO" < "$RUN_LOG"
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
} }
## Cleanup Function ## Cleanup Function
@@ -161,6 +214,10 @@ cleanup_staging() {
log "WARNING: Failed to remove ${CURRENT_STAGING_DIR}" log "WARNING: Failed to remove ${CURRENT_STAGING_DIR}"
fi fi
if [[ -n "${RESTIC_OUTPUT:-}" ]]; then
rm -f "${RESTIC_OUTPUT}"
fi
} }
## Error Handler ## Error Handler
@@ -240,6 +297,7 @@ fi
############################################################################### ###############################################################################
total_size=0.00 total_size=0.00
total_added=0
for compose_file in "${COMPOSE_FILES[@]}"; do for compose_file in "${COMPOSE_FILES[@]}"; do
@@ -380,32 +438,41 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Uploading project backup to Restic..." 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 \ restic backup \
"${STAGING_DIR}" \ "${STAGING_DIR}" \
--json \
--tag docker \ --tag docker \
--tag compose \ --tag compose \
--tag "${project_name}" \ --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}') summary=$(jq -cR 'fromjson? | select(.message_type == "summary")' \
lastsnapid=$(restic snapshots --json | jq -r '.[-1].short_id') "${RESTIC_OUTPUT}" | tail -n 1)
# lastsnapid=$(restic snapshots latest --compact | awk 'NR==3 {print $1}')
snapdata=$(restic snapshots "$lastsnapid") rm -f "${RESTIC_OUTPUT}"
snapnum=$(echo $snapdata | awk '{print $14}') RESTIC_OUTPUT=""
snapsu=$(echo $snapdata | awk '{print $15}')
case "$snapsu" in if [[ -n "$summary" ]]; then
"TiB") log_snapshot_info "$summary"
snapnum=$(echo "scale=2;$snapnum * 1024" | bc)
;; snap_bytes=$(jq -r '.total_bytes_processed // 0' <<< "$summary")
"MiB") added_bytes=$(jq -r '.data_added // 0' <<< "$summary")
snapnum=$(echo "scale=2;$snapnum / 1024" | bc) else
;; log "WARNING: Restic returned no snapshot summary for ${project_name}"
esac snap_bytes=0
snapnum=$(printf "%.2f\n" "$snapnum") added_bytes=0
fi
snapnum=$(awk -v b="$snap_bytes" 'BEGIN{printf "%.2f", b / 1073741824}')
################################################### ###################################################
# Cleanup Local Backup # Cleanup Local Backup
@@ -416,7 +483,8 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Project complete: ${project_name} ${snapnum} GiB" log "Project complete: ${project_name} ${snapnum} GiB"
log "" 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 done
############################################################################### ###############################################################################
@@ -433,6 +501,7 @@ restic forget \
log "All compose projects backed up successfully." log "All compose projects backed up successfully."
log "Total Backup Size: $total_size GiB" log "Total Backup Size: $total_size GiB"
log "Total New Data Uploaded: $(human_bytes "$total_added")"
send_email_log \ send_email_log \
"SUCCESS: Docker Backup (${HOSTNAME_SHORT})" || \ "SUCCESS: Docker Backup (${HOSTNAME_SHORT})" || \