How-To: Raspberry Pi with Docker live Backup to NAS

Attached is a Bash script to live back up a Raspberry Pi running Docker to a QNAP NAS.

NAS configuration:

First, create a shared folder on your NAS and a user account with appropriate permissions.

Share name: NetBackup

NAS IP used in the example: 10.1.1.10.

Username in example: YOURNASSHAREUSER

Password in example: YOURSHAREUSERPASSWORD

So your NAS configuration is done.

Raspberry Pi Configuration:

# packages

sudo apt update
sudo apt install -y cifs-utils pigz

# mountpoint and access (edit your credentials)

sudo mkdir -p /mnt/qnap
echo -e "username=YOURNASSHAREUSER\npassword=YOURSHAREUSERPASSWORD\ndomain=WORKGROUP" | sudo tee /etc/cifs-creds-qnap >/dev/null
sudo chmod 600 /etc/cifs-creds-qnapCode-Sprache: JavaScript (javascript)

# persistent mount (edit your NAS IP)

echo "//10.1.1.10/NetBackup /mnt/qnap cifs _netdev,credentials=/etc/cifs-creds-qnap,iocharset=utf8,vers=3.0,serverino,nofail 0 0" | sudo tee -a /etc/fstabCode-Sprache: PHP (php)

# mount & check

sudo mount -a
ls -la /mnt/qnap

# folder create

sudo mkdir -p /mnt/qnap/rpi4/fullimages

# scriptfile create

sudo vi /usr/local/sbin/rpi_fullimage.sh

# copy script in scriptfile (edit Retention and device defaults)

sudo tee /usr/local/sbin/rpi_fullimage.sh >/dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail

# ========= Einstellungen =========
TARGET_DIR="/mnt/qnap/rpi4/fullimages"   # Zielordner auf QNAP (NetBackup -> /mnt/qnap)
RETENTION_DAYS=180                       # Aufbewahrungsdauer in Tagen
BLOCK_SIZE="16M"                         # Lese-Blockgröße für dd
DEVICE_DEFAULT="/dev/mmcblk0"            # or /dev/sda if SDD over USB: df -h 

# ========= Hilfsfunktionen =========
abort() { echo "[FEHLER] $*" >&2; exit 1; }
info()  { echo "[*] $*"; }
ok()    { echo "[OK] $*"; }

# ========= Vorbedingungen =========
command -v pigz >/dev/null || abort "pigz fehlt. Installiere: sudo apt install -y pigz"
mountpoint -q /mnt/qnap || abort "QNAP nicht gemountet (/mnt/qnap). Prüfe /etc/fstab und 'sudo mount -a'."
mkdir -p "$TARGET_DIR"

DEV="$DEVICE_DEFAULT"
[ -b "$DEV" ] || abort "Blockgerät $DEV nicht gefunden. Bootest du evtl. von USB/NVMe? DEVICE_DEFAULT anpassen."

# Grobe Platzprüfung (mind. ~50% der Gerätegröße frei; gzip komprimiert stark)
FREE_KB=$(df -Pk "$TARGET_DIR" | awk 'NR==2{print $4}')
SECTORS=$(cat /sys/block/$(basename "$DEV")/size)
TOTAL_BYTES=$(( SECTORS * 512 ))
NEEDED_KB=$(( (TOTAL_BYTES / 2) / 1024 ))
[ "$FREE_KB" -ge "$NEEDED_KB" ] || abort "Zu wenig Platz auf QNAP. Frei: ${FREE_KB}KB, benötigt grob >= ${NEEDED_KB}KB."

DATE="$(date +%F_%H-%M-%S)"
IMG="${TARGET_DIR}/rpi4-${DATE}.img.gz"

# ========= Docker: laufende Container pausieren =========
DOCKER_AVAILABLE=true
command -v docker >/dev/null || DOCKER_AVAILABLE=false

RUNNING_IDS=""
if $DOCKER_AVAILABLE; then
  RUNNING_IDS="$(docker ps -q || true)"
  docker_restore() {
    if [ -z "$RUNNING_IDS" ]; then return 0; fi
    for id in $RUNNING_IDS; do
      st="$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null || echo unknown)"
      [ "$st" = "paused" ] && docker unpause "$id" >/dev/null 2>&1 || true
      st="$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null || echo unknown)"
      if [ "$st" = "exited" ] || [ "$st" = "created" ]; then docker start "$id" >/dev/null 2>&1 || true; fi
    done
  }
  trap 'docker_restore' EXIT
  [ -n "$RUNNING_IDS" ] && { info "Pausiere laufende Docker-Container…"; docker pause $RUNNING_IDS || true; }
fi

sync

# ========= Image erstellen (fsync am Zieldatei-dd) =========
info "Erzeuge Image von ${DEV} -> ${IMG}"
dd if="$DEV" bs="$BLOCK_SIZE" status=progress iflag=fullblock \
| pigz -c \
| dd of="$IMG" bs=4M status=progress conv=fsync

# ========= Prüfsumme =========
info "Erzeuge SHA256-Prüfsumme…"
sha256sum "$IMG" > "${IMG}.sha256"

# ========= Docker-Vorzustand wiederherstellen =========
if $DOCKER_AVAILABLE; then
  docker_restore
  trap - EXIT
fi

# ========= Aufräumen =========
info "Entferne Backups älter als ${RETENTION_DAYS} Tage…"
find "$TARGET_DIR" -type f -name "rpi4-*.img.gz" -mtime +$RETENTION_DAYS -delete
find "$TARGET_DIR" -type f -name "rpi4-*.img.gz.sha256" -mtime +$RETENTION_DAYS -delete

ok "Voll-Image abgeschlossen: $IMG"
SHCode-Sprache: PHP (php)

# make script executable

sudo chmod +x /usr/local/sbin/rpi_fullimage.sh

# run script (docker will suspend)

sudo /usr/local/sbin/rpi_fullimage.sh

After reboot if auto mount not work:

# mount & check

sudo mount -a
ls -la /mnt/qnap

# run script (docker will suspend)

sudo /usr/local/sbin/rpi_fullimage.sh

Backup restore (test first on an other drive):

# Linux

sha256sum -c rpi4-*.img.gz.sha256
lsblk
gunzip -c rpi4-*.img.gz | sudo dd of=/dev/sdX bs=16M status=progress conv=fsync
syncCode-Sprache: JavaScript (javascript)

# Mac

diskutil list
diskutil unmountDisk /dev/disk3
gunzip -c rpi4-*.img.gz | sudo dd of=/dev/rdisk3 bs=16m status=progress
diskutil eject /dev/disk3Code-Sprache: JavaScript (javascript)

# Windows / Mac / Linux

Raspberry Pi Imager oder balenaEtcher