Skip to content

Instantly share code, notes, and snippets.

@ProMasoud
Created September 23, 2025 13:35
Show Gist options
  • Select an option

  • Save ProMasoud/35a46e2625c9f4394741777c24b4aeca to your computer and use it in GitHub Desktop.

Select an option

Save ProMasoud/35a46e2625c9f4394741777c24b4aeca to your computer and use it in GitHub Desktop.
This is a universal and highly flexible backup script that uses Restic to create encrypted, deduplicated snapshots of your data. It's designed to back up data from two types of sources: A local filesystem directory on the host machine. A volume inside a running Docker container. The backups are sent to an S3-compatible object storage service (li…
#!/bin/bash
# Universal Docker & Host Backup Script using Restic
#
# This script intelligently backs up a source directory—either from the local
# filesystem or a Docker container—to an S3-compatible backend using restic.
# All configuration can be set via environment variables.
#==============================================================================
# --- ⚙️ CONFIGURATION (Defaults & Environment Overrides) ---
#==============================================================================
# --- S3 Backend Configuration ---
# Env: S3_ENDPOINT
S3_ENDPOINT="${S3_ENDPOINT:-s3.your-provider.com}"
# Env: S3_BUCKET
S3_BUCKET="${S3_BUCKET:-my-restic-backups}"
# Env: AWS_ACCESS_KEY_ID & AWS_SECRET_ACCESS_KEY
# Note: Restic also checks ~/.aws/credentials automatically if these are left empty.
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-your-s3-access-key}"
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-your-s3-secret-key}"
# --- Restic Configuration ---
# Env: RESTIC_PASSWORD_FILE
# **IMPORTANT**: Path to a file containing your restic repository password.
RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/etc/restic/password.txt}"
# --- Retention Policy ---
# Env: RESTIC_RETENTION_ARGS (as a space-separated string)
# Example: export RESTIC_RETENTION_ARGS="--keep-daily 7 --keep-weekly 4"
if [ -n "$RESTIC_RETENTION_ARGS" ]; then
# Use the space-separated string from the environment variable
read -r -a RETENTION_POLICY <<< "$RESTIC_RETENTION_ARGS"
else
# Fall back to the default array defined in the script
RETENTION_POLICY=(
--keep-within 60d
)
fi
# --- Notification Configuration (Optional) ---
# Env: ENABLE_TELEGRAM_NOTIFICATIONS (set to "true" to enable)
ENABLE_TELEGRAM_NOTIFICATIONS="${ENABLE_TELEGRAM_NOTIFICATIONS:-false}"
# Env: TELEGRAM_BOT_TOKEN
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-your-bot-token}"
# Env: TELEGRAM_CHAT_ID
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-your-chat-id}"
#==============================================================================
# --- SCRIPT LOGIC (DO NOT EDIT BELOW THIS LINE) ---
#==============================================================================
set -e
set -o pipefail
usage() {
echo "Usage: $0 SOURCE BACKUP_NAME"
echo ""
echo "Arguments:"
echo " SOURCE The data source to back up. Can be:"
echo " - A local filesystem path (e.g., '/var/data/app')."
echo " - A Docker source prefixed with 'docker:'"
echo " (e.g., 'docker:container_name:/path/inside/container')."
echo " BACKUP_NAME A unique name for this backup (e.g., 'mysql')."
echo ""
echo "Examples:"
echo " # Back up a Docker volume"
echo " $0 'docker:mysql-1:/data' bitwarden"
echo ""
echo " # Back up a local host directory"
echo " $0 /home/user/important-data my-data"
exit 1
}
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] - $1"
}
send_notification() {
if [ "$ENABLE_TELEGRAM_NOTIFICATIONS" = "true" ]; then
curl -s --max-time 15 -d "chat_id=$TELEGRAM_CHAT_ID&disable_web_page_preview=1&text=$1" \
"https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" > /dev/null || log "Warning: Telegram notification failed to send."
fi
}
check_dependencies() {
log "Checking dependencies..."
local missing=0
for cmd in restic; do
if ! command -v "$cmd" &> /dev/null; then
log "❌ Error: Required command '$cmd' is not installed."
missing=1
fi
done
if [[ "$1" == docker:* ]] && ! command -v "docker" &> /dev/null; then
log "❌ Error: 'docker' is required for backing up a Docker source."
missing=1
fi
if [ "$ENABLE_TELEGRAM_NOTIFICATIONS" = "true" ] && ! command -v "curl" &> /dev/null; then
log "❌ Error: 'curl' is required for Telegram notifications."
missing=1
fi
[ "$missing" -eq 1 ] && { log "Please install missing dependencies."; exit 1; }
log "✅ All necessary dependencies are present."
}
main() {
if [ -z "$1" ] || [ -z "$2" ]; then
usage
fi
local raw_source="$1"
local backup_name="$2"
local source_type=""
local backup_source_path=""
case "$raw_source" in
docker:*)
source_type="docker"
backup_source_path="${raw_source#docker:}"
;;
file:*)
source_type="file"
backup_source_path="${raw_source#file:}"
;;
/*)
source_type="file"
backup_source_path="$raw_source"
;;
*)
log "❌ Error: Invalid source format. Path must be absolute or prefixed with 'docker:'."
usage
;;
esac
check_dependencies "$raw_source"
log "▶️ Starting backup for '$backup_name' (type: $source_type)..."
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export RESTIC_PASSWORD_FILE
export RESTIC_REPOSITORY="s3:$S3_ENDPOINT/$S3_BUCKET/$backup_name"
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
log "❌ Error: Restic password file not found at '$RESTIC_PASSWORD_FILE'."
exit 1
fi
log "Checking if restic repository exists..."
if ! restic cat config > /dev/null 2>&1; then
log "⚠️ Restic repository '$RESTIC_REPOSITORY' is not initialized."
log "Please run the following command ONCE to create it:"
log " restic init -r $RESTIC_REPOSITORY"
exit 1
fi
log "✅ Restic repository found."
if [ "$source_type" = "docker" ]; then
local tmp_dir
tmp_dir=$(mktemp -d)
trap 'log "Cleaning up temporary directory..."; rm -rf "$tmp_dir"' EXIT
log "Copying data from Docker source '$backup_source_path'..."
if ! docker cp "$backup_source_path" "$tmp_dir"; then
log "❌ Error: Failed to copy data from Docker."
send_notification "❌❌❌ Backup FAILED for '$backup_name': Docker cp error."
exit 1
fi
local container_path=$(echo "$backup_source_path" | cut -d':' -f2)
backup_source_path="$tmp_dir/$(basename "$container_path")"
else
if [ ! -e "$backup_source_path" ]; then
log "❌ Error: Source path '$backup_source_path' does not exist."
exit 1
fi
log "Source is a local path. No temporary copy needed."
fi
log "📦 Starting restic backup for source: $backup_source_path"
local backup_output
backup_output=$(restic backup "$backup_source_path" --tag "$backup_name" 2>&1)
if [ $? -eq 0 ]; then
log "✅ Backup successful."
log "🧹 Pruning old snapshots according to retention policy..."
restic forget --prune --tag "$backup_name" "${RETENTION_POLICY[@]}"
local summary
summary=$(echo "$backup_output" | grep "added to the repository")
log "Summary: $summary"
send_notification "✅ Backup successful for '$backup_name'. $summary"
else
log "❌ Error: Restic backup FAILED."
log "Restic output:"
echo "$backup_output"
send_notification "❌❌❌ Backup FAILED for '$backup_name'. Check server logs."
exit 1
fi
log "🎉 Backup process for '$backup_name' finished successfully."
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment