composer/plans/Plan-smb-backup.md
padriano bf2d9e40fd add SMB backup plan
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-09 22:28:49 +01:00

5.7 KiB

SMB Share Backup via docker-compose

Overview

Add a dedicated samba-backup container based on Alpine that:

  1. Mounts the local /home/padriano/share directory directly (same bind-mount as the samba container, read-only)
  2. Mounts the remote SMB share via cifs-utils inside the container at sync time
  3. Runs rsync on a cron schedule to sync from local to remote

This is self-contained in docker-compose, requires no new tools on the host, and survives restarts.

Container choice

A minimal alpine image with rsync + cifs-utils + crond. Alpine is preferred for simplicity and control.

Architecture

flowchart LR
    subgraph host [Host filesystem]
        share["/home/padriano/share"]
    end
    subgraph compose [docker-compose]
        samba["samba\n192.168.1.247\n[Share]"]
        backup["samba-backup\n(alpine + rsync + cifs-utils)"]
    end
    subgraph remote [Remote SMB server]
        remoteShare["\\\\remote-server\\ShareName"]
    end

    share -->|"bind mount (ro)"| samba
    share -->|"bind mount (ro)"| backup
    backup -->|"cifs mount + rsync (scheduled)"| remoteShare

Files to change / add

New service in docker-compose.yml

samba-backup:
  image: alpine:latest
  container_name: samba-backup
  restart: unless-stopped
  cap_add:
    - SYS_ADMIN          # required for mount inside container
  security_opt:
    - apparmor:unconfined # needed for CIFS mount
  devices:
    - /dev/fuse
  environment:
    - TZ=${TZ}
    - BACKUP_REMOTE_HOST=${BACKUP_REMOTE_HOST}
    - BACKUP_REMOTE_SHARE=${BACKUP_REMOTE_SHARE}
    - BACKUP_CRON_SCHEDULE=${BACKUP_CRON_SCHEDULE:-0 3 * * *}
  volumes:
    - /home/padriano/share:/source:ro
    - ./samba-backup/entrypoint.sh:/entrypoint.sh:ro
    - ./samba-backup/.credentials:/etc/samba-backup/credentials:ro
  entrypoint: ["/bin/sh", "/entrypoint.sh"]

SYS_ADMIN + apparmor:unconfined are required so the Alpine container can run mount.cifs internally.

Handling remote unavailability

The remote SMB share can be unreachable in two situations:

1. At container startup — the entrypoint does NOT mount at startup. crond is started unconditionally, so the container never crashes due to a missing remote.

2. At sync time (cron fires, remote is down) — the do_backup function tries to mount, skips rsync gracefully if mount fails, then unmounts. The next cron run will try again. No crash, no data loss.

flowchart TD
    start([Container starts]) --> install[apk install deps]
    install --> cron[Register cron job]
    cron --> crond[crond -f loop]
    crond -->|"schedule fires"| tryMount{"mount remote\nSMB share"}
    tryMount -->|"success"| rsync[rsync /source/ → /mnt/remote-smb/]
    tryMount -->|"failure\n(log + skip)"| skip[Skip — log warning]
    rsync --> umount[umount remote share]
    umount --> crond
    skip --> crond

entrypoint.sh

#!/bin/sh
# No set -e: we handle errors explicitly so the container stays up

apk add --no-cache rsync cifs-utils tzdata > /dev/null 2>&1

MOUNT_TARGET=/mnt/remote-smb
LOG=/var/log/samba-backup.log
mkdir -p "$MOUNT_TARGET"

do_backup() {
  TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
  echo "[$TIMESTAMP] Starting backup run" >> "$LOG"

  # Attempt mount; skip sync if unavailable
  if ! mount -t cifs "//${BACKUP_REMOTE_HOST}/${BACKUP_REMOTE_SHARE}" "$MOUNT_TARGET" \
      -o credentials=/etc/samba-backup/credentials,vers=3.0,uid=0,gid=0,file_mode=0660,dir_mode=0770 \
      > /dev/null 2>&1; then
    echo "[$TIMESTAMP] WARNING: Remote share unavailable, skipping sync" >> "$LOG"
    return 0
  fi

  rsync -avz --delete /source/ "${MOUNT_TARGET}/" >> "$LOG" 2>&1
  RSYNC_EXIT=$?

  umount "$MOUNT_TARGET" 2>/dev/null || true

  if [ "$RSYNC_EXIT" -ne 0 ]; then
    echo "[$TIMESTAMP] ERROR: rsync exited with code ${RSYNC_EXIT}" >> "$LOG"
  else
    echo "[$TIMESTAMP] Backup completed successfully" >> "$LOG"
  fi
}

# Write cron job that calls this script in sync-only mode
cat > /etc/crontabs/root << EOF
${BACKUP_CRON_SCHEDULE} /bin/sh /entrypoint.sh --sync-only
EOF

# If called with --sync-only, just run the backup and exit (used by crond)
if [ "$1" = "--sync-only" ]; then
  do_backup
  exit 0
fi

# Normal startup: run an immediate first sync in background, then hand off to crond
do_backup &

crond -f -l 8

samba-backup/.credentials

username=REMOTE_USER
password=REMOTE_PASSWORD
domain=WORKGROUP

.env additions

BACKUP_REMOTE_HOST=192.168.x.x        # remote SMB server IP or hostname
BACKUP_REMOTE_SHARE=ShareName         # share name on remote server
BACKUP_CRON_SCHEDULE=0 3 * * *        # daily at 03:00; adjust as needed

Notes

  • --delete on rsync makes the remote a true mirror; remove it if you want additive-only backups.
  • If you prefer not to give SYS_ADMIN to a container, mount the remote CIFS share on the host and pass it as a second bind-mount volume — removes the privileged requirement but requires host-side setup (/etc/fstab or systemd.mount).
  • Consider a custom image (FROM alpine + RUN apk add rsync cifs-utils tzdata) to avoid apk running on every container restart.
  • Backup logs are written to /var/log/samba-backup.log inside the container. Tail them with:
    docker exec samba-backup tail -f /var/log/samba-backup.log