Files
linux-scripts/caprover-backup.sh
T
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

399 lines
14 KiB
Bash

#!/usr/bin/env bash
# caprover-backup.sh — Comprehensive CapRover backup script
# Author: Phil Connor <contact@mylinux.work>
# License: MIT
# Version: 1.11
#
# Backs up /captain config, Docker volumes (captain-- prefixed),
# and app definitions via CapRover API.
# Supports local, NFS, and S3 (via aws cli or rclone) destinations.
#
# Migration mode (--migrate) stops all CapRover app containers before
# backing up volumes, ensuring database-consistent snapshots. Produces
# a single migration tarball for transfer to a new server.
#
# Usage:
# ./caprover-backup.sh # local backup to /backups/caprover
# ./caprover-backup.sh --migrate # full server migration (stops containers)
# BACKUP_DEST=s3 S3_BUCKET=my-bucket ./caprover-backup.sh
# BACKUP_DEST=rclone RCLONE_REMOTE=myremote:backups ./caprover-backup.sh
# BACKUP_DEST=nfs NFS_MOUNT=/mnt/nfs/backups ./caprover-backup.sh
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration — override via environment variables
# ---------------------------------------------------------------------------
BACKUP_DIR="${BACKUP_DIR:-/backups/caprover}"
BACKUP_DEST="${BACKUP_DEST:-local}" # local | nfs | s3 | rclone
RETENTION_DAYS="${RETENTION_DAYS:-30}"
DATE=$(date +%Y%m%d-%H%M%S)
LOG_FILE="${LOG_FILE:-/var/log/caprover-backup.log}"
MIGRATE=false
# CapRover API settings (for app definition export)
CAPROVER_URL="${CAPROVER_URL:-https://captain.apps.example.com}"
CAPROVER_PASSWORD="${CAPROVER_PASSWORD:-}"
# S3 settings
S3_BUCKET="${S3_BUCKET:-}"
S3_PREFIX="${S3_PREFIX:-caprover-backups}"
# rclone settings
RCLONE_REMOTE="${RCLONE_REMOTE:-}"
# NFS settings
NFS_MOUNT="${NFS_MOUNT:-/mnt/nfs/backups}"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log() {
local msg
msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
echo "$msg" | tee -a "$LOG_FILE"
}
log_error() {
log "ERROR: $1"
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
for arg in "$@"; do
case "$arg" in
--migrate) MIGRATE=true ;;
-h|--help)
sed -n '2,/^$/{ s/^# \?//; p }' "$0"
exit 0
;;
esac
done
# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
preflight() {
if [ "$(id -u)" -ne 0 ]; then
log_error "Run as root."
exit 1
fi
if ! command -v docker &>/dev/null; then
log_error "Docker not found."
exit 1
fi
if [ "$BACKUP_DEST" = "s3" ] && ! command -v aws &>/dev/null; then
log_error "aws CLI not found. Install it or use BACKUP_DEST=rclone."
exit 1
fi
if [ "$BACKUP_DEST" = "rclone" ] && ! command -v rclone &>/dev/null; then
log_error "rclone not found."
exit 1
fi
mkdir -p "$BACKUP_DIR"
mkdir -p "$(dirname "$LOG_FILE")"
}
# ---------------------------------------------------------------------------
# Backup /captain directory
# ---------------------------------------------------------------------------
backup_captain_config() {
log "Backing up /captain directory..."
local dest="${BACKUP_DIR}/captain-config-${DATE}.tar.gz"
if [ ! -d /captain ]; then
log_error "/captain directory not found. Is CapRover installed?"
return 1
fi
tar czf "$dest" -C / captain
log "Captain config saved: $dest ($(du -sh "$dest" | cut -f1))"
}
# ---------------------------------------------------------------------------
# Backup Docker volumes (captain-- prefixed)
# ---------------------------------------------------------------------------
backup_volumes() {
log "Backing up Docker volumes..."
local volumes
volumes=$(docker volume ls -q | grep "^captain--" || true)
if [ -z "$volumes" ]; then
log "No captain-- volumes found. Skipping."
return 0
fi
for vol in $volumes; do
local app_name="${vol#captain--}"
local dest="${BACKUP_DIR}/vol-${app_name}-${DATE}.tar.gz"
log " Backing up volume: $vol"
docker run --rm \
-v "${vol}:/source:ro" \
-v "${BACKUP_DIR}:/backup" \
alpine tar czf "/backup/vol-${app_name}-${DATE}.tar.gz" -C /source .
log " Volume $vol saved: $dest ($(du -sh "$dest" | cut -f1))"
done
}
# ---------------------------------------------------------------------------
# Export app definitions via CapRover API
# ---------------------------------------------------------------------------
export_app_definitions() {
if [ -z "$CAPROVER_PASSWORD" ]; then
log "CAPROVER_PASSWORD not set. Skipping API export."
return 0
fi
log "Exporting app definitions via CapRover API..."
# Get auth token
local token
token=$(curl -s -X POST "${CAPROVER_URL}/api/v2/login" \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-d "{\"password\":\"${CAPROVER_PASSWORD}\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])" 2>/dev/null) || true
if [ -z "$token" ]; then
log_error "Failed to authenticate with CapRover API."
return 1
fi
# Export app definitions
local dest="${BACKUP_DIR}/app-definitions-${DATE}.json"
local http_code
http_code=$(curl -s -o "$dest" -w "%{http_code}" \
"${CAPROVER_URL}/api/v2/user/apps/appDefinitions" \
-H "Content-Type: application/json" \
-H "x-namespace: captain" \
-H "x-captain-auth: ${token}")
if [ "$http_code" = "200" ]; then
log "App definitions saved: $dest"
else
log_error "API returned HTTP $http_code. App definitions export failed."
rm -f "$dest"
return 1
fi
}
# ---------------------------------------------------------------------------
# Upload to remote destination
# ---------------------------------------------------------------------------
upload_remote() {
case "$BACKUP_DEST" in
local)
log "Backup stored locally at $BACKUP_DIR"
;;
nfs)
log "Copying backups to NFS mount: $NFS_MOUNT"
if ! mountpoint -q "$NFS_MOUNT" 2>/dev/null; then
log_error "$NFS_MOUNT is not mounted."
return 1
fi
mkdir -p "${NFS_MOUNT}/caprover"
cp "${BACKUP_DIR}"/*-"${DATE}"* "${NFS_MOUNT}/caprover/"
log "Copied to NFS."
;;
s3)
log "Uploading backups to S3: s3://${S3_BUCKET}/${S3_PREFIX}/"
for f in "${BACKUP_DIR}"/*-"${DATE}"*; do
aws s3 cp "$f" "s3://${S3_BUCKET}/${S3_PREFIX}/$(basename "$f")" --quiet
done
log "S3 upload complete."
;;
rclone)
log "Uploading backups via rclone to: $RCLONE_REMOTE"
for f in "${BACKUP_DIR}"/*-"${DATE}"*; do
rclone copy "$f" "$RCLONE_REMOTE" --quiet
done
log "rclone upload complete."
;;
*)
log_error "Unknown BACKUP_DEST: $BACKUP_DEST"
return 1
;;
esac
}
# ---------------------------------------------------------------------------
# Retention — delete local backups older than RETENTION_DAYS
# ---------------------------------------------------------------------------
apply_retention() {
log "Applying retention policy: deleting backups older than ${RETENTION_DAYS} days..."
local count
count=$(find "$BACKUP_DIR" -name "*.tar.gz" -o -name "*.json" | \
xargs -I{} find {} -mtime +"$RETENTION_DAYS" 2>/dev/null | wc -l)
find "$BACKUP_DIR" \( -name "*.tar.gz" -o -name "*.json" \) \
-mtime +"$RETENTION_DAYS" -delete
log "Removed $count old backup file(s)."
}
# ---------------------------------------------------------------------------
# Migration — stop all CapRover app containers
# ---------------------------------------------------------------------------
stop_captain_containers() {
log "Stopping all CapRover app containers..."
local containers
containers=$(docker ps -q --filter "label=com.docker.swarm.service.name" \
--filter "name=srv-captain--" 2>/dev/null || true)
if [ -z "$containers" ]; then
# fallback: stop services via Docker Swarm
local services
services=$(docker service ls -q --filter "name=srv-captain--" 2>/dev/null || true)
if [ -n "$services" ]; then
local count=0
for svc in $services; do
local svc_name
svc_name=$(docker service inspect --format '{{.Spec.Name}}' "$svc")
log " Scaling down: $svc_name"
docker service scale "$svc_name=0" --detach 2>/dev/null
((count++)) || true
done
log "Scaled down $count service(s). Waiting 10s for graceful shutdown..."
sleep 10
else
log "No CapRover app services found."
fi
else
local count
count=$(echo "$containers" | wc -w)
docker stop $containers
log "Stopped $count container(s). Waiting 5s..."
sleep 5
fi
}
# ---------------------------------------------------------------------------
# Migration — record service state for restore on new server
# ---------------------------------------------------------------------------
save_service_state() {
log "Saving Docker service state..."
local dest="${BACKUP_DIR}/service-state-${DATE}.json"
docker service ls --format '{{json .}}' > "$dest"
log "Service state saved: $dest"
# Save docker info for reference (Swarm tokens, node info)
local info_dest="${BACKUP_DIR}/docker-info-${DATE}.txt"
docker info > "$info_dest" 2>&1
docker node ls >> "$info_dest" 2>/dev/null || true
log "Docker info saved: $info_dest"
}
# ---------------------------------------------------------------------------
# Migration — package everything into a single tarball
# ---------------------------------------------------------------------------
create_migration_bundle() {
local bundle="${BACKUP_DIR}/caprover-migration-${DATE}.tar.gz"
log "Creating migration bundle..."
# Collect all files from this run
tar czf "$bundle" -C "$BACKUP_DIR" \
$(ls -1 "$BACKUP_DIR" | grep "$DATE" | grep -v "caprover-migration")
local size
size=$(du -sh "$bundle" | cut -f1)
log "Migration bundle ready: $bundle ($size)"
log ""
log "========================================="
log " MIGRATION INSTRUCTIONS"
log "========================================="
log "1. Copy bundle to new server:"
log " scp $bundle root@new-server:/backups/"
log ""
log "2. On the new server, install CapRover:"
log " docker run -p 80:80 -p 443:443 -p 3000:3000 \\"
log " -e ACCEPTED_TERMS=true -v /captain:/captain \\"
log " caprover/caprover-edge"
log ""
log "3. Extract the bundle:"
log " mkdir -p /backups/restore && cd /backups/restore"
log " tar xzf caprover-migration-${DATE}.tar.gz"
log ""
log "4. Stop CapRover on new server:"
log " docker service rm captain-captain --force"
log ""
log "5. Restore /captain config:"
log " tar xzf captain-config-${DATE}.tar.gz -C /"
log ""
log "6. Restore Docker volumes:"
log " for f in vol-*-${DATE}.tar.gz; do"
log ' vol="captain--${f#vol-}"'
log ' vol="${vol%-'"${DATE}"'.tar.gz}"'
log " docker volume create \"\$vol\""
log " docker run --rm -v \"\${vol}:/dest\" -v \"\$(pwd):/backup:ro\" \\"
log " alpine sh -c \"tar xzf /backup/\$f -C /dest\""
log " done"
log ""
log "7. Start CapRover and re-deploy apps:"
log " docker run -p 80:80 -p 443:443 -p 3000:3000 \\"
log " -e ACCEPTED_TERMS=true -v /captain:/captain \\"
log " caprover/caprover-edge"
log ""
log "8. Update DNS to point to the new server IP."
log "========================================="
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
if $MIGRATE; then
log "========================================="
log "CapRover MIGRATION backup — ${DATE}"
log "Mode: full migration (containers will be stopped)"
log "========================================="
preflight
local errors=0
export_app_definitions || { ((errors++)) || true; }
save_service_state || { ((errors++)) || true; }
stop_captain_containers
backup_captain_config || { ((errors++)) || true; }
backup_volumes || { ((errors++)) || true; }
create_migration_bundle
if [ "$errors" -gt 0 ]; then
log "Migration backup completed with $errors error(s)."
exit 1
else
log "Migration backup completed successfully."
log "Containers remain stopped. This server is ready to decommission."
fi
else
log "========================================="
log "CapRover backup started — ${DATE}"
log "Destination: ${BACKUP_DEST}"
log "========================================="
preflight
local errors=0
backup_captain_config || { ((errors++)) || true; }
backup_volumes || { ((errors++)) || true; }
export_app_definitions || { ((errors++)) || true; }
upload_remote || { ((errors++)) || true; }
apply_retention
if [ "$errors" -gt 0 ]; then
log "Backup completed with $errors error(s)."
exit 1
else
log "Backup completed successfully."
fi
fi
}
main "$@"