add SMB backup plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c134eb3d7a
commit
bf2d9e40fd
1 changed files with 168 additions and 0 deletions
168
plans/Plan-smb-backup.md
Normal file
168
plans/Plan-smb-backup.md
Normal 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
|
||||||
|
```
|
||||||
Loading…
Add table
Reference in a new issue