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