Automated Docker Backups on OpenMediaVault

Saurab ThakurSaurab Thakur
7 min read

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

Automated Docker Restore on OpenMediaVault

Share this article

You might also like

Table of contents