Compare commits

..
2 Commits
Author SHA1 Message Date
ackitsme 0e07c9ec97 Updated to version 2.0.1
- Modified to save all files to a folder tree for backup rather than creating a single monolithic archive.  This will allow restic to properly manage snapshots.
- Changed exception lookup to case-insensitive
2026-10-18 12:26:59 -07:00
ackitsme e79d26435b Update to version 1.6
Added logging, error handline, command line argument names, and command line help.  Overall script cleanup.
2026-10-18 12:26:09 -07:00
2 changed files with 371 additions and 12 deletions

No files matched your search

+352
View File
@@ -0,0 +1,352 @@
#!/bin/bash
#
# Script Name: AppBackup-restic.sh
# Description:
# Creates a backup of a web application and optionally its MySQL database.
# The script archives the application home directory and uploads the backup
# either to a remote SFTP server or to a Backblaze B2 repository using Restic,
# depending on the host on which it is executed.
#
# Usage:
# AppBackup-restic.sh --appname <name> --appuser <user> [--apppassword <password>]
#
# Author: Charlie Silverman
# Version: 1.6
#
# Revision History:
# 1.0 - Initial version.
# 1.1 - Added logging to file.
# 1.2 - Added strict error handling, input validation, and FQDN support.
# 1.3 - Added trap-based error logging and improved operational visibility.
# 1.4 - Added application-specific log files, interrupt handling,
# and enhanced logging.
# 1.5 - Added command-line help.
# 1.6 - Replaced positional parameters with named command-line options
# and added support for short/long option formats.
#
set -o errexit
set -o nounset
set -o pipefail
#------------------------------------------------------------------------------
# Help Function
#------------------------------------------------------------------------------
show_help() {
cat << EOF
AppBackup-restic.sh v1.6
DESCRIPTION
Creates a backup of a web application's files and optionally its
MySQL database. The backup is uploaded either via SFTP or Restic,
depending on the host.
USAGE
AppBackup-restic.sh --appname <name> --appuser <user> [--apppassword <password>]
OPTIONS
-a, --appname <name>
Name of the application and MySQL database.
Required.
-u, --appuser <user>
Linux account that owns the application files.
Required.
-p, --apppassword <password>
MySQL password and SFTP password (if applicable).
Optional.
-h, --help
Display this help and exit.
EXAMPLES
AppBackup-restic.sh \
--appname KISNetManager \
--appuser kisuser \
--apppassword MySecurePassword
AppBackup-restic.sh \
-a CustomerPortal \
-u portaluser
LOG FILE
/tmp/AppBackup-<ApplicationName>.log
AUTHOR
Charlie Silverman
VERSION
1.6
EOF
}
#------------------------------------------------------------------------------
# Process Command Line Arguments
#------------------------------------------------------------------------------
AppName=""
AppUser=""
AppPassword=""
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--appname)
AppName="$2"
shift 2
;;
--appname=*)
AppName="${1#*=}"
shift
;;
-u|--appuser)
AppUser="$2"
shift 2
;;
--appuser=*)
AppUser="${1#*=}"
shift
;;
-p|--apppassword)
AppPassword="$2"
shift 2
;;
--apppassword=*)
AppPassword="${1#*=}"
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "ERROR: Unknown option '$1'"
echo
show_help
exit 1
;;
esac
done
#------------------------------------------------------------------------------
# Validate Required Parameters
#------------------------------------------------------------------------------
if [[ -z "$AppName" || -z "$AppUser" ]]; then
echo "ERROR: --appname and --appuser are required."
echo
show_help
exit 1
fi
#------------------------------------------------------------------------------
# Runtime Variables
#------------------------------------------------------------------------------
DATE=$(date "+%Y%m%d_%H%M%S")
HOSTNAME_FQDN="$(hostname -f)"
SAFE_APPNAME=$(echo "$AppName" | tr -cs '[:alnum:]_-' '_')
LOGFILE="/tmp/AppBackup-${SAFE_APPNAME}.log"
#------------------------------------------------------------------------------
# Logging Function
#------------------------------------------------------------------------------
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [$$] $1" | tee -a "$LOGFILE"
}
#------------------------------------------------------------------------------
# Error Handling
#------------------------------------------------------------------------------
error_handler() {
local exit_code=$?
local line_number=$1
log "ERROR: Backup failed on line ${line_number} with exit code ${exit_code}"
exit "${exit_code}"
}
interrupt_handler() {
log "ERROR: Backup interrupted by signal"
exit 1
}
trap 'error_handler $LINENO' ERR
trap interrupt_handler SIGINT SIGTERM
#------------------------------------------------------------------------------
# Startup Logging
#------------------------------------------------------------------------------
log "================================================================="
log "Starting backup job"
log "Application: $AppName"
log "Application User: $AppUser"
log "Host: $HOSTNAME_FQDN"
log "Log File: $LOGFILE"
log "================================================================="
#------------------------------------------------------------------------------
# Restic Repository Configuration
#------------------------------------------------------------------------------
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/docker"
export RESTIC_PASSWORD_FILE="/root/.restic-password"
#------------------------------------------------------------------------------
# Host Specific Configuration
#------------------------------------------------------------------------------
HostPrefix=""
if [[ "$HOSTNAME_FQDN" == "ackitserver.ackcentuate.com" ]]; then
if [[ "$AppName" == "KISNetManager" ]]; then
HostPrefix="dev-"
fi
fi
ArchiveFile="${AppName}-${DATE}.tar.bz2"
#------------------------------------------------------------------------------
# Prepare Backup Workspace
#------------------------------------------------------------------------------
log "Changing to backup directory"
cd /home/Backups
#------------------------------------------------------------------------------
# Database Backup
#------------------------------------------------------------------------------
if [[ -n "$AppPassword" ]]; then
log "Creating MySQL database backup"
mkdir -p "/home/${AppUser}/SQLBackup"
mysqldump \
-u "$AppUser" \
--password="$AppPassword" \
"$AppName" \
> "/home/${AppUser}/SQLBackup/${AppName}.sqldump"
log "MySQL dump completed successfully"
else
log "Database password not supplied. Skipping MySQL backup."
fi
#------------------------------------------------------------------------------
# Remove Previous Backup Archives
#------------------------------------------------------------------------------
log "Removing previous backup archives"
rm -f /home/Backups/"${AppName}"*.tar.bz*
rm -f /home/Backups/"${HostPrefix}${AppName}"*.tar.bz*
#------------------------------------------------------------------------------
# Create Archive
#------------------------------------------------------------------------------
log "Creating archive ${HostPrefix}${ArchiveFile}"
tar cjf \
"/home/Backups/${HostPrefix}${ArchiveFile}" \
"/home/${AppUser}/"
log "Archive created successfully"
#------------------------------------------------------------------------------
# Upload Backup
#------------------------------------------------------------------------------
if [[ "$HOSTNAME_FQDN" != "ackitserver.ackcentuate.com" ]]; then
if [[ -z "$AppPassword" ]]; then
log "ERROR: SFTP upload requires --apppassword"
exit 1
fi
log "Uploading archive via SFTP"
lftp "sftp://${AppUser}:${AppPassword}@webbuilder.ackcentuate.com" -e \
"cd /home/Backups; put /home/Backups/${HostPrefix}${ArchiveFile}; bye"
log "SFTP upload completed successfully"
else
log "Loading Restic environment"
source /root/.restic-env
log "Starting Restic backup"
restic backup "/home/${AppUser}/" \
--tag apache \
--tag website \
--tag "$AppName" \
--tag "$HOSTNAME_FQDN" \
>> "$LOGFILE" 2>&1
log "Restic backup completed successfully"
fi
#------------------------------------------------------------------------------
# Cleanup
#------------------------------------------------------------------------------
if [[ -n "$AppPassword" ]]; then
log "Removing temporary SQL backup files"
rm -rf "/home/${AppUser}/SQLBackup"
fi
#------------------------------------------------------------------------------
# Completion
#------------------------------------------------------------------------------
log "Backup completed successfully"
log "Application: $AppName"
log "Archive: ${HostPrefix}${ArchiveFile}"
log "Host: $HOSTNAME_FQDN"
log "================================================================="
+19 -12
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: 1.1.3 ### Version: 2.0.1
### Updated: 2026-08-05 ### Updated: 2026-10-18
### 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:
@@ -25,6 +25,10 @@
# 1.1.1 - Added snapshot size to log # 1.1.1 - Added snapshot size to log
# 1.1.2 - Added aggregate backup size to end of log # 1.1.2 - Added aggregate backup size to end of log
# 1.1.3 - Added GiB to total backup size information # 1.1.3 - Added GiB to total backup size information
# 2.0.0 - Modified to save all files to a folder tree for backup rather than
# creating a single monolithic archive. This will allow restic to
# properly manage snapshots
# 2.0.1 - Changed exception lookup to case-insensitive
set -Eeuo pipefail set -Eeuo pipefail
@@ -68,12 +72,16 @@ log() {
should_skip() { should_skip() {
local project="$1" local project="$1"
shopt -s nocasematch
for pattern in "${SKIP_PROJECTS[@]}"; do for pattern in "${SKIP_PROJECTS[@]}"; do
if [[ "$project" == $pattern ]]; then if [[ "$project" == $pattern ]]; then
return 0 return 0
fi fi
done done
shopt -u nocasematch
return 1 return 1
} }
@@ -148,7 +156,7 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Compose file: ${compose_file}" log "Compose file: ${compose_file}"
log "==================================================" log "=================================================="
STAGING_DIR="/tmp/docker-backup-${project_name}-${TIMESTAMP}" STAGING_DIR="/home/Backups/docker-backup-${project_name}"
mkdir -p \ mkdir -p \
"${STAGING_DIR}/images" \ "${STAGING_DIR}/images" \
@@ -197,12 +205,12 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
# Backup Compose Files # Backup Compose Files
################################################### ###################################################
log "Backing up compose files..." log "Backing up compose files..."
mkdir -p "${STAGING_DIR}/compose"
tar -czf \ cp -a \
"${STAGING_DIR}/${project_name}-compose.tar.gz" \ "${compose_dir}" \
-C "$(dirname "$compose_dir")" \ "${STAGING_DIR}/compose/"
"$(basename "$compose_dir")"
################################################### ###################################################
# Backup Images # Backup Images
@@ -220,9 +228,8 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
log "Saving image: $image" log "Saving image: $image"
docker save "$image" \ docker save "$image" \
| gzip \ > "${STAGING_DIR}/images/${safe_name}.tar"
> "${STAGING_DIR}/images/${safe_name}.tar.gz"
fi fi
done < "${STAGING_DIR}/metadata/images.list" done < "${STAGING_DIR}/metadata/images.list"
@@ -243,7 +250,7 @@ for compose_file in "${COMPOSE_FILES[@]}"; do
-v "${volume}:/source:ro" \ -v "${volume}:/source:ro" \
-v "${STAGING_DIR}/volumes:/backup" \ -v "${STAGING_DIR}/volumes:/backup" \
alpine \ alpine \
tar -czf "/backup/${volume}.tar.gz" -C /source . sh -c "cd /source && tar cf - . | tar xf - -C /backup"
done < "${STAGING_DIR}/metadata/volumes.list" done < "${STAGING_DIR}/metadata/volumes.list"