#!/bin/bash # Space-separated list of containers to stop before backup. # Prefix with context to target a specific Docker context (e.g. "rootless:app-db default:app-web"). # Uses the $GLOBAL_CONTEXT specified below if no context is provided. CONTAINER_NAMES="${CONTAINER_NAMES}" # Space-separated list of volumes to backup. # Prefix with context to target a specific Docker context (e.g. "rootless:app-db_data default:app-uploads"). # Uses the $GLOBAL_CONTEXT specified below if no context is provided. VOLUME_NAMES="${VOLUME_NAMES}" # Default Docker context to use when entries don't specify one. GLOBAL_CONTEXT="${DOCKER_CONTEXT}" # Directory on the host where backup archives are stored. BACKUP_DIR="${BACKUP_DIR:-/data/backups}" # Set to "true" to preview actions without executing them. DRY_RUN="${DRY_RUN:-false}" IFS=' ' read -r -a container_entries <<< "$CONTAINER_NAMES" IFS=' ' read -r -a volume_entries <<< "$VOLUME_NAMES" stopped=() if [ "$DRY_RUN" = "true" ]; then echo "=== DRY RUN MODE - No changes will be made ===" echo "" fi parse_entry() { local entry="$1" if [[ "$entry" == *":"* ]]; then local context="${entry%%:*}" local name="${entry#*:}" echo "$context $name" else local context="${GLOBAL_CONTEXT:-default}" local name="$entry" echo "$context $name" fi } ensure_context() { local ctx="$1" if [ "$DRY_RUN" = "true" ]; then echo " [dry-run] Would switch to context '$ctx'" return fi if [ "$ctx" = "default" ]; then docker context use default >/dev/null 2>&1 return fi if docker context ls --format '{{.Name}}' | grep -q "^${ctx}$"; then docker context use "$ctx" >/dev/null 2>&1 else echo "! Creating Docker Context '$ctx' !" docker context create "$ctx" \ --docker host="unix:///var/run/docker_${ctx}.sock" docker context use "$ctx" >/dev/null 2>&1 fi } restart_containers() { local -n restarts_ref=$1 if [ ${#restarts_ref[@]} -eq 0 ]; then return fi echo "" echo "Re-starting containers..." for entry in "${restarts_ref[@]}"; do read -r ctx name <<< "$(parse_entry "$entry")" ensure_context "$ctx" if [ "$DRY_RUN" = "true" ]; then echo " [dry-run] Would start $name (context: $ctx)" else echo " Starting $name (context: $ctx)" docker start "$name" fi done } current_context="" echo "Stopping containers:" for entry in "${container_entries[@]}"; do [ -z "$entry" ] && continue read -r ctx name <<< "$(parse_entry "$entry")" if [ "$ctx" != "$current_context" ]; then ensure_context "$ctx" current_context="$ctx" fi if [ "$DRY_RUN" = "true" ]; then echo " [dry-run] Would stop $name" stopped+=("$entry") elif docker ps --format '{{.Names}}' | grep -q "^${name}$"; then echo " Stopping $name..." docker stop -t 30 "$name" exit_code=$? if [[ ${exit_code} -ne 0 ]]; then echo " ⚠ Failed to stop $name" restart_containers stopped exit 1 else stopped+=("$entry") echo " ✓ $name stopped" fi else echo " ⚠ $name not running, skipping" fi done echo "" echo "Preparing volumes for export..." for entry in "${volume_entries[@]}"; do [ -z "$entry" ] && continue read -r ctx name <<< "$(parse_entry "$entry")" if [ "$ctx" != "$current_context" ]; then ensure_context "$ctx" current_context="$ctx" fi archive_name="${name}.tar.gz" if [ "$DRY_RUN" = "true" ]; then echo " [dry-run] Would backup $name to ${BACKUP_DIR}/${archive_name}" elif docker volume ls --format "{{.Name}}" | grep -q "^${name}$"; then echo " Backing up $name to ${BACKUP_DIR}/${archive_name}..." docker run --rm \ -v "${name}":/source:ro \ -v "${BACKUP_DIR}":/backup \ alpine:3 tar czf "/backup/${archive_name}" -C /source . echo " ✓ $name exported" else echo " ⚠ Volume $name not found, skipping" fi done restart_containers stopped echo "" echo "Backup complete."