With my new NAS setup and using Ubuntu over Truenas, Unraid etc, I have been looking into my own backup method for docker instances. Thanks to my beautiful friend AI, I was able to create a solution over a day. I’m sharing this here if anyone else has their own docker instances but have no backup method in place (which until recently has been me). I’m also open to any suggestions!
Below is the gist of the code:
- Iterate through a folder containing sub folders of docker instances
- Backup each subfolder individually. Setup so difference docker instances have different configurations
a. Days between a backup. This is because some instances like Plex i don’t want updated often.Whereas my note taking app I want to have backup every day
b. Maximum number of backups to keep. Similar to above, plex I doint want many backups but Joplin I want to keep lots of them.
This script I have added to my cron jobs so that it runs every day at 2am.
#!/bin/bash
# ===============================================
# Script Notes
# ===============================================
# Check your timezone on linux:
# timedatectl | grep 'Time zone'
# Set your timezone:
# sudo timedatectl set-timezone Australia/Brisbane
# Usage
# bash /valinor/helpers/backupDockers.sh /mnt/newbackup
# Can run DRY_RUN as true, will iterate through but not disturb docker instances,
# make backups or delete any files
# ===============================================
# Configuration
# ===============================================
# Number of days between backups
# When the script is run, this will check if the number of days since last backup
# is greater than or equal to the value provided below. Below example shows plex, calibre.
# If folder matches the name, will use instead of default.
declare -A DAYS_BETWEEN_BACKUP=(
[plex]=60
[calibre]=14
[joplin]=1
[default]=7
)
# When the script is run, this will check the number of backups is greater than
# or equal to the value provided below. Below example shows plex, calibre.
# If folder matches the name, will use instead of default.
declare -A MAX_BACKUPS=(
[plex]=3
[calibre]=3
[joplin]=30
[default]=8
)
# Directories to backup. This script will iterate through their subdirectories (only first level) and
# backup each individually
DOCKER_LIBRARIES=("/valinor/docker-dev" "/valinor/docker")
# ===============================================
# Dry-run mode (set to true to test without actions)
# ===============================================
DRY_RUN=false
# DRY_RUN=true
# ===============================================
# Logging setup
# ===============================================
log() {
local LEVEL="$1"
shift
local MSG="[$(date '+%Y-%m-%d %H:%M:%S')] [$LEVEL] $*"
echo "$MSG"
}
log_info() { log "INFO" "$@"; }
log_error() { log "ERROR" "$@"; }
log_debug() { log "DEBUG" "$@"; }
log_warning() { log "WARN" "$@"; }
# ===============================================
# Validate input
# ===============================================
if [[ -z "$1" ]]; then
log_error "Usage: $0 <backup_directory>"
exit 1
fi
BACKUP_DIR="$1"
if [[ ! -d "$BACKUP_DIR" ]]; then
log_error "Error: $BACKUP_DIR does not exist."
exit 1
fi
if [[ ! -w "$BACKUP_DIR" ]]; then
log_error "Error: $BACKUP_DIR is not writable."
exit 1
fi
log_info "Using backup directory: $BACKUP_DIR"
# ===============================================
# Function to check if a backup should be done
# ===============================================
should_backup_instance() {
local BASENAME="$1"
local SUBDIR="$2"
local DAYS=${DAYS_BETWEEN_BACKUP[$BASENAME]:-${DAYS_BETWEEN_BACKUP[default]}}
if [[ "$BASENAME" == "gluetun" ]]; then
log_info "Skipping backup for $BASENAME ($SUBDIR)"
return 1
fi
local TARGET_DIR="$BACKUP_DIR/$SUBDIR"
mkdir -p "$TARGET_DIR"
local LAST_BACKUP_FILE LAST_BACKUP_DATE TODAY DIFF_DAYS
LAST_BACKUP_FILE=$(find "$TARGET_DIR" -maxdepth 1 -type f -name "${BASENAME}_*.tar.gz" -printf "%T@ %p\n" \
| sort -nr | head -n 1 | cut -d' ' -f2-)
if [[ -n "$LAST_BACKUP_FILE" ]]; then
LAST_BACKUP_DATE=$(date -r "$LAST_BACKUP_FILE" +%Y-%m-%d)
TODAY=$(date +%Y-%m-%d)
DIFF_DAYS=$(( ( $(date -d "$TODAY" +%s) - $(date -d "$LAST_BACKUP_DATE" +%s) ) / 86400 ))
# log_info debug for last backup date, today and diff days
# log_debug "Last backup date: $LAST_BACKUP_DATE"
# log_debug "Today's date: $TODAY"
# log_debug "Difference in days: $DIFF_DAYS"
if (( DIFF_DAYS < DAYS )); then
log_info "Backup for $BASENAME ($SUBDIR) was made $DIFF_DAYS day(s) ago on $LAST_BACKUP_DATE"
log_info "Finished processing $BASENAME."
log_info "----------------------------------"
return 1
fi
fi
return 0
}
# ===============================================
# Function to cleanup old backups
# Deletes oldest backups to ensure max number of backups per service is maintained.
# ===============================================
cleanup_backups() {
local BASENAME="$1"
local SUBDIR="$2"
local MAX_BACKUP_COUNT=${MAX_BACKUPS[$BASENAME]:-${MAX_BACKUPS[default]}}
local TARGET_DIR="$BACKUP_DIR/$SUBDIR"
log_info "Cleaning up old backups for $BASENAME in $SUBDIR (keeping max $MAX_BACKUP_COUNT)..."
find "$TARGET_DIR" -maxdepth 1 -type f -name "${BASENAME}_*.tar.gz" -printf "%T@ %p\n" \
| sort -nr | tail -n +$((MAX_BACKUP_COUNT+1)) | cut -d' ' -f2- \
| while read -r OLD_BACKUP; do
log_info "Deleting old backup: $(basename "$OLD_BACKUP")"
[[ "$DRY_RUN" == false ]] && rm -f "$OLD_BACKUP"
done
}
# ===============================================
# Function to process docker instance
# ===============================================
process_docker_instance() {
local DOCKER_INSTANCE="$1"
local BASENAME
BASENAME=$(basename "$DOCKER_INSTANCE")
local SUBDIR
SUBDIR=$(basename "$(dirname "$DOCKER_INSTANCE")")
local TARGET_DIR="$BACKUP_DIR/$SUBDIR"
mkdir -p "$TARGET_DIR"
log_info "Processing $DOCKER_INSTANCE..."
if ! should_backup_instance "$BASENAME" "$SUBDIR"; then
return
fi
cd "$DOCKER_INSTANCE" || { log_error "Failed to cd into $DOCKER_INSTANCE"; return; }
log_info "Stopping Docker Compose services..."
if [[ "$DRY_RUN" == false ]]; then
if ! docker compose down; then
log_error "Failed to stop Docker Compose services for $DOCKER_INSTANCE"
fi
fi
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TAR_NAME="${BASENAME}_${TIMESTAMP}.tar.gz"
log_info "Creating backup $TARGET_DIR/$TAR_NAME..."
if [[ "$DRY_RUN" == false ]]; then
if ! tar -czf "$TARGET_DIR/$TAR_NAME" -C "$DOCKER_INSTANCE" .; then
log_error "Backup creation failed for $DOCKER_INSTANCE"
fi
fi
log_info "Starting Docker Compose services..."
if [[ "$DRY_RUN" == false ]]; then
if ! docker compose up -d; then
log_error "Failed to start Docker Compose services for $DOCKER_INSTANCE"
fi
fi
cleanup_backups "$BASENAME" "$SUBDIR"
log_info "Finished processing $DOCKER_INSTANCE."
log_info "----------------------------------"
}
# ===============================================
# Loop through docker directories
# ===============================================
for DOCKER_LIB in "${DOCKER_LIBRARIES[@]}"; do
if [ ! -d "$DOCKER_LIB" ]; then
log_warning "Subdirectory $DOCKER_LIB does not exist, skipping..."
continue
fi
for DOCKER_INSTANCE in "$DOCKER_LIB"/*; do
[[ -d "$DOCKER_INSTANCE" ]] || continue # skip if not a directory
process_docker_instance "$DOCKER_INSTANCE"
done
done
log_info "All done!"
log_info "*****************************************************************************************"
log_info "*****************************************************************************************"
