Linux Backup Shell Script: Files & Database to Remote Server

linux remote backup shell script

This guide shows a production-ready Linux backup shell script that copies website files and a MySQL/MariaDB database from one server to a remote backup server using rsync over SSH.

What this script backs up

  • Files — directories such as /var/www/html and /etc/nginx
  • Database — compressed mysqldump (.sql.gz)
  • Remote copy — synced to backup@remote-server with dated folders
  • Retention — automatically deletes old remote backups

Prerequisites

  • Two Linux servers (source + remote backup server)
  • SSH key authentication configured (ssh-copy-id backup@remote-ip)
  • rsync, openssh-client, mysqldump installed on source server
  • MySQL/MariaDB credentials (or ~/.my.cnf)

Step 1: Create the backup script on source server

Save the script as /usr/local/bin/remote-backup.sh and make it executable:

sudo nano /usr/local/bin/remote-backup.sh
sudo chmod +x /usr/local/bin/remote-backup.sh

Step 2: Full backup script (scroll to read)

Copy the complete script below. The box is scrollable so the page stays clean.

remote-backup.sh
#!/usr/bin/env bash
#===============================================================================
# remote-backup.sh
# Backup website files + MySQL/MariaDB database to a remote Linux server
# Tested on: AlmaLinux 9, Ubuntu 22.04, Debian 12
# Usage: ./remote-backup.sh
# Cron:  0 2 * * * /usr/local/bin/remote-backup.sh >> /var/log/remote-backup.log 2>&1
#===============================================================================

set -euo pipefail

# ---------- CONFIGURATION (edit these) ----------
REMOTE_USER="backup"
REMOTE_HOST="192.168.1.10"
REMOTE_PATH="/backups/production-server"
SSH_KEY="/root/.ssh/id_ed25519"

# Local paths to back up (space-separated for multiple dirs)
BACKUP_DIRS=("/var/www/html" "/etc/nginx")

# Database (leave DB_NAME empty to skip DB backup)
DB_NAME="webapp"
DB_USER="root"
DB_PASS=""                    # or use ~/.my.cnf for credentials
DB_HOST="127.0.0.1"

LOCAL_STAGING="/tmp/backup-staging"
RETENTION_DAYS=14             # delete remote backups older than N days
DATE=$(date +%F_%H-%M-%S)
HOSTNAME_SHORT=$(hostname -s)
LOG_PREFIX="[$(date '+%F %T')]"
# ------------------------------------------------

log()  { echo "${LOG_PREFIX} $*"; }
fail() { log "ERROR: $*"; exit 1; }

require_cmd() {
  command -v "$1" >/dev/null 2>&1 || fail "Required command not found: $1"
}

require_cmd rsync
require_cmd ssh
require_cmd gzip

SSH_OPTS=(-i "${SSH_KEY}" -o BatchMode=yes -o StrictHostKeyChecking=accept-new)
RSYNC_SSH="ssh ${SSH_OPTS[*]}"

cleanup() {
  rm -rf "${LOCAL_STAGING}"
}
trap cleanup EXIT

log "Starting backup job for ${HOSTNAME_SHORT}"

# Prepare local staging directory
mkdir -p "${LOCAL_STAGING}/files" "${LOCAL_STAGING}/database"

# ---------- DATABASE BACKUP ----------
if [[ -n "${DB_NAME}" ]]; then
  require_cmd mysqldump
  DB_FILE="${LOCAL_STAGING}/database/${DB_NAME}_${DATE}.sql.gz"
  log "Dumping database: ${DB_NAME}"

  if [[ -n "${DB_PASS}" ]]; then
    mysqldump -h "${DB_HOST}" -u "${DB_USER}" -p"${DB_PASS}" \
      --single-transaction --routines --triggers "${DB_NAME}" | gzip -9 > "${DB_FILE}"
  else
    mysqldump -h "${DB_HOST}" -u "${DB_USER}" \
      --single-transaction --routines --triggers "${DB_NAME}" | gzip -9 > "${DB_FILE}"
  fi

  log "Database dump saved: ${DB_FILE}"
else
  log "Skipping database backup (DB_NAME not set)"
fi

# ---------- FILE BACKUP (local copy before rsync) ----------
for SRC in "${BACKUP_DIRS[@]}"; do
  [[ -d "${SRC}" ]] || fail "Backup directory not found: ${SRC}"
  BASENAME=$(basename "${SRC}")
  log "Archiving files: ${SRC}"
  rsync -a "${SRC}/" "${LOCAL_STAGING}/files/${BASENAME}/"
done

# ---------- UPLOAD TO REMOTE SERVER ----------
REMOTE_TARGET="${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/${HOSTNAME_SHORT}/${DATE}/"
log "Uploading to remote: ${REMOTE_TARGET}"

ssh "${SSH_OPTS[@]}" "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${REMOTE_PATH}/${HOSTNAME_SHORT}/${DATE}'"

rsync -avz --delete -e "${RSYNC_SSH}" \
  "${LOCAL_STAGING}/" "${REMOTE_TARGET}"

log "Remote sync completed"

# ---------- RETENTION (remote cleanup) ----------
if [[ "${RETENTION_DAYS}" -gt 0 ]]; then
  log "Removing remote backups older than ${RETENTION_DAYS} days"
  ssh "${SSH_OPTS[@]}" "${REMOTE_USER}@${REMOTE_HOST}" \
    "find '${REMOTE_PATH}/${HOSTNAME_SHORT}' -mindepth 1 -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} +"
fi

log "Backup completed successfully"
exit 0

Scroll inside the box to read the full script.

Step 3: Configure variables

Edit the configuration section at the top of the script:

  • REMOTE_HOST — IP or hostname of backup server
  • BACKUP_DIRS — folders to include
  • DB_NAME, DB_USER, DB_PASS — database settings
  • RETENTION_DAYS — how long to keep old backups on remote server
Linux remote backup shell script terminal output
Running the remote backup script on a Linux server

Step 4: Test the backup manually

sudo /usr/local/bin/remote-backup.sh

Verify files on the remote server:

ssh [email protected] "ls -lah /backups/production-server/$(hostname -s)/"

Step 5: Schedule with cron (daily at 2 AM)

sudo crontab -e

Add this line:

0 2 * * * /usr/local/bin/remote-backup.sh >> /var/log/remote-backup.log 2>&1

Security tips

  • Use a dedicated backup user on the remote server (not root)
  • Restrict SSH key to rsync-only if possible
  • Store DB passwords in /root/.my.cnf with chmod 600
  • Encrypt backups at rest for sensitive data

Related tutorials

Terminal screenshot is an original illustration created for Gnome IT Solutions (blog.gnomeitsolutions.com).