Created
September 23, 2025 13:35
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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