Advertisement
Guest User

Restic Backup Script

a guest
May 13th, 2025
42
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Bash 17.09 KB | Source Code | 0 0
  1. #!/bin/bash
  2. #           RESTIC BACKUP
  3. #
  4. #   Complete local and remote backup script via restic.
  5. #       - Includes container orchestration via komodo
  6. #
  7. # Set up provided env config and place in same folder as .env
  8.  
  9.  
  10. ### "=====================LOGGING===========================" ###
  11.  
  12. log() {
  13.     if [[ "$QUIET" != true ]]; then
  14.         echo "$@"
  15.     fi    
  16. }
  17. vlog() {
  18.     if [[ "$VERBOSE" == true ]]; then
  19.         echo "$@"
  20.     fi
  21. }
  22. run() {
  23.     if [[ "$DRY_RUN" == true ]]; then
  24.         log "[DRY-RUN] $*"
  25.     else
  26.         "$@"
  27.     fi
  28. }
  29. # Set default values for variables
  30. STARTUP=${STARTUP:-false}
  31. FORCE_RESTART=${FORCE_RESTART:-false}
  32. PARALLEL=${PARALLEL:-true}
  33. ENV_FILE=${ENV_FILE:-./.env}
  34.  
  35. ### "=====================MENU==============================" ###
  36.  
  37. while getopts ":s:e:l:r:f:pqvhdt" opt; do
  38.     case $opt in
  39.         h)
  40.             echo "Usage: $0 [options]"
  41.             echo ""
  42.             echo "Options:"
  43.             echo "  -e <env_file>    Path to environment file to source"
  44.             echo "  -l <local_repo>  Path to the local Restic repository"
  45.             echo "  -r <remote_repo> Path to the remote Restic repository"
  46.             echo "  -s true|false    Set restart behaviour for containers"
  47.             echo "  -f               Force shutdown of all containers before restart"
  48.             echo "  -p               Disable parallel backups - INOPERATIVE"
  49.             echo "  -q               Enable quiet mode (minimal output)"
  50.             echo "  -v               Enable verbose mode (debug output)"
  51.             echo "  -h               Show this help message"
  52.             echo ""
  53.             echo "Example:"
  54.             echo "  $0 -e .env -l /mnt/backup/local -r sftp:user@host:/backup -s true -fspv"
  55.             exit 0
  56.             ;;
  57.         s)
  58.             # Set startup (true/false) for containers
  59.             log "Startup set to '${OPTARG}'"
  60.             if [[ "$OPTARG" == "true" ]]; then
  61.                 set -a; STARTUP=true; set +a
  62.             elif [[ "$OPTARG" == "false" ]]; then
  63.                 set -a; STARTUP=false; set +a
  64.             else
  65.                 echo "Invalid value for startup. Use 'true' or 'false'."
  66.                 exit 1
  67.             fi
  68.             ;;
  69.         f)
  70.             # Set force restart (true/false) for containers
  71.             log "Force restart set to '${OPTARG}'"
  72.             if [[ "$OPTARG" == "true" ]]; then
  73.                 set -a; FORCE_RESTART=true; set +a
  74.             elif [[ "$OPTARG" == "false" ]]; then
  75.                 set -a; FORCE_RESTART=false; set +a
  76.             else
  77.                 echo "Invalid value for force restart. Use 'true' or 'false'."
  78.                 exit 1
  79.             fi
  80.             ;;
  81.         d)
  82.             log "Dry run enabled"
  83.             set -a; DRY_RUN=true; set +a
  84.             ;;
  85.         e)
  86.             log "Environment file set to '${OPTARG}'"
  87.             set -a; ENV_FILE=$OPTARG; set +a
  88.             ;;
  89.         l)
  90.             log "Local backup set to '${OPTARG}'"
  91.             set -a; LOCAL_REPOSITORY=$OPTARG; set +a
  92.             ;;
  93.         r)
  94.             log "Remote backup set to '${OPTARG}'"
  95.             set -a; REMOTE_REPOSITORY=$OPTARG; set +a
  96.             ;;
  97.         p)
  98.             log "Parallel backup disabled"
  99.             set -a; PARALLEL=false; set +a
  100.             ;;
  101.         q)
  102.             set +x
  103.             set -a; QUIET=true; set +a
  104.             ;;
  105.         v)
  106.             vlog "Verbose mode enabled"
  107.             set -x
  108.             set -a; VERBOSE=true; set +a
  109.             ;;
  110.         t)
  111.             # Test mode
  112.             log "Test mode enabled"
  113.             set -a; TEST=true; set +a
  114.             ;;
  115.         \?)
  116.             echo "Invalid option: -$OPTARG" >&2
  117.             exit 1
  118.             ;;
  119.         :)
  120.             echo "Option -$OPTARG requires an argument." >&2
  121.             exit 1
  122.             ;;
  123.     esac
  124. done
  125.  
  126. # Shift off the options so you can process positional args
  127. shift $((OPTIND -1))
  128.  
  129. ### "=====================ENVIRONMENT=======================" ###
  130.  
  131. set -o allexport && source ${ENV_FILE} && set +o allexport
  132.  
  133. # Set default repository to remote with a local lfallback
  134. RESTIC_REPOSITORY=${REMOTE_REPOSITORY:-$LOCAL_REPOSITORY}
  135.  
  136. test()
  137. {
  138.     log "Testing..."
  139.  
  140.     rm -rf ./tmp-test-bup && rm -rf ./tmp-test-repo
  141.     mkdir ./tmp-test-bup && touch ./tmp-test-bup/testfile.txt
  142.     mkdir ./tmp-test-repo
  143.  
  144.     set -a; RESTIC_REPOSITORY=./tmp-test-repo; set +a
  145.     set -a; DRY_RUN=true; set +a
  146.  
  147.     if [[ -z "$RESTIC_REPOSITORY" ]]; then
  148.         log "Error: RESTIC_REPOSITORY is not set/used."
  149.     fi
  150.     if [[ -z "$LOCAL_REPOSITORY" ]]; then
  151.         log "Error: LOCAL_REPOSITORY is not set/used."
  152.     fi
  153.     if [[ -z "$REMOTE_REPOSITORY" ]]; then
  154.         log "Error: REMOTE_REPOSITORY is not set/used."
  155.     fi
  156.     if [[ -z "$KOMODO_SERVER_NAME" ]]; then
  157.         log "Error: KOMODO_SERVER_NAME is not set/used."
  158.     fi
  159.     if [[ -z "$KOMODO_SERVER_URL" ]]; then
  160.         log "Error: KOMODO_SERVER_URL is not set/used."
  161.     fi
  162.     if [[ -z "$KOMODO_API_KEY" ]]; then
  163.         log "Error: KOMODO_API_KEY is not set/used."
  164.     fi
  165.     if [[ -z "$KOMODO_API_SECRET" ]]; then
  166.         log "Error: KOMODO_API_SECRET is not set/used."
  167.     fi
  168.     if [[ -z "$RESTIC_CACHE_DIR" ]]; then
  169.         log "Error: RESTIC_CACHE_DIR is not set/used."
  170.     fi
  171.     if [[ -z "$RESTIC_EXCLUDES_FILE" ]]; then
  172.         log "Error: RESTIC_EXCLUDES_FILE is not set/used."
  173.     fi
  174.     if [[ -z "$BACKUP_DIRS" ]]; then
  175.         log "Error: BACKUP_DIRS is not set/used."
  176.     fi
  177.     if [[ -z "$RESTIC_PASSWORD" ]]; then
  178.         log "Error: RESTIC_PASSWORD is not set/used."
  179.     fi
  180.     if [[ -z "$RESTIC_PASSWORD_FILE" ]]; then
  181.         log "Error: RESTIC_PASSWORD_FILE is not set/used."
  182.     fi
  183.     if [[ -z "$RESTIC_REPOSITORY" ]]; then
  184.         log "Error: RESTIC_REPOSITORY is not set/used."
  185.     fi
  186.     if [[ -z "$RESTIC_REPOSITORY_PASSWORD" ]]; then
  187.         log "Error: RESTIC_REPOSITORY_PASSWORD is not set/used."
  188.     fi
  189.  
  190.     run backup ./tmp-test-bup ./tmp-test-repo
  191.  
  192.     rm -rf ./tmp-test-bup && rm -rf ./tmp-test-repo
  193.  
  194.     exit 1
  195. }
  196.  
  197. # Backup commands
  198. backup ()
  199. {
  200.     log "Backing up '$1'..."
  201.    
  202.     if restic cat config -r ${2:-$RESTIC_REPOSITORY} >/dev/null 2>&1; then
  203.         log 'Restic repository initialized'
  204.     else
  205.         log 'Initializing restic repository...' && run restic init -r ${2:-$RESTIC_REPOSITORY}
  206.     fi
  207.    
  208.     run restic backup $1 -r ${2:-$RESTIC_REPOSITORY} --exclude-file=${RESTIC_EXCLUDES_FILE} --no-scan --skip-if-unchanged --ignore-inode --cache-dir=${RESTIC_CACHE_DIR}
  209.     log "Backup of '$1' Complete!"
  210. }
  211.  
  212. komodoShutdownAll()
  213. {
  214.     # Check if the shutdown is already in progress
  215.     SHUTDOWN_ACTIVE=$(curl --silent \
  216.         --header "Content-Type: application/json" \
  217.         --header "X-Api-Key: ${KOMODO_API_KEY}" \
  218.         --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  219.         --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "GetServerActionState", params: {server: $server}}')" \
  220.         ${KOMODO_SERVER_URL}/read | jq -r '.stopping_containers')
  221.     if [ "$SHUTDOWN_ACTIVE" == "true" ]; then
  222.         vlog "Shutdown already in progress, skipping..."
  223.         return
  224.     elif [ "$DRY_RUN" == true ]; then
  225.         log "[DRY-RUN] Shutdown command would be sent to komodo."
  226.         return
  227.     fi
  228.  
  229.     log "Stopping containers..."
  230.     # Execute docker shutdown command via komodo
  231.     curl --silent \
  232.     --header "Content-Type: application/json" \
  233.     --header "X-Api-Key: ${KOMODO_API_KEY}" \
  234.     --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  235.     --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "StopAllContainers", params: {server: $server}}')" \
  236.     "${KOMODO_SERVER_URL}/execute" > /dev/null
  237.  
  238.     if [ $? -ne 0 ]; then
  239.         vlog "Error: Failed to send shutdown command."
  240.         exit 1
  241.     fi
  242.  
  243.     # Wait for the shutdown to complete
  244.     # Max time (seconds) is sleep time * retry limit
  245.     local RETRY_COUNT=0
  246.     local RETRY_LIMIT=20
  247.     local SLEEP_TIME=3
  248.     while :; do
  249.         # Check if the shutdown was successful
  250.         SHUTDOWN_ACTIVE=$(curl --silent \
  251.             --header "Content-Type: application/json" \
  252.             --header "X-Api-Key: ${KOMODO_API_KEY}" \
  253.             --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  254.             --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "GetServerActionState", params: {server: $server}}')" \
  255.             ${KOMODO_SERVER_URL}/read | jq -r '.stopping_containers')
  256.  
  257.         # Check if the shutdown is still active
  258.         if [ "$SHUTDOWN_ACTIVE" == "true" ] && [ $RETRY_COUNT != $RETRY_LIMIT ]; then
  259.             vlog "Containers are stopping..."
  260.             sleep $SLEEP_TIME
  261.             ((RETRY_COUNT++))
  262.         elif [ "$SHUTDOWN_ACTIVE" == "false" ]; then
  263.             vlog "Container shutdown complete."
  264.             break
  265.         else
  266.             vlog "Error: Unable to determine shutdown status."
  267.             exit 1
  268.         fi
  269.     done
  270.     log "All containers shutdown"
  271. }
  272.  
  273. komodoStartAll()
  274. {
  275.     # Check if the startup is already in progress
  276.     CONTAINERS_STARTING=$(curl --silent \
  277.         --header "Content-Type: application/json" \
  278.         --header "X-Api-Key: ${KOMODO_API_KEY}" \
  279.         --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  280.         --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "GetServerActionState", params: {server: $server}}')" \
  281.         ${KOMODO_SERVER_URL}/read | jq -r '.starting_containers')
  282.     if [ "$CONTAINERS_STARTING" == "true" ]; then
  283.         vlog "Startup already in progress, skipping..."
  284.         return
  285.     elif [ "$DRY_RUN" == true ]; then
  286.         log "[DRY-RUN] Startup command would be sent to komodo."
  287.         return
  288.     fi
  289.  
  290.     log "Restarting containers..."
  291.     # Execute docker shutdown command via komodo
  292.     curl --silent \
  293.     --header "Content-Type: application/json" \
  294.     --header "X-Api-Key: ${KOMODO_API_KEY}" \
  295.     --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  296.     --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "StartAllContainers", params: {server: $server}}')" \
  297.     "${KOMODO_SERVER_URL}/execute" > /dev/null
  298.  
  299.     if [ $? -ne 0 ]; then
  300.         vlog "Error: Failed to send startup command."
  301.         exit 1
  302.     fi
  303.  
  304.     # Wait for the startup to complete
  305.     # Max time (seconds) is sleep time * retry limit
  306.     local RETRY_COUNT=0
  307.     local RETRY_LIMIT=5
  308.     local SLEEP_TIME=3
  309.     while :; do
  310.         # Check if the shutdown was successful
  311.         CONTAINERS_STARTING=$(curl --silent \
  312.             --header "Content-Type: application/json" \
  313.             --header "X-Api-Key: ${KOMODO_API_KEY}" \
  314.             --header "X-Api-Secret: ${KOMODO_API_SECRET}" \
  315.             --data "$(jq -n --arg server "$KOMODO_SERVER_NAME" '{type: "GetServerActionState", params: {server: $server}}')" \
  316.             ${KOMODO_SERVER_URL}/read | jq -r '.starting_containers')
  317.  
  318.         # Check if the shutdown is still active
  319.         if [ "$CONTAINERS_STARTING" == "true" ] && [ $RETRY_COUNT != $RETRY_LIMIT ]; then
  320.             vlog "Containers are starting..."
  321.             sleep $SLEEP_TIME
  322.             ((RETRY_COUNT++))
  323.         elif [ "$CONTAINERS_STARTING" == "false" ]; then
  324.             vlog "Startup complete."
  325.             break
  326.         else
  327.             vlog "Error: Unable to determine startup status."
  328.             exit 1
  329.         fi
  330.     done
  331.     log "All containers live !"
  332. }
  333.  
  334. dumpDB()
  335. {
  336.   local common_dbs=("mysql" "mariadb" "postgres")
  337.   local containers
  338.   containers=$(docker ps -a --format "{{.ID}} {{.Image}} {{.Names}}")
  339.  
  340.   while read -r id image name; do
  341.     for db in "${common_dbs[@]}"; do
  342.       if [[ "$image" == *"$db"* ]]; then
  343.         log "🔍 Detected DB container '$name' using image '$image'"
  344.  
  345.         # Extract env vars from the container using docker inspect
  346.         env_output=$(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$id")
  347.  
  348.         # Parse common DB environment variables
  349.         MYSQL_ROOT_PASSWORD=$(echo "$env_output" | grep -E '^MYSQL_ROOT_PASSWORD=' | cut -d= -f2-)
  350.         MYSQL_USER=$(echo "$env_output" | grep -E '^MYSQL_USER=' | cut -d= -f2-)
  351.         MYSQL_PASSWORD=$(echo "$env_output" | grep -E '^MYSQL_PASSWORD=' | cut -d= -f2-)
  352.  
  353.         POSTGRES_USER=$(echo "$env_output" | grep -E '^POSTGRES_USER=' | cut -d= -f2-)
  354.         POSTGRES_PASSWORD=$(echo "$env_output" | grep -E '^POSTGRES_PASSWORD=' | cut -d= -f2-)
  355.  
  356.         dump_file="${DB_DUMP_DIR}/dump-${name}-$(date +%F_%H-%M-%S).sql"
  357.  
  358.         if [[ $DRY_RUN == true ]]; then
  359.           log "[DRY-RUN] Dumping DB from '$name' to '$dump_file'"
  360.           continue
  361.         fi
  362.  
  363.         case $db in
  364.           mysql | mariadb)
  365.             log "📤 Dumping MySQL/MariaDB from '$name'..."
  366.             docker exec "$id" sh -c "mysqldump -u${MYSQL_USER:-root} -p'${MYSQL_PASSWORD:-$MYSQL_ROOT_PASSWORD}' --all-databases" > "$dump_file"
  367.             ;;
  368.           postgres)
  369.             log "📤 Dumping PostgreSQL from '$name'..."
  370.             docker exec "$id" sh -c "pg_dumpall -U '${POSTGRES_USER:-postgres}'" > "$dump_file"
  371.             ;;
  372.         esac
  373.  
  374.         log "✅ Dump completed: $dump_file"
  375.         break
  376.       fi
  377.     done
  378.   done <<< "$containers"
  379. }
  380.  
  381. startBackups()
  382. {
  383.     # Parse ASYNC_BACKUP_DIRS into array
  384.     eval "entries=(${BACKUP_DIRS})"
  385.  
  386.     # Associative array for key-path mapping
  387.     declare -A path_map
  388.  
  389.     rm -f ./tmp-vlog.log
  390.     verbose_log="./.tmp.vlog"
  391.     # Stop any containers matching the provided key and backup respective dir
  392.     for entry in "${entries[@]}"; do
  393.         if [[ "$entry" == *:* && "$entry" =~ ^[^/]+: ]]; then
  394.             # Entry with key:path format
  395.             key="${entry%%:*}"
  396.             path="${entry#*:}"
  397.  
  398.             # Adding a key with a matching tag in the name (eg: 'immich' matches all official containers spawned) ensures data integrity
  399.             log "Stopping containers with matching name '${key}'..."
  400.             run docker stop $(docker ps -a --filter name=${key} --format '{{ .Names }}') > $verbose_log 2>&1
  401.            
  402.             vlog ${verbose_log}
  403.             rm -f ./tmp.vlog
  404.             touch ./.tmp.vlog
  405.  
  406.             backup ${path} $1
  407.         else
  408.             # Keyless path — auto-generate a key
  409.             key="path_$counter"
  410.             path="$entry"
  411.  
  412.             log "Stopping containers with matching path '${path}'..."
  413.  
  414.             entry=${path%/}
  415.             # Check if container mounts entry directory and stops if required
  416.             for cid in $(docker ps -aq); do
  417.                 if docker inspect --format '{{ range .Mounts }}{{ .Source }} {{ end }}' "$cid" \
  418.                 | grep -qE "^$entry(/|$)"; then
  419.                     run docker stop "$cid" > $verbose_log 2>&1
  420.  
  421.                     vlog ${verbose_log}
  422.                     rm -f ./tmp.vlog
  423.                     touch ./.tmp.vlog
  424.                 fi
  425.             done
  426.  
  427.             backup ${path} $1
  428.         fi
  429.         ((counter++))
  430.         path_map["$key"]="$path"
  431.     done
  432.     rm -f ./tmp.vlog
  433. }
  434.  
  435. if [[ "$TEST" == true ]]; then
  436.     log "=====================TESTING==========================="
  437.     test
  438.     log "======================================================="
  439.     log "Test complete, exiting..."
  440.     exit 0
  441. fi
  442.  
  443.  
  444. ##### BACKUP START #####
  445.  
  446.  
  447. log "=====================REPOSITORIES==========================="
  448. log "Local repository: '${LOCAL_REPOSITORY}'"
  449. log "Remote repository: '${REMOTE_REPOSITORY}'"
  450. log "============================================================"
  451.  
  452.  
  453. sleep 3
  454.  
  455.  
  456. log "=====================BACKUP START==========================="
  457. # Default to remote repo
  458. RESTIC_REPOSITORY=${REMOTE_REPOSITORY:-$LOCAL_REPOSITORY}
  459.  
  460. log "======================DATABASE DUMP============================"
  461. dumpDB
  462. backup ${DB_DUMP_DIR}
  463.  
  464. rm -f ./tmp-log.txt > /dev/null 2>&1
  465. touch ./tmp-log.txt
  466. logfile="./.tmp.log"
  467.  
  468. # Check if we are running in parallel mode
  469. if [[ "$PARALLEL" == true && -n "$LOCAL_REPOSITORY" && -n "$REMOTE_REPOSITORY" ]]; then
  470.  
  471.     startBackups ${LOCAL_REPOSITORY} > "${logfile}" 2>&1 &
  472.     local_backup=$!
  473.     log "INFO: Background process started for local backup"
  474.  
  475.     log "=====================REMOTE================================="
  476.     startBackups ${REMOTE_REPOSITORY}
  477.     log "============================================================"
  478.  
  479.     wait $local_backup
  480.  
  481.     log "======================LOCAL================================="
  482.     log cat $logfile
  483.     log "============================================================"
  484. else
  485.     startBackups
  486. fi
  487.  
  488. log "======================BACKUP COMPLETE=========================="
  489.  
  490. log "======================CLEANUP==============================="
  491. log "Cleaning up..."
  492. run restic cache --cleanup > /dev/null 2>&1
  493. rm -f $logfile
  494. log "============================================================"
  495.  
  496. # Check if we are skipping auto-restart for this run
  497. if [[ $STARTUP == "true" ]]; then
  498.     log "======================KOMODO RESTART========================"
  499.  
  500.     if [[ $FORCE_RESTART == "true" ]]; then
  501.         log "Force restart enabled, stopping all containers first..."
  502.         komodoShutdownAll
  503.     fi
  504.  
  505.     komodoStartAll
  506.     log "============================================================"
  507. fi
  508.  
  509. #
  510. ## CZ ##
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement