This guide walks you through setting up a fully automated Docker backup system on your OpenMediaVault NAS, and how to restore it after a fresh install. Youβll also learn how to install Docker and Portainer again from scratch.
π§ Overview
Weβll use systemd timers to automate a script that backs up key Docker folders β volumes, containers, network, Config, and swarm β and compress them to an archive.
Optional Step
π’ Use this Telegram script regularly to get instant alerts from the server.
Itβs lightweight, fast, and keeps me in the loop 24/7.
Highly recommended for any automation or backup setup! π‘
πLevel Up Your Telegram Notifications: Secure Credentials with a .env File
π Step 1: Create the Backup Script
sudo nano /usr/local/bin/docker_backup.sh
#!/bin/bash
# === CONFIGURATION ===
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
BACKUP_DIR="/srv/personal/backups/backups/docker"
LOG_DIR="/srv/personal/backups/logs"
LOG_FILE="$LOG_DIR/docker-backup_$TIMESTAMP.log"
TELEGRAM_SCRIPT="/usr/local/bin/telegram_notify.sh"
# DB container settings
NEXTCLOUD_DB_CONTAINER="nextcloud_db"
NEXTCLOUD_DB_NAME="nextcloud"
NEXTCLOUD_DB_USER="root"
NEXTCLOUD_DB_PASS="DB_PASSWORD"
NEXTCLOUD_DB_DUMP_FILE="$BACKUP_DIR/nextcloud-db_$TIMESTAMP.sql"
IMMICH_DB_CONTAINER="immich_postgres"
IMMICH_DB_NAME="immich"
IMMICH_DB_DUMP_FILE="$BACKUP_DIR/immich-db_$TIMESTAMP.sql"
WEBTREES_DB_CONTAINER="webtrees-db"
WEBTREES_DB_NAME="webtrees"
WEBTREES_DB_USER="webtrees"
WEBTREES_DB_PASS="DB_PASSWORD"
WEBTREES_DB_DUMP_FILE="$BACKUP_DIR/webtrees-db_$TIMESTAMP.sql"
# Docker folders
DOCKER_PATH="/var/lib/docker"
DOCKER_SUBDIRS=("containers" "volumes" "network" "swarm" "config")
DOCKER_BACKUP_PATH="$BACKUP_DIR/docker"
# === FUNCTIONS ===
log() {
echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
}
send_telegram() {
[ -x "$TELEGRAM_SCRIPT" ] && "$TELEGRAM_SCRIPT" "$1"
}
# === START ===
mkdir -p "$BACKUP_DIR" "$LOG_DIR"
log "[INFO] Starting NAS backup..."
send_telegram "π NAS Backup started at $TIMESTAMP"
# === NEXTCLOUD DB DUMP ===
log "[INFO] Dumping Nextcloud DB..."
docker exec "$NEXTCLOUD_DB_CONTAINER" sh -c "exec mysqldump -u$NEXTCLOUD_DB_USER -p$NEXTCLOUD_DB_PASS $NEXTCLOUD_DB_NAME" > "$NEXTCLOUD_DB_DUMP_FILE" 2>>"$LOG_FILE"
if [ $? -eq 0 ]; then
log "[SUCCESS] Nextcloud DB saved: $NEXTCLOUD_DB_DUMP_FILE"
send_telegram "β
Nextcloud DB backup done: $(basename "$NEXTCLOUD_DB_DUMP_FILE")"
ls -1t "$BACKUP_DIR"/nextcloud-db_*.sql | tail -n +2 | xargs -r rm -f
else
log "[ERROR] Nextcloud DB dump failed!"
send_telegram "β Nextcloud DB backup failed!"
exit 1
fi
# === IMMICH DB DUMP ===
log "[INFO] Dumping Immich DB..."
docker exec -t "$IMMICH_DB_CONTAINER" pg_dump -U postgres "$IMMICH_DB_NAME" > "$IMMICH_DB_DUMP_FILE" 2>>"$LOG_FILE"
if [ $? -eq 0 ]; then
log "[SUCCESS] Immich DB saved: $IMMICH_DB_DUMP_FILE"
send_telegram "β
Immich DB backup done: $(basename "$IMMICH_DB_DUMP_FILE")"
ls -1t "$BACKUP_DIR"/immich-db_*.sql | tail -n +2 | xargs -r rm -f
else
log "[ERROR] Immich DB dump failed!"
send_telegram "β Immich DB backup failed!"
exit 1
fi
# === WEBTREES DB DUMP USING mariadb:10.6 ===
log "[INFO] Dumping Webtrees DB using temporary mysqldump container..."
docker run --rm --network container:"$WEBTREES_DB_CONTAINER" \
-e MYSQL_PWD="$WEBTREES_DB_PASS" mariadb:10.6 \
sh -c "mysqldump -h127.0.0.1 -P3306 -u$WEBTREES_DB_USER $WEBTREES_DB_NAME" > "$WEBTREES_DB_DUMP_FILE" 2>>"$LOG_FILE"
if [ $? -eq 0 ]; then
log "[SUCCESS] Webtrees DB saved: $WEBTREES_DB_DUMP_FILE"
send_telegram "β
Webtrees DB backup done: $(basename "$WEBTREES_DB_DUMP_FILE")"
ls -1t "$BACKUP_DIR"/webtrees-db_*.sql | tail -n +2 | xargs -r rm -f
else
log "[ERROR] Webtrees DB dump failed!"
send_telegram "β Webtrees DB backup failed!"
exit 1
fi
# === BACKUP DOCKER CONFIGS ===
log "[INFO] Stopping Docker service..."
systemctl stop docker || { log "[ERROR] Docker stop failed!"; send_telegram "β Docker stop failed!"; exit 1; }
log "[INFO] Backing up Docker config folders..."
mkdir -p "$DOCKER_BACKUP_PATH"
for dir in "${DOCKER_SUBDIRS[@]}"; do
rsync -aHAX --delete "$DOCKER_PATH/$dir" "$DOCKER_BACKUP_PATH"
done
log "[INFO] Starting Docker service..."
systemctl start docker || { log "[ERROR] Docker start failed!"; send_telegram "β Docker start failed!"; exit 1; }
# === CLEAN OLD LOG FILES ===
find "$LOG_DIR" -type f -name "docker-backup_*.log" -mtime +7 -exec rm -f {} \;
log "[SUCCESS] NAS backup completed."
send_telegram "β
NAS Backup completed successfully at $(date +"%Y-%m-%d %H:%M:%S")"
sudo chmod +x /usr/local/bin/docker_backup.sh
π Step 2: Create the Compression Script
sudo nano /usr/local/bin/docker_compress.sh
#!/bin/bash
# === CONFIGURATION ===
SRC_DIR="/srv/personal/backups/backups"
ARCHIVE_DIR="/srv/personal/backups/archives"
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
ARCHIVE_FILE="$ARCHIVE_DIR/docker-backup-$TIMESTAMP.tar.gz"
TEMP_ARCHIVE_FILE="$ARCHIVE_DIR/docker-backup-temp.tar.gz"
# External Telegram script
TELEGRAM_SCRIPT="/usr/local/bin/telegram_notify.sh"
# === Create archive directory if not exists ===
mkdir -p "$ARCHIVE_DIR"
# === Function to send Telegram message ===
send_telegram() {
local message="$1"
if [ -x "$TELEGRAM_SCRIPT" ]; then
"$TELEGRAM_SCRIPT" "$message"
fi
}
send_telegram "π¦ [INFO] Starting Docker backup compression..."
# === Compress backup to temporary archive ===
echo "[INFO] Compressing backup to temporary archive..."
tar -czf "$TEMP_ARCHIVE_FILE" -C "$SRC_DIR" .
if [ $? -eq 0 ]; then
mv "$TEMP_ARCHIVE_FILE" "$ARCHIVE_FILE"
echo "[SUCCESS] Backup compressed to: $ARCHIVE_FILE"
send_telegram "β
[SUCCESS] Backup compressed: $(basename "$ARCHIVE_FILE")"
else
echo "[ERROR] Compression failed!"
send_telegram "β [ERROR] Docker backup compression failed!"
exit 1
fi
# === Safely delete old archives, keep only the latest ===
echo "[INFO] Cleaning up old archives..."
ls -1tr "$ARCHIVE_DIR"/docker-backup-*.tar.gz | head -n -1 | xargs -r rm -f
echo "[INFO] Old archives deleted. Only the latest one is retained."
send_telegram "π§Ή [INFO] Old archives removed. Only the latest backup is kept."
send_telegram "π [DONE] Docker backup archive process completed successfully."
sudo chmod +x /usr/local/bin/docker_compress.sh
π οΈ Step 3: Upload Backup Archive to Google Drive Using Rclone
1. Install Rclone
Begin by installing Rclone on your OpenMediaVault system:
curl https://rclone.org/install.sh | sudo bash
2. Configure Rclone for Google Drive
For detailed instructions on setting up Google Drive with Rclone, refer to the official guide here: How to Set Up Google Drive on Rclone.
3. Add Rclone Upload script
Add a new Rclone Upload script
nano /usr/local/bin/docker_upload_to_gdrive.sh
Use the following script to upload your Docker backup archive to Google Drive:
#!/bin/bash
# === CONFIGURATION ===
ARCHIVE_DIR="/srv/personal/backups/archives"
RCLONE_REMOTE="gdrive"
RCLONE_FOLDER="docker-backups"
TELEGRAM_SCRIPT="/usr/local/bin/telegram_notify.sh"
# === FIND LATEST BACKUP FILE ===
LATEST_BACKUP=$(ls -t "$ARCHIVE_DIR"/docker-backup-*.tar.gz 2>/dev/null | head -n 1)
if [ -z "$LATEST_BACKUP" ]; then
echo "[ERROR] No backup archive found in $ARCHIVE_DIR"
if [ -x "$TELEGRAM_SCRIPT" ]; then
"$TELEGRAM_SCRIPT" "β No Docker backup archive found to upload to Google Drive!"
fi
exit 1
fi
# === FUNCTION: SEND TELEGRAM MESSAGE ===
send_telegram() {
if [ -x "$TELEGRAM_SCRIPT" ]; then
"$TELEGRAM_SCRIPT" "$1"
fi
}
# === UPLOAD TO GOOGLE DRIVE ===
echo "[INFO] Uploading $(basename "$LATEST_BACKUP") to Google Drive..."
rclone copy "$LATEST_BACKUP" "$RCLONE_REMOTE:$RCLONE_FOLDER"
if [ $? -eq 0 ]; then
echo "[SUCCESS] Backup uploaded successfully to Google Drive."
send_telegram "β
Docker backup uploaded: $(basename "$LATEST_BACKUP")"
# === DELETE OLD BACKUPS AFTER SUCCESSFUL UPLOAD ===
echo "[INFO] Looking for older backups to delete..."
OLD_BACKUPS=$(rclone lsf "$RCLONE_REMOTE:$RCLONE_FOLDER" --files-only | grep '^docker-backup-.*\.tar\.gz$' | grep -v "$(basename "$LATEST_BACKUP")")
if [ -n "$OLD_BACKUPS" ]; then
while IFS= read -r file; do
echo "[INFO] Deleting old backup: $file"
rclone delete "$RCLONE_REMOTE:$RCLONE_FOLDER/$file" --drive-use-trash=false
done <<< "$OLD_BACKUPS"
echo "[INFO] Old backups deleted."
else
echo "[INFO] No old backups found to delete."
fi
else
echo "[ERROR] Failed to upload backup to Google Drive!"
send_telegram "β Docker backup upload to Google Drive failed!"
fi
π Permissions
Make the script executable:
chmod +x /usr/local/bin/docker_upload_to_gdrive.sh
This script will find the latest Docker backup archive and upload it to your Google Drive, notifying you via Telegram about the success or failure of the operation.
β° Step 3: Automate It with systemd
1. Create the Service
sudo nano /etc/systemd/system/docker-backup.service
[Unit]
Description=Docker Backup Script
After=network-online.target docker.service
Wants=network-online.target docker-backup.timer
[Service]
Type=oneshot
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/bin/docker_backup.sh
ExecStartPost=/usr/local/bin/docker_compress.sh
ExecStartPost=/usr/local/bin/docker_upload_to_gdrive.sh
2. Create the Timer
sudo nano /etc/systemd/system/docker-backup.timer
[Unit]
[Unit]
Description=Run Docker Backup on Sunday and Wednesday
[Timer]
OnCalendar=Sun,Wed *-*-* 03:05:00
Persistent=true
[Install]
WantedBy=timers.target
3. Reload & Enable
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable --now docker-backup.timer
π§ͺ Test It
sudo systemctl start docker-backup.service
systemctl list-timers | grep docker-backup
ls -lh /srv/personal/docker-archives/
journalctl -u docker-backup.service --no-pager --since "5 minutes ago"
π Backup Structure
Hereβs how the backup directories are organised:
/srv/personal/docker-backups/
βββ volumes/
βββ containers/
βββ network/
βββ config/
βββ swarm/
βββ immich_DB/
βββ webtrees_DB/
βββ nextcloud_DB/
Compressed archives are stored separately for easy retrieval:
/srv/personal/docker-archives/
βββ docker-backup-YYYY-MM-DD_HH-MM-SS.tar.gz
