mirror of
https://github.com/IBICO74/nextcloud-backup.git
synced 2026-07-03 02:57:05 +00:00
Initial commit: Nextcloud backup system with rclone
Automated backup solution for Dockerized Nextcloud installations. Features: - Secure backup-user approach (no sudo required) - Hybrid backup strategy (daily metadata + weekly/yearly full) - rclone support for 40+ cloud providers - Configurable retention policies - Docker-aware with maintenance mode support Includes: - nextcloud-backup.sh: Main backup script (v2, recommended) - nextcloud-backup-v1.sh: Legacy sudo version (reference only) - README.md: Complete documentation - nextcloud-backup.cron: Cron schedule example
This commit is contained in:
commit
19f6a04b51
5 changed files with 848 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Sensitive files
|
||||
*.env
|
||||
*.conf
|
||||
rclone.conf
|
||||
*.log
|
||||
*.txt
|
||||
secrets/
|
||||
passwords.txt
|
||||
*.key
|
||||
*.pem
|
||||
.env.*
|
||||
|
||||
# Backup files
|
||||
*.tar.gz
|
||||
*.sql
|
||||
*.sql.gz
|
||||
backups/
|
||||
logs/
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*~
|
||||
.*.swp
|
||||
156
README.md
Normal file
156
README.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# Nextcloud Backup System
|
||||
|
||||
Automated Nextcloud backup solution using rclone to cloud storage (Jottacloud, Dropbox, Google Drive, etc).
|
||||
|
||||
## Features
|
||||
|
||||
- 🔒 **Secure**: No sudo required, runs as dedicated backup user
|
||||
- 📦 **Hybrid Strategy**: Daily metadata backups + weekly/yearly full backups
|
||||
- 🔄 **Automated**: Runs via cron
|
||||
- ☁️ **Cloud Storage**: Uses rclone (supports 40+ cloud providers)
|
||||
- 🗜️ **Compression**: Automatic tar.gz compression
|
||||
- 📊 **Retention**: Configurable retention policies
|
||||
- 🐳 **Docker Support**: Works with Dockerized Nextcloud
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Ubuntu/Debian Linux server
|
||||
- Docker-based Nextcloud installation
|
||||
- rclone installed and configured
|
||||
- Dedicated backup user with proper group permissions
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Create backup user**:
|
||||
```bash
|
||||
sudo adduser --system --group --home /opt/backup backup-user
|
||||
sudo usermod -aG www-data,docker backup-user
|
||||
```
|
||||
|
||||
2. **Create directory structure**:
|
||||
```bash
|
||||
sudo mkdir -p /opt/backup/{bin,logs,backups}
|
||||
sudo chown -R backup-user:backup-user /opt/backup
|
||||
```
|
||||
|
||||
3. **Configure rclone** (as backup-user):
|
||||
```bash
|
||||
sudo -u backup-user rclone config
|
||||
```
|
||||
|
||||
4. **Install backup script**:
|
||||
```bash
|
||||
sudo cp nextcloud-backup.sh /opt/backup/bin/
|
||||
sudo chmod +x /opt/backup/bin/nextcloud-backup.sh
|
||||
```
|
||||
|
||||
5. **Edit configuration** in script:
|
||||
```bash
|
||||
RCLONE_REMOTE="your-remote:YourPath/Nextcloud"
|
||||
```
|
||||
|
||||
6. **Set read-only permissions** on Nextcloud data:
|
||||
```bash
|
||||
sudo chmod 750 /path/to/nextcloud/data
|
||||
```
|
||||
|
||||
7. **Test backup**:
|
||||
```bash
|
||||
sudo -u backup-user /opt/backup/bin/nextcloud-backup.sh
|
||||
```
|
||||
|
||||
8. **Schedule with cron**:
|
||||
```bash
|
||||
sudo cp nextcloud-backup.cron /etc/cron.d/nextcloud-backup
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit these variables in `nextcloud-backup.sh`:
|
||||
|
||||
```bash
|
||||
# Rclone remote (change to your remote name)
|
||||
RCLONE_REMOTE="jottacloud:ServerBackup/Nextcloud"
|
||||
|
||||
# Docker container name
|
||||
NEXTCLOUD_CONTAINER="nextcloud-app"
|
||||
|
||||
# Retention policy
|
||||
DAILY_RETENTION=10 # Keep 10 daily backups
|
||||
WEEKLY_RETENTION=10 # Keep 10 weekly backups
|
||||
YEARLY_RETENTION=10 # Keep 10 yearly backups
|
||||
```
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
- **Daily** (03:15): Metadata only (database, config, apps) - ~100-200MB
|
||||
- **Weekly** (Sunday): Complete backup including user data - several GB
|
||||
- **Yearly** (January 1st): Full archive for long-term storage
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
✅ **Recommended**: Use dedicated backup user (no sudo)
|
||||
- Read-only access to Nextcloud data
|
||||
- Limited permissions
|
||||
- Group-based access (www-data, docker)
|
||||
|
||||
❌ **Not recommended**: Using sudo
|
||||
- Excessive privileges
|
||||
- Security risk
|
||||
- Audit trail issues
|
||||
|
||||
See `nextcloud-backup-v1.sh` for legacy sudo-based approach (not recommended for new installations).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Denied
|
||||
```bash
|
||||
# Verify backup-user groups
|
||||
id backup-user
|
||||
|
||||
# Should show: www-data, docker
|
||||
|
||||
# Check data directory permissions
|
||||
ls -ld /path/to/nextcloud/data
|
||||
# Should be: drwxr-x--- (750)
|
||||
```
|
||||
|
||||
### Rclone Authentication Failed
|
||||
```bash
|
||||
# Reconfigure as backup-user
|
||||
sudo -u backup-user rclone config reconnect your-remote:
|
||||
```
|
||||
|
||||
### Docker Command Not Found
|
||||
```bash
|
||||
# Add backup-user to docker group
|
||||
sudo usermod -aG docker backup-user
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `nextcloud-backup.sh` - Main backup script (v2, no sudo)
|
||||
- `nextcloud-backup-v1.sh` - Legacy version with sudo (reference only)
|
||||
- `nextcloud-backup.cron` - Cron schedule example
|
||||
- `README.md` - This file
|
||||
|
||||
## Requirements
|
||||
|
||||
- **rclone** - Cloud storage sync tool
|
||||
- **Docker** - For Nextcloud containers
|
||||
- **tar** - Archive creation
|
||||
- **gzip** - Compression
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Feel free to use and modify
|
||||
|
||||
## Author
|
||||
|
||||
Created by IBICO74
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, please open a GitHub issue.
|
||||
237
nextcloud-backup-v1.sh
Executable file
237
nextcloud-backup-v1.sh
Executable file
|
|
@ -0,0 +1,237 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_ROOT="/mnt/data/backups/nextcloud-daily"
|
||||
REMOTE_BASE="jottacloud:ServerBackup/Nextcloud"
|
||||
ENV_FILE="/mnt/data/docker-stacks/nextcloud-stack/.env"
|
||||
DATA_ROOT="/mnt/data/docker/volumes/nextcloud-stack_nextcloud-data/_data"
|
||||
RCLONE_CONFIG="/home/kau005/.config/rclone/rclone.conf"
|
||||
DB_CONTAINER="nextcloud-db"
|
||||
APP_CONTAINER="nextcloud-app"
|
||||
TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
WORKDIR="${BACKUP_ROOT}/${TIMESTAMP}"
|
||||
LOG_DIR="/var/log/nextcloud-backup"
|
||||
LOG_FILE="${LOG_DIR}/${TIMESTAMP}.log"
|
||||
RETENTION_DAYS=10
|
||||
RETENTION_WEEKS=10
|
||||
RETENTION_YEARS=10
|
||||
MAINTENANCE_SET=0
|
||||
sudo chown kau005:kau005 "$WORKDIR" "$LOG_DIR" 2>/dev/null || true
|
||||
sudo chown kau005:kau005 "$WORKDIR" "$LOG_DIR" 2>/dev/null || true
|
||||
mkdir -p "$WORKDIR" "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
require_file() {
|
||||
local path="$1"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
log "ERROR: Finner ikke nødvendig fil: $path"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_dir() {
|
||||
local path="$1"
|
||||
if ! sudo test -d "$path"; then
|
||||
log "ERROR: Finner ikke nødvendig katalog: $path"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ $MAINTENANCE_SET -eq 1 ]]; then
|
||||
log "Deaktiverer maintenance mode"
|
||||
if ! docker exec -u www-data "$APP_CONTAINER" php occ maintenance:mode --off >>"$LOG_FILE" 2>&1; then
|
||||
log "ADVARSEL: klarte ikke å slå av maintenance mode"
|
||||
else
|
||||
MAINTENANCE_SET=0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
require_file "$ENV_FILE"
|
||||
require_dir "$DATA_ROOT"
|
||||
require_file "$RCLONE_CONFIG"
|
||||
|
||||
MYSQL_USER="$(awk -F= '/^MYSQL_USER=/{print $2; exit}' "$ENV_FILE")"
|
||||
MYSQL_PASSWORD="$(awk -F= '/^MYSQL_PASSWORD=/{print $2; exit}' "$ENV_FILE")"
|
||||
MYSQL_DATABASE="$(awk -F= '/^MYSQL_DATABASE=/{print $2; exit}' "$ENV_FILE")"
|
||||
|
||||
: "${MYSQL_USER:=nextcloud}"
|
||||
: "${MYSQL_PASSWORD:?Fant ikke MYSQL_PASSWORD i $ENV_FILE}"
|
||||
: "${MYSQL_DATABASE:=nextcloud}"
|
||||
|
||||
export RCLONE_CONFIG
|
||||
|
||||
log "Starter Nextcloud-backup ${TIMESTAMP}"
|
||||
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "$APP_CONTAINER"; then
|
||||
log "ERROR: Finner ikke applikasjonscontaineren '$APP_CONTAINER'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Aktiverer maintenance mode"
|
||||
if docker exec -u www-data "$APP_CONTAINER" php occ maintenance:mode --on >>"$LOG_FILE" 2>&1; then
|
||||
MAINTENANCE_SET=1
|
||||
else
|
||||
log "ERROR: Klarte ikke å slå på maintenance mode"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_DUMP="${WORKDIR}/nextcloud-db-${TIMESTAMP}.sql.gz"
|
||||
log " > Tar database-dump til ${DB_DUMP}"
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx "$DB_CONTAINER"; then
|
||||
log "ERROR: Finner ikke databasecontaineren '$DB_CONTAINER'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker exec "$DB_CONTAINER" bash -c "MYSQL_PWD='$MYSQL_PASSWORD' mysqldump --single-transaction --quick --lock-tables=false -u '$MYSQL_USER' '$MYSQL_DATABASE'" | gzip >"$DB_DUMP"; then
|
||||
log "ERROR: Klarte ikke å hente database-dump"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONFIG_ARCHIVE="${WORKDIR}/nextcloud-config-${TIMESTAMP}.tar.gz"
|
||||
log " > Arkiverer konfigurasjon til ${CONFIG_ARCHIVE}"
|
||||
(
|
||||
cd "$DATA_ROOT"
|
||||
tar -czf "$CONFIG_ARCHIVE" config custom_apps themes 2>/dev/null || true
|
||||
)
|
||||
|
||||
STACK_DIR="/mnt/data/docker-stacks/nextcloud-stack"
|
||||
if [[ -d "$STACK_DIR" ]]; then
|
||||
log " > Kopierer docker-stack-filer"
|
||||
cp -p "$STACK_DIR"/.env "$WORKDIR/stack.env"
|
||||
cp -p "$STACK_DIR"/docker-compose.yml "$WORKDIR/docker-compose.yml" || true
|
||||
fi
|
||||
|
||||
echo "database_dump=${DB_DUMP##*/}" >"${WORKDIR}/manifest.txt"
|
||||
echo "config_archive=${CONFIG_ARCHIVE##*/}" >>"${WORKDIR}/manifest.txt"
|
||||
|
||||
log "Laster opp metadata til Jottacloud"
|
||||
if ! rclone copy "$WORKDIR" "${REMOTE_BASE}/daily/${TIMESTAMP}" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
|
||||
log "ERROR: rclone copy for metadata feilet"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ukentlig data-backup (synkroniseres hver dag, ny uke starter på mandag)
|
||||
DAY_OF_WEEK=$(date +%u)
|
||||
WEEK_STAMP="$(date +%Y-W%V)" # Norsk ukenummer (ISO 8601)
|
||||
WEEKLY_REMOTE="${REMOTE_BASE}/weekly/${WEEK_STAMP}"
|
||||
|
||||
if [[ "$DAY_OF_WEEK" == "1" ]]; then
|
||||
# Mandag: Start ny uke med full sync (ny referanse)
|
||||
log "=== UKENTLIG BACKUP - NY UKE (full sync) ==="
|
||||
log " > Laster opp metadata til ${WEEK_STAMP}"
|
||||
if ! rclone copy "$WORKDIR" "${WEEKLY_REMOTE}/metadata/" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
|
||||
log "ADVARSEL: Metadata upload til ukentlig backup feilet"
|
||||
fi
|
||||
|
||||
log " > Starter full sync av data-mappe til ${WEEK_STAMP}/data/ (dette tar tid)..."
|
||||
if sudo rclone sync "${DATA_ROOT}/data" "${WEEKLY_REMOTE}/data/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--progress \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--log-level INFO \
|
||||
--stats 2m >>"$LOG_FILE" 2>&1; then
|
||||
log " > Full data-sync fullført for uke ${WEEK_STAMP}"
|
||||
else
|
||||
log "ADVARSEL: Data-sync feilet"
|
||||
fi
|
||||
else
|
||||
# Tirsdag-søndag: Inkrementell oppdatering (kun nye/endrede filer)
|
||||
log "=== DAGLIG DATA-BACKUP (inkrementell) ==="
|
||||
log " > Oppdaterer data for uke ${WEEK_STAMP} (kun endringer)..."
|
||||
if sudo rclone copy "${DATA_ROOT}/data" "${WEEKLY_REMOTE}/data/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--update \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--log-level INFO \
|
||||
--stats 1m >>"$LOG_FILE" 2>&1; then
|
||||
log " > Inkrementell data-backup fullført"
|
||||
else
|
||||
log "ADVARSEL: Inkrementell data-backup feilet"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Årlig backup (1. januar) - FULL DATA SNAPSHOT
|
||||
MONTH_DAY=$(date +%m-%d)
|
||||
if [[ "$MONTH_DAY" == "01-01" ]]; then
|
||||
log "=== ÅRLIG BACKUP (full snapshot) ==="
|
||||
YEAR_STAMP="$(date +%Y)"
|
||||
YEARLY_REMOTE="${REMOTE_BASE}/yearly/${YEAR_STAMP}"
|
||||
|
||||
log " > Laster opp metadata til ${YEAR_STAMP}"
|
||||
if ! rclone copy "$WORKDIR" "${YEARLY_REMOTE}/metadata/" --log-level INFO --stats 30s >>"$LOG_FILE" 2>&1; then
|
||||
log "ADVARSEL: Metadata upload til årlig backup feilet"
|
||||
fi
|
||||
|
||||
log " > Starter full sync av data-mappe til ${YEAR_STAMP}/data/ (dette tar tid)..."
|
||||
if sudo rclone sync "${DATA_ROOT}/data" "${YEARLY_REMOTE}/data/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--progress \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--log-level INFO \
|
||||
--stats 2m >>"$LOG_FILE" 2>&1; then
|
||||
log " > Årlig data-snapshot fullført for ${YEAR_STAMP}"
|
||||
else
|
||||
log "ADVARSEL: Årlig data-snapshot feilet"
|
||||
fi
|
||||
fi
|
||||
log "Roterer backups i Jottacloud"
|
||||
|
||||
# Daglige backups (behold 10 nyeste)
|
||||
DAILY_DIRS=$(rclone lsf "${REMOTE_BASE}/daily/" --dirs-only --max-age 365d 2>>"$LOG_FILE" | sort -r || true)
|
||||
if [[ -n "$DAILY_DIRS" ]]; then
|
||||
DAILY_COUNT=$(echo "$DAILY_DIRS" | wc -l)
|
||||
if [[ $DAILY_COUNT -gt $RETENTION_DAYS ]]; then
|
||||
log " > Sletter $((DAILY_COUNT - RETENTION_DAYS)) gamle daglige backups"
|
||||
echo "$DAILY_DIRS" | tail -n +$((RETENTION_DAYS + 1)) | while read -r dir; do
|
||||
if [[ -n "$dir" ]]; then
|
||||
log " - Sletter daily/${dir}"
|
||||
rclone purge "${REMOTE_BASE}/daily/${dir}" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ukentlige backups (behold 10 nyeste)
|
||||
WEEKLY_DIRS=$(rclone lsf "${REMOTE_BASE}/weekly/" --dirs-only --max-age 3650d 2>>"$LOG_FILE" | sort -r || true)
|
||||
if [[ -n "$WEEKLY_DIRS" ]]; then
|
||||
WEEKLY_COUNT=$(echo "$WEEKLY_DIRS" | wc -l)
|
||||
if [[ $WEEKLY_COUNT -gt $RETENTION_WEEKS ]]; then
|
||||
log " > Sletter $((WEEKLY_COUNT - RETENTION_WEEKS)) gamle ukentlige backups"
|
||||
echo "$WEEKLY_DIRS" | tail -n +$((RETENTION_WEEKS + 1)) | while read -r dir; do
|
||||
if [[ -n "$dir" ]]; then
|
||||
log " - Sletter weekly/${dir}"
|
||||
rclone purge "${REMOTE_BASE}/weekly/${dir}" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Årlige backups (behold 10 nyeste)
|
||||
YEARLY_DIRS=$(rclone lsf "${REMOTE_BASE}/yearly/" --dirs-only 2>>"$LOG_FILE" | sort -r || true)
|
||||
if [[ -n "$YEARLY_DIRS" ]]; then
|
||||
YEARLY_COUNT=$(echo "$YEARLY_DIRS" | wc -l)
|
||||
if [[ $YEARLY_COUNT -gt $RETENTION_YEARS ]]; then
|
||||
log " > Sletter $((YEARLY_COUNT - RETENTION_YEARS)) gamle årlige backups"
|
||||
echo "$YEARLY_DIRS" | tail -n +$((RETENTION_YEARS + 1)) | while read -r dir; do
|
||||
if [[ -n "$dir" ]]; then
|
||||
log " - Sletter yearly/${dir}"
|
||||
rclone purge "${REMOTE_BASE}/yearly/${dir}" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Opprydning av lokale staging-kataloger eldre enn ${RETENTION_DAYS} dager"
|
||||
sudo find "$BACKUP_ROOT" -mindepth 1 -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" -exec rm -rf {} + || true
|
||||
|
||||
log "Backup fullført"
|
||||
8
nextcloud-backup.cron
Normal file
8
nextcloud-backup.cron
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Nextcloud Backup - Cron Schedule
|
||||
# Runs daily at 03:15 as backup-user
|
||||
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
|
||||
# Daily backup at 03:15
|
||||
15 3 * * * backup-user /opt/backup/bin/nextcloud-backup.sh >> /opt/backup/logs/cron.log 2>&1
|
||||
423
nextcloud-backup.sh
Executable file
423
nextcloud-backup.sh
Executable file
|
|
@ -0,0 +1,423 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Nextcloud Backup Script v2.0 - UTEN SUDO
|
||||
# Kjører som: backup-user (medlem av www-data og docker grupper)
|
||||
#
|
||||
# Backup-strategi:
|
||||
# - Daglig: Metadata (database + config + docker-filer)
|
||||
# - Ukentlig (mandag): Full sync av data
|
||||
# - Daglig (tirsdag-søndag): Inkrementell copy av endrede filer
|
||||
# - Årlig (1. januar): Full sync til yearly/
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================================
|
||||
# KONFIGURASJON
|
||||
# ============================================================================
|
||||
|
||||
# Stier
|
||||
NEXTCLOUD_DOCKER_ROOT="/mnt/data/docker-stacks/nextcloud-stack"
|
||||
NEXTCLOUD_DATA_ROOT="/mnt/data/docker/volumes/nextcloud-stack_nextcloud-data/_data"
|
||||
BACKUP_ROOT="/opt/backup"
|
||||
LOCAL_BACKUP_DIR="$BACKUP_ROOT/backups"
|
||||
LOG_DIR="$BACKUP_ROOT/logs"
|
||||
RCLONE_CONFIG="/opt/backup/.config/rclone/rclone.conf"
|
||||
|
||||
# Docker containers
|
||||
DB_CONTAINER="nextcloud-db"
|
||||
APP_CONTAINER="nextcloud-app"
|
||||
|
||||
# Rclone remote
|
||||
RCLONE_REMOTE="jottacloudbackup:ServerBackup/Nextcloud"
|
||||
|
||||
# Retention (antall backups å beholde)
|
||||
DAILY_RETENTION=10
|
||||
WEEKLY_RETENTION=10
|
||||
YEARLY_RETENTION=10
|
||||
|
||||
# ============================================================================
|
||||
# LOGGING
|
||||
# ============================================================================
|
||||
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
LOG_FILE="$LOG_DIR/backup-$TIMESTAMP.log"
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] ERROR: $*" | tee -a "$LOG_FILE" >&2
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# ERROR HANDLING
|
||||
# ============================================================================
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
log_error "Backup failed with exit code: $exit_code"
|
||||
else
|
||||
log "✅ Backup completed successfully"
|
||||
fi
|
||||
exit $exit_code
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# ============================================================================
|
||||
# ENVIRONMENT VALIDATION
|
||||
# ============================================================================
|
||||
|
||||
log "========================================="
|
||||
log "🚀 Starting Nextcloud Backup v2.0"
|
||||
log "========================================="
|
||||
log "User: $(whoami)"
|
||||
log "Groups: $(groups)"
|
||||
log "Timestamp: $TIMESTAMP"
|
||||
log ""
|
||||
|
||||
# Check required commands
|
||||
for cmd in docker rclone tar gzip; do
|
||||
if ! command -v $cmd &> /dev/null; then
|
||||
log_error "Required command not found: $cmd"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify we're running as backup-user
|
||||
if [ "$(whoami)" != "backup-user" ]; then
|
||||
log_error "This script must run as backup-user"
|
||||
log_error "Current user: $(whoami)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify group membership
|
||||
if ! groups | grep -q www-data; then
|
||||
log_error "backup-user is not in www-data group"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! groups | grep -q docker; then
|
||||
log_error "backup-user is not in docker group"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify rclone config exists
|
||||
if [ ! -f "$RCLONE_CONFIG" ]; then
|
||||
log_error "rclone config not found: $RCLONE_CONFIG"
|
||||
log_error "Run: sudo -u backup-user rclone config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test rclone connection
|
||||
log "Testing rclone connection..."
|
||||
if ! rclone about "$RCLONE_REMOTE" --config "$RCLONE_CONFIG" &> /dev/null; then
|
||||
log_error "rclone connection test failed"
|
||||
exit 1
|
||||
fi
|
||||
log "✅ rclone connection OK"
|
||||
|
||||
# ============================================================================
|
||||
# DETERMINE BACKUP TYPE
|
||||
# ============================================================================
|
||||
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
DAY_OF_WEEK=$(date +%u) # 1 = Monday, 7 = Sunday
|
||||
MONTH_DAY=$(date +%m-%d)
|
||||
WEEK_NUMBER=$(date +%Y-W%V)
|
||||
YEAR=$(date +%Y)
|
||||
|
||||
# Determine if this is a weekly backup day (Monday)
|
||||
IS_WEEKLY_BACKUP=false
|
||||
if [ "$DAY_OF_WEEK" = "1" ]; then
|
||||
IS_WEEKLY_BACKUP=true
|
||||
log "📅 Weekly backup day (Monday)"
|
||||
fi
|
||||
|
||||
# Determine if this is yearly backup day (January 1st)
|
||||
IS_YEARLY_BACKUP=false
|
||||
if [ "$MONTH_DAY" = "01-01" ]; then
|
||||
IS_YEARLY_BACKUP=true
|
||||
log "📅 Yearly backup day (January 1st)"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# METADATA BACKUP (DAILY)
|
||||
# ============================================================================
|
||||
|
||||
log ""
|
||||
log "========================================="
|
||||
log "📦 METADATA BACKUP"
|
||||
log "========================================="
|
||||
|
||||
DAILY_BACKUP_PATH="$RCLONE_REMOTE/daily/$TIMESTAMP"
|
||||
METADATA_DIR="$LOCAL_BACKUP_DIR/metadata-$TIMESTAMP"
|
||||
mkdir -p "$METADATA_DIR"
|
||||
|
||||
# 1. Database backup
|
||||
log "Backing up database..."
|
||||
DB_FILE="$METADATA_DIR/database.sql"
|
||||
if docker exec "$DB_CONTAINER" mysqldump \
|
||||
-u root \
|
||||
-p"$(grep MYSQL_ROOT_PASSWORD "$NEXTCLOUD_DOCKER_ROOT/.env" | cut -d= -f2)" \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--lock-tables=false \
|
||||
nextcloud > "$DB_FILE" 2>> "$LOG_FILE"; then
|
||||
|
||||
gzip "$DB_FILE"
|
||||
DB_SIZE=$(du -h "$DB_FILE.gz" | cut -f1)
|
||||
log "✅ Database backup: $DB_SIZE"
|
||||
else
|
||||
log_error "Database backup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Config backup
|
||||
log "Backing up config..."
|
||||
CONFIG_TAR="$METADATA_DIR/config.tar.gz"
|
||||
if tar -czf "$CONFIG_TAR" -C "$NEXTCLOUD_DATA_ROOT" config/ 2>> "$LOG_FILE"; then
|
||||
CONFIG_SIZE=$(du -h "$CONFIG_TAR" | cut -f1)
|
||||
log "✅ Config backup: $CONFIG_SIZE"
|
||||
else
|
||||
log_error "Config backup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Docker stack files backup
|
||||
log "Backing up Docker stack files..."
|
||||
DOCKER_TAR="$METADATA_DIR/docker-stack.tar.gz"
|
||||
if tar -czf "$DOCKER_TAR" \
|
||||
-C "$NEXTCLOUD_DOCKER_ROOT" \
|
||||
docker-compose.yml \
|
||||
.env 2>> "$LOG_FILE"; then
|
||||
|
||||
DOCKER_SIZE=$(du -h "$DOCKER_TAR" | cut -f1)
|
||||
log "✅ Docker stack backup: $DOCKER_SIZE"
|
||||
else
|
||||
log_error "Docker stack backup failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Upload metadata to Jottacloud
|
||||
log "Uploading metadata to Jottacloud..."
|
||||
if rclone copy "$METADATA_DIR" "$DAILY_BACKUP_PATH/metadata/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" \
|
||||
--log-level INFO \
|
||||
--stats 1m \
|
||||
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
||||
|
||||
log "✅ Metadata uploaded to $DAILY_BACKUP_PATH"
|
||||
else
|
||||
log_error "Metadata upload failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup local metadata backup
|
||||
rm -rf "$METADATA_DIR"
|
||||
log "🧹 Cleaned up local metadata files"
|
||||
|
||||
# ============================================================================
|
||||
# DATA BACKUP (WEEKLY + INCREMENTAL)
|
||||
# ============================================================================
|
||||
|
||||
log ""
|
||||
log "========================================="
|
||||
log "💾 DATA BACKUP"
|
||||
log "========================================="
|
||||
|
||||
DATA_SOURCE="$NEXTCLOUD_DATA_ROOT/data/"
|
||||
|
||||
# Check if data directory exists and is readable
|
||||
if [ ! -d "$DATA_SOURCE" ]; then
|
||||
log_error "Data directory not found: $DATA_SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -r "$DATA_SOURCE" ]; then
|
||||
log_error "Data directory not readable: $DATA_SOURCE"
|
||||
log_error "Verify that backup-user is in www-data group"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATA_SIZE=$(du -sh "$DATA_SOURCE" 2>/dev/null | cut -f1 || echo "unknown")
|
||||
log "Data directory size: $DATA_SIZE"
|
||||
|
||||
# Weekly backup: Full sync (Monday)
|
||||
if [ "$IS_WEEKLY_BACKUP" = true ]; then
|
||||
log "🔄 Weekly full sync to: weekly/$WEEK_NUMBER/data/"
|
||||
WEEKLY_DEST="$RCLONE_REMOTE/weekly/$WEEK_NUMBER/data/"
|
||||
|
||||
if rclone sync "$DATA_SOURCE" "$WEEKLY_DEST" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" \
|
||||
--log-level INFO \
|
||||
--stats 5m \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--exclude '.Trash-*/**' \
|
||||
--exclude '**/.~*' \
|
||||
--exclude '**/~$*' \
|
||||
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
||||
|
||||
log "✅ Weekly data sync completed"
|
||||
else
|
||||
log_error "Weekly data sync failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Also copy metadata to weekly folder
|
||||
log "Copying metadata to weekly folder..."
|
||||
rclone copy "$DAILY_BACKUP_PATH/metadata/" "$RCLONE_REMOTE/weekly/$WEEK_NUMBER/metadata/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
else
|
||||
# Daily incremental: Copy only new/changed files (Tuesday-Sunday)
|
||||
log "📋 Daily incremental copy to: weekly/$WEEK_NUMBER/data/"
|
||||
WEEKLY_DEST="$RCLONE_REMOTE/weekly/$WEEK_NUMBER/data/"
|
||||
|
||||
if rclone copy "$DATA_SOURCE" "$WEEKLY_DEST" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" \
|
||||
--log-level INFO \
|
||||
--stats 5m \
|
||||
--update \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--exclude '.Trash-*/**' \
|
||||
--exclude '**/.~*' \
|
||||
--exclude '**/~$*' \
|
||||
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
||||
|
||||
log "✅ Daily incremental copy completed"
|
||||
else
|
||||
log_error "Daily incremental copy failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# YEARLY BACKUP (January 1st)
|
||||
# ============================================================================
|
||||
|
||||
if [ "$IS_YEARLY_BACKUP" = true ]; then
|
||||
log ""
|
||||
log "========================================="
|
||||
log "🗓️ YEARLY BACKUP"
|
||||
log "========================================="
|
||||
|
||||
YEARLY_DEST="$RCLONE_REMOTE/yearly/$YEAR/data/"
|
||||
|
||||
log "🔄 Yearly full sync to: yearly/$YEAR/data/"
|
||||
if rclone sync "$DATA_SOURCE" "$YEARLY_DEST" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" \
|
||||
--log-level INFO \
|
||||
--stats 5m \
|
||||
--transfers 4 \
|
||||
--checkers 8 \
|
||||
--exclude '.Trash-*/**' \
|
||||
--exclude '**/.~*' \
|
||||
--exclude '**/~$*' \
|
||||
--progress 2>&1 | tee -a "$LOG_FILE"; then
|
||||
|
||||
log "✅ Yearly data sync completed"
|
||||
else
|
||||
log_error "Yearly data sync failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy metadata to yearly folder
|
||||
log "Copying metadata to yearly folder..."
|
||||
rclone copy "$DAILY_BACKUP_PATH/metadata/" "$RCLONE_REMOTE/yearly/$YEAR/metadata/" \
|
||||
--config "$RCLONE_CONFIG" \
|
||||
--log-file "$LOG_FILE" 2>&1 | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# ROTATION (CLEANUP OLD BACKUPS)
|
||||
# ============================================================================
|
||||
|
||||
log ""
|
||||
log "========================================="
|
||||
log "🧹 ROTATION"
|
||||
log "========================================="
|
||||
|
||||
# Daily rotation
|
||||
log "Rotating daily backups (keeping $DAILY_RETENTION)..."
|
||||
DAILY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/daily/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
||||
DAILY_COUNT=$(echo "$DAILY_BACKUPS" | grep -c . || echo 0)
|
||||
|
||||
if [ "$DAILY_COUNT" -gt "$DAILY_RETENTION" ]; then
|
||||
DAILY_TO_DELETE=$(echo "$DAILY_BACKUPS" | tail -n +$((DAILY_RETENTION + 1)))
|
||||
echo "$DAILY_TO_DELETE" | while read -r dir; do
|
||||
if [ -n "$dir" ]; then
|
||||
log " Deleting old daily backup: $dir"
|
||||
rclone purge "$RCLONE_REMOTE/daily/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
log "✅ Daily retention: $DAILY_COUNT backups"
|
||||
|
||||
# Weekly rotation
|
||||
log "Rotating weekly backups (keeping $WEEKLY_RETENTION)..."
|
||||
WEEKLY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/weekly/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
||||
WEEKLY_COUNT=$(echo "$WEEKLY_BACKUPS" | grep -c . || echo 0)
|
||||
|
||||
if [ "$WEEKLY_COUNT" -gt "$WEEKLY_RETENTION" ]; then
|
||||
WEEKLY_TO_DELETE=$(echo "$WEEKLY_BACKUPS" | tail -n +$((WEEKLY_RETENTION + 1)))
|
||||
echo "$WEEKLY_TO_DELETE" | while read -r dir; do
|
||||
if [ -n "$dir" ]; then
|
||||
log " Deleting old weekly backup: $dir"
|
||||
rclone purge "$RCLONE_REMOTE/weekly/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
log "✅ Weekly retention: $WEEKLY_COUNT backups"
|
||||
|
||||
# Yearly rotation
|
||||
log "Rotating yearly backups (keeping $YEARLY_RETENTION)..."
|
||||
YEARLY_BACKUPS=$(rclone lsf "$RCLONE_REMOTE/yearly/" --config "$RCLONE_CONFIG" --dirs-only 2>/dev/null | sort -r || echo "")
|
||||
YEARLY_COUNT=$(echo "$YEARLY_BACKUPS" | grep -c . || echo 0)
|
||||
|
||||
if [ "$YEARLY_COUNT" -gt "$YEARLY_RETENTION" ]; then
|
||||
YEARLY_TO_DELETE=$(echo "$YEARLY_BACKUPS" | tail -n +$((YEARLY_RETENTION + 1)))
|
||||
echo "$YEARLY_TO_DELETE" | while read -r dir; do
|
||||
if [ -n "$dir" ]; then
|
||||
log " Deleting old yearly backup: $dir"
|
||||
rclone purge "$RCLONE_REMOTE/yearly/$dir" --config "$RCLONE_CONFIG" 2>> "$LOG_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
log "✅ Yearly retention: $YEARLY_COUNT backups"
|
||||
|
||||
# ============================================================================
|
||||
# SUMMARY
|
||||
# ============================================================================
|
||||
|
||||
log ""
|
||||
log "========================================="
|
||||
log "📊 BACKUP SUMMARY"
|
||||
log "========================================="
|
||||
log "Date: $CURRENT_DATE"
|
||||
log "Type: $([ "$IS_YEARLY_BACKUP" = true ] && echo "Yearly + Weekly + Daily" || ([ "$IS_WEEKLY_BACKUP" = true ] && echo "Weekly + Daily" || echo "Daily"))"
|
||||
log "Duration: $SECONDS seconds"
|
||||
log "Log file: $LOG_FILE"
|
||||
log ""
|
||||
|
||||
# Storage usage
|
||||
log "Jottacloud storage:"
|
||||
rclone about "$RCLONE_REMOTE" --config "$RCLONE_CONFIG" 2>/dev/null | head -3 | tee -a "$LOG_FILE" || log "Could not retrieve storage info"
|
||||
|
||||
log ""
|
||||
log "========================================="
|
||||
log "✅ BACKUP COMPLETED SUCCESSFULLY"
|
||||
log "========================================="
|
||||
Loading…
Add table
Reference in a new issue