Files
BashScripts/dockerbackup.sh
T
2026-08-04 20:42:00 -07:00

327 lines
9.6 KiB
Bash

#!/usr/bin/env bash
###############################################################################
### Docker Stack Backup Script
### Created by Charlie Silverman - ackitsme3@gmail.com
###############################################################################
### Version: 1.1.2
### Updated: 2027-08-04
### Purpose: Backs up all docker stacks in a given directory to Restic bucket
###############################################################################
### Requirements:
### Restic environment file: /root/.restic-env
### File format:
### export AWS_ACCESS_KEY_ID="[S3 Bucket Access Key ID]"
### export AWS_SECRET_ACCESS_KEY="[S3 Secret Access Key]"
### export RESTIC_REPOSITORY="Restic repository URL"
### export RESTIC_PASSWORD_FILE="Filepath to text file with restic password"
### Exception list: Optionally store a list of exceptions in the file
### dockerbackup-exception.list in the same directory
### as this file. File should be one entry per line.
### Each entry can contain wildcards if neeeded.
###############################################################################
# Version History:
# 1.0.0 - Initial build
# 1.1.0 - Added exception list via text file
# 1.1.1 - Added snapshot size to log
# 1.1.2 - Added aggregate backup size to end of log
set -Eeuo pipefail
###############################################################################
# Configuration
###############################################################################
COMPOSE_ROOT="/home/ackitsme/docker"
LOG_DIR="/home/ackitsme/docker/backuplogs"
LOG_FILE="${LOG_DIR}/docker-backup_$(date +%Y%m%d-%H%M%S).log"
# Restic repository configuration
export RESTIC_REPOSITORY="s3:https://s3.us-west-004.backblazeb2.com/ackitsbucket/docker"
export RESTIC_ENV_FILE="/root/.restic-env"
# Load Restic environment
source $RESTIC_ENV_FILE
HOSTNAME_SHORT="$(hostname -s)"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
# Load exceptions, if any
if [ -f "dockerbackup-exception.list" ]; then
mapfile -t SKIP_PROJECTS < dockerbackup-exception.list
fi
###############################################################################
# Functions
###############################################################################
## Log to screen and log file
log() {
local message="[$(date '+%F %T')] $*"
echo "$message"
echo "$message" >> "$LOG_FILE"
}
## Check if specified project is in the exception list
should_skip() {
local project="$1"
for pattern in "${SKIP_PROJECTS[@]}"; do
if [[ "$project" == $pattern ]]; then
return 0
fi
done
return 1
}
## Check if docker container is running
is_container_running() {
local container_name="$1"
docker inspect -f '{{.State.Running}}' "$container_name" 2>/dev/null | grep -q '^true$'
}
## Restart composer stack
restart_stack() {
local compose_dir="$1"
log "Restarting stack: ${compose_dir}"
(
sudo -u ackitsme docker compose up -d
su - ackitsme -c "cd ${compose_dir};docker compose up -d"
) || log "WARNING: Failed to restart stack ${compose_dir}"
}
###############################################################################
# Verify Requirements
###############################################################################
for cmd in docker restic tar gzip; do
command -v "$cmd" >/dev/null 2>&1 || {
echo "Missing required command: $cmd"
exit 1
}
done
###############################################################################
# Locate Compose Files
###############################################################################
mapfile -t COMPOSE_FILES < <(
find "$COMPOSE_ROOT" \
\( -name "docker-compose.yml" \
-o -name "docker-compose.yaml" \
-o -name "compose.yml" \
-o -name "compose.yaml" \) \
| sort
)
if [ "${#COMPOSE_FILES[@]}" -eq 0 ]; then
log "No compose files found."
exit 0
fi
###############################################################################
# Process Each Compose Project
###############################################################################
total_size=0
for compose_file in "${COMPOSE_FILES[@]}"; do
compose_dir="$(dirname "$compose_file")"
project_name="$(basename "$compose_dir")"
if should_skip "$project_name"; then
log "Skipping project: ${project_name}"
continue
fi
log "=================================================="
log "Processing project: ${project_name}"
log "Compose file: ${compose_file}"
log "=================================================="
STAGING_DIR="/tmp/docker-backup-${project_name}-${TIMESTAMP}"
mkdir -p \
"${STAGING_DIR}/images" \
"${STAGING_DIR}/volumes" \
"${STAGING_DIR}/metadata"
###################################################
# Gather Information Before Shutdown
###################################################
cd "$compose_dir"
docker compose config --images \
| sort -u \
> "${STAGING_DIR}/metadata/images.list" || true
docker compose ps -q \
> "${STAGING_DIR}/metadata/containers.list" || true
###################################################
# Determine Volumes Used By Containers
###################################################
> "${STAGING_DIR}/metadata/volumes.list"
while read -r container_id; do
[ -z "$container_id" ] && continue
docker inspect "$container_id" \
--format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{println}}{{end}}{{end}}'
done < "${STAGING_DIR}/metadata/containers.list" \
| sort -u \
> "${STAGING_DIR}/metadata/volumes.list"
###################################################
# Stop Stack
###################################################
log "Stopping stack..."
docker compose down --timeout 60
###################################################
# Backup Compose Files
###################################################
log "Backing up compose files..."
tar -czf \
"${STAGING_DIR}/${project_name}-compose.tar.gz" \
-C "$(dirname "$compose_dir")" \
"$(basename "$compose_dir")"
###################################################
# Backup Images
###################################################
log "Backing up images..."
while read -r image; do
[ -z "$image" ] && continue
if docker image inspect "$image" >/dev/null 2>&1; then
safe_name=$(echo "$image" | sed 's#[/:]#_#g')
log "Saving image: $image"
docker save "$image" \
| gzip \
> "${STAGING_DIR}/images/${safe_name}.tar.gz"
fi
done < "${STAGING_DIR}/metadata/images.list"
###################################################
# Backup Volumes
###################################################
log "Backing up volumes..."
while read -r volume; do
[ -z "$volume" ] && continue
log "Saving volume: $volume"
docker run --rm \
-v "${volume}:/source:ro" \
-v "${STAGING_DIR}/volumes:/backup" \
alpine \
tar -czf "/backup/${volume}.tar.gz" -C /source .
done < "${STAGING_DIR}/metadata/volumes.list"
###################################################
# Metadata
###################################################
docker image ls \
> "${STAGING_DIR}/metadata/docker-images.txt"
docker volume ls \
> "${STAGING_DIR}/metadata/docker-volumes.txt"
docker network ls \
> "${STAGING_DIR}/metadata/docker-networks.txt"
docker compose config \
> "${STAGING_DIR}/metadata/rendered-compose.yml" || true
###################################################
# Restart Stack
###################################################
restart_stack "${compose_dir}"
###################################################
# Restic Backup
###################################################
log "Uploading project backup to Restic..."
restic backup \
"${STAGING_DIR}" \
--tag docker \
--tag compose \
--tag "${project_name}" \
--tag "${HOSTNAME_SHORT}"
###################################################
# Get backup size
###################################################
lastsnap=`restic ls latest`
lastsnapid=$(echo "$lastsnap" | awk '{print $2}')
snapdata=`restic snapshots $lastsnapid`
snapnum=$(echo $snapdata | awk '{print $14}')
snapsu=$(echo $snapdata | awk '{print $15}')
case "$snapsu" in
"TiB")
snapnum=$(echo "scale=2;$snapnum * 1024" | bc)
;;
"MiB")
snapnum=$(echo "scale=2;$snapnum / 1024" | bc)
;;
esac
snapnum=$(printf "%.2f\n" "$snapnum")
###################################################
# Cleanup Local Backup
###################################################
rm -rf "${STAGING_DIR}"
log "Project complete: ${project_name} ${snapnum} GiB"
log ""
total_size=$(bc <<< "$total_size + $snapnum")
done
###############################################################################
# Retention Policy
###############################################################################
log "Applying Restic retention policy..."
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--prune
log "All compose projects backed up successfully."
log "Total Backup Size: $total_size"