Regular backups are the only reliable defense against server failures, CMS update errors, or cyberattacks. While most control panels (Plesk, cPanel) include built-in backup tools, the archives they generate are often stored in isolated system directories that are inaccessible via standard FTP/SFTP accounts.
In this article, we will cover how to build a universal Bash script tailored for VPS/VDS or hosting environments with SSH access. The script automatically dumps MySQL/MariaDB databases, archives files across multiple websites, aggregates them into a single archive, maintains a detailed execution log, and deletes backups older than 7 days.
Requirements & Limitations
This solution is designed for VPS/VDS servers or web hosting with SSH access. Requirements include:
- Command-line access (SSH / Bash).
- Installed utilities:
mysqldump,tar,find,cron. - Adequate I/O and RAM limits (on cheap Shared hosting, compression of large sites may be killed by resource limits or timeouts).
The Bash Script
Create a file named backup.sh in your home directory and paste the following code:
#!/usr/bin/env bash
set -euo pipefail
# === CONFIGURATION ===
VHOST_ROOT="$HOME"
BACKUP_DIR="${VHOST_ROOT}/MyBackup"
LOG_FILE="${VHOST_ROOT}/backup.log"
DB_HOST="localhost"
# List of website directories (relative to VHOST_ROOT)
SITES=(
"httpdocs"
"site2.org.ua"
"site3.org.ua"
)
# List of databases in "DB_NAME:DB_USER:DB_PASS" format
DATABASES=(
"db_site1:user1:pass1"
"db_site2:user2:pass2"
"db_site3:user3:pass3"
)
# === LOGGING & ERROR HANDLING ===
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
error_handler() {
local line_no="$1"
local exit_code="${2:-1}"
log "ERROR: Script interrupted at line ${line_no} with exit code ${exit_code}."
[ -d "${TMP_DIR:-}" ] && rm -rf "$TMP_DIR"
exit "$exit_code"
}
trap 'error_handler ${LINENO} $?' ERR
# === MAIN PROCESS ===
log "--- START: Backup procedure initiated ---"
DATE=$(date +%Y-%m-%d_%H-%M)
TMP_DIR="${VHOST_ROOT}/tmp/backup_${DATE}"
ARCHIVE_NAME="full_backup_${DATE}.tar.gz"
mkdir -p "${TMP_DIR}/db" "${TMP_DIR}/files" "${BACKUP_DIR}"
# 1. Database dumps (using non-blocking flags for InnoDB)
for db_entry in "${DATABASES[@]}"; do
IFS=":" read -r db_name db_user db_pass <<< "$db_entry"
log "DB: Dumping database '${db_name}'..."
mysqldump -h "${DB_HOST}" -u"${db_user}" -p"${db_pass}" \
--single-transaction --quick "${db_name}" > "${TMP_DIR}/db/${db_name}.sql"
log "DB: Dump '${db_name}' created successfully."
done
# 2. Website files archiving
for site in "${SITES[@]}"; do
if [ -d "${VHOST_ROOT}/${site}" ]; then
log "FILES: Archiving '${site}'..."
tar_status=0
tar -czf "${TMP_DIR}/files/${site}.tar.gz" \
--exclude="${site}/var/cache" \
--exclude="${site}/tmp" \
-C "${VHOST_ROOT}" "$site" || tar_status=$?
# Exit code 1 in tar indicates "file changed as we read it" (warning)
if [ "$tar_status" -eq 1 ]; then
log "WARNING: Files in '${site}' changed during archiving."
elif [ "$tar_status" -gt 1 ]; then
log "ERROR: Critical tar failure in '${site}', code: $tar_status"
exit "$tar_status"
fi
log "FILES: Archive for '${site}' created."
fi
done
# 3. Final archive assembly
log "ARCHIVE: Creating final bundle ${ARCHIVE_NAME}..."
tar -czf "${BACKUP_DIR}/${ARCHIVE_NAME}" -C "${TMP_DIR}" .
log "ARCHIVE: Saved to ${BACKUP_DIR}/${ARCHIVE_NAME}."
# 4. Rotation (deleting backups older than 7 days)
log "ROTATION: Removing backups older than 7 days..."
find "${BACKUP_DIR}" -type f -name "full_backup_*.tar.gz" -mtime +7 -delete
# 5. Temporary directory cleanup
rm -rf "${TMP_DIR}"
log "--- SUCCESS: Backup completed successfully ---"
Setup Instructions
1. Make the Script Executable
Grant execution permissions to the script:
chmod +x ~/backup.sh2. Configure Cron Job
Open the current user’s crontab editor:
crontab -eAdd a schedule to run the script every Sunday at 03:00 AM:
0 3 * * 0 /bin/bash /home/username/backup.sh > /dev/null 2>&1Monitoring & Logging
Every step of the execution is logged to backup.log. In the event of an unexpected interruption (such as disk quota limits or database credential errors), the log records the exact line number and error code where execution halted.
Inspect the latest log entries at any time using:
tail -n 30 ~/backup.logSummary: You now have a lightweight, self-contained backup solution that automatically delivers ready-to-download archives to an accessible FTP/SFTP directory without relying on heavy CMS plugins.
