a1a17e81a1
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.
399 lines
14 KiB
Bash
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 "$@"
|