add SMB backup plan

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
padriano 2026-05-09 22:28:49 +01:00
parent c134eb3d7a
commit bf2d9e40fd

168
plans/Plan-smb-backup.md Normal file
View file

@ -0,0 +1,168 @@
# 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
```mermaid
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
- [`docker-compose.yml`](../docker-compose.yml) — add `samba-backup` service
- [`samba-backup/entrypoint.sh`](../samba-backup/entrypoint.sh) — script: mounts remote share on each cron run, runs rsync
- [`samba-backup/.credentials`](../samba-backup/.credentials) — remote SMB credentials (added to `.gitignore`)
- [`.env`](../.env) / [`.env.example`](../.env.example) — add `BACKUP_REMOTE_HOST`, `BACKUP_REMOTE_SHARE`, `BACKUP_CRON_SCHEDULE`
## New service in docker-compose.yml
```yaml
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.
```mermaid
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
```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
```ini
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
```